master
c 541 lines 19.7 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "dyncfg-internals.h"
4 #include "dyncfg.h"
5
6 struct dyncfg_call {
7 ND_UUID transaction;
8 BUFFER *payload;
9 const char *function;
10 const char *id;
11 const char *add_name;
12 const char *source;
13 DYNCFG_CMDS cmd;
14 rrd_function_result_callback_t result_cb;
15 void *result_cb_data;
16 bool from_dyncfg_echo;
17 };
18
19 // ----------------------------------------------------------------------------
20
21 ENUM_STR_MAP_DEFINE(DYNCFG_CMDS) = {
22 { DYNCFG_CMD_GET, "get" },
23 { DYNCFG_CMD_SCHEMA, "schema" },
24 { DYNCFG_CMD_UPDATE, "update" },
25 { DYNCFG_CMD_ADD, "add" },
26 { DYNCFG_CMD_TEST, "test" },
27 { DYNCFG_CMD_REMOVE, "remove" },
28 { DYNCFG_CMD_ENABLE, "enable" },
29 { DYNCFG_CMD_DISABLE, "disable" },
30 { DYNCFG_CMD_RESTART, "restart" },
31 { DYNCFG_CMD_USERCONFIG, "userconfig" },
32
33 // terminator
34 { 0, NULL }
35 };
36
37 ENUM_STR_DEFINE_FUNCTIONS(DYNCFG_CMDS, DYNCFG_CMD_NONE, "none");
38
39 static void dyncfg_log_user_action(DYNCFG *df, struct dyncfg_call *dc) {
40 if(dc->cmd == DYNCFG_CMD_USERCONFIG || dc->cmd == DYNCFG_CMD_GET || dc->cmd == DYNCFG_CMD_SCHEMA)
41 return;
42
43 const char *type;
44 switch(df->type) {
45 default:
46 case DYNCFG_TYPE_SINGLE:
47 type = "on";
48 break;
49
50 case DYNCFG_TYPE_TEMPLATE:
51 type = "on template";
52 break;
53 case DYNCFG_TYPE_JOB:
54 type = "on job";
55 break;
56 }
57
58 USER_AUTH req;
59 if(!user_auth_from_source(dc->source, &req)) {
60 ND_LOG_STACK lgs[] = {
61 ND_LOG_FIELD_TXT(NDF_MODULE, "DYNCFG"),
62 ND_LOG_FIELD_STR(NDF_NIDL_NODE, localhost->hostname),
63 ND_LOG_FIELD_TXT(NDF_REQUEST, dc->function),
64 ND_LOG_FIELD_UUID(NDF_TRANSACTION_ID, &dc->transaction.uuid),
65 ND_LOG_FIELD_UUID(NDF_MESSAGE_ID, &dyncfg_user_action_msgid),
66 ND_LOG_FIELD_END(),
67 };
68 ND_LOG_STACK_PUSH(lgs);
69
70 nd_log(NDLS_DAEMON, NDLP_NOTICE,
71 "DYNCFG USER ACTION '%s' %s%s%s '%s' from source: %s",
72 DYNCFG_CMDS_2str(dc->cmd),
73 dc->add_name ? dc->add_name : "",
74 dc->add_name ? " " : "",
75 type, dc->id, dc->source);
76
77 return;
78 }
79
80 char access_str[1024];
81 http_access2txt(access_str, sizeof(access_str), " ", req.access);
82
83 ND_LOG_STACK lgs[] = {
84 ND_LOG_FIELD_TXT(NDF_MODULE, "DYNCFG"),
85 ND_LOG_FIELD_STR(NDF_NIDL_NODE, localhost->hostname),
86 ND_LOG_FIELD_TXT(NDF_REQUEST, dc->function),
87 ND_LOG_FIELD_UUID(NDF_TRANSACTION_ID, &dc->transaction.uuid),
88 ND_LOG_FIELD_UUID(NDF_MESSAGE_ID, &dyncfg_user_action_msgid),
89
90 ND_LOG_FIELD_UUID(NDF_ACCOUNT_ID, &req.cloud_account_id.uuid),
91 ND_LOG_FIELD_TXT(NDF_SRC_IP, req.client_ip),
92 ND_LOG_FIELD_TXT(NDF_SRC_FORWARDED_FOR, req.forwarded_for),
93 ND_LOG_FIELD_TXT(NDF_USER_NAME, req.client_name),
94 ND_LOG_FIELD_TXT(NDF_USER_ROLE, http_id2user_role(req.user_role)),
95 ND_LOG_FIELD_CB(NDF_USER_ACCESS, log_cb_http_access_to_hex, &req.access),
96 ND_LOG_FIELD_END(),
97 };
98 ND_LOG_STACK_PUSH(lgs);
99
100 nd_log(NDLS_DAEMON, NDLP_NOTICE,
101 "DYNCFG USER ACTION '%s' %s%s%s '%s' by user '%s', IP '%s'",
102 DYNCFG_CMDS_2str(dc->cmd),
103 dc->add_name ? dc->add_name : "",
104 dc->add_name ? " " : "",
105 type, dc->id, req.client_name,
106 req.forwarded_for[0] ? req.forwarded_for : req.client_ip);
107 }
108
109 // ----------------------------------------------------------------------------
110 // we intercept the config function calls of the plugin
111
112 static void dyncfg_function_intercept_job_successfully_added(DYNCFG *df_template, int code, struct dyncfg_call *dc) {
113 size_t id_size = strlen(dc->id) + strlen(dc->add_name) + 2;
114 CLEAN_CHAR_P *id = mallocz(id_size);
115 snprintfz(id, id_size, "%s:%s", dc->id, dc->add_name);
116
117 RRDHOST *host = dyncfg_rrdhost(df_template);
118 if(!host) {
119 nd_log(NDLS_DAEMON, NDLP_ERR,
120 "DYNCFG: cannot add job '%s' because host is missing", id);
121 }
122 else {
123 const DICTIONARY_ITEM *item = dyncfg_add_internal(
124 host,
125 id,
126 string2str(df_template->path),
127 dyncfg_status_from_successful_response(code),
128 DYNCFG_TYPE_JOB,
129 DYNCFG_SOURCE_TYPE_DYNCFG,
130 dc->source,
131 (df_template->cmds & ~DYNCFG_CMD_ADD) | DYNCFG_CMD_GET | DYNCFG_CMD_UPDATE | DYNCFG_CMD_TEST |
132 DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE | DYNCFG_CMD_REMOVE,
133 0,
134 0,
135 df_template->sync,
136 df_template->view_access,
137 df_template->edit_access,
138 df_template->execute_cb,
139 df_template->execute_cb_data,
140 false);
141
142 // adding does not create df->dyncfg
143 // we have to do it here
144
145 DYNCFG *df = dictionary_acquired_item_value(item);
146 SWAP(df->dyncfg.payload, dc->payload);
147 dyncfg_set_dyncfg_source_from_txt(df, dc->source);
148 df->dyncfg.user_disabled = false;
149 df->dyncfg.source_type = DYNCFG_SOURCE_TYPE_DYNCFG;
150 df->dyncfg.status = dyncfg_status_from_successful_response(code);
151
152 dyncfg_file_save(id, df); // updates also the df->dyncfg timestamps
153 dyncfg_update_status_on_successful_add_or_update(df, code);
154
155 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
156 }
157 }
158
159 static void dyncfg_function_intercept_job_successfully_updated(DYNCFG *df, int code, struct dyncfg_call *dc) {
160 df->dyncfg.status = dyncfg_status_from_successful_response(code);
161 df->dyncfg.source_type = DYNCFG_SOURCE_TYPE_DYNCFG;
162 SWAP(df->dyncfg.payload, dc->payload);
163 dyncfg_set_dyncfg_source_from_txt(df, dc->source);
164
165 dyncfg_update_status_on_successful_add_or_update(df, code);
166 df->cmds = dyncfg_sanitize_cmds(df->type, df->current.source_type, df->cmds);
167 }
168
169 void dyncfg_function_intercept_result_cb(BUFFER *wb, int code, void *result_cb_data) {
170 struct dyncfg_call *dc = result_cb_data;
171
172 bool called_from_dyncfg_echo = dc->from_dyncfg_echo;
173
174 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item_advanced(dyncfg_globals.nodes, dc->id, -1);
175 if(item) {
176 DYNCFG *df = dictionary_acquired_item_value(item);
177 bool old_user_disabled = df->dyncfg.user_disabled;
178 bool save_required = false;
179
180 if (!called_from_dyncfg_echo) {
181 // the command was sent by a user
182
183 if (DYNCFG_RESP_SUCCESS(code)) {
184 if (dc->cmd == DYNCFG_CMD_ADD) {
185 dyncfg_function_intercept_job_successfully_added(df, code, dc);
186 } else if (dc->cmd == DYNCFG_CMD_UPDATE) {
187 dyncfg_function_intercept_job_successfully_updated(df, code, dc);
188 save_required = true;
189 }
190 else if (dc->cmd == DYNCFG_CMD_ENABLE) {
191 df->dyncfg.user_disabled = false;
192 }
193 else if (dc->cmd == DYNCFG_CMD_DISABLE) {
194 df->dyncfg.user_disabled = true;
195 }
196 else if (dc->cmd == DYNCFG_CMD_REMOVE) {
197 dyncfg_file_delete(dc->id);
198 dictionary_del(dyncfg_globals.nodes, dc->id);
199 }
200
201 if (save_required || old_user_disabled != df->dyncfg.user_disabled)
202 dyncfg_file_save(dc->id, df);
203
204 dyncfg_log_user_action(df, dc);
205 }
206 else
207 nd_log(NDLS_DAEMON, NDLP_ERR,
208 "DYNCFG: plugin returned code %d to user initiated call: %s", code, dc->function);
209 }
210 else {
211 // the command was sent by dyncfg
212 // these are handled by the echo callback, we don't need to do anything here
213 ;
214 }
215
216 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
217 }
218
219 if(dc->result_cb)
220 dc->result_cb(wb, code, dc->result_cb_data);
221
222 buffer_free(dc->payload);
223 freez((void *)dc->function);
224 freez((void *)dc->id);
225 freez((void *)dc->source);
226 freez((void *)dc->add_name);
227 freez(dc);
228 }
229
230 // ----------------------------------------------------------------------------
231
232 static void dyncfg_apply_action_on_all_template_jobs(struct rrd_function_execute *rfe, const char *template_id, DYNCFG_CMDS c) {
233 STRING *template = string_strdupz(template_id);
234 DYNCFG *df;
235
236 size_t all = 0, done = 0;
237 dfe_start_read(dyncfg_globals.nodes, df) {
238 if(df->template == template && df->type == DYNCFG_TYPE_JOB)
239 all++;
240 }
241 dfe_done(df);
242
243 if(rfe->progress.cb)
244 rfe->progress.cb(rfe->transaction, rfe->progress.data, done, all);
245
246 dfe_start_reentrant(dyncfg_globals.nodes, df) {
247 if(df->template == template && df->type == DYNCFG_TYPE_JOB) {
248 DYNCFG_CMDS cmd_to_send_to_plugin = c;
249
250 if(c == DYNCFG_CMD_ENABLE)
251 cmd_to_send_to_plugin = df->dyncfg.user_disabled ? DYNCFG_CMD_DISABLE : DYNCFG_CMD_ENABLE;
252 else if(c == DYNCFG_CMD_DISABLE)
253 cmd_to_send_to_plugin = DYNCFG_CMD_DISABLE;
254
255 dyncfg_echo(df_dfe.item, df, df_dfe.name, cmd_to_send_to_plugin);
256
257 if(rfe->progress.cb)
258 rfe->progress.cb(rfe->transaction, rfe->progress.data, ++done, all);
259 }
260 }
261 dfe_done(df);
262
263 string_freez(template);
264 }
265
266 // ----------------------------------------------------------------------------
267 // the callback for all config functions
268
269 static int dyncfg_intercept_early_error(struct rrd_function_execute *rfe, int rc, const char *msg) {
270 rc = dyncfg_default_response(rfe->result.wb, rc, msg);
271
272 if(rfe->result.cb)
273 rfe->result.cb(rfe->result.wb, rc, rfe->result.data);
274
275 return rc;
276 }
277
278 const DICTIONARY_ITEM *dyncfg_get_template_of_new_job(const char *job_id) {
279 CLEAN_CHAR_P *id_copy = strdupz(job_id);
280
281 char *colon = strrchr(id_copy, ':');
282 if(!colon) return NULL;
283
284 *colon = '\0';
285 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id_copy);
286 if(!item) return NULL;
287
288 DYNCFG *df = dictionary_acquired_item_value(item);
289 if(df->type != DYNCFG_TYPE_TEMPLATE) {
290 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
291 return NULL;
292 }
293
294 return item;
295 }
296
297 int dyncfg_function_intercept_cb(struct rrd_function_execute *rfe, void *data __maybe_unused) {
298
299 // IMPORTANT: this function MUST call the result_cb even on failures
300
301 bool called_from_dyncfg_echo = rrd_function_has_this_original_result_callback(rfe->transaction, dyncfg_echo_cb);
302 bool has_payload = rfe->payload && buffer_strlen(rfe->payload) ? true : false;
303 bool make_the_call_to_plugin = true;
304
305 int rc = HTTP_RESP_INTERNAL_SERVER_ERROR;
306 DYNCFG_CMDS cmd;
307 const DICTIONARY_ITEM *item = NULL;
308
309 CLEAN_CHAR_P *buf = strdupz(rfe->function);
310
311 char *words[20];
312 size_t num_words = quoted_strings_splitter_whitespace(buf, words, 20);
313
314 size_t i = 0;
315 char *config = get_word(words, num_words, i++);
316 char *id = get_word(words, num_words, i++);
317 char *cmd_str = get_word(words, num_words, i++);
318 char *add_name = get_word(words, num_words, i++);
319
320 if(!config || !*config || strcmp(config, PLUGINSD_FUNCTION_CONFIG) != 0)
321 return dyncfg_intercept_early_error(
322 rfe, HTTP_RESP_BAD_REQUEST,
323 "dyncfg functions intercept: this is not a dyncfg request");
324
325 cmd = dyncfg_cmds2id(cmd_str);
326 if(cmd == DYNCFG_CMD_NONE)
327 return dyncfg_intercept_early_error(
328 rfe, HTTP_RESP_BAD_REQUEST,
329 "dyncfg functions intercept: invalid command received");
330
331 if(cmd == DYNCFG_CMD_ADD || cmd == DYNCFG_CMD_TEST || cmd == DYNCFG_CMD_USERCONFIG) {
332 if(cmd == DYNCFG_CMD_TEST && (!add_name || !*add_name)) {
333 // backwards compatibility for TEST without a name
334 char *colon = strrchr(id, ':');
335 if(colon) {
336 *colon = '\0';
337 add_name = ++colon;
338 }
339 else
340 add_name = "test";
341 }
342
343 if(!add_name || !*add_name)
344 return dyncfg_intercept_early_error(
345 rfe, HTTP_RESP_BAD_REQUEST,
346 "dyncfg functions intercept: this action requires a name");
347
348 if(!called_from_dyncfg_echo) {
349 size_t nid_size = strlen(id) + strlen(add_name) + 2;
350 CLEAN_CHAR_P *nid = mallocz(nid_size);
351 snprintfz(nid, nid_size, "%s:%s", id, add_name);
352
353 if (cmd == DYNCFG_CMD_ADD && dictionary_get(dyncfg_globals.nodes, nid))
354 return dyncfg_intercept_early_error(
355 rfe, HTTP_RESP_BAD_REQUEST,
356 "dyncfg functions intercept: a configuration with this name already exists");
357 }
358 }
359
360 if((cmd == DYNCFG_CMD_ADD || cmd == DYNCFG_CMD_UPDATE || cmd == DYNCFG_CMD_TEST || cmd == DYNCFG_CMD_USERCONFIG) && !has_payload)
361 return dyncfg_intercept_early_error(
362 rfe, HTTP_RESP_BAD_REQUEST,
363 "dyncfg functions intercept: this action requires a payload");
364
365 if((cmd != DYNCFG_CMD_ADD && cmd != DYNCFG_CMD_UPDATE && cmd != DYNCFG_CMD_TEST && cmd != DYNCFG_CMD_USERCONFIG) && has_payload)
366 return dyncfg_intercept_early_error(
367 rfe, HTTP_RESP_BAD_REQUEST,
368 "dyncfg functions intercept: this action does not require a payload");
369
370 item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id);
371 if(!item) {
372 if(cmd == DYNCFG_CMD_TEST || cmd == DYNCFG_CMD_USERCONFIG) {
373 // this may be a test on a new job
374 item = dyncfg_get_template_of_new_job(id);
375 }
376
377 if(!item)
378 return dyncfg_intercept_early_error(
379 rfe, HTTP_RESP_NOT_FOUND,
380 "dyncfg functions intercept: id is not found");
381 }
382
383 DYNCFG *df = dictionary_acquired_item_value(item);
384
385 // 1. check the permissions of the request
386
387 switch(cmd) {
388 case DYNCFG_CMD_GET:
389 case DYNCFG_CMD_SCHEMA:
390 case DYNCFG_CMD_USERCONFIG:
391 if(!http_access_user_has_enough_access_level_for_endpoint(rfe->user_access, df->view_access)) {
392 make_the_call_to_plugin = false;
393 rc = dyncfg_default_response(
394 rfe->result.wb, HTTP_RESP_FORBIDDEN,
395 "dyncfg: you don't have enough view permissions to execute this command");
396 }
397 break;
398
399 case DYNCFG_CMD_ENABLE:
400 case DYNCFG_CMD_DISABLE:
401 case DYNCFG_CMD_ADD:
402 case DYNCFG_CMD_TEST:
403 case DYNCFG_CMD_UPDATE:
404 case DYNCFG_CMD_REMOVE:
405 case DYNCFG_CMD_RESTART:
406 if(!http_access_user_has_enough_access_level_for_endpoint(rfe->user_access, df->edit_access)) {
407 make_the_call_to_plugin = false;
408 rc = dyncfg_default_response(
409 rfe->result.wb, HTTP_RESP_FORBIDDEN,
410 "dyncfg: you don't have enough edit permissions to execute this command");
411 }
412 break;
413
414 default: {
415 make_the_call_to_plugin = false;
416 rc = dyncfg_default_response(
417 rfe->result.wb, HTTP_RESP_INTERNAL_SERVER_ERROR,
418 "dyncfg: permissions for this command are not set");
419 }
420 break;
421 }
422
423 // 2. validate the request parameters
424
425 if(make_the_call_to_plugin) {
426 if (!(df->cmds & cmd)) {
427 nd_log(NDLS_DAEMON, NDLP_ERR,
428 "DYNCFG: this command is not supported by the configuration node: %s", rfe->function);
429
430 make_the_call_to_plugin = false;
431 rc = dyncfg_default_response(
432 rfe->result.wb, HTTP_RESP_BAD_REQUEST,
433 "dyncfg functions intercept: this command is not supported by this configuration node");
434 }
435 else if (cmd == DYNCFG_CMD_ADD) {
436 if (df->type != DYNCFG_TYPE_TEMPLATE) {
437 make_the_call_to_plugin = false;
438 rc = dyncfg_default_response(
439 rfe->result.wb, HTTP_RESP_BAD_REQUEST,
440 "dyncfg functions intercept: add command is only allowed in templates");
441
442 nd_log(NDLS_DAEMON, NDLP_ERR,
443 "DYNCFG: add command can only be applied on templates, not %s: %s",
444 dyncfg_id2type(df->type), rfe->function);
445 }
446 }
447 else if (
448 cmd == DYNCFG_CMD_ENABLE && df->type == DYNCFG_TYPE_JOB &&
449 dyncfg_is_user_disabled(string2str(df->template))) {
450 nd_log(NDLS_DAEMON, NDLP_ERR,
451 "DYNCFG: cannot enable a job of a disabled template: %s",
452 rfe->function);
453
454 make_the_call_to_plugin = false;
455 rc = dyncfg_default_response(
456 rfe->result.wb, HTTP_RESP_BAD_REQUEST,
457 "dyncfg functions intercept: this job belongs to disabled template");
458 }
459 }
460
461 // 3. check if it is one of the commands we should execute
462
463 if(make_the_call_to_plugin) {
464 if (cmd & (DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE | DYNCFG_CMD_RESTART) && df->type == DYNCFG_TYPE_TEMPLATE) {
465 if (!called_from_dyncfg_echo) {
466 bool old_user_disabled = df->dyncfg.user_disabled;
467 if (cmd == DYNCFG_CMD_ENABLE)
468 df->dyncfg.user_disabled = false;
469 else if (cmd == DYNCFG_CMD_DISABLE)
470 df->dyncfg.user_disabled = true;
471
472 if (df->dyncfg.user_disabled != old_user_disabled)
473 dyncfg_file_save(id, df);
474
475 // log it
476 {
477 struct dyncfg_call dc = {
478 .function = rfe->function,
479 .id = id,
480 .source = rfe->source,
481 .add_name = add_name,
482 .cmd = cmd,
483 .result_cb = NULL,
484 .result_cb_data = NULL,
485 .payload = rfe->payload,
486 .from_dyncfg_echo = called_from_dyncfg_echo,
487 };
488 uuid_copy(dc.transaction.uuid, *rfe->transaction);
489
490 dyncfg_log_user_action(df, &dc);
491 }
492 }
493
494 dyncfg_apply_action_on_all_template_jobs(rfe, id, cmd);
495
496 rc = dyncfg_default_response(rfe->result.wb, HTTP_RESP_OK, "applied to all template job");
497 make_the_call_to_plugin = false;
498 }
499 else if (cmd == DYNCFG_CMD_SCHEMA) {
500 bool loaded = false;
501 if (df->type == DYNCFG_TYPE_JOB) {
502 if (df->template)
503 loaded = dyncfg_get_schema(string2str(df->template), rfe->result.wb);
504 } else
505 loaded = dyncfg_get_schema(id, rfe->result.wb);
506
507 if (loaded) {
508 rfe->result.wb->content_type = CT_APPLICATION_JSON;
509 rfe->result.wb->expires = now_realtime_sec();
510 rc = HTTP_RESP_OK;
511 make_the_call_to_plugin = false;
512 }
513 }
514 }
515
516 // 4. execute the command
517
518 if(make_the_call_to_plugin) {
519 struct dyncfg_call *dc = callocz(1, sizeof(*dc));
520 uuid_copy(dc->transaction.uuid, *rfe->transaction);
521 dc->function = strdupz(rfe->function);
522 dc->id = strdupz(id);
523 dc->source = rfe->source ? strdupz(rfe->source) : NULL;
524 dc->add_name = (add_name) ? strdupz(add_name) : NULL;
525 dc->cmd = cmd;
526 dc->result_cb = rfe->result.cb;
527 dc->result_cb_data = rfe->result.data;
528 dc->payload = buffer_dup(rfe->payload);
529 dc->from_dyncfg_echo = called_from_dyncfg_echo;
530
531 rfe->result.cb = dyncfg_function_intercept_result_cb;
532 rfe->result.data = dc;
533
534 rc = df->execute_cb(rfe, df->execute_cb_data);
535 }
536 else if(rfe->result.cb)
537 rfe->result.cb(rfe->result.wb, rc, rfe->result.data);
538
539 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
540 return rc;
541 }