diff options
author | Christian Poessinger <christian@poessinger.com> | 2021-05-02 19:07:13 +0200 |
---|---|---|
committer | Christian Poessinger <christian@poessinger.com> | 2021-05-02 19:07:49 +0200 |
commit | 7d2e07fd4502aed3b841484855031ca8a48aebba (patch) | |
tree | 7861b20208fd1089b0d6e67f72523d0ca6d745da /map_tacplus_user.c | |
download | libtacplus-map-7d2e07fd4502aed3b841484855031ca8a48aebba.tar.gz libtacplus-map-7d2e07fd4502aed3b841484855031ca8a48aebba.zip |
Initial import of libtacplus-map (1.0.1-cl3u3)
Diffstat (limited to 'map_tacplus_user.c')
-rw-r--r-- | map_tacplus_user.c | 689 |
1 files changed, 689 insertions, 0 deletions
diff --git a/map_tacplus_user.c b/map_tacplus_user.c new file mode 100644 index 0000000..47ddf78 --- /dev/null +++ b/map_tacplus_user.c @@ -0,0 +1,689 @@ +/* + * Copyright 2015, 2016, 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. + * + * Author: olson@cumulusnetworks.com> + */ + +#include <stdlib.h> +#include <stdio.h> +#include <syslog.h> +#include <string.h> +#include <unistd.h> +#include <strings.h> +#include <libaudit.h> +#include <errno.h> +#include <fcntl.h> +#include <dirent.h> +#include <sys/stat.h> +#include <sys/file.h> + +#include "map_tacplus_user.h" + +static const char *libname = "libtacplus_map"; + +static const char *mapfile = MAP_TACPLUS_FILE; + +static int debug; /* for developer debug */ + +#define MATCH_MAPPED 1 /* match mapped name in mapfile */ +#define MATCH_LOGIN 2 /* match login name in mapfile */ +/* + * see if a mapping file entry matches; the pid needs to be valid and + * the process still alive, to be consider a match. + * name can be NULL, if we are looking for a UID match + * rather than a name match. + * If auid and/or session are -1, they are wildcards, only match + * on other data. + * "which" controls which name we match on. + */ +static int is_mapmatch(struct tacacs_mapping *map, int which, const char *name, + uid_t auid, unsigned session) +{ + if(map->tac_mapversion > MAP_FILE_VERSION || !map->tac_mapversion) + syslog(LOG_WARNING, "%s version of tacacs client_map_file %d" + " != expected %d proceeding anyway", libname, map->tac_mapversion, + MAP_FILE_VERSION); + if((session == -1 || map->tac_session == session) && + (auid == -1 || map->tac_mapuid == auid)) { + if(!name) + return 1; /* usually cleanup, just auid and session match */ + switch(which) { + case MATCH_MAPPED: + if(!strcmp(name, map->tac_mappedname)) + return 1; + break; + case MATCH_LOGIN: + if(!strcmp(name, map->tac_logname)) + return 1; + break; + default: + syslog(LOG_WARNING, "%s invalid lookup type %d", libname, which); + break; + } + } + return 0; +} + + +/* + * Lookup the mapname (i.e. tacacs15) to see if there is a match, in the + * mapping file. and return the mapped original login name, if so. Otherwise + * returns the mapname passed as first argument. Passing mapname as NULL + * requests match on auid and session only. + * + * This only works while a mapped user is logged in, and since the auid and + * session are lookup keys, only for processes that are descendents + * of the mapped login, unless they are passed as wildcards (-1) + * + * we need to look up the auid locally only, to avoid recursing into the + * tacacs code. This could cause problems if somebody is using local + * users, ldap, and tacacs, but we just require that the mapped user always + * be a local user. Since the local user password isn't supposed to be + * used, that should be OK. + * + * We take a shared lock to prevent looking at the file while it's being + * updated. + * + * If returned pointer != first arg, caller should free it. + * There isn't a really good way to validate that an entry is still + * live, without searching through all the /proc/PID/sessionid files. + * + * If mapname is NULL, only match on auid & session. Used for audit records + * and cleanup. + * + * We don't record the PID because we can't get it right under all + * circumstances. If we could, it would help sanity checks. + * + * If somebody kills, e.g., the session parent login or sshd, nothing is + * left around to do the cleanup, and the entry could remain forever. + * update_loguid() does this on every add and delete. + */ +char *lookup_logname(const char *mapname, uid_t auid, unsigned session, + char **host, uint16_t *flags) +{ + struct tacacs_mapping map; + char *origuser = (char *)mapname; /* if no match, return original */ + int fd, cnt; + + if (flags) + *flags = 0; /* for early returns */ + fd = open(mapfile, O_RDONLY, 0600); + if(fd == -1) + return (char *)mapname; /* not using tacacs or might be earlier error */ + + if(flock(fd, LOCK_SH)) + syslog(LOG_WARNING, "%s lock of tacacs client_map_file %s failed: %m, " + "proceeding anyway", libname, mapfile); + + while((cnt=read(fd, &map, sizeof map)) == sizeof map) { + if(is_mapmatch(&map, MATCH_MAPPED, mapname, auid, session)) { + origuser = strndup(map.tac_logname, sizeof map.tac_logname); + if(!origuser) { + syslog(LOG_WARNING, + "%s failed to allocate memory, user %.*s: %m", + libname, (int)sizeof map.tac_logname, + map.tac_logname); + origuser = (char *)mapname; + } + if(host) + *host = strndup(map.tac_rhost, sizeof map.tac_rhost); + if (flags) + *flags = map.tac_mapflags; /* for early returns */ + break; + } + } + if(cnt > 0 && cnt != sizeof map) + syslog(LOG_WARNING, + "%s corrupted tacacs client_map_file %s: read wrong size %d", + libname, mapfile, cnt); + (void)flock(fd, LOCK_UN); + close(fd); + return origuser; +} + +/* + * Similar to lookup_logname(), but by uid. + * Returns the original login username, and the mapped name + * in the copied to the buffered pointed to by mapped + * If auid and/or session are -1, they are wildcards, take + * the first matching uid from the mapfile + * Returns NULL if not found. + * + * NOTE: if this function ABI changes, sudo's plugins/sudoers/parse.c + * must be changed to match, since it dlopens and looks up and calls + * this function. + */ +char *lookup_mapuid(uid_t uid, uid_t auid, unsigned session, + char *mappedname, size_t maplen, uint16_t *flags) +{ + struct tacacs_mapping map; + int fd, cnt; + char *loginname = NULL; + + fd = open(mapfile, O_RDONLY, 0600); + if(fd == -1) + return NULL; /* not using tacacs or might be earlier error */ + + if(flock(fd, LOCK_SH)) + syslog(LOG_WARNING, "%s lock of tacacs client_map_file %s failed: %m, " + "proceeding anyway", libname, mapfile); + + while((cnt=read(fd, &map, sizeof map)) == sizeof map) { + if(map.tac_mapuid == uid && + is_mapmatch(&map, MATCH_LOGIN, NULL, auid, session)) { + loginname = strdup(map.tac_logname); /* this may leak */ + snprintf(mappedname, maplen, "%s", map.tac_mappedname); + if (flags) + *flags = map.tac_mapflags; /* for early returns */ + break; + } + } + if(cnt > 0 && cnt != sizeof map) + syslog(LOG_WARNING, + "%s corrupted tacacs client_map_file %s: read wrong size %d", + libname, mapfile, cnt); + (void)flock(fd, LOCK_UN); + close(fd); + return loginname; +} + +/* + * Like lookup_logname(), but matches on the original login name, + * and returns the matching mapped name (e.g, tacacs0) if found, + * otherwise returns the logname argument. auid and session + * will most commonly be -1 wildcards for this function. + */ +char *lookup_mapname(const char *logname, uid_t auid, unsigned session, + char **host, uint16_t *flags) +{ + struct tacacs_mapping map; + char *mappeduser = (char *)logname; /* if no match, return original */ + int fd, cnt; + + if (flags) + *flags = 0; /* for early returns */ + fd = open(mapfile, O_RDONLY, 0600); + if(fd == -1) + return (char *)logname; /* not using tacacs or might be earlier error */ + + if(flock(fd, LOCK_SH)) + syslog(LOG_WARNING, "%s lock of tacacs client_map_file %s failed: %m, " + "proceeding anyway", libname, mapfile); + + while((cnt=read(fd, &map, sizeof map)) == sizeof map) { + if(is_mapmatch(&map, MATCH_LOGIN, logname, auid, session)) { + mappeduser = strndup(map.tac_mappedname, sizeof map.tac_mappedname); + if(!mappeduser) { + syslog(LOG_WARNING, + "%s failed to allocate memory, user %.*s: %m", + libname, (int)sizeof map.tac_mappedname, + map.tac_mappedname); + mappeduser = (char*)logname; + } + if(host) + *host = strndup(map.tac_rhost, sizeof map.tac_rhost); + if (flags) + *flags = map.tac_mapflags; /* for early returns */ + break; + } + } + if(cnt > 0 && cnt != sizeof map) + syslog(LOG_WARNING, + "%s corrupted tacacs client_map_file %s: read wrong size %d", + libname, mapfile, cnt); + (void)flock(fd, LOCK_UN); + close(fd); + return mappeduser; +} + +/* + * there isn't an API to get the audit sessionid, so this will + * do. Returns sessionid if we can read it, else 0. + * 0 is not a valid sessionid; default if no auditing is -1U + * Don't cache the value, since it can change. + * We export it for users of this library. + * + * NOTE: if this function ABI changes, sudo's plugins/sudoers/parse.c + * must be changed to match, since it dlopens and looks up and calls + * this function. + */ +unsigned +map_get_sessionid(void) +{ + int fd = -1, cnt; + unsigned id = 0U; + static char buf[12]; + + fd = open("/proc/self/sessionid", O_RDONLY); + if(fd != -1) { + cnt = read(fd, buf, sizeof(buf)); + close(fd); + } + if(fd != -1 && cnt > 0) { + id = strtoul(buf, NULL, 0); + } + return id; +} + +/* + * open the map file, creating if necessary, and verifying permissions + */ +static int +open_map() +{ + int fd; + struct stat st; + + /* + * create exclusive, for first time use; if that fails (regardless + * of errno), try a normal open. + */ + fd = open(mapfile, O_CREAT|O_RDWR|O_EXCL, 0644); + if(fd == -1) + fd = open(mapfile, O_RDWR, 0600); + else + (void)fchmod(fd, 0644); /* deal with restrictive umask settings */ + if(fd == -1) { /* directory missing? What else? */ + syslog(LOG_ERR, "%s unable to open tacacs client_map_file %s: %m", + libname, mapfile); + } + else { + if(fstat(fd, &st) == 0 && !(st.st_mode & S_IROTH)) { + if(fchmod(fd, st.st_mode | S_IROTH)) + syslog(LOG_ERR, "%s unable to chmod tacacs " + "client_map_file %s: %m", libname, mapfile); + } + } + return fd; +} + +/* + * Lookup a sessionid for all /proc/PID/sessionid + * If a match is found, or there are lookup errors, return 0, else return 1. + */ +static int +invalid_session(int mapsess) +{ + DIR *dp; + struct dirent *dptr; + int ret = 0; + + dp = opendir("/proc"); + if(!dp) + return 0; + while((dptr = readdir(dp))) { + char *eptr; + if(strtoul(dptr->d_name, &eptr, 10) && !*eptr) { + /* all numeric, it's a PID */ + char nmbuf[128]; /* always short path */ + char sess_str[16]; + int fd, cnt, sess=0; + snprintf(nmbuf, sizeof nmbuf, "/proc/%s/sessionid", dptr->d_name); + fd = open(nmbuf, O_RDONLY); + if(fd == -1) + syslog(LOG_DEBUG, "%s: %s open fails: %m", libname, nmbuf); + else { + cnt = read(fd, sess_str, sizeof sess_str - 1); + close(fd); + if(cnt > 0) { + sess_str[cnt] = '\0'; + sess = strtoul(sess_str, &eptr, 0); + if(sess == mapsess) { + goto done; + } + } + } + } + } + ret = 1; +done: + closedir(dp); + return ret; +} + +/* + * check for stale (invalid) entries, and clean them up if found. + * Called with the flock() held. + * + * Always write the version to be our current version number. + * If it was different, we warned in is_match(). + */ +static void +chk_cleanup_map(int fd) +{ + struct tacacs_mapping map, tmap; + int cnt; + + if(lseek(fd, 0, SEEK_SET)) + return; + + memset(&map, 0, sizeof(map)); /* make sure it's sane */ + map.tac_mapversion = MAP_FILE_VERSION; + (void)gettimeofday((struct timeval *)&map.tac_tv, NULL); + + while((cnt=read(fd, &tmap, sizeof tmap)) == sizeof tmap) { + if(!tmap.tac_mapversion || tmap.tac_mapversion > MAP_FILE_VERSION || + ((tmap.tac_mapuid || tmap.tac_mappedname) && + tmap.tac_session && invalid_session(tmap.tac_session))) { + off_t off = (off_t)-cnt; + syslog(LOG_WARNING, "%s: Cleaning up stale entry in %s uid=%d, " + "sess=%d, mapuser=%s", libname, mapfile, tmap.tac_mapuid, + tmap.tac_session, tmap.tac_mappedname); + if(lseek(fd, off, SEEK_CUR) == -1) { + syslog(LOG_ERR, + "%s: rewrite seek failed on tacacs client_map_file %s: %m", + libname, mapfile); + break; /* we can't do anything else */ + } + else if(write(fd, &map, sizeof map) != sizeof map) { + /* future lookups will fail... */ + syslog(LOG_ERR, "%s unable to write tacacs client_map_file " + "%s: %m", libname, mapfile); + } + } + } + + /* + * could lead to missing other entries if this was the add call and it + * fails, but there isn't much we can do about it. + */ + (void)lseek(fd, 0, SEEK_SET); +} + + +/* + * Create an entry for the mapped user in our lookup file, with the info + * that will be needed by the audit and nss plugins. + * + * if olduser is NULL, then we are doing cleanup after logout, etc. + * If olduser is non-null we are writing the mapping entry to the map file + * If adding a mapping entry, walk the file to see if there is an unused + * entry that we can re-use. We take an exclusive flock here, shared in + * the lookup code, to avoid corrupting the file. + * + * Because there is a possibility of stale entries, validate and cleanup + * whenever we are doing the update. + * Stale entries can occur when somebody kills, e.g., the session parent + * login or sshd, nothing is left around to do the cleanup, and the entry could + * remain forever. update_loguid() does this on every add and delete. + * + * This would be static, but it needs to be exported to pam_tacplus. + * It is not a public entry point. +*/ + +static void +update_loguid(char *newuser, char *olduser, char *rhost, uint16_t flags) +{ + struct tacacs_mapping map, tmap; + int fd, cnt, foundmatch = 0; + uid_t auid; + unsigned session; + + fd = open_map(); + if(fd == -1) + return; + + if(flock(fd, LOCK_EX)) + syslog(LOG_WARNING, "%s unable to lock tacacs client_map_file %s: %m," + " proceeding anyway", libname, mapfile); + + if(olduser) /* check and cleanup before adding */ + chk_cleanup_map(fd); + + memset(&map, 0, sizeof(map)); /* make sure it's sane */ + auid = audit_getloginuid(); + session = map_get_sessionid(); + + if(olduser) { + /* so we can map back for later accounting and for nss_tacplus; newuser + * *should* always be non-null. olduser will be NULL at logout */ + snprintf(map.tac_logname, sizeof map.tac_logname, "%s", + newuser ? newuser : ""); + snprintf(map.tac_mappedname, sizeof map.tac_mappedname, "%s", + olduser ? olduser : ""); + snprintf(map.tac_rhost, sizeof map.tac_rhost, "%s", + rhost ? rhost : ""); + map.tac_mapuid = auid; + map.tac_session = session; + map.tac_mapflags = (uint16_t)(flags & MAP_USERHOMEDIR); + } + + (void)gettimeofday((struct timeval *)&map.tac_tv, NULL); + map.tac_mapversion = MAP_FILE_VERSION; + + while(!foundmatch && (cnt=read(fd, &tmap, sizeof tmap)) == sizeof tmap) { + if(olduser && !tmap.tac_mapuid && !tmap.tac_session) { + foundmatch = 1; /* found an empty slot to use. */ + } + if(!olduser && is_mapmatch(&tmap, MATCH_LOGIN, newuser, auid, + session)) { + foundmatch = 1; + } + } + if(cnt > 0 && cnt != sizeof map) + syslog(LOG_WARNING, + "%s: corrupted tacacs client_map_file %s: incorrect size %d read", + libname, mapfile, cnt); + + if(!olduser && !foundmatch) { + goto done; + } + + if(foundmatch) { /* found entry to overwrite, either to NULL or re-use */ + off_t off = (off_t)-cnt; + if(lseek(fd, off, SEEK_CUR) == -1) { + syslog(LOG_ERR, + "%s: rewrite seek failed on tacacs client_map_file %s: %m", + libname, mapfile); + goto done; + } + } + else if(!newuser) { + /* + * if we didn't find entry to clear, something went wrong, + * so don't write an empty entry at the end. + */ + goto done; + } + + /* either overwrite an existing entry, or write new at end */ + if(write(fd, &map, sizeof map) != sizeof map) { + /* future lookups will fail... */ + syslog(LOG_ERR, "%s unable to write tacacs client_map_file %s: %m", + libname, mapfile); + } +done: + if(!olduser) /* check and cleanup after deleting */ + chk_cleanup_map(fd); + (void)flock(fd, LOCK_UN); + (void)fsync(fd); + close(fd); +} + +/* entry point from pam_tacplus for cleanup on close (logout) */ +void +__update_loguid(char *newuser) +{ + update_loguid(newuser, NULL, NULL, 0); +} + +/* + * Set the audit login uid to be immutable; not supported on older libs/kernsls + */ +void set_auid_immutable(void) +{ + static int first = 1; + int fd; + + if(!first) + return; + first = 0; /* only try once */ + fd = audit_open(); + if(fd == -1) + return; /* this should never happen */ + if(audit_set_loginuid_immutable(fd) < 0) + syslog(LOG_WARNING, "%s: Unable to set loginuid to be immutable: %m", libname); + close(fd); +} + +/* + * Check to see if login name found in /etc/passwd. If so, use it. If not + * try to map to a localuser tacacsN where N <= to the TACACS+ privilege level. + * The NSS lookup code needs to match this same algorithm. + * + * Returns 1 if user was mapped (!islocal), 0 if not mapped + */ +int +update_mapuser(char *user, unsigned priv_level, char *rhost, unsigned flags) +{ + FILE *pwfile; + struct passwd *ent; + char tacuser[9]; /* "tacacs" + up to two digits plus 0 */ + int islocal, foundtac; + unsigned priv = priv_level; + unsigned isrestrict = 0; + uid_t luid=0, tuid=0; + + pwfile = fopen("/etc/passwd", "r"); + if(!pwfile) { + syslog(LOG_WARNING, "%s: failed to open /etc/passwd: %m", libname); + return 0; + } + +recheck: + snprintf(tacuser, sizeof tacuser, "tacacs%u", priv); + for(islocal = foundtac = 0; (!islocal || !foundtac) && + (ent = fgetpwent(pwfile)); ) { + if(!ent->pw_name) + continue; /* shouldn't happen */ + if(!strcmp(ent->pw_name, tacuser)) { + foundtac++; + isrestrict = *ent->pw_shell == 'r'; + tuid = ent->pw_uid; + } + } + if(islocal || foundtac) { + uint16_t homeflag; + fclose(pwfile); + pwfile = NULL; + /* + * If priv-level==N, and tacacsN isnt local, but tacacsM (0<=M<N) + * is present, we fallback to that lower level (with a warning logged). + * This sets the session ID (/proc/PID/sessionid) as a side effect, and + * that sessionid will remain the same for all child processes (unless + * something "incorrectly", calls audit_setloginuid() again. + * + * We call it here, instead of requiring pam_loginuid in pam.d/sshd, + * login, etc. because we need the info earlier than it is really + * possible via the normal pam auth/session sequencing. + */ + audit_setloginuid(islocal?luid:tuid); /* set auid */ + /* + * if USERHOMEDIR is set, we'll save that flag, and libnss-tacplus + * will return the login name in the pw_dir field replacing the + * local tacacsN homedir, unless the shell is a restricted shell, + * indicating that per-command authorization is enabled. + */ + homeflag = (foundtac && !isrestrict) ? flags&MAP_USERHOMEDIR : 0; + update_loguid(user, islocal?user:tacuser, rhost, homeflag); + set_auid_immutable(); + if(debug && !islocal && priv != priv_level) + syslog(LOG_DEBUG, "%s: Did not find local tacacs%u , using %s", + libname, priv_level, tacuser); + } + else if(priv > 0) { + priv--; + rewind(pwfile); + goto recheck; + } + if(pwfile) + fclose(pwfile); + return !islocal; +} + + +/* + * lookup a uid only in the local password file (to avoid tacacs recursion). + * This is supposed to be the mapped user, which should always be a local + * user, so we don't need to care about ldap or other remote mechanisms. + * Returns a pointer to strdup'ed memory, if found. Caller must free, + * or it will leak. + */ +static char *lookup_local_uid(uid_t auid) +{ + FILE *pwfile; + struct passwd *ent; + char *pwname = NULL; /* will be strdup'ed on success */ + + pwfile = fopen("/etc/passwd", "r"); + if(!pwfile) { + syslog(LOG_WARNING, "%s: failed to open /etc/passwd: %m", libname); + return NULL; + } + while((ent = fgetpwent(pwfile)) && ent->pw_uid != auid) + ; + if(ent) + pwname = strdup(ent->pw_name); + fclose(pwfile); + return pwname; +} + +/* + * If a mapped user entry already exists, we are probably being + * used for su or sudo, so we need to get the original user password, + * rather than the mapped user (the generic NSS lookup doesn't need + * the password). + * Never lookup for uid == 0 (login process, or root doing sudo), to avoid + * causing any issues (and because it's pointless). + * + * If auid != uid, and audit session ID already set, then do the lookup. + * + * We return strndup'ed memory on success, which will be leaked if not freed. + * That's OK, given that this is typically called only once per program, and + * that usernames are short. + */ +char *get_user_to_auth(char *pamuser) +{ + char *mapuser, *origuser; + unsigned session; + uid_t auid; + + if(pamuser == NULL) + return NULL; + + auid = audit_getloginuid(); + if(auid == (uid_t)-1 || !auid) + return pamuser; + session = map_get_sessionid(); + if(session == ~0U) /* sessionid not set or not enabled */ + return pamuser; + + mapuser = lookup_local_uid(auid); + if(!mapuser) + return pamuser; + + if(strcmp(pamuser, mapuser)) { + free(mapuser); + return pamuser; + } + free(mapuser); /* done now */ + + /* returns malloced string of original user, if found, which will + * be a memory leak, but that shouldn't matter + */ + origuser = lookup_logname(pamuser, auid, session, NULL, NULL); + return origuser ? origuser : pamuser; +} |