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 <mod_custom.h>
26 
27 #include <syntax.h>
28 #include <string_lib.h>      // StringStartsWith()
29 #include <string_sequence.h> // SeqStrginFromString()
30 #include <policy.h>          // Promise
31 #include <eval_context.h>    // cfPS(), EvalContextVariableGet()
32 #include <attributes.h>      // GetClassContextAttributes(), IsClassesBodyConstraint()
33 #include <expand.h>          // ExpandScalar()
34 #include <var_expressions.h> // StringContainsUnresolved(), StringIsBareNonScalarRef()
35 #include <map.h>             // Map*
36 
37 static Map *custom_modules = NULL;
38 
39 static const ConstraintSyntax promise_constraints[] = {
40     CONSTRAINT_SYNTAX_GLOBAL,
41     ConstraintSyntaxNewString(
42         "path", "", "Path to promise module", SYNTAX_STATUS_NORMAL),
43     ConstraintSyntaxNewString(
44         "interpreter", "", "Path to interpreter", SYNTAX_STATUS_NORMAL),
45     ConstraintSyntaxNewNull()};
46 
47 const BodySyntax CUSTOM_PROMISE_BLOCK_SYNTAX =
48     BodySyntaxNew("promise", promise_constraints, NULL, SYNTAX_STATUS_NORMAL);
49 
FindCustomPromiseType(const Promise * promise)50 Body *FindCustomPromiseType(const Promise *promise)
51 {
52     assert(promise != NULL);
53 
54     const char *const promise_type = PromiseGetPromiseType(promise);
55     const Policy *const policy =
56         promise->parent_section->parent_bundle->parent_policy;
57     Seq *custom_promise_types = policy->custom_promise_types;
58     const size_t length = SeqLength(custom_promise_types);
59     for (size_t i = 0; i < length; ++i)
60     {
61         Body *current = SeqAt(custom_promise_types, i);
62         if (StringEqual(current->name, promise_type))
63         {
64             return current;
65         }
66     }
67     return NULL;
68 }
69 
GetInterpreterAndPath(EvalContext * ctx,Body * promise_block,char ** interpreter_out,char ** path_out)70 static bool GetInterpreterAndPath(
71     EvalContext *ctx,
72     Body *promise_block,
73     char **interpreter_out,
74     char **path_out)
75 {
76     assert(promise_block != NULL);
77     assert(interpreter_out != NULL);
78     assert(path_out != NULL);
79 
80     char *interpreter = NULL;
81     char *path = NULL;
82 
83     const char *promise_type = promise_block->name;
84     Seq *promise_block_attributes = promise_block->conlist;
85     const size_t length = SeqLength(promise_block_attributes);
86 
87     for (size_t i = 0; i < length; ++i)
88     {
89         Constraint *attribute = SeqAt(promise_block_attributes, i);
90         const char *name = attribute->lval;
91         const char *value = RvalScalarValue(attribute->rval);
92 
93         if (StringEqual("interpreter", name))
94         {
95             free(interpreter);
96             interpreter = ExpandScalar(ctx, NULL, NULL, value, NULL);
97         }
98         else if (StringEqual("path", name))
99         {
100             free(path);
101             path = ExpandScalar(ctx, NULL, NULL, value, NULL);
102         }
103         else
104         {
105             debug_abort_if_reached();
106         }
107     }
108 
109     if (path == NULL)
110     {
111         Log(LOG_LEVEL_ERR,
112             "Custom promise type '%s' missing path",
113             promise_type);
114         free(interpreter);
115         free(path);
116         return false;
117     }
118 
119     *interpreter_out = interpreter;
120     *path_out = path;
121     return true;
122 }
123 
PromiseModule_LogJson(JsonElement * object,const Promise * pp)124 static inline bool PromiseModule_LogJson(JsonElement *object, const Promise *pp)
125 {
126     const char *level_string = JsonObjectGetAsString(object, "level");
127     const char *message = JsonObjectGetAsString(object, "message");
128 
129     assert(level_string != NULL && message != NULL);
130     const LogLevel level = LogLevelFromString(level_string);
131     assert(level != LOG_LEVEL_NOTHING);
132 
133     if (pp != NULL)
134     {
135         /* Check if there is a log level specified for the particular promise. */
136         const char *value = PromiseGetConstraintAsRval(pp, "log_level", RVAL_TYPE_SCALAR);
137         if (value != NULL)
138         {
139             LogLevel specific = ActionAttributeLogLevelFromString(value);
140             if (specific < level)
141             {
142                 /* Do not log messages that have a higher log level than the log
143                  * level specified for the promise (e.g. 'info' messages when
144                  * 'error' was requested for the promise). */
145                 return false;
146             }
147         }
148     }
149 
150     Log(level, "%s", message);
151 
152     // We want to keep track of whether the module logged an error,
153     // it must log errors for not kept promises and validation errors.
154     const bool logged_error =
155         (level == LOG_LEVEL_ERR || level == LOG_LEVEL_CRIT);
156     return logged_error;
157 }
158 
PromiseModule_ParseResultClasses(char * value)159 static inline JsonElement *PromiseModule_ParseResultClasses(char *value)
160 {
161     JsonElement *result_classes = JsonArrayCreate(1);
162     char *delim = strchr(value, ',');
163     while (delim != NULL)
164     {
165         *delim = '\0';
166         JsonArrayAppendString(result_classes, value);
167         value = delim + 1;
168         delim = strchr(value, ',');
169     }
170     JsonArrayAppendString(result_classes, value);
171     return result_classes;
172 }
173 
PromiseModule_Receive(PromiseModule * module,const Promise * pp)174 static JsonElement *PromiseModule_Receive(PromiseModule *module, const Promise *pp)
175 {
176     assert(module != NULL);
177 
178     bool line_based = !(module->json);
179 
180     char *line = NULL;
181     size_t size = 0;
182     bool empty_line = false;
183     JsonElement *log_array = JsonArrayCreate(10);
184     JsonElement *response = NULL;
185 
186     if (line_based)
187     {
188         response = JsonObjectCreate(10);
189     }
190 
191     bool logged_error = false;
192 
193     ssize_t bytes;
194     while (!empty_line
195            && ((bytes = getline(&line, &size, module->output)) > 0))
196     {
197         assert(bytes > 0);
198         assert(line != NULL);
199 
200         assert(line[bytes] == '\0');
201         assert(line[bytes - 1] == '\n');
202         line[bytes - 1] = '\0';
203 
204         // Log only non-empty lines:
205         if (bytes > 1)
206         {
207             Log(LOG_LEVEL_DEBUG, "Received line from module: '%s'", line);
208         }
209 
210         if (line[0] == '\0')
211         {
212             empty_line = true;
213         }
214         else if (StringStartsWith(line, "log_"))
215         {
216             const char *const equal_sign = strchr(line, '=');
217             assert(equal_sign != NULL);
218             if (equal_sign == NULL)
219             {
220                 Log(LOG_LEVEL_ERR,
221                     "Promise module sent invalid log line: '%s'",
222                     line);
223                 // Skip this line but keep parsing
224                 FREE_AND_NULL(line);
225                 size = 0;
226                 continue;
227             }
228             const char *const message = equal_sign + 1;
229             const char *const level_start = line + strlen("log_");
230             const size_t level_length = equal_sign - level_start;
231             char *const level = xstrndup(level_start, level_length);
232             assert(strlen(level) == level_length);
233 
234             JsonElement *log_message = JsonObjectCreate(2);
235             JsonObjectAppendString(log_message, "level", level);
236             JsonObjectAppendString(log_message, "message", message);
237             logged_error |= PromiseModule_LogJson(log_message, pp);
238             JsonArrayAppendObject(log_array, log_message);
239 
240             free(level);
241         }
242         else if (line_based)
243         {
244             const char *const equal_sign = strchr(line, '=');
245             assert(equal_sign != NULL);
246             if (equal_sign == NULL)
247             {
248                 Log(LOG_LEVEL_ERR,
249                     "Promise module sent invalid line: '%s'",
250                     line);
251             }
252             else
253             {
254                 const char *const value = equal_sign + 1;
255                 const size_t key_length = equal_sign - line;
256                 char *const key = xstrndup(line, key_length);
257                 assert(strlen(key) == key_length);
258                 if (StringEqual(key, "result_classes"))
259                 {
260                     char *result_classes_str = xstrdup(value);
261                     JsonElement *result_classes = PromiseModule_ParseResultClasses(result_classes_str);
262                     JsonObjectAppendArray(response, key, result_classes);
263                     free(result_classes_str);
264                 }
265                 else
266                 {
267                     JsonObjectAppendString(response, key, value);
268                 }
269                 free(key);
270             }
271         }
272         else // JSON protocol:
273         {
274             assert(strlen(line) > 0);
275             assert(response == NULL); // Should be first and only line
276             const char *data = line;  // JsonParse() moves this while parsing
277             JsonParseError err = JsonParse(&data, &response);
278             if (err != JSON_PARSE_OK)
279             {
280                 assert(response == NULL);
281                 Log(LOG_LEVEL_ERR,
282                     "Promise module '%s' sent invalid JSON",
283                     module->path);
284                 free(line);
285                 return NULL;
286             }
287             assert(response != NULL);
288         }
289 
290         FREE_AND_NULL(line);
291         size = 0;
292     }
293 
294     if (response == NULL)
295     {
296         // This can happen if using the JSON protocol, and the module sends
297         // nothing (newlines) or only log= lines.
298         assert(!line_based);
299         Log(LOG_LEVEL_ERR,
300             "The '%s' promise module sent an invalid/incomplete response with JSON based protocol",
301             module->path);
302         return NULL;
303     }
304 
305     if (line_based)
306     {
307         JsonObjectAppendArray(response, "log", log_array);
308         log_array = NULL;
309     }
310     else
311     {
312         JsonElement *json_log_messages = JsonObjectGet(response, "log");
313 
314         // Log messages inside JSON data haven't been printed yet,
315         // do it now:
316         if (json_log_messages != NULL)
317         {
318             size_t length = JsonLength(json_log_messages);
319             for (size_t i = 0; i < length; ++i)
320             {
321                 logged_error |= PromiseModule_LogJson(
322                     JsonArrayGet(json_log_messages, i), pp);
323             }
324         }
325 
326         JsonElement *merged = NULL;
327         bool had_log_lines = (log_array != NULL && JsonLength(log_array) > 0);
328         if (json_log_messages == NULL && !had_log_lines)
329         {
330             // No log messages at all, no need to add anything to JSON
331         }
332         else if (!had_log_lines)
333         {
334             // No separate log lines before JSON data, leave JSON as is
335         }
336         else if (had_log_lines && (json_log_messages == NULL))
337         {
338             // Separate log lines, but no log messages in JSON data
339             JsonObjectAppendArray(response, "log", log_array);
340             log_array = NULL;
341         }
342         else
343         {
344             // both log messages as separate lines and in JSON, merge:
345             merged = JsonMerge(log_array, json_log_messages);
346             JsonObjectAppendArray(response, "log", merged);
347             // json_log_messages will be destroyed since we append over it
348         }
349     }
350     JsonDestroy(log_array);
351 
352     assert(response != NULL);
353     JsonObjectAppendBool(response, "_logged_error", logged_error);
354     return response;
355 }
356 
PromiseModule_SendMessage(PromiseModule * module,Seq * message)357 static void PromiseModule_SendMessage(PromiseModule *module, Seq *message)
358 {
359     assert(module != NULL);
360 
361     const size_t length = SeqLength(message);
362     for (size_t i = 0; i < length; ++i)
363     {
364         const char *line = SeqAt(message, i);
365         const size_t line_length = strlen(line);
366         assert(line_length > 0 && memchr(line, '\n', line_length) == NULL);
367         fprintf(module->input, "%s\n", line);
368     }
369     fprintf(module->input, "\n");
370     fflush(module->input);
371 }
372 
PromiseModule_ReceiveHeader(PromiseModule * module)373 static Seq *PromiseModule_ReceiveHeader(PromiseModule *module)
374 {
375     assert(module != NULL);
376 
377     // Read header:
378     char *line = NULL;
379     size_t size = 0;
380     ssize_t bytes = getline(&line, &size, module->output);
381     if (bytes <= 0)
382     {
383         Log(LOG_LEVEL_ERR,
384             "Did not receive header from promise module '%s'",
385             module->path);
386         free(line);
387         return NULL;
388     }
389     if (line[bytes - 1] != '\n')
390     {
391         Log(LOG_LEVEL_ERR,
392             "Promise module '%s %s' sent an invalid header with no newline: '%s'",
393             module->interpreter,
394             module->path,
395             line);
396         free(line);
397         return NULL;
398     }
399     line[bytes - 1] = '\0';
400 
401     Log(LOG_LEVEL_DEBUG, "Received header from promise module: '%s'", line);
402 
403     Seq *header = SeqStringFromString(line, ' ');
404 
405     FREE_AND_NULL(line);
406     size = 0;
407 
408     // Read empty line:
409     bytes = getline(&line, &size, module->output);
410     if (bytes != 1 || line[0] != '\n')
411     {
412         Log(LOG_LEVEL_ERR,
413             "Promise module '%s %s' failed to send empty line after header: '%s'",
414             module->interpreter,
415             module->path,
416             line);
417         SeqDestroy(header);
418         free(line);
419         return NULL;
420     }
421 
422     free(line);
423     return header;
424 }
425 
426 // Internal function, use PromiseModule_Terminate instead
PromiseModule_DestroyInternal(PromiseModule * module)427 static void PromiseModule_DestroyInternal(PromiseModule *module)
428 {
429     assert(module != NULL);
430 
431     free(module->path);
432     free(module->interpreter);
433 
434     cf_pclose_full_duplex(&(module->fds));
435     free(module);
436 }
437 
PromiseModule_Start(char * interpreter,char * path)438 static PromiseModule *PromiseModule_Start(char *interpreter, char *path)
439 {
440     assert(path != NULL);
441 
442     if ((interpreter != NULL) && (access(interpreter, X_OK) != 0))
443     {
444         Log(LOG_LEVEL_ERR,
445             "Promise module interpreter '%s' is not an executable file",
446             interpreter);
447         return NULL;
448     }
449 
450     if ((interpreter == NULL) && (access(path, X_OK) != 0))
451     {
452         Log(LOG_LEVEL_ERR,
453             "Promise module path '%s' is not an executable file",
454             path);
455         return NULL;
456     }
457 
458     if (access(path, F_OK) != 0)
459     {
460         Log(LOG_LEVEL_ERR,
461             "Promise module '%s' does not exist",
462             path);
463         return NULL;
464     }
465 
466     PromiseModule *module = xcalloc(1, sizeof(PromiseModule));
467 
468     module->interpreter = interpreter;
469     module->path = path;
470 
471     char command[CF_BUFSIZE];
472     if (interpreter == NULL)
473     {
474         snprintf(command, CF_BUFSIZE, "%s", path);
475     }
476     else
477     {
478         snprintf(command, CF_BUFSIZE, "%s %s", interpreter, path);
479     }
480 
481     Log(LOG_LEVEL_VERBOSE, "Starting custom promise module '%s' with command '%s'",
482         path, command);
483     module->fds = cf_popen_full_duplex_streams(command, false, true);
484     module->output = module->fds.read_stream;
485     module->input = module->fds.write_stream;
486     module->message = NULL;
487 
488     fprintf(module->input, "cf-agent %s v1\n\n", Version());
489     fflush(module->input);
490 
491     Seq *header = PromiseModule_ReceiveHeader(module);
492 
493     if (header == NULL)
494     {
495         // error logged in PromiseModule_ReceiveHeader()
496 
497         /* Make sure 'path' and 'interpreter' are not free'd twice (the calling
498          * code frees them if it gets NULL). */
499         module->path = NULL;
500         module->interpreter = NULL;
501         PromiseModule_DestroyInternal(module);
502         return NULL;
503     }
504 
505     assert(SeqLength(header) >= 3);
506     Seq *flags = SeqSplit(header, 3);
507     const size_t flags_length = SeqLength(flags);
508     for (size_t i = 0; i < flags_length; ++i)
509     {
510         const char *const flag = SeqAt(flags, i);
511         if (StringEqual(flag, "json_based"))
512         {
513             module->json = true;
514         }
515         else if (StringEqual(flag, "line_based"))
516         {
517             module->json = false;
518         }
519     }
520     SeqDestroy(flags);
521 
522     SeqDestroy(header);
523 
524     return module;
525 }
526 
PromiseModule_AppendString(PromiseModule * module,const char * key,const char * value)527 static void PromiseModule_AppendString(
528     PromiseModule *module, const char *key, const char *value)
529 {
530     assert(module != NULL);
531 
532     if (module->message == NULL)
533     {
534         module->message = JsonObjectCreate(10);
535     }
536     JsonObjectAppendString(module->message, key, value);
537 }
538 
PromiseModule_AppendInteger(PromiseModule * module,const char * key,int64_t value)539 static void PromiseModule_AppendInteger(
540     PromiseModule *module, const char *key, int64_t value)
541 {
542     assert(module != NULL);
543 
544     if (module->message == NULL)
545     {
546         module->message = JsonObjectCreate(10);
547     }
548     JsonObjectAppendInteger64(module->message, key, value);
549 }
550 
PromiseModule_AppendAttribute(PromiseModule * module,const char * key,JsonElement * value)551 static void PromiseModule_AppendAttribute(
552     PromiseModule *module, const char *key, JsonElement *value)
553 {
554     assert(module != NULL);
555 
556     if (module->message == NULL)
557     {
558         module->message = JsonObjectCreate(10);
559     }
560 
561     JsonElement *attributes = JsonObjectGet(module->message, "attributes");
562     if (attributes == NULL)
563     {
564         attributes = JsonObjectCreate(10);
565         JsonObjectAppendObject(module->message, "attributes", attributes);
566     }
567 
568     JsonObjectAppendElement(attributes, key, value);
569 }
570 
PromiseModule_Send(PromiseModule * module)571 static void PromiseModule_Send(PromiseModule *module)
572 {
573     assert(module != NULL);
574 
575     if (module->json)
576     {
577         Writer *w = FileWriter(module->input);
578         JsonWriteCompact(w, module->message);
579         FileWriterDetach(w);
580         DESTROY_AND_NULL(JsonDestroy, module->message);
581         fprintf(module->input, "\n\n");
582         fflush(module->input);
583         return;
584     }
585 
586     Seq *message = SeqNew(10, free);
587 
588     JsonIterator iter = JsonIteratorInit(module->message);
589     const char *key;
590     while ((key = JsonIteratorNextKey(&iter)) != NULL)
591     {
592         if (StringEqual("attributes", key))
593         {
594             JsonElement *attributes = JsonIteratorCurrentValue(&iter);
595             JsonIterator attr_iter = JsonIteratorInit(attributes);
596 
597             const char *attr_name;
598             while ((attr_name = JsonIteratorNextKey(&attr_iter)) != NULL)
599             {
600                 const char *attr_val = JsonPrimitiveGetAsString(
601                     JsonIteratorCurrentValue(&attr_iter));
602                 char *attr_line = NULL;
603                 xasprintf(&attr_line, "attribute_%s=%s", attr_name, attr_val);
604                 SeqAppend(message, attr_line);
605             }
606         }
607         else
608         {
609             const char *value =
610                 JsonPrimitiveGetAsString(JsonIteratorCurrentValue(&iter));
611             char *line = NULL;
612             xasprintf(&line, "%s=%s", key, value);
613             SeqAppend(message, line);
614         }
615     }
616 
617     PromiseModule_SendMessage(module, message);
618     SeqDestroy(message);
619     DESTROY_AND_NULL(JsonDestroy, module->message);
620 }
621 
TryToGetContainerFromScalarRef(const EvalContext * ctx,const char * scalar,JsonElement ** out)622 static inline bool TryToGetContainerFromScalarRef(const EvalContext *ctx, const char *scalar, JsonElement **out)
623 {
624     if (StringIsBareNonScalarRef(scalar))
625     {
626         /* Resolve a potential 'data' variable reference. */
627         const size_t scalar_len = strlen(scalar);
628         char *var_ref_str = xstrndup(scalar + 2, scalar_len - 3);
629         VarRef *ref = VarRefParse(var_ref_str);
630 
631         DataType type = CF_DATA_TYPE_NONE;
632         const void *val = EvalContextVariableGet(ctx, ref, &type);
633         free(var_ref_str);
634         VarRefDestroy(ref);
635 
636         if ((val != NULL) && (type == CF_DATA_TYPE_CONTAINER))
637         {
638             if (out != NULL)
639             {
640                 *out = JsonCopy(val);
641             }
642             return true;
643         }
644     }
645     return false;
646 }
647 
PromiseModule_AppendAllAttributes(PromiseModule * module,const EvalContext * ctx,const Promise * pp)648 static void PromiseModule_AppendAllAttributes(
649     PromiseModule *module, const EvalContext *ctx, const Promise *pp)
650 {
651     assert(module != NULL);
652     assert(pp != NULL);
653 
654     const size_t attributes = SeqLength(pp->conlist);
655     for (size_t i = 0; i < attributes; i++)
656     {
657         const Constraint *attribute = SeqAt(pp->conlist, i);
658         const char *const name = attribute->lval;
659         assert(!StringEqual(name, "ifvarclass")); // Not allowed by validation
660         if (IsClassesBodyConstraint(name)
661             || StringEqual(name, "if")
662             || StringEqual(name, "ifvarclass")
663             || StringEqual(name, "unless")
664             || StringEqual(name, "depends_on")
665             || StringEqual(name, "with"))
666         {
667             // Evaluated by agent and not sent to module, skip
668             continue;
669         }
670 
671         if (StringEqual(attribute->lval, "log_level"))
672         {
673             /* Passed to the module as 'log_level' request field, not as an attribute. */
674             continue;
675         }
676 
677         JsonElement *value = NULL;
678         if (attribute->rval.type == RVAL_TYPE_SCALAR)
679         {
680             /* Could be a '@(container)' reference. */
681             if (!TryToGetContainerFromScalarRef(ctx, RvalScalarValue(attribute->rval), &value))
682             {
683                 /* Didn't resolve to a container value, let's just use the
684                  * scalar value as-is. */
685                 value = RvalToJson(attribute->rval);
686             }
687         }
688         if ((attribute->rval.type == RVAL_TYPE_LIST) ||
689             (attribute->rval.type == RVAL_TYPE_CONTAINER))
690         {
691             value = RvalToJson(attribute->rval);
692         }
693 
694         if (value != NULL)
695         {
696             PromiseModule_AppendAttribute(module, name, value);
697         }
698         else
699         {
700             Log(LOG_LEVEL_VERBOSE,
701                 "Unsupported type of the '%s' attribute (%c), cannot be sent to custom promise module",
702                 name, attribute->rval.type);
703         }
704     }
705 }
706 
CheckPrimitiveForUnexpandedVars(JsonElement * primitive,ARG_UNUSED void * data)707 static bool CheckPrimitiveForUnexpandedVars(JsonElement *primitive, ARG_UNUSED void *data)
708 {
709     assert(JsonGetElementType(primitive) == JSON_ELEMENT_TYPE_PRIMITIVE);
710 
711     /* Stop the iteration if a variable expression is found. */
712     return (!StringContainsUnresolved(JsonPrimitiveGetAsString(primitive)));
713 }
714 
CheckObjectForUnexpandedVars(JsonElement * object,ARG_UNUSED void * data)715 static bool CheckObjectForUnexpandedVars(JsonElement *object, ARG_UNUSED void *data)
716 {
717     assert(JsonGetType(object) == JSON_TYPE_OBJECT);
718 
719     /* Stop the iteration if a variable expression is found among children
720      * keys. (elements inside the object are checked separately) */
721     JsonIterator iter = JsonIteratorInit(object);
722     while (JsonIteratorHasMore(&iter))
723     {
724         const char *key = JsonIteratorNextKey(&iter);
725         if (StringContainsUnresolved(key))
726         {
727             return false;
728         }
729     }
730     return true;
731 }
732 
CustomPromise_IsFullyResolved(const EvalContext * ctx,const Promise * pp,bool nonscalars_allowed)733 static inline bool CustomPromise_IsFullyResolved(const EvalContext *ctx, const Promise *pp, bool nonscalars_allowed)
734 {
735     assert(pp != NULL);
736 
737     if (StringContainsUnresolved(pp->promiser))
738     {
739         return false;
740     }
741     const size_t attributes = SeqLength(pp->conlist);
742     for (size_t i = 0; i < attributes; i++)
743     {
744         const Constraint *attribute = SeqAt(pp->conlist, i);
745         if (IsClassesBodyConstraint(attribute->lval))
746         {
747             /* Not passed to the modules, handled on the agent side. */
748             continue;
749         }
750         if (StringEqual(attribute->lval, "log_level"))
751         {
752             /* Passed to the module as 'log_level' request field, not as an attribute. */
753             continue;
754         }
755         if (StringEqual(attribute->lval, "unless"))
756         {
757             /* unless can actually have unresolved variables here,
758                it defaults to evaluate in case of unresolved variables,
759                to be the true opposite of if. (if would skip).*/
760             continue;
761         }
762         if ((attribute->rval.type == RVAL_TYPE_FNCALL) ||
763             (!nonscalars_allowed && (attribute->rval.type != RVAL_TYPE_SCALAR)))
764         {
765             return false;
766         }
767         if (attribute->rval.type == RVAL_TYPE_SCALAR)
768         {
769             const char *const value = RvalScalarValue(attribute->rval);
770             if (StringContainsUnresolved(value) && !TryToGetContainerFromScalarRef(ctx, value, NULL))
771             {
772                 return false;
773             }
774         }
775         else if (attribute->rval.type == RVAL_TYPE_LIST)
776         {
777             assert(nonscalars_allowed);
778             for (Rlist *rl = RvalRlistValue(attribute->rval); rl != NULL; rl = rl->next)
779             {
780                 assert(rl->val.type == RVAL_TYPE_SCALAR);
781                 const char *const value = RvalScalarValue(rl->val);
782                 if (StringContainsUnresolved(value))
783                 {
784                     return false;
785                 }
786             }
787         }
788         else
789         {
790             assert(nonscalars_allowed);
791             assert(attribute->rval.type == RVAL_TYPE_CONTAINER);
792             JsonElement *attr_data = RvalContainerValue(attribute->rval);
793             return JsonWalk(attr_data, CheckObjectForUnexpandedVars, NULL,
794                             CheckPrimitiveForUnexpandedVars, NULL);
795         }
796     }
797     return true;
798 }
799 
800 
HasResultAndResultIsValid(JsonElement * response)801 static inline bool HasResultAndResultIsValid(JsonElement *response)
802 {
803     const char *const result = JsonObjectGetAsString(response, "result");
804     return ((result != NULL) && StringEqual(result, "valid"));
805 }
806 
LogLevelToRequestFromModule(const Promise * pp)807 static inline const char *LogLevelToRequestFromModule(const Promise *pp)
808 {
809     LogLevel log_level = LogGetGlobalLevel();
810 
811     /* Check if there is a log level specified for the particular promise. */
812     const char *value = PromiseGetConstraintAsRval(pp, "log_level", RVAL_TYPE_SCALAR);
813     if (value != NULL)
814     {
815         LogLevel specific = ActionAttributeLogLevelFromString(value);
816 
817         /* Promise-specific log level cannot go above the global log level
818          * (e.g. no 'info' messages for a particular promise if the global level
819          * is 'error'). */
820         log_level = MIN(log_level, specific);
821     }
822 
823     // We will never request LOG_LEVEL_NOTHING or LOG_LEVEL_CRIT from the
824     // module:
825     if (log_level < LOG_LEVEL_ERR)
826     {
827         assert((log_level == LOG_LEVEL_NOTHING) || (log_level == LOG_LEVEL_CRIT));
828         return LogLevelToString(LOG_LEVEL_ERR);
829     }
830     return LogLevelToString(log_level);
831 }
832 
PromiseModule_Validate(PromiseModule * module,const EvalContext * ctx,const Promise * pp)833 static bool PromiseModule_Validate(PromiseModule *module, const EvalContext *ctx, const Promise *pp)
834 {
835     assert(module != NULL);
836     assert(pp != NULL);
837 
838     const char *const promise_type = PromiseGetPromiseType(pp);
839     const char *const promiser = pp->promiser;
840 
841     PromiseModule_AppendString(module, "operation", "validate_promise");
842     PromiseModule_AppendString(module, "log_level", LogLevelToRequestFromModule(pp));
843     PromiseModule_AppendString(module, "promise_type", promise_type);
844     PromiseModule_AppendString(module, "promiser", promiser);
845     PromiseModule_AppendInteger(module, "line_number", pp->offset.line);
846     PromiseModule_AppendString(module, "filename", PromiseGetBundle(pp)->source_path);
847     PromiseModule_AppendAllAttributes(module, ctx, pp);
848     PromiseModule_Send(module);
849 
850     // Prints errors / log messages from module:
851     JsonElement *response = PromiseModule_Receive(module, pp);
852 
853     if (response == NULL)
854     {
855         // Error already printed in PromiseModule_Receive()
856         return false;
857     }
858 
859     // TODO: const bool logged_error = JsonObjectGetAsBool(response, "_logged_error");
860     const bool logged_error = JsonPrimitiveGetAsBool(
861         JsonObjectGet(response, "_logged_error"));
862     const bool valid = HasResultAndResultIsValid(response);
863 
864     JsonDestroy(response);
865 
866     if (!valid)
867     {
868         // Detailed error messages from module should already have been printed
869         const char *const filename =
870             pp->parent_section->parent_bundle->source_path;
871         const size_t line = pp->offset.line;
872         Log(LOG_LEVEL_VERBOSE,
873             "%s promise with promiser '%s' failed validation (%s:%zu)",
874             promise_type,
875             promiser,
876             filename,
877             line);
878 
879         if (!logged_error)
880         {
881             Log(LOG_LEVEL_CRIT,
882                 "Bug in promise module - No error(s) logged for invalid %s promise with promiser '%s' (%s:%zu)",
883                 promise_type,
884                 promiser,
885                 filename,
886                 line);
887         }
888     }
889 
890     return valid;
891 }
892 
PromiseModule_Evaluate(PromiseModule * module,EvalContext * ctx,const Promise * pp)893 static PromiseResult PromiseModule_Evaluate(
894     PromiseModule *module, EvalContext *ctx, const Promise *pp)
895 {
896     assert(module != NULL);
897     assert(pp != NULL);
898 
899     const char *const promise_type = PromiseGetPromiseType(pp);
900     const char *const promiser = pp->promiser;
901 
902     PromiseModule_AppendString(module, "operation", "evaluate_promise");
903     PromiseModule_AppendString(
904         module, "log_level", LogLevelToRequestFromModule(pp));
905     PromiseModule_AppendString(module, "promise_type", promise_type);
906     PromiseModule_AppendString(module, "promiser", promiser);
907     PromiseModule_AppendInteger(module, "line_number", pp->offset.line);
908     PromiseModule_AppendString(module, "filename", PromiseGetBundle(pp)->source_path);
909 
910     PromiseModule_AppendAllAttributes(module, ctx, pp);
911     PromiseModule_Send(module);
912 
913     JsonElement *response = PromiseModule_Receive(module, pp);
914     if (response == NULL)
915     {
916         // Log from PromiseModule_Receive
917         return PROMISE_RESULT_FAIL;
918     }
919 
920     JsonElement *result_classes = JsonObjectGetAsArray(response, "result_classes");
921     if (result_classes != NULL)
922     {
923         const size_t n_classes = JsonLength(result_classes);
924         for (size_t i = 0; i < n_classes; i++)
925         {
926             const char *class_name = JsonArrayGetAsString(result_classes, i);
927             assert(class_name != NULL);
928             EvalContextClassPutSoft(ctx, class_name, CONTEXT_SCOPE_BUNDLE, "source=promise-module");
929         }
930     }
931 
932     PromiseResult result;
933     const char *const result_str = JsonObjectGetAsString(response, "result");
934     // TODO: const bool logged_error = JsonObjectGetAsBool(response, "_logged_error");
935     const bool logged_error = JsonPrimitiveGetAsBool(
936         JsonObjectGet(response, "_logged_error"));
937 
938     /* Attributes needed for setting outcome classes etc. */
939     Attributes a = GetClassContextAttributes(ctx, pp);
940 
941     if (result_str == NULL)
942     {
943         result = PROMISE_RESULT_FAIL;
944         cfPS(
945             ctx,
946             LOG_LEVEL_ERR,
947             result,
948             pp,
949             &a,
950             "Promise module did not return a result for promise evaluation (%s promise, promiser: '%s' module: '%s')",
951             promise_type,
952             promiser,
953             module->path);
954     }
955     else if (StringEqual(result_str, "kept"))
956     {
957         result = PROMISE_RESULT_NOOP;
958         cfPS(
959             ctx,
960             LOG_LEVEL_VERBOSE,
961             result,
962             pp,
963             &a,
964             "Promise with promiser '%s' was kept by promise module '%s'",
965             promiser,
966             module->path);
967     }
968     else if (StringEqual(result_str, "not_kept"))
969     {
970         result = PROMISE_RESULT_FAIL;
971         cfPS(
972             ctx,
973             LOG_LEVEL_VERBOSE,
974             result,
975             pp,
976             &a,
977             "Promise with promiser '%s' was not kept by promise module '%s'",
978             promiser,
979             module->path);
980         if (!logged_error)
981         {
982             const char *const filename =
983                 pp->parent_section->parent_bundle->source_path;
984             const size_t line = pp->offset.line;
985             Log(LOG_LEVEL_CRIT,
986                 "Bug in promise module - Failed to log errors for not kept %s promise with promiser '%s' (%s:%zu)",
987                 promise_type,
988                 promiser,
989                 filename,
990                 line);
991         }
992     }
993     else if (StringEqual(result_str, "repaired"))
994     {
995         result = PROMISE_RESULT_CHANGE;
996         cfPS(
997             ctx,
998             LOG_LEVEL_VERBOSE,
999             result,
1000             pp,
1001             &a,
1002             "Promise with promiser '%s' was repaired by promise module '%s'",
1003             promiser,
1004             module->path);
1005     }
1006     else if (StringEqual(result_str, "error"))
1007     {
1008         result = PROMISE_RESULT_FAIL;
1009         cfPS(
1010             ctx,
1011             LOG_LEVEL_ERR,
1012             result,
1013             pp,
1014             &a,
1015             "An unexpected error occured in promise module (%s promise, promiser: '%s' module: '%s')",
1016             promise_type,
1017             promiser,
1018             module->path);
1019     }
1020     else
1021     {
1022         result = PROMISE_RESULT_FAIL;
1023         cfPS(
1024             ctx,
1025             LOG_LEVEL_ERR,
1026             result,
1027             pp,
1028             &a,
1029             "Promise module returned unacceptable result: '%s' (%s promise, promiser: '%s' module: '%s')",
1030             result_str,
1031             promise_type,
1032             promiser,
1033             module->path);
1034     }
1035 
1036     JsonDestroy(response);
1037     return result;
1038 }
1039 
PromiseModule_Terminate(PromiseModule * module,const Promise * pp)1040 static void PromiseModule_Terminate(PromiseModule *module, const Promise *pp)
1041 {
1042     if (module != NULL)
1043     {
1044         PromiseModule_AppendString(module, "operation", "terminate");
1045         PromiseModule_Send(module);
1046 
1047         JsonElement *response = PromiseModule_Receive(module, pp);
1048         JsonDestroy(response);
1049 
1050         PromiseModule_DestroyInternal(module);
1051     }
1052 }
1053 
PromiseModule_Terminate_untyped(void * data)1054 static void PromiseModule_Terminate_untyped(void *data)
1055 {
1056     PromiseModule *module = data;
1057     PromiseModule_Terminate(module, NULL);
1058 }
1059 
InitializeCustomPromises()1060 bool InitializeCustomPromises()
1061 {
1062     /* module_path -> PromiseModule map */
1063     custom_modules = MapNew(StringHash_untyped,
1064                             StringEqual_untyped,
1065                             free,
1066                             PromiseModule_Terminate_untyped);
1067     assert(custom_modules != NULL);
1068 
1069     return (custom_modules != NULL);
1070 }
1071 
FinalizeCustomPromises()1072 void FinalizeCustomPromises()
1073 {
1074     MapDestroy(custom_modules);
1075 }
1076 
EvaluateCustomPromise(EvalContext * ctx,const Promise * pp)1077 PromiseResult EvaluateCustomPromise(EvalContext *ctx, const Promise *pp)
1078 {
1079     assert(ctx != NULL);
1080     assert(pp != NULL);
1081 
1082     Body *promise_block = FindCustomPromiseType(pp);
1083     if (promise_block == NULL)
1084     {
1085         Log(LOG_LEVEL_ERR,
1086             "Undefined promise type '%s'",
1087             PromiseGetPromiseType(pp));
1088         return PROMISE_RESULT_FAIL;
1089     }
1090 
1091     /* Attributes needed for setting outcome classes etc. */
1092     Attributes a = GetClassContextAttributes(ctx, pp);
1093 
1094     char *interpreter = NULL;
1095     char *path = NULL;
1096 
1097     bool success = GetInterpreterAndPath(ctx, promise_block, &interpreter, &path);
1098 
1099     if (!success)
1100     {
1101         assert(interpreter == NULL && path == NULL);
1102         /* Details logged in GetInterpreterAndPath() */
1103         cfPS(ctx, LOG_LEVEL_NOTHING, PROMISE_RESULT_FAIL, pp, &a, NULL);
1104         return PROMISE_RESULT_FAIL;
1105     }
1106 
1107     PromiseModule *module = MapGet(custom_modules, path);
1108     if (module == NULL)
1109     {
1110         module = PromiseModule_Start(interpreter, path);
1111         if (module != NULL)
1112         {
1113             MapInsert(custom_modules, xstrdup(path), module);
1114         }
1115         else
1116         {
1117             free(interpreter);
1118             free(path);
1119             // Error logged in PromiseModule_Start()
1120             cfPS(ctx, LOG_LEVEL_NOTHING, PROMISE_RESULT_FAIL, pp, &a, NULL);
1121             return PROMISE_RESULT_FAIL;
1122         }
1123     }
1124     else
1125     {
1126         if (!StringEqual(interpreter, module->interpreter))
1127         {
1128             Log(LOG_LEVEL_ERR, "Conflicting interpreter specifications for custom promise module '%s'"
1129                 " (started with '%s' and '%s' requested for promise '%s' of type '%s')",
1130                 path, module->interpreter, interpreter, pp->promiser, PromiseGetPromiseType(pp));
1131             free(interpreter);
1132             free(path);
1133             cfPS(ctx, LOG_LEVEL_NOTHING, PROMISE_RESULT_FAIL, pp, &a, NULL);
1134             return PROMISE_RESULT_FAIL;
1135         }
1136         free(interpreter);
1137         free(path);
1138     }
1139 
1140     // TODO: Do validation earlier (cf-promises --full-check)
1141     bool valid = PromiseModule_Validate(module, ctx, pp);
1142 
1143     if (valid)
1144     {
1145         valid = CustomPromise_IsFullyResolved(ctx, pp, module->json);
1146         if ((!valid) && (EvalContextGetPass(ctx) == CF_DONEPASSES - 1))
1147         {
1148             Log(LOG_LEVEL_ERR,
1149                 "%s promise with promiser '%s' has unresolved/unexpanded variables",
1150                 PromiseGetPromiseType(pp),
1151                 pp->promiser);
1152         }
1153     }
1154 
1155     PromiseResult result;
1156     if (valid)
1157     {
1158         result = PromiseModule_Evaluate(module, ctx, pp);
1159     }
1160     else
1161     {
1162         // PromiseModule_Validate() already printed an error
1163         Log(LOG_LEVEL_VERBOSE,
1164             "%s promise with promiser '%s' will be skipped because it failed validation",
1165             PromiseGetPromiseType(pp),
1166             pp->promiser);
1167         cfPS(ctx, LOG_LEVEL_NOTHING, PROMISE_RESULT_FAIL, pp, &a, NULL);
1168         result = PROMISE_RESULT_FAIL; // TODO: Investigate if DENIED is more
1169                                       // appropriate
1170     }
1171 
1172     return result;
1173 }
1174