mirror of https://gitee.com/openkylin/pam.git
670 lines
15 KiB
C
670 lines
15 KiB
C
/* 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>
|
|
|
|
#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;
|
|
|
|
/*
|
|
* 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 <security/_pam_macros.h>
|
|
#include <security/pam_modules.h>
|
|
#include <security/pam_ext.h>
|
|
#include <security/pam_modutil.h>
|
|
|
|
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_CRIT, "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 */
|