From 6135c45347b6173e305fda66eef138bde693b795 Mon Sep 17 00:00:00 2001 From: Stefan Schubert Date: Fri, 3 Dec 2021 14:33:20 +0100 Subject: pam_env: Use vendor specific pam_env.conf and environment as fallback Use the vendor directory as fallback for a distribution provided default config if there is no one in /etc. * Makefile.am: Add libeconf setting. * pam_env.c: Take care about the fallback configuration in the vendor directory. * pam_env.8.xml: Add description for the vendor directory. * pam_env.conf.5.xml: Add description for the vendor directory. * tst-pam_env-retval.c: Add tests for libeconf. * configure.ac: Add ECONF settings for building man pages. --- configure.ac | 12 +- modules/pam_env/.gitignore | 1 + modules/pam_env/Makefile.am | 4 +- modules/pam_env/pam_env.8.xml | 48 +++++- modules/pam_env/pam_env.c | 298 ++++++++++++++++++++++++++++++----- modules/pam_env/pam_env.conf.5.xml | 20 ++- modules/pam_env/tst-pam_env-retval.c | 60 +++++++ 7 files changed, 394 insertions(+), 49 deletions(-) create mode 100644 modules/pam_env/.gitignore diff --git a/configure.ac b/configure.ac index 0892049e..538195e5 100644 --- a/configure.ac +++ b/configure.ac @@ -516,17 +516,17 @@ if test -n "$LIBSELINUX" ; then LIBS=$BACKUP_LIBS fi +ECONF_CFLAGS= +ECONF_LIBS= AC_ARG_ENABLE([econf], AS_HELP_STRING([--disable-econf], [do not use libeconf]), - [WITH_ECONF=$enableval], WITH_ECONF=yes) -if test "$WITH_ECONF" = "yes" ; then - AC_CHECK_LIB([econf],[econf_readDirsWithCallback],[ECONF_LIBS="-leconf"],[ECONF_LIBS=""]) - if test -n "$ECONF_LIBS" ; then - ECONF_CFLAGS="-DUSE_ECONF=1 $ECONF_CFLAGS" - fi + [WITH_ECONF=$enableval], [WITH_ECONF=yes]) +if test "$WITH_ECONF" = "yes"; then + PKG_CHECK_MODULES([ECONF], [libeconf >= 0.5.0], [ECONF_CFLAGS="-DUSE_ECONF=1 $ECONF_CFLAGS"], [:]) fi AC_SUBST([ECONF_CFLAGS]) AC_SUBST([ECONF_LIBS]) + AC_ARG_ENABLE([vendordir], AS_HELP_STRING([--enable-vendordir=DIR], [Directory for distribution provided configuration files]),,[]) if test -n "$enable_vendordir"; then diff --git a/modules/pam_env/.gitignore b/modules/pam_env/.gitignore new file mode 100644 index 00000000..4c5b234b --- /dev/null +++ b/modules/pam_env/.gitignore @@ -0,0 +1 @@ +tst-pam_env-retval diff --git a/modules/pam_env/Makefile.am b/modules/pam_env/Makefile.am index 02cd9d37..b99a83ec 100644 --- a/modules/pam_env/Makefile.am +++ b/modules/pam_env/Makefile.am @@ -18,14 +18,14 @@ securelibdir = $(SECUREDIR) secureconfdir = $(SCONFIGDIR) AM_CFLAGS = -I$(top_srcdir)/libpam/include -I$(top_srcdir)/libpamc/include \ - $(WARN_CFLAGS) + $(WARN_CFLAGS) -DSYSCONFDIR=\"$(sysconfdir)\" $(ECONF_CFLAGS) AM_LDFLAGS = -no-undefined -avoid-version -module if HAVE_VERSIONING AM_LDFLAGS += -Wl,--version-script=$(srcdir)/../modules.map endif securelib_LTLIBRARIES = pam_env.la -pam_env_la_LIBADD = $(top_builddir)/libpam/libpam.la +pam_env_la_LIBADD = $(top_builddir)/libpam/libpam.la $(ECONF_LIBS) check_PROGRAMS = tst-pam_env-retval tst_pam_env_retval_LDADD = $(top_builddir)/libpam/libpam.la diff --git a/modules/pam_env/pam_env.8.xml b/modules/pam_env/pam_env.8.xml index 75ff862b..d7687d6c 100644 --- a/modules/pam_env/pam_env.8.xml +++ b/modules/pam_env/pam_env.8.xml @@ -52,13 +52,55 @@ variables as well as PAM_ITEMs such as PAM_RHOST. - + + Rules for (un)setting of variables can be defined in an own config + file. The path to this file can be specified with the + conffile option. + If this file does not exist, the default rules are taken from the + config files /etc/security/pam_env.conf and + /etc/security/pam_env.conf.d/*.conf. + If the file /etc/security/pam_env.conf does not + exist, the rules are taken from the files + %vendordir%/security/pam_env.conf, + %vendordir%/security/pam_env.conf.d/*.conf and + /etc/security/pam_env.conf.d/*.conf in that order. + + + By default rules for (un)setting of variables are taken from the + config file /etc/security/pam_env.conf. + If this file does not exist %vendordir%/security/pam_env.conf is used. + An alternate file can be specified with the conffile + option, which overrules all other files. + + By default rules for (un)setting of variables are taken from the config file /etc/security/pam_env.conf. An alternate file can be specified with the conffile option. - + + Environment variables can be defined in a file with simple KEY=VAL + pairs on separate lines. The path to this file can be specified with the + envfile option. + If this file has not been defined, the settings are read from the + files /etc/security/environment and + /etc/security/environment.d/*. + If the file /etc/environment does not exist, the + settings are read from the files %vendordir%/environment, + %vendordir%/environment.d/* and + /etc/environment.d/* in that order. + And last but not least, with the readenv option this mechanism can + be completely disabled. + + + Second a file (/etc/environment by default) with simple + KEY=VAL pairs on separate lines will be read. + If this file does not exist, %vendordir%/etc/environment is used. + With the envfile option an alternate file can be specified, + which overrules all other files. + And with the readenv option this can be completely disabled. + + Second a file (/etc/environment by default) with simple KEY=VAL pairs on separate lines will be read. With the envfile option an alternate file can be specified. @@ -224,12 +266,14 @@ FILES + /usr/etc/security/pam_env.conf /etc/security/pam_env.conf Default configuration file + /usr/etc/environment /etc/environment Default environment file diff --git a/modules/pam_env/pam_env.c b/modules/pam_env/pam_env.c index 64a58645..aabab799 100644 --- a/modules/pam_env/pam_env.c +++ b/modules/pam_env/pam_env.c @@ -7,6 +7,9 @@ */ #define DEFAULT_ETC_ENVFILE "/etc/environment" +#ifdef VENDORDIR +#define VENDOR_DEFAULT_ETC_ENVFILE (VENDORDIR "/etc/environment") +#endif #define DEFAULT_READ_ENVFILE 1 #define DEFAULT_USER_ENVFILE ".pam_environment" @@ -25,6 +28,9 @@ #include #include #include +#ifdef USE_ECONF +#include +#endif #include #include @@ -42,6 +48,9 @@ typedef struct var { } VAR; #define DEFAULT_CONF_FILE (SCONFIGDIR "/pam_env.conf") +#ifdef VENDOR_SCONFIGDIR +#define VENDOR_DEFAULT_CONF_FILE (VENDOR_SCONFIGDIR "/pam_env.conf") +#endif #define BUF_SIZE 8192 #define MAX_ENV 8192 @@ -56,6 +65,16 @@ typedef struct var { /* This is a special value used to designate an empty string */ static char quote='\0'; +static void free_string_array(char **array) +{ + if (array == NULL) + return; + for (char **entry = array; *entry != NULL; ++entry) { + free(*entry); + } + free(array); +} + /* argument parsing */ #define PAM_DEBUG_ARG 0x01 @@ -68,10 +87,10 @@ _pam_parse (const pam_handle_t *pamh, int argc, const char **argv, int ctrl=0; *user_envfile = DEFAULT_USER_ENVFILE; - *envfile = DEFAULT_ETC_ENVFILE; + *envfile = NULL; *readenv = DEFAULT_READ_ENVFILE; *user_readenv = DEFAULT_USER_READ_ENVFILE; - *conffile = DEFAULT_CONF_FILE; + *conffile = NULL; /* step through arguments */ for (; argc-- > 0; ++argv) { @@ -119,6 +138,148 @@ _pam_parse (const pam_handle_t *pamh, int argc, const char **argv, return ctrl; } +#ifdef USE_ECONF + +#define ENVIRONMENT "environment" +#define PAM_ENV "pam_env" + +static int +isDirectory(const char *path) { + struct stat statbuf; + if (stat(path, &statbuf) != 0) + return 0; + return S_ISDIR(statbuf.st_mode); +} + +static int +econf_read_file(const pam_handle_t *pamh, const char *filename, const char *delim, + const char *name, const char *suffix, const char *subpath, + char ***lines) +{ + econf_file *key_file = NULL; + econf_err error; + size_t key_number = 0; + char **keys = NULL; + const char *base_dir = ""; + + if (filename != NULL) { + if (isDirectory(filename)) { + /* Set base directory which can be different from root */ + D(("filename argument is a directory: %s", filename)); + base_dir = filename; + } else { + /* Read only one file */ + error = econf_readFile (&key_file, filename, delim, "#"); + D(("File name is: %s", filename)); + if (error != ECONF_SUCCESS) { + pam_syslog(pamh, LOG_ERR, "Unable to open env file: %s: %s", filename, + econf_errString(error)); + if (error == ECONF_NOFILE) + return PAM_IGNORE; + else + return PAM_ABORT; + } + } + } + if (filename == NULL || base_dir[0] != '\0') { + /* Read and merge all setting in e.g. /usr/etc and /etc */ + char *vendor_dir = NULL, *sysconf_dir; + if (subpath != NULL && subpath[0] != '\0') { +#ifdef VENDORDIR + if (asprintf(&vendor_dir, "%s%s/%s/", base_dir, VENDORDIR, subpath) < 0) { + pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); + return PAM_BUF_ERR; + } +#endif + if (asprintf(&sysconf_dir, "%s%s/%s/", base_dir, SYSCONFDIR, subpath) < 0) { + pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); + free(vendor_dir); + return PAM_BUF_ERR; + } + } else { +#ifdef VENDORDIR + if (asprintf(&vendor_dir, "%s%s/", base_dir, VENDORDIR) < 0) { + pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); + return PAM_BUF_ERR; + } +#endif + if (asprintf(&sysconf_dir, "%s%s/", base_dir, SYSCONFDIR) < 0) { + pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); + free(vendor_dir); + return PAM_BUF_ERR; + } + } + + D(("Read configuration from directory %s and %s", vendor_dir, sysconf_dir)); + error = econf_readDirs (&key_file, vendor_dir, sysconf_dir, name, suffix, + delim, "#"); + free(vendor_dir); + free(sysconf_dir); + if (error != ECONF_SUCCESS) { + if (error == ECONF_NOFILE) { + pam_syslog(pamh, LOG_ERR, "Configuration file not found: %s%s", name, suffix); + return PAM_IGNORE; + } else { + char *error_filename = NULL; + uint64_t error_line = 0; + + econf_errLocation(&error_filename, &error_line); + pam_syslog(pamh, LOG_ERR, "Unable to read configuration file %s line %ld: %s", + error_filename, + error_line, + econf_errString(error)); + free(error_filename); + return PAM_ABORT; + } + } + } + + error = econf_getKeys(key_file, NULL, &key_number, &keys); + if (error != ECONF_SUCCESS && error != ECONF_NOKEY) { + pam_syslog(pamh, LOG_ERR, "Unable to read keys: %s", + econf_errString(error)); + econf_freeFile(key_file); + return PAM_ABORT; + } + + *lines = malloc((key_number +1)* sizeof(char**)); + if (*lines == NULL) { + pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); + econf_free(keys); + econf_freeFile(key_file); + return PAM_BUF_ERR; + } + + (*lines)[key_number] = 0; + + for (size_t i = 0; i < key_number; i++) { + char *val; + + error = econf_getStringValue (key_file, NULL, keys[i], &val); + if (error != ECONF_SUCCESS) { + pam_syslog(pamh, LOG_ERR, "Unable to get string from key %s: %s", + keys[i], + econf_errString(error)); + } else { + if (asprintf(&(*lines)[i],"%s%c%s", keys[i], delim[0], val) < 0) { + pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); + econf_free(keys); + econf_freeFile(key_file); + free_string_array(*lines); + free (val); + return PAM_BUF_ERR; + } + free (val); + } + } + + econf_free(keys); + econf_free(key_file); + return PAM_SUCCESS; +} + +#else + /* * This is where we read a line of the PAM config file. The line may be * preceded by lines of comments and also extended with "\\\n" @@ -212,6 +373,52 @@ _assemble_line(FILE *f, char *buffer, int buf_len) return used; } +static int read_file(const pam_handle_t *pamh, const char*filename, char ***lines) +{ + FILE *conf; + char buffer[BUF_SIZE]; + + D(("Parsed file name is: %s", filename)); + + if ((conf = fopen(filename,"r")) == NULL) { + pam_syslog(pamh, LOG_ERR, "Unable to open env file: %s", filename); + return PAM_IGNORE; + } + + size_t i = 0; + *lines = malloc((i + 1)* sizeof(char**)); + if (*lines == NULL) { + pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); + (void) fclose(conf); + return PAM_BUF_ERR; + } + (*lines)[i] = 0; + while (_assemble_line(conf, buffer, BUF_SIZE) > 0) { + char **tmp = NULL; + D(("Read line: %s", buffer)); + tmp = realloc(*lines, (++i + 1) * sizeof(char**)); + if (tmp == NULL) { + pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); + (void) fclose(conf); + free_string_array(*lines); + return PAM_BUF_ERR; + } + *lines = tmp; + (*lines)[i-1] = strdup(buffer); + if ((*lines)[i-1] == NULL) { + pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); + (void) fclose(conf); + free_string_array(*lines); + return PAM_BUF_ERR; + } + (*lines)[i] = 0; + } + + (void) fclose(conf); + return PAM_SUCCESS; +} +#endif + static int _parse_line(const pam_handle_t *pamh, const char *buffer, VAR *var) { @@ -626,34 +833,38 @@ static int _parse_config_file(pam_handle_t *pamh, int ctrl, const char *file) { int retval; - char buffer[BUF_SIZE]; - FILE *conf; VAR Var, *var=&Var; - - D(("Called.")); + char **conf_list = NULL; var->name=NULL; var->defval=NULL; var->override=NULL; - D(("Config file name is: %s", file)); + D(("Called.")); +#ifdef USE_ECONF + /* If "file" is not NULL, only this file will be parsed. */ + retval = econf_read_file(pamh, file, " \t", PAM_ENV, ".conf", "security", &conf_list); +#else + /* Only one file will be parsed. So, file has to be set. */ + if (file == NULL) /* No filename has been set via argv. */ + file = DEFAULT_CONF_FILE; +#ifdef VENDOR_DEFAULT_CONF_FILE /* - * Lets try to open the config file, parse it and process - * any variables found. - */ - - if ((conf = fopen(file,"r")) == NULL) { - pam_syslog(pamh, LOG_ERR, "Unable to open config file: %s: %m", file); - return PAM_IGNORE; + * Check whether file is available. + * If it does not exist, fall back to VENDOR_DEFAULT_CONF_FILE file. + */ + struct stat stat_buffer; + if (stat(file, &stat_buffer) != 0 && errno == ENOENT) { + file = VENDOR_DEFAULT_CONF_FILE; } +#endif + retval = read_file(pamh, file, &conf_list); +#endif - /* _pam_assemble_line will provide a complete line from the config file, - * with all comments removed and any escaped newlines fixed up - */ - - while (( retval = _assemble_line(conf, buffer, BUF_SIZE)) > 0) { - D(("Read line: %s", buffer)); + if (retval != PAM_SUCCESS) + return retval; - if ((retval = _parse_line(pamh, buffer, var)) == GOOD_LINE) { + for (char **conf = conf_list; *conf != NULL; ++conf) { + if ((retval = _parse_line(pamh, *conf, var)) == GOOD_LINE) { retval = _check_var(pamh, var); if (DEFINE_VAR == retval) { @@ -668,11 +879,10 @@ _parse_config_file(pam_handle_t *pamh, int ctrl, const char *file) _clean_var(var); - } /* while */ - - (void) fclose(conf); + } /* for */ /* tidy up */ + free_string_array(conf_list); _clean_var(var); /* We could have got here prematurely, * this is safe though */ D(("Exit.")); @@ -683,19 +893,33 @@ static int _parse_env_file(pam_handle_t *pamh, int ctrl, const char *file) { int retval=PAM_SUCCESS, i, t; - char buffer[BUF_SIZE], *key, *mark; - FILE *conf; - - D(("Env file name is: %s", file)); - - if ((conf = fopen(file,"r")) == NULL) { - pam_syslog(pamh, LOG_ERR, "Unable to open env file: %s: %m", file); - return PAM_IGNORE; + char *key, *mark; + char **env_list = NULL; + +#ifdef USE_ECONF + retval = econf_read_file(pamh, file, "=", ENVIRONMENT, "", "", &env_list); +#else + /* Only one file will be parsed. So, file has to be set. */ + if (file == NULL) /* No filename has been set via argv. */ + file = DEFAULT_ETC_ENVFILE; +#ifdef VENDOR_DEFAULT_ETC_ENVFILE + /* + * Check whether file is available. + * If it does not exist, fall back to VENDOR_DEFAULT_ETC_ENVFILE; file. + */ + struct stat stat_buffer; + if (stat(file, &stat_buffer) != 0 && errno == ENOENT) { + file = VENDOR_DEFAULT_ETC_ENVFILE; } +#endif + retval = read_file(pamh, file, &env_list); +#endif - while (_assemble_line(conf, buffer, BUF_SIZE) > 0) { - D(("Read line: %s", buffer)); - key = buffer; + if (retval != PAM_SUCCESS) + return retval == PAM_IGNORE ? PAM_SUCCESS : retval; + + for (char **env = env_list; *env != NULL; ++env) { + key = *env; /* skip leading white space */ key += strspn(key, " \n\t"); @@ -767,11 +991,11 @@ _parse_env_file(pam_handle_t *pamh, int ctrl, const char *file) pam_syslog(pamh, LOG_DEBUG, "pam_putenv(\"%s\")", key); } + free(*env); } - (void) fclose(conf); - /* tidy up */ + free(env_list); D(("Exit.")); return retval; } diff --git a/modules/pam_env/pam_env.conf.5.xml b/modules/pam_env/pam_env.conf.5.xml index fca046fe..5c0dbcb8 100644 --- a/modules/pam_env/pam_env.conf.5.xml +++ b/modules/pam_env/pam_env.conf.5.xml @@ -20,7 +20,15 @@ DESCRIPTION - + + The /usr/etc/security/pam_env.conf and + /etc/security/pam_env.conf files specify + the environment variables to be set, unset or modified by + pam_env8. + When someone logs in, these files are read and the environment + variables are set according. + + The /etc/security/pam_env.conf file specifies the environment variables to be set, unset or modified by pam_env8. @@ -61,7 +69,15 @@ at front) can be used to mark this line as a comment line. - + + The /usr/etc/environment and /etc/environment files specify + the environment variables to be set. These files must consist of simple + NAME=VALUE pairs on separate lines. + The pam_env8 + module will read these files after the pam_env.conf + file. + + The /etc/environment file specifies the environment variables to be set. The file must consist of simple NAME=VALUE pairs on separate lines. diff --git a/modules/pam_env/tst-pam_env-retval.c b/modules/pam_env/tst-pam_env-retval.c index 6b9b3065..99e2e2a5 100644 --- a/modules/pam_env/tst-pam_env-retval.c +++ b/modules/pam_env/tst-pam_env-retval.c @@ -17,11 +17,18 @@ #define MODULE_NAME "pam_env" #define TEST_NAME "tst-" MODULE_NAME "-retval" +#define TEST_NAME_DIR TEST_NAME ".dir" static const char service_file[] = TEST_NAME ".service"; static const char missing_file[] = TEST_NAME ".missing"; +static const char dir[] = TEST_NAME_DIR; +static const char dir_usr[] = TEST_NAME_DIR "/usr"; +static const char dir_usr_etc[] = TEST_NAME_DIR "/usr/etc"; +static const char dir_usr_etc_security[] = TEST_NAME_DIR "/usr/etc/security"; static const char my_conf[] = TEST_NAME ".conf"; static const char my_env[] = TEST_NAME ".env"; +static const char usr_env[] = TEST_NAME_DIR "/usr/etc/environment"; +static const char usr_conf[] = TEST_NAME_DIR "/usr/etc/security/pam_env.conf"; static struct pam_conv conv; @@ -30,6 +37,11 @@ setup(void) { FILE *fp; + ASSERT_EQ(0, mkdir(dir, 0755)); + ASSERT_EQ(0, mkdir(dir_usr, 0755)); + ASSERT_EQ(0, mkdir(dir_usr_etc, 0755)); + ASSERT_EQ(0, mkdir(dir_usr_etc_security, 0755)); + ASSERT_NE(NULL, fp = fopen(my_conf, "w")); ASSERT_LT(0, fprintf(fp, "EDITOR\tDEFAULT=vim\n" @@ -41,6 +53,18 @@ setup(void) "test_value=foo\n" "test2_value=bar\n")); ASSERT_EQ(0, fclose(fp)); + + ASSERT_NE(NULL, fp = fopen(usr_env, "w")); + ASSERT_LT(0, fprintf(fp, + "usr_etc_test=foo\n" + "usr_etc_test2=bar\n")); + ASSERT_EQ(0, fclose(fp)); + + ASSERT_NE(NULL, fp = fopen(usr_conf, "w")); + ASSERT_LT(0, fprintf(fp, + "PAGER DEFAULT=emacs\n" + "MANPAGER DEFAULT=less\n")); + ASSERT_EQ(0, fclose(fp)); } static void @@ -48,6 +72,12 @@ cleanup(void) { ASSERT_EQ(0, unlink(my_conf)); ASSERT_EQ(0, unlink(my_env)); + ASSERT_EQ(0, unlink(usr_env)); + ASSERT_EQ(0, unlink(usr_conf)); + ASSERT_EQ(0, rmdir(dir_usr_etc_security)); + ASSERT_EQ(0, rmdir(dir_usr_etc)); + ASSERT_EQ(0, rmdir(dir_usr)); + ASSERT_EQ(0, rmdir(dir)); } static void @@ -191,6 +221,36 @@ main(void) const char *env2[] = { "test_value=foo", "test2_value=bar", NULL }; check_env(env2); +#if defined (USE_ECONF) && defined (VENDORDIR) + + /* envfile is a directory. So values will be read from {TEST_NAME_DIR}/usr/etc and {TEST_NAME_DIR}/etc */ + ASSERT_NE(NULL, fp = fopen(service_file, "w")); + ASSERT_LT(0, fprintf(fp, "#%%PAM-1.0\n" + "session required %s/.libs/%s.so" + " conffile=%s envfile=%s/%s/\n", + cwd, MODULE_NAME, + "/dev/null", + cwd, dir)); + ASSERT_EQ(0, fclose(fp)); + + const char *env3[] = {"usr_etc_test=foo", "usr_etc_test2=bar", NULL}; + check_env(env3); + + /* conffile is a directory. So values will be read from {TEST_NAME_DIR}/usr/etc and {TEST_NAME_DIR}/etc */ + ASSERT_NE(NULL, fp = fopen(service_file, "w")); + ASSERT_LT(0, fprintf(fp, "#%%PAM-1.0\n" + "session required %s/.libs/%s.so" + " conffile=%s/%s/ envfile=%s\n", + cwd, MODULE_NAME, + cwd, dir, + "/dev/null")); + ASSERT_EQ(0, fclose(fp)); + + const char *env4[] = {"PAGER=emacs", "MANPAGER=less", NULL}; + check_env(env4); + +#endif + /* cleanup */ cleanup(); ASSERT_EQ(0, unlink(service_file)); -- cgit v1.2.1