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 <platform.h>
26 #include <unistd.h>
27 #include <json.h>
28 #include <set.h>                /* StringSet */
29 #include <string_lib.h>
30 #include <known_dirs.h>         /* GetDataDir() */
31 #include <var_expressions.h>    /* VarRef, StringContainsUnresolved() */
32 #include <eval_context.h>       /* EvalContext*() */
33 #include <files_names.h>        /* JoinPaths() */
34 
35 #include <cmdb.h>
36 
37 #define HOST_SPECIFIC_DATA_FILE "host_specific.json"
38 #define HOST_SPECIFIC_DATA_MAX_SIZE (5 * 1024 * 1024) /* maximum size of the host-specific.json file */
39 
40 #define CMDB_NAMESPACE "data"
41 #define CMDB_VARIABLES_TAGS "tags"
42 #define CMDB_VARIABLES_DATA "value"
43 #define CMDB_CLASSES_TAGS "tags"
44 #define CMDB_CLASSES_CLASS_EXPRESSIONS "class_expressions"
45 #define CMDB_CLASSES_REGULAR_EXPRESSIONS "regular_expressions"
46 #define CMDB_COMMENT_KEY "comment"
47 
ReadJsonFile(const char * filename,LogLevel log_level,size_t size_max)48 JsonElement *ReadJsonFile(const char *filename, LogLevel log_level, size_t size_max)
49 {
50     assert(filename != NULL);
51 
52     JsonElement *doc = NULL;
53     JsonParseError err = JsonParseFile(filename, size_max, &doc);
54 
55     if (err == JSON_PARSE_ERROR_NO_SUCH_FILE)
56     {
57         Log(log_level, "Could not open JSON file %s", filename);
58         return NULL;
59     }
60 
61     if (err != JSON_PARSE_OK ||
62         doc == NULL)
63     {
64         Log(log_level, "Could not parse JSON file %s: %s", filename, JsonParseErrorToString(err));
65     }
66 
67     return doc;
68 }
69 
CheckPrimitiveForUnexpandedVars(JsonElement * primitive,ARG_UNUSED void * data)70 static bool CheckPrimitiveForUnexpandedVars(JsonElement *primitive, ARG_UNUSED void *data)
71 {
72     assert(JsonGetElementType(primitive) == JSON_ELEMENT_TYPE_PRIMITIVE);
73 
74     /* Stop the iteration if a variable expression is found. */
75     return (!StringContainsUnresolved(JsonPrimitiveGetAsString(primitive)));
76 }
77 
CheckObjectForUnexpandedVars(JsonElement * object,ARG_UNUSED void * data)78 static bool CheckObjectForUnexpandedVars(JsonElement *object, ARG_UNUSED void *data)
79 {
80     assert(JsonGetType(object) == JSON_TYPE_OBJECT);
81 
82     /* Stop the iteration if a variable expression is found among children
83      * keys. (elements inside the object are checked separately) */
84     JsonIterator iter = JsonIteratorInit(object);
85     while (JsonIteratorHasMore(&iter))
86     {
87         const char *key = JsonIteratorNextKey(&iter);
88         if (StringContainsUnresolved(key))
89         {
90             return false;
91         }
92     }
93     return true;
94 }
95 
GetCMDBVariableRef(const char * key)96 static VarRef *GetCMDBVariableRef(const char *key)
97 {
98     VarRef *ref = VarRefParse(key);
99     if (ref->ns == NULL)
100     {
101         ref->ns = xstrdup(CMDB_NAMESPACE);
102     }
103     else
104     {
105         if (ref->scope == NULL)
106         {
107             Log(LOG_LEVEL_ERR, "Invalid variable specification in CMDB data: '%s'"
108                 " (bundle name has to be specified if namespace is specified)", key);
109             VarRefDestroy(ref);
110             return NULL;
111         }
112     }
113 
114     if (ref->scope == NULL)
115     {
116         ref->scope = xstrdup("variables");
117     }
118     return ref;
119 }
120 
AddCMDBVariable(EvalContext * ctx,const char * key,const VarRef * ref,JsonElement * data,StringSet * tags,const char * comment)121 static bool AddCMDBVariable(EvalContext *ctx, const char *key, const VarRef *ref,
122                             JsonElement *data, StringSet *tags, const char *comment)
123 {
124     assert(ctx != NULL);
125     assert(key != NULL);
126     assert(ref != NULL);
127     assert(data != NULL);
128     assert(tags != NULL);
129 
130     bool ret;
131     if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE)
132     {
133         char *value = JsonPrimitiveToString(data);
134         Log(LOG_LEVEL_VERBOSE, "Installing CMDB variable '%s:%s.%s=%s'",
135             ref->ns, ref->scope, key, value);
136         ret = EvalContextVariablePutTagsSetWithComment(ctx, ref, value, CF_DATA_TYPE_STRING,
137                                                        tags, comment);
138         free(value);
139     }
140     else if ((JsonGetType(data) == JSON_TYPE_ARRAY) &&
141              JsonArrayContainsOnlyPrimitives(data))
142     {
143         // map to slist if the data only has primitives
144         Log(LOG_LEVEL_VERBOSE, "Installing CMDB slist variable '%s:%s.%s'",
145             ref->ns, ref->scope, key);
146         Rlist *data_rlist = RlistFromContainer(data);
147         ret = EvalContextVariablePutTagsSetWithComment(ctx, ref,
148                                                        data_rlist, CF_DATA_TYPE_STRING_LIST,
149                                                        tags, comment);
150         RlistDestroy(data_rlist);
151     }
152     else
153     {
154         // install as a data container
155         Log(LOG_LEVEL_VERBOSE, "Installing CMDB data container variable '%s:%s.%s'",
156             ref->ns, ref->scope, key);
157         ret = EvalContextVariablePutTagsSetWithComment(ctx, ref,
158                                                        data, CF_DATA_TYPE_CONTAINER,
159                                                        tags, comment);
160     }
161     if (!ret)
162     {
163         /* On success, EvalContextVariablePutTagsSet() consumes the tags set,
164          * otherwise, we shall destroy it. */
165         StringSetDestroy(tags);
166     }
167     return ret;
168 }
169 
ReadCMDBVars(EvalContext * ctx,JsonElement * vars)170 static bool ReadCMDBVars(EvalContext *ctx, JsonElement *vars)
171 {
172     assert(vars != NULL);
173 
174     if (JsonGetType(vars) != JSON_TYPE_OBJECT)
175     {
176         Log(LOG_LEVEL_ERR, "Invalid 'vars' CMDB data, must be a JSON object");
177         return false;
178     }
179 
180     if (!JsonWalk(vars, CheckObjectForUnexpandedVars, NULL, CheckPrimitiveForUnexpandedVars, NULL))
181     {
182         Log(LOG_LEVEL_ERR, "Invalid 'vars' CMDB data, cannot contain variable references");
183         return false;
184     }
185 
186     JsonIterator iter = JsonIteratorInit(vars);
187     while (JsonIteratorHasMore(&iter))
188     {
189         const char *key = JsonIteratorNextKey(&iter);
190         JsonElement *data = JsonObjectGet(vars, key);
191 
192         VarRef *ref = GetCMDBVariableRef(key);
193         if (ref == NULL)
194         {
195             continue;
196         }
197 
198         StringSet *tags = StringSetNew();
199         StringSetAdd(tags, xstrdup(CMDB_SOURCE_TAG));
200         bool ret = AddCMDBVariable(ctx, key, ref, data, tags, NULL);
201         VarRefDestroy(ref);
202         if (!ret)
203         {
204             /* Details should have been logged already. */
205             Log(LOG_LEVEL_ERR, "Failed to add CMDB variable '%s'", key);
206         }
207     }
208     return true;
209 }
210 
GetTagsFromJsonTags(const char * item_type,const char * key,const JsonElement * json_tags,const char * default_tag)211 static StringSet *GetTagsFromJsonTags(const char *item_type,
212                                       const char *key,
213                                       const JsonElement *json_tags,
214                                       const char *default_tag)
215 {
216     StringSet *tags = NULL;
217     if (JSON_NOT_NULL(json_tags))
218     {
219         if ((JsonGetType(json_tags) != JSON_TYPE_ARRAY) ||
220             (!JsonArrayContainsOnlyPrimitives((JsonElement*) json_tags)))
221         {
222             Log(LOG_LEVEL_ERR,
223                 "Invalid json_tags information for %s '%s' in CMDB data:"
224                 " must be a JSON array of strings",
225                 item_type, key);
226         }
227         else
228         {
229             tags = JsonArrayToStringSet(json_tags);
230             if (tags == NULL)
231             {
232                 Log(LOG_LEVEL_ERR,
233                     "Invalid json_tags information %s '%s' in CMDB data:"
234                     " must be a JSON array of strings",
235                     item_type, key);
236             }
237         }
238     }
239     if (tags == NULL)
240     {
241         tags = StringSetNew();
242     }
243     StringSetAdd(tags, xstrdup(default_tag));
244 
245     return tags;
246 }
247 
GetCMDBComment(const char * item_type,const char * identifier,const JsonElement * json_object)248 static inline const char *GetCMDBComment(const char *item_type, const char *identifier,
249                                          const JsonElement *json_object)
250 {
251     assert(JsonGetType(json_object) == JSON_TYPE_OBJECT);
252 
253     JsonElement *json_comment = JsonObjectGet(json_object, CMDB_COMMENT_KEY);
254     if (NULL_JSON(json_comment))
255     {
256         return NULL;
257     }
258 
259     if (JsonGetType(json_comment) != JSON_TYPE_STRING)
260     {
261         Log(LOG_LEVEL_ERR,
262             "Invalid type of the 'comment' field for the '%s' %s in CMDB data, must be a string",
263             identifier, item_type);
264         return NULL;
265     }
266 
267     return JsonPrimitiveGetAsString(json_comment);
268 }
269 
270 /** Uses the new format allowing metadata (CFE-3633) */
ReadCMDBVariables(EvalContext * ctx,JsonElement * variables)271 static bool ReadCMDBVariables(EvalContext *ctx, JsonElement *variables)
272 {
273     assert(variables != NULL);
274 
275     if (JsonGetType(variables) != JSON_TYPE_OBJECT)
276     {
277         Log(LOG_LEVEL_ERR, "Invalid 'variables' CMDB data, must be a JSON object");
278         return false;
279     }
280 
281     if (!JsonWalk(variables, CheckObjectForUnexpandedVars, NULL, CheckPrimitiveForUnexpandedVars, NULL))
282     {
283         Log(LOG_LEVEL_ERR, "Invalid 'variables' CMDB data, cannot contain variable references");
284         return false;
285     }
286 
287     JsonIterator iter = JsonIteratorInit(variables);
288     while (JsonIteratorHasMore(&iter))
289     {
290         const char *key = JsonIteratorNextKey(&iter);
291 
292         VarRef *ref = GetCMDBVariableRef(key);
293         if (ref == NULL)
294         {
295             continue;
296         }
297 
298         JsonElement *const var_info = JsonObjectGet(variables, key);
299 
300         JsonElement *data;
301         StringSet *tags;
302         const char *comment = NULL;
303 
304         if (JsonGetType(var_info) == JSON_TYPE_OBJECT)
305         {
306             data = JsonObjectGet(var_info, CMDB_VARIABLES_DATA);
307 
308             if (data == NULL)
309             {
310                 Log(LOG_LEVEL_ERR, "Missing value in '%s' variable specification in CMDB data (value field is required)", key);
311                 VarRefDestroy(ref);
312                 continue;
313             }
314 
315             JsonElement *json_tags = JsonObjectGet(var_info, CMDB_VARIABLES_TAGS);
316             tags = GetTagsFromJsonTags("variable", key, json_tags, CMDB_SOURCE_TAG);
317             comment = GetCMDBComment("variable", key, var_info);
318         }
319         else
320         {
321             // Just a bare value, like in "vars", no metadata
322             data = var_info;
323             tags = GetTagsFromJsonTags("variable", key, NULL, CMDB_SOURCE_TAG);
324         }
325 
326         assert(tags != NULL);
327         assert(data != NULL);
328 
329         bool ret = AddCMDBVariable(ctx, key, ref, data, tags, comment);
330         VarRefDestroy(ref);
331         if (!ret)
332         {
333             /* Details should have been logged already. */
334             Log(LOG_LEVEL_ERR, "Failed to add CMDB variable '%s'", key);
335         }
336     }
337     return true;
338 }
339 
AddCMDBClass(EvalContext * ctx,const char * key,StringSet * tags,const char * comment)340 static bool AddCMDBClass(EvalContext *ctx, const char *key, StringSet *tags, const char *comment)
341 {
342     assert(ctx != NULL);
343     assert(key != NULL);
344     assert(tags != NULL);
345 
346     bool ret;
347     Log(LOG_LEVEL_VERBOSE, "Installing CMDB class '%s'", key);
348 
349     if (strchr(key, ':') != NULL)
350     {
351         char *ns_class_name = xstrdup(key);
352         char *sep = strchr(ns_class_name, ':');
353         *sep = '\0';
354         key = sep + 1;
355         ret = EvalContextClassPutSoftNSTagsSetWithComment(ctx, ns_class_name, key,
356                                                           CONTEXT_SCOPE_NAMESPACE, tags, comment);
357         free(ns_class_name);
358     }
359     else
360     {
361         ret = EvalContextClassPutSoftNSTagsSetWithComment(ctx, CMDB_NAMESPACE, key,
362                                                           CONTEXT_SCOPE_NAMESPACE, tags, comment);
363     }
364     if (!ret)
365     {
366         /* On success, EvalContextClassPutSoftNSTagsSetWithComment() consumes
367          * the tags set, otherwise, we shall destroy it. */
368         StringSetDestroy(tags);
369     }
370 
371     return ret;
372 }
373 
ReadCMDBClasses(EvalContext * ctx,JsonElement * classes)374 static bool ReadCMDBClasses(EvalContext *ctx, JsonElement *classes)
375 {
376     assert(classes != NULL);
377 
378     if (JsonGetType(classes) != JSON_TYPE_OBJECT)
379     {
380         Log(LOG_LEVEL_ERR, "Invalid 'classes' CMDB data, must be a JSON object");
381         return false;
382     }
383 
384     if (!JsonWalk(classes, CheckObjectForUnexpandedVars, NULL, CheckPrimitiveForUnexpandedVars, NULL))
385     {
386         Log(LOG_LEVEL_ERR, "Invalid 'classes' CMDB data, cannot contain variable references");
387         return false;
388     }
389 
390     JsonIterator iter = JsonIteratorInit(classes);
391     while (JsonIteratorHasMore(&iter))
392     {
393         const char *key = JsonIteratorNextKey(&iter);
394         JsonElement *data = JsonObjectGet(classes, key);
395         if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE)
396         {
397             const char *expr = JsonPrimitiveGetAsString(data);
398             if (!StringEqual(expr, "any::"))
399             {
400                 Log(LOG_LEVEL_ERR,
401                     "Invalid class specification '%s' in CMDB data, only \"any::\" allowed", expr);
402                 continue;
403             }
404 
405             StringSet *default_tags = StringSetNew();
406             StringSetAdd(default_tags, xstrdup(CMDB_SOURCE_TAG));
407             bool ret = AddCMDBClass(ctx, key, default_tags, NULL);
408             if (!ret)
409             {
410                 /* Details should have been logged already. */
411                 Log(LOG_LEVEL_ERR, "Failed to add CMDB class '%s'", key);
412             }
413         }
414         else if (JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY &&
415                  JsonArrayContainsOnlyPrimitives(data))
416         {
417             if ((JsonLength(data) != 1) ||
418                 (!StringEqual(JsonPrimitiveGetAsString(JsonArrayGet(data, 0)), "any::")))
419             {
420                 Log(LOG_LEVEL_ERR,
421                     "Invalid class specification '%s' in CMDB data, only '[\"any::\"]' allowed",
422                     JsonPrimitiveGetAsString(JsonArrayGet(data, 0)));
423                 continue;
424             }
425             StringSet *default_tags = StringSetNew();
426             StringSetAdd(default_tags, xstrdup(CMDB_SOURCE_TAG));
427             bool ret = AddCMDBClass(ctx, key, default_tags, NULL);
428             if (!ret)
429             {
430                 /* Details should have been logged already. */
431                 Log(LOG_LEVEL_ERR, "Failed to add CMDB class '%s'", key);
432             }
433         }
434         else if (JsonGetContainerType(data) == JSON_CONTAINER_TYPE_OBJECT)
435         {
436             const JsonElement *class_exprs = JsonObjectGet(data, CMDB_CLASSES_CLASS_EXPRESSIONS);
437             const JsonElement *reg_exprs = JsonObjectGet(data, CMDB_CLASSES_REGULAR_EXPRESSIONS);
438             const JsonElement *json_tags = JsonObjectGet(data, CMDB_CLASSES_TAGS);
439 
440             if (JSON_NOT_NULL(class_exprs) &&
441                 (JsonGetType(class_exprs) != JSON_TYPE_ARRAY ||
442                  JsonLength(class_exprs) > 1 ||
443                  (JsonLength(class_exprs) == 1 &&
444                   !StringEqual(JsonPrimitiveGetAsString(JsonArrayGet(class_exprs, 0)), "any::"))))
445             {
446                 Log(LOG_LEVEL_ERR,
447                     "Invalid class expression rules for class '%s' in CMDB data,"
448                     " only '[]' or '[\"any::\"]' allowed", key);
449                 continue;
450             }
451             if (JSON_NOT_NULL(reg_exprs) &&
452                 (JsonGetType(reg_exprs) != JSON_TYPE_ARRAY ||
453                  JsonLength(reg_exprs) > 1 ||
454                  (JsonLength(reg_exprs) == 1 &&
455                   !StringEqual(JsonPrimitiveGetAsString(JsonArrayGet(reg_exprs, 0)), "any"))))
456             {
457                 Log(LOG_LEVEL_ERR,
458                     "Invalid regular expression rules for class '%s' in CMDB data,"
459                     " only '[]' or '[\"any\"]' allowed", key);
460                 continue;
461             }
462 
463             StringSet *tags = GetTagsFromJsonTags("class", key, json_tags, CMDB_SOURCE_TAG);
464             const char *comment = GetCMDBComment("class", key, data);
465             bool ret = AddCMDBClass(ctx, key, tags, comment);
466             if (!ret)
467             {
468                 /* Details should have been logged already. */
469                 Log(LOG_LEVEL_ERR, "Failed to add CMDB class '%s'", key);
470             }
471         }
472         else
473         {
474             Log(LOG_LEVEL_ERR, "Invalid CMDB class data for class '%s'", key);
475         }
476     }
477     return true;
478 }
479 
LoadCMDBData(EvalContext * ctx)480 bool LoadCMDBData(EvalContext *ctx)
481 {
482     char file_path[PATH_MAX] = {0};
483     strncpy(file_path, GetDataDir(), sizeof(file_path));
484     JoinPaths(file_path, sizeof(file_path), HOST_SPECIFIC_DATA_FILE);
485     if (access(file_path, F_OK) != 0)
486     {
487         Log(LOG_LEVEL_VERBOSE, "No host-specific JSON data available at '%s'", file_path);
488         return true;            /* not an error */
489     }
490     if (access(file_path, R_OK) != 0)
491     {
492         Log(LOG_LEVEL_ERR, "Cannot read host-spefic JSON data from '%s'",
493             file_path);
494         return false;
495     }
496 
497     JsonElement *data = ReadJsonFile(file_path, LOG_LEVEL_ERR, HOST_SPECIFIC_DATA_MAX_SIZE);
498     if (data == NULL)
499     {
500         /* Details are logged by ReadJsonFile() */
501         return false;
502     }
503     if (JsonGetType(data) != JSON_TYPE_OBJECT)
504     {
505         Log(LOG_LEVEL_ERR, "Invalid CMDB contents in '%s', must be a JSON object", file_path);
506         JsonDestroy(data);
507         return false;
508     }
509 
510     Log(LOG_LEVEL_VERBOSE, "Loaded CMDB data file '%s', installing contents", file_path);
511 
512     JsonIterator iter = JsonIteratorInit(data);
513     while (JsonIteratorHasMore(&iter))
514     {
515         const char *key = JsonIteratorNextKey(&iter);
516         /* Only vars and classes allowed in CMDB data */
517         if (!IsStrIn(key, (const char*[4]){"vars", "classes", "variables", NULL}))
518         {
519             Log(LOG_LEVEL_WARNING, "Invalid key '%s' in the CMDB data file '%s', skipping it",
520                 key, file_path);
521         }
522     }
523 
524     bool success = true;
525     JsonElement *vars = JsonObjectGet(data, "vars");
526     if (JSON_NOT_NULL(vars) && !ReadCMDBVars(ctx, vars))
527     {
528         success = false;
529     }
530     /* Uses the new format allowing metadata (CFE-3633) */
531     JsonElement *variables = JsonObjectGet(data, "variables");
532     if (JSON_NOT_NULL(variables) && !ReadCMDBVariables(ctx, variables))
533     {
534         success = false;
535     }
536     JsonElement *classes = JsonObjectGet(data, "classes");
537     if (JSON_NOT_NULL(classes) && !ReadCMDBClasses(ctx, classes))
538     {
539         success = false;
540     }
541 
542     JsonDestroy(data);
543     return success;
544 }
545