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