Browse Source

ndsudo - a helper to run privileged commands (#16614)

* ndsudo command

* added help

* make ndsudo setuid to root

* fix megacli binary name on FreeBSD

* move ndsudo to collectors/plugins.d/

* address PR comments

* do not print the command line argument, instead print its index

---------

Co-authored-by: Ilya Mashchenko <ilya@netdata.cloud>
Costa Tsaousis 1 year ago
parent
commit
0c8b46cbfd

+ 8 - 0
CMakeLists.txt

@@ -1478,6 +1478,14 @@ if(ENABLE_PLUGIN_CUPS)
     endif()
 endif()
 
+set(NDSUDO_FILES collectors/plugins.d/ndsudo.c)
+
+add_executable(ndsudo ${NDSUDO_FILES})
+
+install(TARGETS ndsudo
+        COMPONENT ndsudo
+        DESTINATION usr/libexec/netdata/plugins.d)
+
 if(ENABLE_PLUGIN_CGROUP_NETWORK)
     set(CGROUP_NETWORK_FILES collectors/cgroups.plugin/cgroup-network.c)
 

+ 305 - 0
collectors/plugins.d/ndsudo.c

@@ -0,0 +1,305 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdbool.h>
+
+#define MAX_SEARCH 2
+#define MAX_PARAMETERS 128
+#define ERROR_BUFFER_SIZE 1024
+
+struct command {
+    const char *name;
+    const char *params;
+    const char *search[MAX_SEARCH];
+} allowed_commands[] = {
+    {
+        .name = "nvme-list",
+        .params = "list --output-format=json",
+        .search = {
+            [0] = "nvme",
+            [1] = NULL,
+        },
+    },
+    {
+        .name = "nvme-smart-log",
+        .params = "smart-log {{device}} --output-format=json",
+        .search = {
+            [0] = "nvme",
+            [1] = NULL,
+        },
+    },
+    {
+        .name = "megacli-disk-info",
+        .params = "-LDPDInfo -aAll -NoLog",
+        .search = {
+            [0] = "megacli",
+            [1] = "MegaCli",
+        },
+    },
+    {
+        .name = "megacli-battery-info",
+        .params = "-AdpBbuCmd -aAll -NoLog",
+        .search = {
+            [0] = "megacli",
+            [1] = "MegaCli",
+        },
+    },
+    {
+        .name = "arcconf-ld-info",
+        .params = "GETCONFIG 1 LD",
+        .search = {
+            [0] = "arcconf",
+            [1] = NULL,
+        },
+    },
+    {
+        .name = "arcconf-pd-info",
+        .params = "GETCONFIG 1 PD",
+        .search = {
+            [0] = "arcconf",
+            [1] = NULL,
+        },
+    }
+};
+
+bool command_exists_in_dir(const char *dir, const char *cmd, char *dst, size_t dst_size) {
+    snprintf(dst, dst_size, "%s/%s", dir, cmd);
+    return access(dst, X_OK) == 0;
+}
+
+bool command_exists_in_PATH(const char *cmd, char *dst, size_t dst_size) {
+    if(!dst || !dst_size)
+        return false;
+
+    char *path = getenv("PATH");
+    if(!path)
+        return false;
+
+    char *path_copy = strdup(path);
+    if (!path_copy)
+        return false;
+
+    char *dir;
+    bool found = false;
+    dir = strtok(path_copy, ":");
+    while(dir && !found) {
+        found = command_exists_in_dir(dir, cmd, dst, dst_size);
+        dir = strtok(NULL, ":");
+    }
+
+    free(path_copy);
+    return found;
+}
+
+struct command *find_command(const char *cmd) {
+    size_t size = sizeof(allowed_commands) / sizeof(allowed_commands[0]);
+    for(size_t i = 0; i < size ;i++) {
+        if(strcmp(cmd, allowed_commands[i].name) == 0)
+            return &allowed_commands[i];
+    }
+
+    return NULL;
+}
+
+bool check_string(const char *str, size_t index, char *err, size_t err_size) {
+    const char *s = str;
+    while(*s) {
+        char c = *s++;
+        if(!((c >= 'A' && c <= 'Z') ||
+             (c >= 'a' && c <= 'z') ||
+             (c >= '0' && c <= '9') ||
+              c == ' ' || c == '_' || c == '-' || c == '/' || c == '.')) {
+            snprintf(err, err_size, "command line argument No %zu includes invalid character '%c'", index, c);
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool check_params(int argc, char **argv, char *err, size_t err_size) {
+    for(int i = 0 ; i < argc ;i++)
+        if(!check_string(argv[i], i, err, err_size))
+            return false;
+
+    return true;
+}
+
+char *find_variable_in_argv(const char *variable, int argc, char **argv, char *err, size_t err_size) {
+    for (int i = 1; i < argc - 1; i++) {
+        if (strcmp(argv[i], variable) == 0)
+            return strdup(argv[i + 1]);
+    }
+
+    snprintf(err, err_size, "variable '%s' is required, but was not provided in the command line parameters", variable);
+
+    return NULL;
+}
+
+bool search_and_replace_params(struct command *cmd, char **params, size_t max_params, const char *filename, int argc, char **argv, char *err, size_t err_size) {
+    if (!cmd || !params || !max_params) {
+        snprintf(err, err_size, "search_and_replace_params() internal error");
+        return false;
+    }
+
+    const char *delim = " ";
+    char *token;
+    char *temp_params = strdup(cmd->params);
+    if (!temp_params) {
+        snprintf(err, err_size, "search_and_replace_params() cannot allocate memory");
+        return false;
+    }
+
+    size_t param_count = 0;
+    params[param_count++] = strdup(filename);
+
+    token = strtok(temp_params, delim);
+    while (token && param_count < max_params - 1) {
+        size_t len = strlen(token);
+
+        char *value = NULL;
+
+        if (strncmp(token, "{{", 2) == 0 && strncmp(token + len - 2, "}}", 2) == 0) {
+            token[0] = '-';
+            token[1] = '-';
+            token[len - 2] = '\0';
+
+            value = find_variable_in_argv(token, argc, argv, err, err_size);
+        }
+        else
+            value = strdup(token);
+
+        if(!value)
+            goto cleanup;
+
+        params[param_count++] = value;
+        token = strtok(NULL, delim);
+    }
+
+    params[param_count] = NULL; // Null-terminate the params array
+    free(temp_params);
+    return true;
+
+cleanup:
+    if(!err[0])
+        snprintf(err, err_size, "memory allocation failure");
+
+    free(temp_params);
+    for (size_t i = 0; i < param_count; ++i) {
+        free(params[i]);
+        params[i] = NULL;
+    }
+    return false;
+}
+
+void show_help() {
+    fprintf(stdout, "\n");
+    fprintf(stdout, "ndsudo\n");
+    fprintf(stdout, "\n");
+    fprintf(stdout, "(C) Netdata Inc.\n");
+    fprintf(stdout, "\n");
+    fprintf(stdout, "A helper to allow Netdata run privileged commands.\n");
+    fprintf(stdout, "\n");
+    fprintf(stdout, "  --test\n");
+    fprintf(stdout, "    print the generated command that will be run, without running it.\n");
+    fprintf(stdout, "\n");
+    fprintf(stdout, "  --help\n");
+    fprintf(stdout, "    print this message.\n");
+    fprintf(stdout, "\n");
+
+    fprintf(stdout, "The following commands are supported:\n\n");
+
+    size_t size = sizeof(allowed_commands) / sizeof(allowed_commands[0]);
+    for(size_t i = 0; i < size ;i++) {
+        fprintf(stdout, "- Command    : %s\n", allowed_commands[i].name);
+        fprintf(stdout, "  Executables: ");
+        for(size_t j = 0; j < MAX_SEARCH && allowed_commands[i].search[j] ;j++) {
+            fprintf(stdout, "%s ", allowed_commands[i].search[j]);
+        }
+        fprintf(stdout, "\n");
+        fprintf(stdout, "  Parameters : %s\n\n", allowed_commands[i].params);
+    }
+
+    fprintf(stdout, "The program searches for executables in the system path.\n");
+    fprintf(stdout, "\n");
+    fprintf(stdout, "Variables given as {{variable}} are expected on the command line as:\n");
+    fprintf(stdout, "  --variable VALUE\n");
+    fprintf(stdout, "\n");
+    fprintf(stdout, "VALUE can include space, A-Z, a-z, 0-9, _, -, /, and .\n");
+    fprintf(stdout, "\n");
+}
+
+int main(int argc, char *argv[]) {
+    char error_buffer[ERROR_BUFFER_SIZE] = "";
+
+    if (argc < 2) {
+        fprintf(stderr, "at least 2 parameters are needed, but %d were given.\n", argc);
+        return 1;
+    }
+
+    if(!check_params(argc, argv, error_buffer, sizeof(error_buffer))) {
+        fprintf(stderr, "invalid characters in parameters: %s\n", error_buffer);
+        return 2;
+    }
+
+    bool test = false;
+    const char *cmd = argv[1];
+    if(strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) {
+        show_help();
+        exit(0);
+    }
+    else if(strcmp(cmd, "--test") == 0) {
+        cmd = argv[2];
+        test = true;
+    }
+
+    struct command *command = find_command(cmd);
+    if(!command) {
+        fprintf(stderr, "command not recognized: %s\n", cmd);
+        return 3;
+    }
+
+    bool found = false;
+    char filename[FILENAME_MAX];
+
+    for(size_t i = 0; i < MAX_SEARCH && !found ;i++) {
+        if(command->search[i]) {
+            found = command_exists_in_PATH(command->search[i], filename, sizeof(filename));
+            if(!found) {
+                size_t len = strlen(error_buffer);
+                snprintf(&error_buffer[len], sizeof(error_buffer) - len, "%s ", command->search[i]);
+            }
+        }
+    }
+
+    if(!found) {
+        fprintf(stderr, "%s: not available in PATH.\n", error_buffer);
+        return 4;
+    }
+    else
+        error_buffer[0] = '\0';
+
+    char *params[MAX_PARAMETERS];
+    if(!search_and_replace_params(command, params, MAX_PARAMETERS, filename, argc, argv, error_buffer, sizeof(error_buffer))) {
+        fprintf(stderr, "command line parameters are not satisfied: %s\n", error_buffer);
+        return 5;
+    }
+
+    if(test) {
+        fprintf(stderr, "Command to run: \n");
+
+        for(size_t i = 0; i < MAX_PARAMETERS && params[i] ;i++)
+            fprintf(stderr, "'%s' ", params[i]);
+
+        fprintf(stderr, "\n");
+
+        exit(0);
+    }
+    else {
+        char *clean_env[] = {NULL};
+        execve(filename, params, clean_env);
+        perror("execve"); // execve only returns on error
+        return 6;
+    }
+}

+ 1 - 0
contrib/debian/netdata.postinst

@@ -41,6 +41,7 @@ case "$1" in
 
     grep /usr/libexec/netdata /var/lib/dpkg/info/netdata.list | xargs -n 30 chown root:netdata
 
+    chmod 4750 /usr/libexec/netdata/plugins.d/ndsudo
     chmod 4750 /usr/libexec/netdata/plugins.d/cgroup-network
     chmod 4750 /usr/libexec/netdata/plugins.d/local-listeners
 

+ 5 - 0
netdata-installer.sh

@@ -1553,6 +1553,11 @@ if [ "$(id -u)" -eq 0 ]; then
     run chmod 4750 "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/local-listeners"
   fi
 
+  if [ -f "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/ndsudo" ]; then
+    run chown "root:${NETDATA_GROUP}" "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/ndsudo"
+    run chmod 4750 "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/ndsudo"
+  fi
+
 else
   # non-privileged user installation
   run chown "${NETDATA_USER}:${NETDATA_GROUP}" "${NETDATA_LOG_DIR}"

+ 3 - 0
netdata.spec.in

@@ -715,6 +715,9 @@ rm -rf "${RPM_BUILD_ROOT}"
 # local-listeners detects the local processes that are listening for connections
 %attr(4750,root,netdata) %{_libexecdir}/%{name}/plugins.d/local-listeners
 
+# ndsudo a helper to run privileged commands
+%attr(4750,root,netdata) %{_libexecdir}/%{name}/plugins.d/ndsudo
+
 # Enforce 0644 for files and 0755 for directories
 # for the netdata web directory
 %defattr(0644,root,root,0755)

+ 1 - 0
packaging/docker/Dockerfile

@@ -122,6 +122,7 @@ RUN addgroup --gid ${NETDATA_GID} --system "${DOCKER_GRP}" && \
                 freeipmi.plugin \
                 go.d.plugin \
                 perf.plugin \
+                ndsudo \
                 slabinfo.plugin \
                 systemd-journal.plugin; do \
         [ -f "/usr/libexec/netdata/plugins.d/$name" ] && chmod 4755 "/usr/libexec/netdata/plugins.d/$name"; \

+ 2 - 2
packaging/makeself/install-or-update.sh

@@ -172,7 +172,7 @@ fi
 
 progress "changing plugins ownership and permissions"
 
-for x in apps.plugin perf.plugin slabinfo.plugin debugfs.plugin freeipmi.plugin ioping cgroup-network local-listeners ebpf.plugin nfacct.plugin xenstat.plugin python.d.plugin charts.d.plugin go.d.plugin ioping.plugin cgroup-network-helper.sh; do
+for x in ndsudo apps.plugin perf.plugin slabinfo.plugin debugfs.plugin freeipmi.plugin ioping cgroup-network local-listeners ebpf.plugin nfacct.plugin xenstat.plugin python.d.plugin charts.d.plugin go.d.plugin ioping.plugin cgroup-network-helper.sh; do
   f="usr/libexec/netdata/plugins.d/${x}"
   if [ -f "${f}" ]; then
     run chown root:${NETDATA_GROUP} "${f}"
@@ -192,7 +192,7 @@ if command -v setcap >/dev/null 2>&1; then
 
     run setcap "cap_net_admin,cap_net_raw=eip" "usr/libexec/netdata/plugins.d/go.d.plugin"
 else
-  for x in apps.plugin perf.plugin slabinfo.plugin debugfs.plugin; do
+  for x in ndsudo apps.plugin perf.plugin slabinfo.plugin debugfs.plugin; do
     f="usr/libexec/netdata/plugins.d/${x}"
     run chmod 4750 "${f}"
   done