diff options
Diffstat (limited to 'support.c')
-rw-r--r-- | support.c | 649 |
1 files changed, 649 insertions, 0 deletions
diff --git a/support.c b/support.c new file mode 100644 index 0000000..54d8124 --- /dev/null +++ b/support.c @@ -0,0 +1,649 @@ +/* support.c - support functions for pam_tacplus.c + * + * Copyright (C) 2010, Pawel Krawczyk <pawel.krawczyk@hush.com> and + * Jeroen Nijhof <jeroen@jeroennijhof.nl> + * Copyright 2016, 2017, 2018 Cumulus Networks, Inc. All rights reserved. + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program - see the file COPYING. + */ + +#define PAM_SM_AUTH +#define PAM_SM_ACCOUNT +#define PAM_SM_SESSION +#define PAM_SM_PASSWORD + +#include "support.h" +#include "pam_tacplus.h" + +#include <stdlib.h> +#include <string.h> +#include <pwd.h> +#include <errno.h> +#include <fcntl.h> +#include <ctype.h> +#include <signal.h> +#include <sys/stat.h> +#include <sys/wait.h> + +tacplus_server_t tac_srv[TAC_PLUS_MAXSERVERS]; +extern tacplus_server_t active_server; +int tac_srv_no = 0; +static int tac_key_no; +static int debug; /* so we don't need to get from pam */ +static int printed_servers; /* only debug server list once */ + +char tac_service[64]; +char tac_protocol[64]; +char tac_prompt[64]; +char *__vrfname; +struct sockaddr src_sockaddr; +struct addrinfo src_addr_info; +struct addrinfo *tac_src_addr_info; +unsigned tac_use_tachome; + +#define MAX_INCL 8 /* max config level nesting */ + +#include <utmpx.h> +/* original name passed in via PAM; this makes this library not usable for + * multiple calls for different users, but that should be OK for PAM. That's + * handled by calling _reset_saved_user from pam_sm_open_session() + */ +static char orig_user[__UT_NAMESIZE]; + +/* used when we have a persistent connection */ +void _reset_saved_user(pam_handle_t *pamh, int debug) +{ + if (*orig_user && debug) + pam_syslog(pamh, LOG_DEBUG, "re-entered, clearing saved userid=%s", + orig_user); + *orig_user = 0; +} + +/* These functions return static info, overwritten on subsequent calls. + * Since orig_user is static global, we aren't multi-threaded, but can + * handle multiple users, as log as pam_sm_open_session is called for + * each, because we'll call _reset_saved_user() to clear. + */ +void _pam_get_user(pam_handle_t *pamh, char **user) { + int retval; + + if (!user) + return; + + if (*orig_user) { + *user = orig_user; /* never our modified user */ + return; + } + retval = pam_get_user(pamh, (void *)user, "Username: "); + if (retval != PAM_SUCCESS || *user == NULL || **user == '\0') { + pam_syslog(pamh, LOG_ERR, "unable to obtain username"); + *user = NULL; + } + else + strncpy(orig_user, *user, sizeof (orig_user)-1); +} + + +/* These functions return static info, overwritten on subsequent calls. */ +void _pam_get_terminal(pam_handle_t *pamh, char **tty) { + int retval; + + if (!tty) + return; + + retval = pam_get_item(pamh, PAM_TTY, (void *)tty); + if (retval != PAM_SUCCESS || *tty == NULL || **tty == '\0') { + *tty = ttyname(STDIN_FILENO); + if(*tty == NULL || **tty == '\0') + *tty = "unknown"; + } +} + +/* These functions return static info, overwritten on subsequent calls. */ +void _pam_get_rhost(pam_handle_t *pamh, char **rhost) { + int retval; + + if (!rhost) + return; + + retval = pam_get_item(pamh, PAM_RHOST, (void *)rhost); + if (retval != PAM_SUCCESS || *rhost == NULL || **rhost == '\0') { + *rhost = "unknown"; + } +} + +int converse(pam_handle_t * pamh, int nargs, const struct pam_message *message, + struct pam_response **response) { + + int retval; + struct pam_conv *conv; + + if ((retval = pam_get_item (pamh, PAM_CONV, (const void **)&conv)) == + PAM_SUCCESS) { + retval = conv->conv(nargs, &message, response, conv->appdata_ptr); + + if (retval != PAM_SUCCESS) + pam_syslog(pamh, LOG_ERR, "converse returned %d" + "that is: %s", retval, pam_strerror (pamh, retval)); + } else { + pam_syslog(pamh, LOG_ERR, "converse failed to get pam_conv"); + } + + return retval; +} + +/* stolen from pam_stress */ +int tacacs_get_password (pam_handle_t * pamh, int flags + ,int ctrl, char **password) { + + const void *pam_pass; + char *pass = NULL; + + if (ctrl & PAM_TAC_DEBUG) + syslog (LOG_DEBUG, "%s: called", __func__); + + if ( (ctrl & (PAM_TAC_TRY_FIRST_PASS | PAM_TAC_USE_FIRST_PASS)) + && (pam_get_item(pamh, PAM_AUTHTOK, &pam_pass) == PAM_SUCCESS) + && (pam_pass != NULL) ) { + if ((pass = strdup(pam_pass)) == NULL) + return PAM_BUF_ERR; + } else if ((ctrl & PAM_TAC_USE_FIRST_PASS)) { + pam_syslog(pamh, LOG_WARNING, "no forwarded password"); + return PAM_PERM_DENIED; + } else { + struct pam_message msg; + struct pam_response *resp = NULL; + int retval; + + /* set up conversation call */ + msg.msg_style = PAM_PROMPT_ECHO_OFF; + + if (!tac_prompt[0]) { + msg.msg = "Password: "; + } else { + msg.msg = tac_prompt; + } + + if ((retval = converse (pamh, 1, &msg, &resp)) != PAM_SUCCESS) + return retval; + + if (resp != NULL) { + if (resp->resp == NULL && (ctrl & PAM_TAC_DEBUG)) + pam_syslog(pamh, LOG_DEBUG, "%s: NULL authtok given", + __func__); + + pass = resp->resp; /* remember this! */ + resp->resp = NULL; + + free(resp); + resp = NULL; + } else { + if (ctrl & PAM_TAC_DEBUG) { + pam_syslog(pamh, LOG_DEBUG, "getting password, but NULL" + " returned!?"); + } + return PAM_CONV_ERR; + } + } + + /* + FIXME *password can still turn out as NULL + and it can't be free()d when it's NULL + */ + *password = pass; /* this *MUST* be free()'d by this module */ + + if(ctrl & PAM_TAC_DEBUG) + syslog(LOG_DEBUG, "%s: obtained password", __func__); + + return PAM_SUCCESS; +} + +static void reset_config(void) +{ + int i; + + for (i = 0; i < tac_key_no; i++) { + if (tac_srv[i].key) + free(tac_srv[i].key); + tac_srv[i].not_resp = 0; + } + memset(tac_srv, 0, sizeof(tacplus_server_t) * TAC_PLUS_MAXSERVERS); + active_server.addr = NULL; /* be sure no refs into freed mem */ + tac_src_addr_info = NULL; + tac_key_no = 0; + tac_srv_no = 0; + printed_servers = 0; +} + +/* Convert ip address string to address info. + * It returns 0 on success, or -1 otherwise + * It supports ipv4 only. + */ +int ip_addr_str_to_addr_info (const char *srcaddr, struct addrinfo *p_addr_info) +{ + struct sockaddr_in *s_in; + + s_in = (struct sockaddr_in *)p_addr_info->ai_addr; + s_in->sin_family = AF_INET; + s_in->sin_addr.s_addr = INADDR_ANY; + + if (inet_pton(AF_INET, srcaddr, &(s_in->sin_addr)) == 1) { + p_addr_info->ai_family = AF_INET; + p_addr_info->ai_addrlen = sizeof (struct sockaddr_in); + return 0; + } + return -1; +} + +static int parse_argfile(pam_handle_t *, const char *, int); + +/* + * parse arguments, one at a time. Separate routine + * so we can have arguments in include files, and use + * common code. + */ +static int parse_arg(pam_handle_t *pamh, const char *arg, int top) { + int ctrl = 0; + + if(!strncmp (arg, "include=", 8)) { + /* + * allow include files, useful for centralizing tacacs + * server IP address and secret. + */ + if(arg[8]) /* else treat as empty config */ + ctrl |= parse_argfile(pamh, arg + 8, top); + } + else if (!strcmp (arg, "debug")) { /* all */ + ctrl |= PAM_TAC_DEBUG; + } else if (!strncmp (arg, "debug=", 6)) { /* allow debug=Digits also */ + unsigned val = (unsigned)strtoul(arg+6, NULL, 0); + if (val) + ctrl |= PAM_TAC_DEBUG; + } else if (!strcmp (arg, "use_first_pass")) { + ctrl |= PAM_TAC_USE_FIRST_PASS; + } else if (!strcmp (arg, "try_first_pass")) { + ctrl |= PAM_TAC_TRY_FIRST_PASS; + } else if (!strncmp (arg, "service=", 8)) { /* author & acct */ + tac_xstrcpy (tac_service, arg + 8, sizeof(tac_service)); + } else if (!strncmp (arg, "protocol=", 9)) { /* author & acct */ + tac_xstrcpy (tac_protocol, arg + 9, sizeof(tac_protocol)); + } else if (!strncmp (arg, "prompt=", 7)) { /* authentication */ + tac_xstrcpy (tac_prompt, arg + 7, sizeof(tac_prompt)); + /* Replace _ with space */ + int chr; + for (chr = 0; chr < strlen(tac_prompt); chr++) { + if (tac_prompt[chr] == '_') { + tac_prompt[chr] = ' '; + } + } + } else if (!strncmp (arg, "login=", 6)) { + tac_xstrcpy (tac_login, arg + 6, sizeof(tac_login)); + } else if (!strncmp (arg, "user_homedir=", 13)) { + tac_use_tachome = strtoul(arg+13, NULL, 0); + } else if (!strcmp (arg, "acct_all")) { + ctrl |= PAM_TAC_ACCT; + } else if (!strncmp (arg, "server=", 7)) { /* authen & acct */ + if(tac_srv_no < TAC_PLUS_MAXSERVERS) { + struct addrinfo hints, *servers, *server; + int rv; + char *close_bracket, *server_name, *port, server_buf[256]; + + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; /* use IPv4 or IPv6, whichever */ + hints.ai_socktype = SOCK_STREAM; + + if (strlen(arg + 7) >= sizeof(server_buf)) { + pam_syslog(pamh, LOG_ERR, "server address too long, sorry"); + goto done; + } + strcpy(server_buf, arg + 7); + + if (*server_buf == '[' && + (close_bracket = strchr(server_buf, ']')) != NULL) { + /* Check for URI syntax */ + server_name = server_buf + 1; + port = strchr(close_bracket, ':'); + *close_bracket = '\0'; + } else { /* Fall back to traditional syntax */ + server_name = server_buf; + port = strchr(server_buf, ':'); + } + if (port != NULL) { + *port = '\0'; + port++; + } + if ((rv = getaddrinfo(server_name, (port == NULL) ? "49" : port, + &hints, &servers)) == 0) { + for(server = servers; server != NULL && + tac_srv_no < TAC_PLUS_MAXSERVERS; + server = server->ai_next) { + tac_srv[tac_srv_no].addr = server; + /* use current key, if our index not yet set */ + if(tac_key_no && !tac_srv[tac_srv_no].key) + tac_srv[tac_srv_no].key = + tac_xstrdup(tac_srv[tac_key_no-1].key); + tac_srv_no++; + } + } else { + pam_syslog(pamh, LOG_ERR, + "skip invalid server: %s (getaddrinfo: %s)", + server_name, gai_strerror(rv)); + } + } else { + pam_syslog(pamh, LOG_ERR, "maximum number of servers (%d) exceeded," + " skipping", TAC_PLUS_MAXSERVERS); + } + } else if (!strncmp (arg, "secret=", 7)) { + int i; + /* no need to complain if too many on this one */ + if(tac_key_no < TAC_PLUS_MAXSERVERS) { + if((tac_srv[tac_key_no].key = tac_xstrdup(arg+7))) + tac_key_no++; + else + pam_syslog(pamh, LOG_ERR, "unable to copy server secret" + " %d: %m", tac_key_no); + } + + /* if 'secret=' was given after a 'server=' parameter, + * fill in any unset keys up to current server number. */ + for(i = tac_srv_no-1; i >= 0; i--) { + if (tac_srv[i].key) + continue; + + tac_srv[i].key = tac_xstrdup(arg + 7); + } + } else if (!strncmp (arg, "timeout=", 8)) { + char *argend; + int val = (unsigned)strtol(arg+8, &argend, 0); + if (argend != (arg+8) && val >= 0) { + tac_timeout = val; + tac_readtimeout_enable = 1; + } + else + pam_syslog(pamh, LOG_WARNING, "invalid option value (%s)", arg); + } else if(!strncmp(arg, "vrf=", 4)) { + __vrfname = tac_xstrdup(arg + 4); + } else if (!strncmp (arg, "source_ip=", 10)) { + const char *srcip = arg + 10; + /* if source ip address, convert it to addr info */ + memset (&src_addr_info, 0, sizeof (struct addrinfo)); + memset (&src_sockaddr, 0, sizeof (struct sockaddr)); + src_addr_info.ai_addr = &src_sockaddr; + if (ip_addr_str_to_addr_info (srcip, &src_addr_info) == 0) + tac_src_addr_info = &src_addr_info; + else { + tac_src_addr_info = NULL; /* for re-parsing or errors */ + pam_syslog(pamh, LOG_WARNING, + "unable to convert %s to an IPv4 address", arg); + } + } else { + /* + * Don't complain about acct_all, we don't use it, + * but may be set for accounting code. + */ + if(strncmp(arg, "acct_all=", 9)) + pam_syslog(pamh, LOG_WARNING, "unrecognized option: %s", arg); + } +done: + debug = ctrl & PAM_TAC_DEBUG; + return ctrl; +} + +static int parse_argfile(pam_handle_t *pamh, const char *file, int top) { + FILE *conf; + char lbuf[256]; + int ctrl = 0; + struct stat st, *lst; + static struct stat lastconf[MAX_INCL]; + static char *filelist[MAX_INCL]; + static int ctrl_list[MAX_INCL]; + static int conf_parsed = 0; + + if(top > MAX_INCL) { + pam_syslog(pamh, LOG_NOTICE, "Config file include depth > %d," + " ignoring %s", MAX_INCL, file); + return 1; + } + + lst = &lastconf[top-1]; + if(conf_parsed && top == 1) { + /* + * Check to see if the config file(s) have changed since last time, + * If not, we don't want to re-parse, since the file parsing is + * invoked from each of the pam.d account, auth, session, etc. files + * This is somewhat complicated by the include file mechanism. + * + * Changes to config files while PAM is running will be rare, + * since a PAM session rarely runs more than a minute, except + * for the session cleanup at exit, which could be hours or days + * + * When we have nested includes, we have to check all the config + * files we saw previously, not just the top level config file. + * If no changes, don't reparse anything, but return the ctrl + * value from the previous parsing. If a change was required, + * reset the config values. + * + * That could include any server from earlier on the command line, but + * there is no other sane way to handle this, but at least it's + * predictable. + */ + int i; + for(i=0; i < MAX_INCL; i++) { + struct stat *cst; + cst = &lastconf[i]; + if(!cst->st_ino || !filelist[i]) { /* end of files */ + return ctrl_list[top-1]; + } + if (stat(filelist[i], &st) || st.st_ino != cst->st_ino || + st.st_mtime != cst->st_mtime || st.st_ctime != cst->st_ctime) + break; /* found removed or different file, so re-parse */ + } + reset_config(); + } + + /* don't check for failures, we'll just skip, don't want to error out */ + filelist[top-1] = strdup(file); + + conf = fopen(file, "r"); + if(conf == NULL) { + pam_syslog(pamh, LOG_ERR, "Unable to open config file %s: %m", file); + return 0; + } + + if (fstat(fileno(conf), lst) != 0) + memset(lst, 0, sizeof *lst); /* avoid stale data, no warning */ + + while (fgets(lbuf, sizeof lbuf, conf)) { + if(*lbuf == '#' || isspace(*lbuf)) + continue; /* skip comments, white space lines, etc. */ + strtok(lbuf, " \t\n\r\f"); /* terminate buffer at first whitespace */ + ctrl |= parse_arg(pamh, lbuf, top + 1); + } + fclose(conf); + conf_parsed = 1; + ctrl_list[top-1] = ctrl; + return ctrl; +} + +/* + * This has re-parse every time, because we can have different parameters + * For different pam.d files. We don't change configured variables (from + * earlier command lines, or config file) that aren't overridden on this + * command line. + */ +int _pam_parse (pam_handle_t *pamh, int argc, const char **argv) { + int i, ctrl = 0, reset_servers = 0; + + /* + * Now that we have a config file, and don't have everything on the + * pam.d config file lines, we shouldn't clear any information here, + * we should only clear whatever is going to be (re)set. + * + * Because we can be called multiple times, we need to reset the state + * each time we go through this function for each of the args that are + * present on the command line. It turns out that the only ones that + * matter are related to the server. + * We need to free allocated memory to avoid memory leaks; for now, + * that's only the key. + * + * We are duplicating keyword parsing here to some degree, but it's + * limited, and this seems like the cleanest way to do it + * + * We make the limiting assumption that if server(s) are specified + * on the command line, that shared secrets will also be specified, + * and we clear the whole tac_srv array. That is, server and secret + * need to both be given on the command line, if either is given + * + * This also reduces timeouts when one or more servers (from the + * config file) are down, and we move from one pam type to another + * (session, account, auth). + */ + for (i=0; i<argc && !reset_servers; i++) { + if (!strncmp(argv[i], "server=", 7) || !strncmp (argv[i], "secret=", 7)) + reset_servers = 1; + } + + if (reset_servers) { + reset_config(); + } + + for (ctrl = 0; argc-- > 0; ++argv) + ctrl |= parse_arg(pamh, *argv, 1); + + if (ctrl & PAM_TAC_DEBUG) { + int n; + + if (reset_servers) + pam_syslog(pamh, LOG_DEBUG, "%d servers defined on pam cmdline", + tac_srv_no); + else + pam_syslog(pamh, LOG_DEBUG, "%d servers defined", tac_srv_no); + + if (!printed_servers) { + printed_servers = 1; + for(n = 0; n < tac_srv_no; n++) { + /* do not log the shared secret, it's a security issue */ + pam_syslog(pamh, LOG_DEBUG, "server[%d] { addr=%s }", + n, tac_ntop(tac_srv[n].addr->ai_addr)); + } + + pam_syslog(pamh, LOG_DEBUG, "tac_service='%s' tac_protocol='%s'" + "tac_prompt='%s' tac_login='%s' source_ip='%s'", + tac_service, tac_protocol , tac_prompt, tac_login, + tac_src_addr_info ? + tac_ntop(tac_src_addr_info->ai_addr) : "unset"); + } + } + return ctrl; +} /* _pam_parse */ + + +/* + * when login is successful (from pam account entry point, after authorization + * succeeds), update our local mapping data, and if we are using the tacacs + * username in the home directory, create the home directory if needed (using + * the mkhomedir_helper program). The code to exec mkhomedir_helper is based on + * pam_mkhomedir.c + */ +void update_mapped(pam_handle_t *pamh, char *user, unsigned level, char *rhost) +{ + struct passwd *pw; + struct stat st; + int rc, retval, child, restore = 0; + struct sigaction newsa, oldsa; + const char *path = "/sbin/mkhomedir_helper"; + + if (!update_mapuser(user, level, rhost, tac_use_tachome)) + return; + + /* + * if we mapped the user name, set SUDO_PROMPT in env so that + * it prompts as the login user, not the mapped user, unless (unlikely) + * the prompt has already been set. Set SUDO_USER as well, for + * consistency. + */ + if (!pam_getenv(pamh, "SUDO_PROMPT")) { + char nprompt[strlen("SUDO_PROMPT=[sudo] password for ") + + strlen(user) + 3]; /* + 3 for ": " and the \0 */ + snprintf(nprompt, sizeof nprompt, + "SUDO_PROMPT=[sudo] password for %s: ", user); + if (pam_putenv(pamh, nprompt) != PAM_SUCCESS) + pam_syslog(pamh, LOG_NOTICE, "failed to set PAM sudo prompt (%s)", + nprompt); + } + if (!pam_getenv(pamh, "SUDO_USER")) { + char sudouser[strlen("SUDO_USER=") + + strlen(user) + 1]; /* + 1 for the \0 */ + snprintf(sudouser, sizeof sudouser, + "SUDO_USER=%s", user); + if (pam_putenv(pamh, sudouser) != PAM_SUCCESS) + pam_syslog(pamh, LOG_NOTICE, "failed to set PAM sudo user (%s)", + sudouser); + } + + if (!tac_use_tachome) + return; + + pw = getpwnam(user); /* this should never fail, at this point... */ + if (!pw) { + pam_syslog(pamh, LOG_NOTICE, "Unable to get passwd entry for user" + " (%s)", user); + return; + } + + if (stat(pw->pw_dir, &st) == 0) + return; + if (debug) + pam_syslog(pamh, LOG_NOTICE, "creating home directory %s for user %s", + pw->pw_dir, user); + + /* + * This code arranges that the demise of the child does not cause + * the application to receive a signal it is not expecting - which + * may kill the application or worse. Based on pam_mkhomedir.c + */ + memset(&newsa, '\0', sizeof(newsa)); + newsa.sa_handler = SIG_DFL; + if (sigaction(SIGCHLD, &newsa, &oldsa) == 0) + restore = 1; + + child = fork(); + if(child == -1) { + pam_syslog(pamh, LOG_ERR, "fork to exec %s %s failed: %m", path, user); + return; + } + if(child == 0) { + execl(path, path, user, NULL); + pam_syslog(pamh, LOG_ERR, "exec %s %s failed: %m", path, user); + exit(1); + } + + while ((rc=waitpid(child, &retval, 0)) < 0 && errno == EINTR) + ; + if(rc < 0) + pam_syslog(pamh, LOG_ERR, "waitpid for exec of %s %s failed: %m", path, + user); + else if(!WIFEXITED(retval)) + pam_syslog(pamh, LOG_ERR, "%s %s abnormal exit: 0x%x", path, user, + retval); + else { + retval = WEXITSTATUS(retval); + if(retval) + pam_syslog(pamh, LOG_ERR, "%s %s abnormal exit: %d", path, user, + retval); + } + + if (restore) + sigaction(SIGCHLD, &oldsa, NULL); +} |