/* pam_time module */ /* * Written by Andrew Morgan 1996/6/22 * (File syntax and much other inspiration from the shadow package * shadow-960129) * Field parsing rewritten by Tomas Mraz */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_LIBAUDIT #include #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; /* * here, we make definitions for the externally accessible functions * in this file (these definitions are required for static modules * but strongly encouraged generally) they are used to instruct the * modules include file to define their prototypes. */ #define PAM_SM_ACCOUNT #include #include #include #include static int _pam_parse (const pam_handle_t *pamh, int argc, const char **argv) { int ctrl = 0; /* step through arguments */ for (; argc-- > 0; ++argv) { /* generic options */ if (!strcmp(*argv, "debug")) { ctrl |= PAM_DEBUG_ARG; } else if (!strcmp(*argv, "noaudit")) { ctrl |= PAM_NO_AUDIT; } 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) { 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_ERR, "out of memory"); D(("no memory")); *state = STATE_EOF; return -1; } *from = 0; *state = STATE_NL; fd = open(PAM_TIME_CONF, O_RDONLY); if (fd < 0) { pam_syslog(pamh, LOG_ERR, "error opening %s: %m", PAM_TIME_CONF); _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", PAM_TIME_CONF); 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; } 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; } 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) { 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); if (!buffer || !buffer[0]) { /* empty line .. ? */ continue; } ++count; if (state != STATE_FIELD) { pam_syslog(pamh, LOG_ERR, "%s: malformed rule #%d", PAM_TIME_CONF, 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); if (state != STATE_FIELD) { pam_syslog(pamh, LOG_ERR, "%s: malformed rule #%d", PAM_TIME_CONF, 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); if (state != STATE_FIELD) { pam_syslog(pamh, LOG_ERR, "%s: malformed rule #%d", PAM_TIME_CONF, 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); if (state == STATE_FIELD) { pam_syslog(pamh, LOG_ERR, "%s: poorly terminated rule #%d", PAM_TIME_CONF, 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; int ctrl; int rv; ctrl = _pam_parse(pamh, argc, argv); /* 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 == NULL || *user == '\0') { pam_syslog(pamh, LOG_ERR, "can not get the username"); 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); 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 */