diff options
Diffstat (limited to 'modules/pam_time/pam_time.c')
-rw-r--r-- | modules/pam_time/pam_time.c | 677 |
1 files changed, 677 insertions, 0 deletions
diff --git a/modules/pam_time/pam_time.c b/modules/pam_time/pam_time.c new file mode 100644 index 0000000..089ae22 --- /dev/null +++ b/modules/pam_time/pam_time.c @@ -0,0 +1,677 @@ +/* + * pam_time module + * + * Written by Andrew Morgan <morgan@linux.kernel.org> 1996/6/22 + * (File syntax and much other inspiration from the shadow package + * shadow-960129) + * Field parsing rewritten by Tomas Mraz <tm@t8m.info> + */ + +#include "config.h" + +#include <sys/file.h> +#include <stdio.h> +#include <stdlib.h> +#include <ctype.h> +#include <unistd.h> +#include <stdarg.h> +#include <time.h> +#include <syslog.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <netdb.h> + +#include <security/_pam_macros.h> +#include <security/pam_modules.h> +#include <security/pam_ext.h> +#include <security/pam_modutil.h> +#include "pam_inline.h" + +#ifdef HAVE_LIBAUDIT +#include <libaudit.h> +#endif + +#define PAM_TIME_BUFLEN 1000 +#define FIELD_SEPARATOR ';' /* this is new as of .02 */ + +#define PAM_DEBUG_ARG 0x0001 +#define PAM_NO_AUDIT 0x0002 + +#ifndef TRUE +# define TRUE 1 +#endif +#ifndef FALSE +# define FALSE 0 +#endif + +typedef enum { AND, OR } operator; + +static int +_pam_parse (const pam_handle_t *pamh, int argc, const char **argv, const char **conffile) +{ + int ctrl = 0; + + *conffile = PAM_TIME_CONF; + /* step through arguments */ + for (; argc-- > 0; ++argv) { + const char *str; + + /* generic options */ + + if (!strcmp(*argv, "debug")) { + ctrl |= PAM_DEBUG_ARG; + } else if (!strcmp(*argv, "noaudit")) { + ctrl |= PAM_NO_AUDIT; + } else if ((str = pam_str_skip_prefix(*argv, "conffile=")) != NULL) { + if (str[0] == '\0') { + pam_syslog(pamh, LOG_ERR, + "conffile= specification missing argument - ignored"); + } else { + *conffile = str; + D(("new Configuration File: %s", *conffile)); + } + } else { + pam_syslog(pamh, LOG_ERR, "unknown option: %s", *argv); + } + } + + return ctrl; +} + +/* --- static functions for checking whether the user should be let in --- */ + +static char * +shift_buf(char *mem, int from) +{ + char *start = mem; + while ((*mem = mem[from]) != '\0') + ++mem; + memset(mem, '\0', PAM_TIME_BUFLEN - (mem - start)); + return mem; +} + +static void +trim_spaces(char *buf, char *from) +{ + while (from > buf) { + --from; + if (*from == ' ') + *from = '\0'; + else + break; + } +} + +#define STATE_NL 0 /* new line starting */ +#define STATE_COMMENT 1 /* inside comment */ +#define STATE_FIELD 2 /* field following */ +#define STATE_EOF 3 /* end of file or error */ + +static int +read_field(const pam_handle_t *pamh, int fd, char **buf, int *from, int *state, const char *file) +{ + char *to; + char *src; + int i; + char c; + int onspace; + + /* is buf set ? */ + if (! *buf) { + *buf = (char *) calloc(1, PAM_TIME_BUFLEN+1); + if (! *buf) { + pam_syslog(pamh, LOG_CRIT, "out of memory"); + D(("no memory")); + *state = STATE_EOF; + return -1; + } + *from = 0; + *state = STATE_NL; + fd = open(file, O_RDONLY); + if (fd < 0) { + pam_syslog(pamh, LOG_ERR, "error opening %s: %m", file); + _pam_drop(*buf); + *state = STATE_EOF; + return -1; + } + } + + + if (*from > 0) + to = shift_buf(*buf, *from); + else + to = *buf; + + while (fd != -1 && to - *buf < PAM_TIME_BUFLEN) { + i = pam_modutil_read(fd, to, PAM_TIME_BUFLEN - (to - *buf)); + if (i < 0) { + pam_syslog(pamh, LOG_ERR, "error reading %s: %m", file); + close(fd); + memset(*buf, 0, PAM_TIME_BUFLEN); + _pam_drop(*buf); + *state = STATE_EOF; + return -1; + } else if (!i) { + close(fd); + fd = -1; /* end of file reached */ + } + + to += i; + } + + if (to == *buf) { + /* nothing previously in buf, nothing read */ + _pam_drop(*buf); + *state = STATE_EOF; + return -1; + } + + memset(to, '\0', PAM_TIME_BUFLEN - (to - *buf)); + + to = *buf; + onspace = 1; /* delete any leading spaces */ + + for (src = to; (c=*src) != '\0'; ++src) { + if (*state == STATE_COMMENT && c != '\n') { + continue; + } + + switch (c) { + case '\n': + *state = STATE_NL; + *to = '\0'; + *from = (src - *buf) + 1; + trim_spaces(*buf, to); + return fd; + + case '\t': + case ' ': + if (!onspace) { + onspace = 1; + *to++ = ' '; + } + break; + + case '!': + onspace = 1; /* ignore following spaces */ + *to++ = '!'; + break; + + case '#': + *state = STATE_COMMENT; + break; + + case FIELD_SEPARATOR: + *state = STATE_FIELD; + *to = '\0'; + *from = (src - *buf) + 1; + trim_spaces(*buf, to); + return fd; + + case '\\': + if (src[1] == '\n') { + ++src; /* skip it */ + break; + } + /* fallthrough */ + default: + *to++ = c; + onspace = 0; + } + if (src > to) + *src = '\0'; /* clearing */ + } + + if (*state != STATE_COMMENT) { + *state = STATE_COMMENT; + pam_syslog(pamh, LOG_ERR, "field too long - ignored"); + **buf = '\0'; + } else { + *to = '\0'; + trim_spaces(*buf, to); + } + + *from = 0; + return fd; +} + +/* read a member from a field */ + +static int +logic_member(const char *string, int *at) +{ + int c,to; + int done=0; + int token=0; + + to=*at; + do { + c = string[to++]; + + switch (c) { + + case '\0': + --to; + done = 1; + break; + + case '&': + case '|': + case '!': + if (token) { + --to; + } + done = 1; + break; + + default: + if (isalpha(c) || c == '*' || isdigit(c) || c == '_' + || c == '-' || c == '.' || c == '/' || c == ':') { + token = 1; + } else if (token) { + --to; + done = 1; + } else { + ++*at; + } + } + } while (!done); + + return to - *at; +} + +typedef enum { VAL, OP } expect; + +static int +logic_field(pam_handle_t *pamh, const void *me, const char *x, int rule, + int (*agrees)(pam_handle_t *pamh, + const void *, const char *, int, int)) +{ + int left=FALSE, right, not=FALSE; + operator oper=OR; + int at=0, l; + expect next=VAL; + + while ((l = logic_member(x,&at))) { + int c = x[at]; + + if (next == VAL) { + if (c == '!') + not = !not; + else if (isalpha(c) || c == '*' || isdigit(c) || c == '_' + || c == '-' || c == '.' || c == '/' || c == ':') { + right = not ^ agrees(pamh, me, x+at, l, rule); + if (oper == AND) + left &= right; + else + left |= right; + next = OP; + } else { + pam_syslog(pamh, LOG_ERR, + "garbled syntax; expected name (rule #%d)", + rule); + return FALSE; + } + } else { /* OP */ + switch (c) { + case '&': + oper = AND; + break; + case '|': + oper = OR; + break; + default: + pam_syslog(pamh, LOG_ERR, + "garbled syntax; expected & or | (rule #%d)", + rule); + D(("%c at %d",c,at)); + return FALSE; + } + next = VAL; + not = FALSE; + } + at += l; + } + + return left; +} + +static int +is_same(pam_handle_t *pamh UNUSED, const void *A, const char *b, + int len, int rule UNUSED) +{ + int i; + const char *a; + + a = A; + for (i=0; len > 0; ++i, --len) { + if (b[i] != a[i]) { + if (b[i++] == '*') { + return (!--len || !strncmp(b+i,a+strlen(a)-len,len)); + } else + return FALSE; + } + } + + /* Ok, we know that b is a substring from A and does not contain + wildcards, but now the length of both strings must be the same, + too. In this case it means, a[i] has to be the end of the string. */ + if (a[i] != '\0') + return FALSE; + + return ( !len ); +} + +typedef struct { + int day; /* array of 7 bits, one set for today */ + int minute; /* integer, hour*100+minute for now */ +} TIME; + +static struct day { + const char *d; + int bit; +} const days[11] = { + { "su", 01 }, + { "mo", 02 }, + { "tu", 04 }, + { "we", 010 }, + { "th", 020 }, + { "fr", 040 }, + { "sa", 0100 }, + { "wk", 076 }, + { "wd", 0101 }, + { "al", 0177 }, + { NULL, 0 } +}; + +static TIME +time_now(void) +{ + struct tm *local; + time_t the_time; + TIME this; + + the_time = time((time_t *)0); /* get the current time */ + local = localtime(&the_time); + this.day = days[local->tm_wday].bit; + this.minute = local->tm_hour*100 + local->tm_min; + + D(("day: 0%o, time: %.4d", this.day, this.minute)); + return this; +} + +/* take the current date and see if the range "date" passes it */ +static int +check_time(pam_handle_t *pamh, const void *AT, const char *times, + int len, int rule) +{ + int not,pass; + int marked_day, time_start, time_end; + const TIME *at; + int i,j=0; + + at = AT; + D(("chcking: 0%o/%.4d vs. %s", at->day, at->minute, times)); + + if (times == NULL) { + /* this should not happen */ + pam_syslog(pamh, LOG_CRIT, + "internal error in file %s at line %d", + __FILE__, __LINE__); + return FALSE; + } + + if (times[j] == '!') { + ++j; + not = TRUE; + } else { + not = FALSE; + } + + for (marked_day = 0; len > 0 && isalpha(times[j]); --len) { + int this_day=-1; + + D(("%c%c ?", times[j], times[j+1])); + for (i=0; days[i].d != NULL; ++i) { + if (tolower(times[j]) == days[i].d[0] + && tolower(times[j+1]) == days[i].d[1] ) { + this_day = days[i].bit; + break; + } + } + j += 2; + if (this_day == -1) { + pam_syslog(pamh, LOG_ERR, "bad day specified (rule #%d)", rule); + return FALSE; + } + marked_day ^= this_day; + } + if (marked_day == 0) { + pam_syslog(pamh, LOG_ERR, "no day specified"); + return FALSE; + } + D(("day range = 0%o", marked_day)); + + time_start = 0; + for (i=0; len > 0 && i < 4 && isdigit(times[i+j]); ++i, --len) { + time_start *= 10; + time_start += times[i+j]-'0'; /* is this portable? */ + } + j += i; + + if (times[j] == '-') { + time_end = 0; + for (i=1; len > 0 && i < 5 && isdigit(times[i+j]); ++i, --len) { + time_end *= 10; + time_end += times[i+j]-'0'; /* is this portable */ + } + j += i; + } else + time_end = -1; + + D(("i=%d, time_end=%d, times[j]='%c'", i, time_end, times[j])); + if (i != 5 || time_end == -1) { + pam_syslog(pamh, LOG_ERR, "no/bad times specified (rule #%d)", rule); + return TRUE; + } + D(("times(%d to %d)", time_start,time_end)); + D(("marked_day = 0%o", marked_day)); + + /* compare with the actual time now */ + + pass = FALSE; + if (time_start < time_end) { /* start < end ? --> same day */ + if ((at->day & marked_day) && (at->minute >= time_start) + && (at->minute < time_end)) { + D(("time is listed")); + pass = TRUE; + } + } else { /* spans two days */ + if ((at->day & marked_day) && (at->minute >= time_start)) { + D(("caught on first day")); + pass = TRUE; + } else { + marked_day <<= 1; + marked_day |= (marked_day & 0200) ? 1:0; + D(("next day = 0%o", marked_day)); + if ((at->day & marked_day) && (at->minute <= time_end)) { + D(("caught on second day")); + pass = TRUE; + } + } + } + + return (not ^ pass); +} + +static int +check_account(pam_handle_t *pamh, const char *service, + const char *tty, const char *user, const char *file) +{ + int from=0, state=STATE_NL, fd=-1; + char *buffer=NULL; + int count=0; + TIME here_and_now; + int retval=PAM_SUCCESS; + + here_and_now = time_now(); /* find current time */ + do { + int good=TRUE,intime; + + /* here we get the service name field */ + + fd = read_field(pamh, fd, &buffer, &from, &state, file); + if (!buffer || !buffer[0]) { + /* empty line .. ? */ + continue; + } + ++count; + + if (state != STATE_FIELD) { + pam_syslog(pamh, LOG_ERR, + "%s: malformed rule #%d", file, count); + continue; + } + + good = logic_field(pamh, service, buffer, count, is_same); + D(("with service: %s", good ? "passes":"fails" )); + + /* here we get the terminal name field */ + + fd = read_field(pamh, fd, &buffer, &from, &state, file); + if (state != STATE_FIELD) { + pam_syslog(pamh, LOG_ERR, + "%s: malformed rule #%d", file, count); + continue; + } + good &= logic_field(pamh, tty, buffer, count, is_same); + D(("with tty: %s", good ? "passes":"fails" )); + + /* here we get the username field */ + + fd = read_field(pamh, fd, &buffer, &from, &state, file); + if (state != STATE_FIELD) { + pam_syslog(pamh, LOG_ERR, + "%s: malformed rule #%d", file, count); + continue; + } + /* If buffer starts with @, we are using netgroups */ + if (buffer[0] == '@') +#ifdef HAVE_INNETGR + good &= innetgr (&buffer[1], NULL, user, NULL); +#else + pam_syslog (pamh, LOG_ERR, "pam_time does not have netgroup support"); +#endif + else + good &= logic_field(pamh, user, buffer, count, is_same); + D(("with user: %s", good ? "passes":"fails" )); + + /* here we get the time field */ + + fd = read_field(pamh, fd, &buffer, &from, &state, file); + if (state == STATE_FIELD) { + pam_syslog(pamh, LOG_ERR, + "%s: poorly terminated rule #%d", file, count); + continue; + } + + intime = logic_field(pamh, &here_and_now, buffer, count, check_time); + D(("with time: %s", intime ? "passes":"fails" )); + + if (good && !intime) { + /* + * for security parse whole file.. also need to ensure + * that the buffer is free()'d and the file is closed. + */ + retval = PAM_PERM_DENIED; + } else { + D(("rule passed")); + } + } while (state != STATE_EOF); + + return retval; +} + +/* --- public account management functions --- */ + +int +pam_sm_acct_mgmt(pam_handle_t *pamh, int flags UNUSED, + int argc, const char **argv) +{ + const void *service=NULL, *void_tty=NULL; + const char *tty; + const char *user=NULL; + const char *conf_file = NULL; + int ctrl; + int rv; + + ctrl = _pam_parse(pamh, argc, argv, &conf_file); + + if (ctrl & PAM_DEBUG_ARG) { + pam_syslog(pamh, LOG_DEBUG, "conffile=%s", conf_file); + } + + /* set service name */ + + if (pam_get_item(pamh, PAM_SERVICE, &service) + != PAM_SUCCESS || service == NULL) { + pam_syslog(pamh, LOG_ERR, "cannot find the current service name"); + return PAM_ABORT; + } + + /* set username */ + + if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || *user == '\0') { + pam_syslog(pamh, LOG_NOTICE, "cannot determine user name"); + return PAM_USER_UNKNOWN; + } + + /* set tty name */ + + if (pam_get_item(pamh, PAM_TTY, &void_tty) != PAM_SUCCESS + || void_tty == NULL) { + D(("PAM_TTY not set, probing stdin")); + tty = ttyname(STDIN_FILENO); + if (tty == NULL) { + tty = ""; + } + if (pam_set_item(pamh, PAM_TTY, tty) != PAM_SUCCESS) { + pam_syslog(pamh, LOG_ERR, "couldn't set tty name"); + return PAM_ABORT; + } + } + else + tty = void_tty; + + if (tty[0] == '/') { /* full path */ + const char *t; + tty++; + if ((t = strchr(tty, '/')) != NULL) { + tty = t + 1; + } + } + + /* good, now we have the service name, the user and the terminal name */ + + D(("service=%s", service)); + D(("user=%s", user)); + D(("tty=%s", tty)); + + rv = check_account(pamh, service, tty, user, conf_file); + if (rv != PAM_SUCCESS) { +#ifdef HAVE_LIBAUDIT + if (!(ctrl & PAM_NO_AUDIT)) { + pam_modutil_audit_write(pamh, AUDIT_ANOM_LOGIN_TIME, + "pam_time", rv); /* ignore return value as we fail anyway */ + } +#endif + if (ctrl & PAM_DEBUG_ARG) { + pam_syslog(pamh, LOG_DEBUG, "user %s rejected", user); + } + } + return rv; +} + +/* end of module definition */ |