From 46a3d9f65394acda9f8c10719795eebb530e82c5 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Fri, 22 May 2026 10:42:58 +0200 Subject: [PATCH] Native Helper Logging --- README.md | 20 ++++ check_login.c | 292 ++++++++++++++++++++++++++++++++++++++++++++--- config.h.cmake | 3 + settings.pl | 5 +- smart.cmake | 3 +- smart.conf.cmake | 3 + smart_userlist.c | 279 +++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 578 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index bf56ce7..04a1215 100644 --- a/README.md +++ b/README.md @@ -431,6 +431,26 @@ Typical access URLs: For production use, HTTPS should be preferred. +## Native helper logging + +The native helper programs `check_login` and `smart_userlist` read their log +destination and verbosity from `smart.conf` when called by the WebUI. + +They use the same Perl frontend settings: + +```perl +$smart_log_path = '/var/log/mars_nwe/smart.log'; +$smart_debug_level = 'info'; +``` + +The generated `config.h` also provides fallback defaults for these values, so +the helpers can still write useful diagnostics when they are executed manually +or before `smart.conf` could be loaded. + +`check_login` logs authentication and authorization results, but never logs the +submitted password. `smart_userlist` keeps its tab-separated user-list output +on stdout unchanged and writes diagnostics only to the configured log file. + ## Unix user discovery helper The WebUI user editor can assign a MARS_NWE bindery user to a local Unix user. diff --git a/check_login.c b/check_login.c index 4df728c..46b31d6 100644 --- a/check_login.c +++ b/check_login.c @@ -4,28 +4,264 @@ Check username/password combination using PAM and require membership in the configured SMArT administrator Unix group. - Copyright 2001 Wilmer van der Gaast - Updated for MARS_NWE SMArT group-restricted login. + Usage: + check_login [smart.conf] - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. + Passwords are never written to the log. */ +#include #include #include #include #include +#include #include #include #include #include +#include #include +#include "config.h" + int my_conv(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr); static int user_in_group(const char *username, const char *groupname); + +#define SMART_LOG_ERROR 0 +#define SMART_LOG_WARNING 1 +#define SMART_LOG_INFO 2 +#define SMART_LOG_DEBUG 3 +#define SMART_LOG_TRACE 4 + +typedef struct { + char log_path[512]; + char debug_level[64]; + char admin_group[256]; + int level; +} smart_helper_config_t; + +static void trim(char *s) +{ + char *p = s; + size_t len; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + + if (p != s) { + memmove(s, p, strlen(p) + 1); + } + + len = strlen(s); + while (len > 0 && isspace((unsigned char)s[len - 1])) { + s[len - 1] = '\0'; + len--; + } +} + +static void strip_quotes(char *s) +{ + size_t len = strlen(s); + + if (len >= 2) { + if ((s[0] == '\'' && s[len - 1] == '\'') || + (s[0] == '"' && s[len - 1] == '"')) { + memmove(s, s + 1, len - 2); + s[len - 2] = '\0'; + } + } +} + +static int parse_perl_assignment(const char *line, char *key, size_t ksz, char *val, size_t vsz) +{ + const char *p = line; + size_t ki = 0; + size_t vi = 0; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (*p != '$') { + return 0; + } + p++; + + while (*p && (isalnum((unsigned char)*p) || *p == '_')) { + if (ki + 1 < ksz) { + key[ki++] = *p; + } + p++; + } + key[ki] = '\0'; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (*p != '=') { + return 0; + } + p++; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + + while (*p && *p != ';' && *p != '\n' && *p != '\r') { + if (vi + 1 < vsz) { + val[vi++] = *p; + } + p++; + } + val[vi] = '\0'; + + trim(key); + trim(val); + strip_quotes(val); + + return key[0] != '\0'; +} + +static int parse_log_level(const char *value) +{ + char buf[64]; + size_t i; + + if (value == NULL || value[0] == '\0') { + return SMART_LOG_INFO; + } + + snprintf(buf, sizeof(buf), "%s", value); + trim(buf); + + for (i = 0; buf[i]; i++) { + buf[i] = (char)tolower((unsigned char)buf[i]); + } + + if (strcmp(buf, "error") == 0 || strcmp(buf, "err") == 0 || strcmp(buf, "0") == 0) { + return SMART_LOG_ERROR; + } + if (strcmp(buf, "warning") == 0 || strcmp(buf, "warn") == 0 || strcmp(buf, "1") == 0) { + return SMART_LOG_WARNING; + } + if (strcmp(buf, "info") == 0 || strcmp(buf, "2") == 0) { + return SMART_LOG_INFO; + } + if (strcmp(buf, "debug") == 0 || strcmp(buf, "3") == 0) { + return SMART_LOG_DEBUG; + } + if (strcmp(buf, "trace") == 0 || strcmp(buf, "4") == 0) { + return SMART_LOG_TRACE; + } + + return SMART_LOG_INFO; +} + +static const char *level_name(int level) +{ + if (level <= SMART_LOG_ERROR) { + return "ERROR"; + } + if (level == SMART_LOG_WARNING) { + return "WARNING"; + } + if (level == SMART_LOG_DEBUG) { + return "DEBUG"; + } + if (level >= SMART_LOG_TRACE) { + return "TRACE"; + } + return "INFO"; +} + +static void smart_cfg_init(smart_helper_config_t *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + snprintf(cfg->log_path, sizeof(cfg->log_path), "%s", DEFAULT_SMART_LOG_PATH); + snprintf(cfg->debug_level, sizeof(cfg->debug_level), "%s", DEFAULT_SMART_LOG_LEVEL); + snprintf(cfg->admin_group, sizeof(cfg->admin_group), "%s", "root"); + cfg->level = parse_log_level(cfg->debug_level); +} + +static void smart_cfg_load(smart_helper_config_t *cfg, const char *path) +{ + FILE *fh; + char line[2048]; + + if (path == NULL || path[0] == '\0') { + return; + } + + fh = fopen(path, "r"); + if (fh == NULL) { + return; + } + + while (fgets(line, sizeof(line), fh) != NULL) { + char key[256]; + char val[1024]; + + if (!parse_perl_assignment(line, key, sizeof(key), val, sizeof(val))) { + continue; + } + + if (strcmp(key, "smart_log_path") == 0) { + snprintf(cfg->log_path, sizeof(cfg->log_path), "%s", val); + } else if (strcmp(key, "smart_debug_level") == 0 || + strcmp(key, "smart_log_level") == 0) { + snprintf(cfg->debug_level, sizeof(cfg->debug_level), "%s", val); + cfg->level = parse_log_level(val); + } else if (strcmp(key, "smart_admin_group") == 0) { + snprintf(cfg->admin_group, sizeof(cfg->admin_group), "%s", val); + } + } + + fclose(fh); +} + +static void helper_log(smart_helper_config_t *cfg, const char *component, int level, const char *fmt, ...) +{ + FILE *fh = stderr; + int close_fh = 0; + time_t now; + struct tm tm_now; + char tbuf[64]; + va_list ap; + + if (cfg != NULL && level > cfg->level) { + return; + } + + if (cfg != NULL && cfg->log_path[0] != '\0') { + fh = fopen(cfg->log_path, "a"); + if (fh != NULL) { + close_fh = 1; + } else { + fh = stderr; + } + } + + now = time(NULL); + localtime_r(&now, &tm_now); + strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tm_now); + + fprintf(fh, "[%s] [%s] [SMArT helper] [%s] ", tbuf, level_name(level), component); + + va_start(ap, fmt); + vfprintf(fh, fmt, ap); + va_end(ap); + + fputc('\n', fh); + fflush(fh); + + if (close_fh) { + fclose(fh); + } +} + + static struct pam_conv conv = { my_conv, NULL @@ -39,10 +275,14 @@ int main( int argc, char **argv ) pam_handle_t *pamh = NULL; int retval, st = 1; const char *admin_group; + const char *smart_conf = DEFAULT_SMART_CONF; + smart_helper_config_t cfg; + + smart_cfg_init(&cfg); if( argc < 4 ) { - fprintf( stderr, "Usage: %s \n", argv[0] ); + fprintf( stderr, "Usage: %s [smart.conf]\n", argv[0] ); return( 3 ); } @@ -50,27 +290,50 @@ int main( int argc, char **argv ) pass = argv[2]; admin_group = argv[3]; + if (argc >= 5 && argv[4] != NULL && argv[4][0] != '\0') { + smart_conf = argv[4]; + } + + smart_cfg_load(&cfg, smart_conf); + + if (admin_group == NULL || admin_group[0] == '\0' || strcmp(admin_group, "-") == 0) { + admin_group = cfg.admin_group; + } + if( user == NULL || user[0] == '\0' || pass == NULL || admin_group == NULL || admin_group[0] == '\0' ) { + helper_log(&cfg, "check_login", SMART_LOG_ERROR, "invalid helper arguments"); return( 3 ); } + helper_log(&cfg, "check_login", SMART_LOG_INFO, "authentication requested user='%s' admin_group='%s'", user, admin_group); + retval = pam_start( "smart", user, &conv, &pamh ); if( retval == PAM_SUCCESS ) retval = pam_authenticate( pamh, PAM_SILENT ); if( retval == PAM_SUCCESS ) st = retval = pam_acct_mgmt( pamh, PAM_SILENT ); - if( pamh != NULL && pam_end( pamh, retval ) != PAM_SUCCESS ) + if( pamh != NULL && pam_end( pamh, retval ) != PAM_SUCCESS ) { + helper_log(&cfg, "check_login", SMART_LOG_ERROR, "pam_end failed user='%s'", user); return( 1 ); + } - if( st != PAM_SUCCESS ) + if( st != PAM_SUCCESS ) { + helper_log(&cfg, "check_login", SMART_LOG_WARNING, "pam authentication failed user='%s' pam_status=%d", user, st); return( 1 ); + } - if( ! user_in_group( user, admin_group ) ) + helper_log(&cfg, "check_login", SMART_LOG_DEBUG, "pam authentication ok user='%s'", user); + + if( ! user_in_group( user, admin_group ) ) { + helper_log(&cfg, "check_login", SMART_LOG_WARNING, "group authorization failed user='%s' required_group='%s'", user, admin_group); return( 2 ); + } + + helper_log(&cfg, "check_login", SMART_LOG_INFO, "login accepted user='%s' required_group='%s'", user, admin_group); return( 0 ); } @@ -103,10 +366,6 @@ static int user_in_group(const char *username, const char *groupname) } #if defined(__linux__) || defined(__GLIBC__) - /* - getgrouplist() asks NSS for supplementary groups, so files, LDAP, SSSD, - NIS, etc. follow the local nsswitch.conf configuration. - */ getgrouplist( username, pw->pw_gid, NULL, &ngroups ); if( ngroups > 0 ) @@ -132,9 +391,6 @@ static int user_in_group(const char *username, const char *groupname) } #endif - /* - Portable fallback: check the group's explicit member list. - */ if( gr->gr_mem != NULL ) { for( i = 0; gr->gr_mem[i] != NULL; i++ ) @@ -165,7 +421,7 @@ int my_conv(int num_msg, const struct pam_message **msg, struct pam_response **r for( i = 0; i < num_msg; i ++ ) { - reply[i].resp = (char *) strdup( pass ); /* Just give the password... It's all we know */ + reply[i].resp = (char *) strdup( pass ); reply[i].resp_retcode = 0; } diff --git a/config.h.cmake b/config.h.cmake index 7acbe57..fee3b2d 100644 --- a/config.h.cmake +++ b/config.h.cmake @@ -12,6 +12,9 @@ #define LOG_PATH_DEFAULT "@MARS_NWE_LOG_DIR@/nwwebui.log" +#define DEFAULT_SMART_LOG_PATH "@MARS_NWE_LOG_DIR@/smart.log" +#define DEFAULT_SMART_LOG_LEVEL "info" + #define LOG_LEVEL_ERROR 0 #define LOG_LEVEL_WARNING 1 #define LOG_LEVEL_INFO 2 diff --git a/settings.pl b/settings.pl index 1dd43b7..94948c6 100644 --- a/settings.pl +++ b/settings.pl @@ -1086,9 +1086,10 @@ sub smart_unix_users_for_select() my @users = (); my %seen = (); - my $helper = '/usr/lib64/mars_nwe/smart_userlist'; + my $helper = defined( $smart_userlist_path ) && $smart_userlist_path ne '' ? $smart_userlist_path : '@MARS_NWE_INSTALL_FULL_LIBEXECDIR@/smart_userlist'; + my $conf_path = defined( $smart_conf_path ) && $smart_conf_path ne '' ? $smart_conf_path : '@MARS_NWE_INSTALL_FULL_CONFDIR@/smart.conf'; - if( -x $helper && open( my $fh, '-|', $helper ) ) + if( -x $helper && open( my $fh, '-|', $helper, '--config', $conf_path ) ) { while( my $line = <$fh> ) { diff --git a/smart.cmake b/smart.cmake index 8e21f5e..b400702 100644 --- a/smart.cmake +++ b/smart.cmake @@ -448,7 +448,8 @@ sub check_login_password( $$ ) my $admin_group = defined( $smart_admin_group ) && $smart_admin_group ne '' ? $smart_admin_group : 'root'; - my $rc = system( $smart_check_login, $user, $pass, $admin_group ); + my $conf_path = defined( $smart_conf_path ) && $smart_conf_path ne '' ? $smart_conf_path : '@MARS_NWE_INSTALL_FULL_CONFDIR@/smart.conf'; + my $rc = system( $smart_check_login, $user, $pass, $admin_group, $conf_path ); if( $rc == 0 ) { diff --git a/smart.conf.cmake b/smart.conf.cmake index 004cdd3..6327420 100644 --- a/smart.conf.cmake +++ b/smart.conf.cmake @@ -64,6 +64,9 @@ $smart_log_path = '@MARS_NWE_LOG_DIR@/smart.log'; # Path to the PAM-based login helper used for SMArT authentication. $smart_check_login = '@MARS_NWE_INSTALL_FULL_LIBEXECDIR@/check_login'; +# Path to the native Unix-user enumeration helper used by the user editor. +$smart_userlist_path = '@MARS_NWE_INSTALL_FULL_LIBEXECDIR@/smart_userlist'; + # Unix group allowed to log in to the SMArT/nwwebui admin interface. # # Authentication is still done through PAM service "smart", but a user must diff --git a/smart_userlist.c b/smart_userlist.c index 133d7d3..45c01fc 100644 --- a/smart_userlist.c +++ b/smart_userlist.c @@ -3,24 +3,262 @@ List local/NSS users for the WebUI. - PAM itself cannot enumerate users. User enumeration is done through NSS - getpwent(), so /etc/nsswitch.conf is honored (files, sss, ldap, nis, ...). - Optionally, each user can be checked with pam_acct_mgmt() against the "smart" - PAM service. + Usage: + smart_userlist [--config /etc/mars_nwe/smart.conf] [--all] + [--min-uid UID] [--pam-check] [--pam-service SERVICE] - Output format: + Output format on stdout stays unchanged: usernameuidgidgecoshomeshell */ +#include #include #include #include +#include #include #include #include #include +#include #include +#include "config.h" + + +#define SMART_LOG_ERROR 0 +#define SMART_LOG_WARNING 1 +#define SMART_LOG_INFO 2 +#define SMART_LOG_DEBUG 3 +#define SMART_LOG_TRACE 4 + +typedef struct { + char log_path[512]; + char debug_level[64]; + char admin_group[256]; + int level; +} smart_helper_config_t; + +static void trim(char *s) +{ + char *p = s; + size_t len; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + + if (p != s) { + memmove(s, p, strlen(p) + 1); + } + + len = strlen(s); + while (len > 0 && isspace((unsigned char)s[len - 1])) { + s[len - 1] = '\0'; + len--; + } +} + +static void strip_quotes(char *s) +{ + size_t len = strlen(s); + + if (len >= 2) { + if ((s[0] == '\'' && s[len - 1] == '\'') || + (s[0] == '"' && s[len - 1] == '"')) { + memmove(s, s + 1, len - 2); + s[len - 2] = '\0'; + } + } +} + +static int parse_perl_assignment(const char *line, char *key, size_t ksz, char *val, size_t vsz) +{ + const char *p = line; + size_t ki = 0; + size_t vi = 0; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (*p != '$') { + return 0; + } + p++; + + while (*p && (isalnum((unsigned char)*p) || *p == '_')) { + if (ki + 1 < ksz) { + key[ki++] = *p; + } + p++; + } + key[ki] = '\0'; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (*p != '=') { + return 0; + } + p++; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + + while (*p && *p != ';' && *p != '\n' && *p != '\r') { + if (vi + 1 < vsz) { + val[vi++] = *p; + } + p++; + } + val[vi] = '\0'; + + trim(key); + trim(val); + strip_quotes(val); + + return key[0] != '\0'; +} + +static int parse_log_level(const char *value) +{ + char buf[64]; + size_t i; + + if (value == NULL || value[0] == '\0') { + return SMART_LOG_INFO; + } + + snprintf(buf, sizeof(buf), "%s", value); + trim(buf); + + for (i = 0; buf[i]; i++) { + buf[i] = (char)tolower((unsigned char)buf[i]); + } + + if (strcmp(buf, "error") == 0 || strcmp(buf, "err") == 0 || strcmp(buf, "0") == 0) { + return SMART_LOG_ERROR; + } + if (strcmp(buf, "warning") == 0 || strcmp(buf, "warn") == 0 || strcmp(buf, "1") == 0) { + return SMART_LOG_WARNING; + } + if (strcmp(buf, "info") == 0 || strcmp(buf, "2") == 0) { + return SMART_LOG_INFO; + } + if (strcmp(buf, "debug") == 0 || strcmp(buf, "3") == 0) { + return SMART_LOG_DEBUG; + } + if (strcmp(buf, "trace") == 0 || strcmp(buf, "4") == 0) { + return SMART_LOG_TRACE; + } + + return SMART_LOG_INFO; +} + +static const char *level_name(int level) +{ + if (level <= SMART_LOG_ERROR) { + return "ERROR"; + } + if (level == SMART_LOG_WARNING) { + return "WARNING"; + } + if (level == SMART_LOG_DEBUG) { + return "DEBUG"; + } + if (level >= SMART_LOG_TRACE) { + return "TRACE"; + } + return "INFO"; +} + +static void smart_cfg_init(smart_helper_config_t *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + snprintf(cfg->log_path, sizeof(cfg->log_path), "%s", DEFAULT_SMART_LOG_PATH); + snprintf(cfg->debug_level, sizeof(cfg->debug_level), "%s", DEFAULT_SMART_LOG_LEVEL); + snprintf(cfg->admin_group, sizeof(cfg->admin_group), "%s", "root"); + cfg->level = parse_log_level(cfg->debug_level); +} + +static void smart_cfg_load(smart_helper_config_t *cfg, const char *path) +{ + FILE *fh; + char line[2048]; + + if (path == NULL || path[0] == '\0') { + return; + } + + fh = fopen(path, "r"); + if (fh == NULL) { + return; + } + + while (fgets(line, sizeof(line), fh) != NULL) { + char key[256]; + char val[1024]; + + if (!parse_perl_assignment(line, key, sizeof(key), val, sizeof(val))) { + continue; + } + + if (strcmp(key, "smart_log_path") == 0) { + snprintf(cfg->log_path, sizeof(cfg->log_path), "%s", val); + } else if (strcmp(key, "smart_debug_level") == 0 || + strcmp(key, "smart_log_level") == 0) { + snprintf(cfg->debug_level, sizeof(cfg->debug_level), "%s", val); + cfg->level = parse_log_level(val); + } else if (strcmp(key, "smart_admin_group") == 0) { + snprintf(cfg->admin_group, sizeof(cfg->admin_group), "%s", val); + } + } + + fclose(fh); +} + +static void helper_log(smart_helper_config_t *cfg, const char *component, int level, const char *fmt, ...) +{ + FILE *fh = stderr; + int close_fh = 0; + time_t now; + struct tm tm_now; + char tbuf[64]; + va_list ap; + + if (cfg != NULL && level > cfg->level) { + return; + } + + if (cfg != NULL && cfg->log_path[0] != '\0') { + fh = fopen(cfg->log_path, "a"); + if (fh != NULL) { + close_fh = 1; + } else { + fh = stderr; + } + } + + now = time(NULL); + localtime_r(&now, &tm_now); + strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tm_now); + + fprintf(fh, "[%s] [%s] [SMArT helper] [%s] ", tbuf, level_name(level), component); + + va_start(ap, fmt); + vfprintf(fh, fmt, ap); + va_end(ap); + + fputc('\n', fh); + fflush(fh); + + if (close_fh) { + fclose(fh); + } +} + + static int empty_conv(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) { @@ -103,7 +341,13 @@ int main(int argc, char **argv) int include_system = 0; int pam_check = 0; const char *pam_service = "smart"; + const char *smart_conf = DEFAULT_SMART_CONF; int i; + unsigned long emitted = 0; + unsigned long skipped = 0; + smart_helper_config_t cfg; + + smart_cfg_init(&cfg); for (i = 1; i < argc; i++) { if (strcmp(argv[i], "--all") == 0) { @@ -113,6 +357,8 @@ int main(int argc, char **argv) char *end = NULL; unsigned long v = strtoul(argv[++i], &end, 10); if (end == NULL || *end != '\0') { + smart_cfg_load(&cfg, smart_conf); + helper_log(&cfg, "smart_userlist", SMART_LOG_ERROR, "invalid --min-uid value"); fprintf(stderr, "Invalid --min-uid value\n"); return 2; } @@ -121,32 +367,46 @@ int main(int argc, char **argv) pam_check = 1; } else if (strcmp(argv[i], "--pam-service") == 0 && i + 1 < argc) { pam_service = argv[++i]; + } else if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) { + smart_conf = argv[++i]; } else { + smart_cfg_load(&cfg, smart_conf); + helper_log(&cfg, "smart_userlist", SMART_LOG_ERROR, "invalid command line"); fprintf(stderr, - "Usage: %s [--all] [--min-uid UID] [--pam-check] [--pam-service SERVICE]\n", + "Usage: %s [--config FILE] [--all] [--min-uid UID] [--pam-check] [--pam-service SERVICE]\n", argv[0]); return 2; } } + smart_cfg_load(&cfg, smart_conf); + + helper_log(&cfg, "smart_userlist", SMART_LOG_DEBUG, + "user enumeration started include_system=%d min_uid=%lu pam_check=%d pam_service='%s'", + include_system, (unsigned long) min_uid, pam_check, pam_service); + errno = 0; setpwent(); while ((pw = getpwent()) != NULL) { if (!is_safe_name(pw->pw_name)) { + skipped++; continue; } if (!include_system && pw->pw_uid < min_uid) { + skipped++; continue; } if (!include_system && (strcmp(pw->pw_name, "root") == 0 || strcmp(pw->pw_name, "nobody") == 0)) { + skipped++; continue; } if (pam_check && !pam_account_ok(pam_service, pw->pw_name)) { + skipped++; continue; } @@ -158,14 +418,21 @@ int main(int argc, char **argv) putchar('\t'); print_sanitized(pw->pw_shell); putchar('\n'); + + emitted++; } endpwent(); if (errno != 0) { + helper_log(&cfg, "smart_userlist", SMART_LOG_ERROR, "getpwent failed: %s", strerror(errno)); perror("getpwent"); return 1; } + helper_log(&cfg, "smart_userlist", SMART_LOG_DEBUG, + "user enumeration finished emitted=%lu skipped=%lu", + emitted, skipped); + return 0; }