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