// SPDX-License-Identifier: GPL-3.0-or-later #include "dyncfg-internals.h" #include "dyncfg.h" // ---------------------------------------------------------------------------- // we intercept the config function calls of the plugin struct dyncfg_call { BUFFER *payload; char *function; char *id; char *add_name; char *source; DYNCFG_CMDS cmd; rrd_function_result_callback_t result_cb; void *result_cb_data; bool from_dyncfg_echo; }; static void dyncfg_function_intercept_job_successfully_added(DYNCFG *df_template, int code, struct dyncfg_call *dc) { char id[strlen(dc->id) + 1 + strlen(dc->add_name) + 1]; snprintfz(id, sizeof(id), "%s:%s", dc->id, dc->add_name); RRDHOST *host = dyncfg_rrdhost(df_template); if(!host) { nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: cannot add job '%s' because host is missing", id); } else { const DICTIONARY_ITEM *item = dyncfg_add_internal( host, id, string2str(df_template->path), dyncfg_status_from_successful_response(code), DYNCFG_TYPE_JOB, DYNCFG_SOURCE_TYPE_DYNCFG, dc->source, (df_template->cmds & ~DYNCFG_CMD_ADD) | DYNCFG_CMD_GET | DYNCFG_CMD_UPDATE | DYNCFG_CMD_TEST | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE | DYNCFG_CMD_REMOVE, 0, 0, df_template->sync, df_template->view_access, df_template->edit_access, df_template->execute_cb, df_template->execute_cb_data, false); // adding does not create df->dyncfg // we have to do it here DYNCFG *df = dictionary_acquired_item_value(item); SWAP(df->dyncfg.payload, dc->payload); dyncfg_set_dyncfg_source_from_txt(df, dc->source); df->dyncfg.user_disabled = false; df->dyncfg.source_type = DYNCFG_SOURCE_TYPE_DYNCFG; df->dyncfg.status = dyncfg_status_from_successful_response(code); dyncfg_file_save(id, df); // updates also the df->dyncfg timestamps dyncfg_update_status_on_successful_add_or_update(df, code); dictionary_acquired_item_release(dyncfg_globals.nodes, item); } } static void dyncfg_function_intercept_job_successfully_updated(DYNCFG *df, int code, struct dyncfg_call *dc) { df->dyncfg.status = dyncfg_status_from_successful_response(code); df->dyncfg.source_type = DYNCFG_SOURCE_TYPE_DYNCFG; SWAP(df->dyncfg.payload, dc->payload); dyncfg_set_dyncfg_source_from_txt(df, dc->source); dyncfg_update_status_on_successful_add_or_update(df, code); } void dyncfg_function_intercept_result_cb(BUFFER *wb, int code, void *result_cb_data) { struct dyncfg_call *dc = result_cb_data; bool called_from_dyncfg_echo = dc->from_dyncfg_echo; const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item_advanced(dyncfg_globals.nodes, dc->id, -1); if(item) { DYNCFG *df = dictionary_acquired_item_value(item); bool old_user_disabled = df->dyncfg.user_disabled; bool save_required = false; if (!called_from_dyncfg_echo) { // the command was sent by a user if (DYNCFG_RESP_SUCCESS(code)) { if (dc->cmd == DYNCFG_CMD_ADD) { dyncfg_function_intercept_job_successfully_added(df, code, dc); } else if (dc->cmd == DYNCFG_CMD_UPDATE) { dyncfg_function_intercept_job_successfully_updated(df, code, dc); save_required = true; } else if (dc->cmd == DYNCFG_CMD_ENABLE) { df->dyncfg.user_disabled = false; } else if (dc->cmd == DYNCFG_CMD_DISABLE) { df->dyncfg.user_disabled = true; } else if (dc->cmd == DYNCFG_CMD_REMOVE) { dyncfg_file_delete(dc->id); dictionary_del(dyncfg_globals.nodes, dc->id); } if (save_required || old_user_disabled != df->dyncfg.user_disabled) dyncfg_file_save(dc->id, df); } else nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: plugin returned code %d to user initiated call: %s", code, dc->function); } else { // the command was sent by dyncfg // these are handled by the echo callback, we don't need to do anything here ; } dictionary_acquired_item_release(dyncfg_globals.nodes, item); } if(dc->result_cb) dc->result_cb(wb, code, dc->result_cb_data); buffer_free(dc->payload); freez(dc->function); freez(dc->id); freez(dc->source); freez(dc->add_name); freez(dc); } // ---------------------------------------------------------------------------- static void dyncfg_apply_action_on_all_template_jobs(struct rrd_function_execute *rfe, const char *template_id, DYNCFG_CMDS c) { STRING *template = string_strdupz(template_id); DYNCFG *df; size_t all = 0, done = 0; dfe_start_read(dyncfg_globals.nodes, df) { if(df->template == template && df->type == DYNCFG_TYPE_JOB) all++; } dfe_done(df); if(rfe->progress.cb) rfe->progress.cb(rfe->progress.data, done, all); dfe_start_reentrant(dyncfg_globals.nodes, df) { if(df->template == template && df->type == DYNCFG_TYPE_JOB) { DYNCFG_CMDS cmd_to_send_to_plugin = c; if(c == DYNCFG_CMD_ENABLE) cmd_to_send_to_plugin = df->dyncfg.user_disabled ? DYNCFG_CMD_DISABLE : DYNCFG_CMD_ENABLE; else if(c == DYNCFG_CMD_DISABLE) cmd_to_send_to_plugin = DYNCFG_CMD_DISABLE; dyncfg_echo(df_dfe.item, df, df_dfe.name, cmd_to_send_to_plugin); if(rfe->progress.cb) rfe->progress.cb(rfe->progress.data, ++done, all); } } dfe_done(df); string_freez(template); } // ---------------------------------------------------------------------------- // the callback for all config functions static int dyncfg_intercept_early_error(struct rrd_function_execute *rfe, int rc, const char *msg) { rc = dyncfg_default_response(rfe->result.wb, rc, msg); if(rfe->result.cb) rfe->result.cb(rfe->result.wb, rc, rfe->result.data); return rc; } const DICTIONARY_ITEM *dyncfg_get_template_of_new_job(const char *job_id) { char id_copy[strlen(job_id) + 1]; memcpy(id_copy, job_id, sizeof(id_copy)); char *colon = strrchr(id_copy, ':'); if(!colon) return NULL; *colon = '\0'; const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id_copy); if(!item) return NULL; DYNCFG *df = dictionary_acquired_item_value(item); if(df->type != DYNCFG_TYPE_TEMPLATE) { dictionary_acquired_item_release(dyncfg_globals.nodes, item); return NULL; } return item; } int dyncfg_function_intercept_cb(struct rrd_function_execute *rfe, void *data __maybe_unused) { // IMPORTANT: this function MUST call the result_cb even on failures bool called_from_dyncfg_echo = rrd_function_has_this_original_result_callback(rfe->transaction, dyncfg_echo_cb); bool has_payload = rfe->payload && buffer_strlen(rfe->payload) ? true : false; bool make_the_call_to_plugin = true; int rc = HTTP_RESP_INTERNAL_SERVER_ERROR; DYNCFG_CMDS cmd; const DICTIONARY_ITEM *item = NULL; char buf[strlen(rfe->function) + 1]; memcpy(buf, rfe->function, sizeof(buf)); char *words[20]; size_t num_words = quoted_strings_splitter_pluginsd(buf, words, 20); size_t i = 0; char *config = get_word(words, num_words, i++); char *id = get_word(words, num_words, i++); char *cmd_str = get_word(words, num_words, i++); char *add_name = get_word(words, num_words, i++); if(!config || !*config || strcmp(config, PLUGINSD_FUNCTION_CONFIG) != 0) return dyncfg_intercept_early_error( rfe, HTTP_RESP_BAD_REQUEST, "dyncfg functions intercept: this is not a dyncfg request"); cmd = dyncfg_cmds2id(cmd_str); if(cmd == DYNCFG_CMD_NONE) return dyncfg_intercept_early_error( rfe, HTTP_RESP_BAD_REQUEST, "dyncfg functions intercept: invalid command received"); if(cmd == DYNCFG_CMD_ADD || cmd == DYNCFG_CMD_TEST || cmd == DYNCFG_CMD_USERCONFIG) { if(cmd == DYNCFG_CMD_TEST && (!add_name || !*add_name)) { // backwards compatibility for TEST without a name char *colon = strrchr(id, ':'); if(colon) { *colon = '\0'; add_name = ++colon; } else add_name = "test"; } if(!add_name || !*add_name) return dyncfg_intercept_early_error( rfe, HTTP_RESP_BAD_REQUEST, "dyncfg functions intercept: this action requires a name"); if(!called_from_dyncfg_echo) { char nid[strlen(id) + strlen(add_name) + 2]; snprintfz(nid, sizeof(nid), "%s:%s", id, add_name); if (cmd == DYNCFG_CMD_ADD && dictionary_get(dyncfg_globals.nodes, nid)) return dyncfg_intercept_early_error( rfe, HTTP_RESP_BAD_REQUEST, "dyncfg functions intercept: a configuration with this name already exists"); } } if((cmd == DYNCFG_CMD_ADD || cmd == DYNCFG_CMD_UPDATE || cmd == DYNCFG_CMD_TEST || cmd == DYNCFG_CMD_USERCONFIG) && !has_payload) return dyncfg_intercept_early_error( rfe, HTTP_RESP_BAD_REQUEST, "dyncfg functions intercept: this action requires a payload"); if((cmd != DYNCFG_CMD_ADD && cmd != DYNCFG_CMD_UPDATE && cmd != DYNCFG_CMD_TEST && cmd != DYNCFG_CMD_USERCONFIG) && has_payload) return dyncfg_intercept_early_error( rfe, HTTP_RESP_BAD_REQUEST, "dyncfg functions intercept: this action does not require a payload"); item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id); if(!item) { if(cmd == DYNCFG_CMD_TEST || cmd == DYNCFG_CMD_USERCONFIG) { // this may be a test on a new job item = dyncfg_get_template_of_new_job(id); } if(!item) return dyncfg_intercept_early_error( rfe, HTTP_RESP_NOT_FOUND, "dyncfg functions intercept: id is not found"); } DYNCFG *df = dictionary_acquired_item_value(item); // 1. check the permissions of the request switch(cmd) { case DYNCFG_CMD_GET: case DYNCFG_CMD_SCHEMA: case DYNCFG_CMD_USERCONFIG: if(!http_access_user_has_enough_access_level_for_endpoint(rfe->user_access, df->view_access)) { make_the_call_to_plugin = false; rc = dyncfg_default_response( rfe->result.wb, HTTP_RESP_FORBIDDEN, "dyncfg: you don't have enough view permissions to execute this command"); } break; case DYNCFG_CMD_ENABLE: case DYNCFG_CMD_DISABLE: case DYNCFG_CMD_ADD: case DYNCFG_CMD_TEST: case DYNCFG_CMD_UPDATE: case DYNCFG_CMD_REMOVE: case DYNCFG_CMD_RESTART: if(!http_access_user_has_enough_access_level_for_endpoint(rfe->user_access, df->edit_access)) { make_the_call_to_plugin = false; rc = dyncfg_default_response( rfe->result.wb, HTTP_RESP_FORBIDDEN, "dyncfg: you don't have enough edit permissions to execute this command"); } break; default: { make_the_call_to_plugin = false; rc = dyncfg_default_response( rfe->result.wb, HTTP_RESP_INTERNAL_SERVER_ERROR, "dyncfg: permissions for this command are not set"); } break; } // 2. validate the request parameters if(make_the_call_to_plugin) { if (!(df->cmds & cmd)) { nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: this command is not supported by the configuration node: %s", rfe->function); make_the_call_to_plugin = false; rc = dyncfg_default_response( rfe->result.wb, HTTP_RESP_BAD_REQUEST, "dyncfg functions intercept: this command is not supported by this configuration node"); } else if (cmd == DYNCFG_CMD_ADD) { if (df->type != DYNCFG_TYPE_TEMPLATE) { make_the_call_to_plugin = false; rc = dyncfg_default_response( rfe->result.wb, HTTP_RESP_BAD_REQUEST, "dyncfg functions intercept: add command is only allowed in templates"); nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: add command can only be applied on templates, not %s: %s", dyncfg_id2type(df->type), rfe->function); } } else if ( cmd == DYNCFG_CMD_ENABLE && df->type == DYNCFG_TYPE_JOB && dyncfg_is_user_disabled(string2str(df->template))) { nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: cannot enable a job of a disabled template: %s", rfe->function); make_the_call_to_plugin = false; rc = dyncfg_default_response( rfe->result.wb, HTTP_RESP_BAD_REQUEST, "dyncfg functions intercept: this job belongs to disabled template"); } } // 3. check if it is one of the commands we should execute if(make_the_call_to_plugin) { if (cmd & (DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE | DYNCFG_CMD_RESTART) && df->type == DYNCFG_TYPE_TEMPLATE) { if (!called_from_dyncfg_echo) { bool old_user_disabled = df->dyncfg.user_disabled; if (cmd == DYNCFG_CMD_ENABLE) df->dyncfg.user_disabled = false; else if (cmd == DYNCFG_CMD_DISABLE) df->dyncfg.user_disabled = true; if (df->dyncfg.user_disabled != old_user_disabled) dyncfg_file_save(id, df); } dyncfg_apply_action_on_all_template_jobs(rfe, id, cmd); rc = dyncfg_default_response(rfe->result.wb, HTTP_RESP_OK, "applied to all template job"); make_the_call_to_plugin = false; } else if (cmd == DYNCFG_CMD_SCHEMA) { bool loaded = false; if (df->type == DYNCFG_TYPE_JOB) { if (df->template) loaded = dyncfg_get_schema(string2str(df->template), rfe->result.wb); } else loaded = dyncfg_get_schema(id, rfe->result.wb); if (loaded) { rfe->result.wb->content_type = CT_APPLICATION_JSON; rfe->result.wb->expires = now_realtime_sec(); rc = HTTP_RESP_OK; make_the_call_to_plugin = false; } } } // 4. execute the command if(make_the_call_to_plugin) { struct dyncfg_call *dc = callocz(1, sizeof(*dc)); dc->function = strdupz(rfe->function); dc->id = strdupz(id); dc->source = rfe->source ? strdupz(rfe->source) : NULL; dc->add_name = (add_name) ? strdupz(add_name) : NULL; dc->cmd = cmd; dc->result_cb = rfe->result.cb; dc->result_cb_data = rfe->result.data; dc->payload = buffer_dup(rfe->payload); dc->from_dyncfg_echo = called_from_dyncfg_echo; rfe->result.cb = dyncfg_function_intercept_result_cb; rfe->result.data = dc; rc = df->execute_cb(rfe, df->execute_cb_data); } else if(rfe->result.cb) rfe->result.cb(rfe->result.wb, rc, rfe->result.data); dictionary_acquired_item_release(dyncfg_globals.nodes, item); return rc; }