1 /*
2   Copyright 2021 Northern.tech AS
3 
4   This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5 
6   This program is free software; you can redistribute it and/or modify it
7   under the terms of the GNU General Public License as published by the
8   Free Software Foundation; version 3.
9 
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14 
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
18 
19   To the extent this program is licensed as part of the Enterprise
20   versions of CFEngine, the applicable Commercial Open Source License
21   (COSL) may apply to this file if you as a licensee so wish it. See
22   included file COSL.txt.
23 */
24 
25 #include <verify_classes.h>
26 
27 #include <attributes.h>
28 #include <matching.h>
29 #include <files_names.h>
30 #include <fncall.h>
31 #include <rlist.h>
32 #include <expand.h>
33 #include <promises.h>
34 #include <conversion.h>
35 #include <logic_expressions.h>
36 #include <string_lib.h>                                  /* StringHash */
37 #include <regex.h>                                       /* StringMatchFull */
38 
39 
40 static bool EvalClassExpression(EvalContext *ctx, Constraint *cp, const Promise *pp);
41 
ValidClassName(const char * str)42 static bool ValidClassName(const char *str)
43 {
44     ParseResult res = ParseExpression(str, 0, strlen(str));
45 
46     if (res.result)
47     {
48         FreeExpression(res.result);
49     }
50 
51     assert(res.position >= 0);
52     return res.result && (size_t) res.position == strlen(str);
53 }
54 
VerifyClassPromise(EvalContext * ctx,const Promise * pp,ARG_UNUSED void * param)55 PromiseResult VerifyClassPromise(EvalContext *ctx, const Promise *pp, ARG_UNUSED void *param)
56 {
57     assert(pp != NULL);
58     assert(param == NULL);
59 
60     Log(LOG_LEVEL_DEBUG, "Evaluating classes promise: %s", pp->promiser);
61 
62     Attributes a = GetClassContextAttributes(ctx, pp);
63 
64     if (!StringMatchFull("[a-zA-Z0-9_]+", pp->promiser))
65     {
66         Log(LOG_LEVEL_VERBOSE, "Class identifier '%s' contains illegal characters - canonifying", pp->promiser);
67         CanonifyNameInPlace(pp->promiser);
68     }
69 
70     if (a.context.nconstraints > 1)
71     {
72         cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, &a, "Irreconcilable constraints in classes for '%s'", pp->promiser);
73         return PROMISE_RESULT_FAIL;
74     }
75 
76     if (a.context.expression == NULL ||
77         EvalClassExpression(ctx, a.context.expression, pp))
78     {
79         if (a.context.expression == NULL)
80         {
81             Log(LOG_LEVEL_DEBUG, "Setting class '%s' without an expression, implying 'any'", pp->promiser);
82         }
83 
84         if (!ValidClassName(pp->promiser))
85         {
86             cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, &a,
87                  "Attempted to name a class '%s', which is an illegal class identifier", pp->promiser);
88             return PROMISE_RESULT_FAIL;
89         }
90         else
91         {
92             StringSet *tags = StringSetNew();
93             StringSetAdd(tags, xstrdup("source=promise"));
94 
95             for (const Rlist *rp = PromiseGetConstraintAsList(ctx, "meta", pp); rp; rp = rp->next)
96             {
97                 StringSetAdd(tags, xstrdup(RlistScalarValue(rp)));
98             }
99 
100             const char *comment = PromiseGetConstraintAsRval(pp, "comment", RVAL_TYPE_SCALAR);
101 
102             bool inserted = false;
103             if (/* Persistent classes are always global: */
104                 a.context.persistent > 0 ||
105                 /* Namespace-scope is global: */
106                 a.context.scope == CONTEXT_SCOPE_NAMESPACE ||
107                 /* If there is no explicit scope, common bundles define global
108                  * classes, other bundles define local classes: */
109                 (a.context.scope == CONTEXT_SCOPE_NONE &&
110                  strcmp(PromiseGetBundle(pp)->type, "common") == 0))
111             {
112                 Log(LOG_LEVEL_VERBOSE, "C:     +  Global class: %s",
113                     pp->promiser);
114                 inserted = EvalContextClassPutSoftTagsSetWithComment(ctx, pp->promiser,
115                                                                      CONTEXT_SCOPE_NAMESPACE,
116                                                                      tags, comment);
117             }
118             else
119             {
120                 Log(LOG_LEVEL_VERBOSE, "C:     +  Private class: %s",
121                     pp->promiser);
122                 inserted = EvalContextClassPutSoftTagsSetWithComment(ctx, pp->promiser,
123                                                                      CONTEXT_SCOPE_BUNDLE,
124                                                                      tags, comment);
125             }
126 
127             if (a.context.persistent > 0)
128             {
129                 Log(LOG_LEVEL_VERBOSE,
130                     "C:     +  Persistent class: '%s' (%d minutes)",
131                     pp->promiser, a.context.persistent);
132                 Buffer *buf = StringSetToBuffer(tags, ',');
133                 EvalContextHeapPersistentSave(ctx, pp->promiser, a.context.persistent,
134                                               CONTEXT_STATE_POLICY_RESET, BufferData(buf));
135                 BufferDestroy(buf);
136             }
137             if (inserted && (comment != NULL))
138             {
139                 Log(LOG_LEVEL_VERBOSE, "Added class '%s' with comment '%s'", pp->promiser, comment);
140             }
141 
142 
143             if (!inserted)
144             {
145                 StringSetDestroy(tags);
146             }
147 
148             return PROMISE_RESULT_NOOP;
149         }
150     }
151 
152     return PROMISE_RESULT_NOOP;
153 }
154 
SelectClass(EvalContext * ctx,const Rlist * list,const Promise * pp)155 static bool SelectClass(EvalContext *ctx, const Rlist *list, const Promise *pp)
156 {
157     int count = RlistLen(list);
158 
159     if (count == 0)
160     {
161         Log(LOG_LEVEL_ERR, "No classes to select on RHS");
162         PromiseRef(LOG_LEVEL_ERR, pp);
163         return false;
164     }
165     else if (count == 1 && IsVarList(RlistScalarValue(list)))
166     {
167         Log(LOG_LEVEL_VERBOSE,
168             "select_class: Can not expand list '%s' for setting class.",
169             RlistScalarValue(list));
170         PromiseRef(LOG_LEVEL_VERBOSE, pp);
171         return false;
172     }
173 
174     assert(list);
175 
176     // Max:    (strlen of VFQNAME)   (strlen of VIPADDRESS)   (strlen of max 64 bit integer    )   '++\0'
177     char splay[sizeof(VFQNAME) - 1 + sizeof(VIPADDRESS) - 1 + sizeof("18446744073709551615") - 1 + 2 + 1];
178     snprintf(splay, sizeof(splay), "%s+%s+%ju", VFQNAME, VIPADDRESS, (uintmax_t)getuid());
179     unsigned int hash = StringHash(splay, 0);
180     int n = hash % count;
181 
182     while (n > 0 && list->next != NULL)
183     {
184         n--;
185         list = list->next;
186     }
187 
188     /* We are not having expanded variable or list at this point,
189      * so we can not set select_class. */
190     if (IsExpandable(RlistScalarValue(list)))
191     {
192         Log(LOG_LEVEL_VERBOSE,
193             "select_class: Can not use not expanded element '%s' for setting class.",
194             RlistScalarValue(list));
195         PromiseRef(LOG_LEVEL_VERBOSE, pp);
196         return false;
197     }
198 
199     EvalContextClassPutSoft(ctx, RlistScalarValue(list),
200                             CONTEXT_SCOPE_NAMESPACE, "source=promise");
201     return true;
202 }
203 
DistributeClass(EvalContext * ctx,const Rlist * dist,const Promise * pp)204 static bool DistributeClass(EvalContext *ctx, const Rlist *dist, const Promise *pp)
205 {
206     int total = 0;
207     const Rlist *rp;
208 
209     for (rp = dist; rp != NULL; rp = rp->next)
210     {
211         int result = IntFromString(RlistScalarValue(rp));
212 
213         if (result < 0)
214         {
215             Log(LOG_LEVEL_ERR, "Negative integer in class distribution");
216             PromiseRef(LOG_LEVEL_ERR, pp);
217             return false;
218         }
219 
220         total += result;
221     }
222 
223     if (total == 0)
224     {
225         Log(LOG_LEVEL_ERR, "An empty distribution was specified on RHS");
226         PromiseRef(LOG_LEVEL_ERR, pp);
227         return false;
228     }
229 
230     double fluct = drand48() * total;
231     assert(0 <= fluct && fluct < total);
232 
233     for (rp = dist; rp != NULL; rp = rp->next)
234     {
235         fluct -= IntFromString(RlistScalarValue(rp));
236         if (fluct < 0)
237         {
238             break;
239         }
240     }
241     assert(rp);
242 
243     char buffer[CF_MAXVARSIZE];
244     snprintf(buffer, CF_MAXVARSIZE, "%s_%s", pp->promiser, RlistScalarValue(rp));
245 
246     if (strcmp(PromiseGetBundle(pp)->type, "common") == 0)
247     {
248         EvalContextClassPutSoft(ctx, buffer, CONTEXT_SCOPE_NAMESPACE,
249                                 "source=promise");
250     }
251     else
252     {
253         EvalContextClassPutSoft(ctx, buffer, CONTEXT_SCOPE_BUNDLE,
254                                 "source=promise");
255     }
256 
257     return true;
258 }
259 
260 enum combine_t { c_or, c_and, c_xor }; // Class combinations
EvalBoolCombination(EvalContext * ctx,const Rlist * list,enum combine_t logic)261 static bool EvalBoolCombination(EvalContext *ctx, const Rlist *list,
262                                 enum combine_t logic)
263 {
264     bool result = (logic == c_and);
265 
266     for (const Rlist *rp = list; rp != NULL; rp = rp->next)
267     {
268         // tolerate unexpanded entries here and interpret as "class not set"
269         bool here = (rp->val.type == RVAL_TYPE_SCALAR &&
270                      IsDefinedClass(ctx, RlistScalarValue(rp)));
271 
272         // shortcut "and" and "or"
273         switch (logic)
274         {
275         case c_or:
276             if (here)
277             {
278                 return true;
279             }
280             break;
281 
282         case c_and:
283             if (!here)
284             {
285                 return false;
286             }
287             break;
288 
289         default:
290             result ^= here;
291             break;
292         }
293     }
294 
295     return result;
296 }
297 
EvalClassExpression(EvalContext * ctx,Constraint * cp,const Promise * pp)298 static bool EvalClassExpression(EvalContext *ctx, Constraint *cp, const Promise *pp)
299 {
300     assert(pp);
301 
302     if (cp == NULL) // ProgrammingError ?  We'll crash RSN anyway ...
303     {
304         Log(LOG_LEVEL_ERR,
305             "EvalClassExpression internal diagnostic discovered an ill-formed condition");
306     }
307 
308     if (!IsDefinedClass(ctx, pp->classes))
309     {
310         return false;
311     }
312 
313     if (IsDefinedClass(ctx, pp->promiser))
314     {
315         if (PromiseGetConstraintAsInt(ctx, "persistence", pp) == 0)
316         {
317             Log(LOG_LEVEL_VERBOSE,
318                 " ?> Cancelling cached persistent class %s",
319                 pp->promiser);
320             EvalContextHeapPersistentRemove(pp->promiser);
321         }
322         return false;
323     }
324 
325     switch (cp->rval.type)
326     {
327         Rval rval;
328         FnCall *fp;
329 
330     case RVAL_TYPE_FNCALL:
331         fp = RvalFnCallValue(cp->rval);
332         /* Special expansion of functions for control, best effort only: */
333         FnCallResult res = FnCallEvaluate(ctx, PromiseGetPolicy(pp), fp, pp);
334 
335         FnCallDestroy(fp);
336         cp->rval = res.rval;
337         break;
338 
339     case RVAL_TYPE_LIST:
340         for (Rlist *rp = cp->rval.item; rp != NULL; rp = rp->next)
341         {
342             rval = EvaluateFinalRval(ctx, PromiseGetPolicy(pp), NULL,
343                                      "this", rp->val, true, pp);
344             RvalDestroy(rp->val);
345             rp->val = rval;
346         }
347         break;
348 
349     default:
350         rval = ExpandPrivateRval(ctx, NULL, "this", cp->rval.item, cp->rval.type);
351         RvalDestroy(cp->rval);
352         cp->rval = rval;
353         break;
354     }
355 
356     if (strcmp(cp->lval, "expression") == 0)
357     {
358         return (cp->rval.type == RVAL_TYPE_SCALAR &&
359                 IsDefinedClass(ctx, RvalScalarValue(cp->rval)));
360     }
361 
362     if (strcmp(cp->lval, "not") == 0)
363     {
364         return (cp->rval.type == RVAL_TYPE_SCALAR &&
365                 !IsDefinedClass(ctx, RvalScalarValue(cp->rval)));
366     }
367 
368     /* If we get here, anything remaining on the RHS must be a clist */
369     if (cp->rval.type != RVAL_TYPE_LIST)
370     {
371         Log(LOG_LEVEL_ERR, "RHS of promise body attribute '%s' is not a list", cp->lval);
372         PromiseRef(LOG_LEVEL_ERR, pp);
373         return true;
374     }
375 
376     // Class selection
377     if (strcmp(cp->lval, "select_class") == 0)
378     {
379         return SelectClass(ctx, cp->rval.item, pp);
380     }
381 
382     // Class distributions
383     if (strcmp(cp->lval, "dist") == 0)
384     {
385         return DistributeClass(ctx, cp->rval.item, pp);
386     }
387 
388     /* Combine with and/or/xor: */
389     if (strcmp(cp->lval, "or") == 0)
390     {
391         return EvalBoolCombination(ctx, cp->rval.item, c_or);
392     }
393     else if (strcmp(cp->lval, "and") == 0)
394     {
395         return EvalBoolCombination(ctx, cp->rval.item, c_and);
396     }
397     else if (strcmp(cp->lval, "xor") == 0)
398     {
399         return EvalBoolCombination(ctx, cp->rval.item, c_xor);
400     }
401 
402     return false;
403 }
404