1 /*
2 * pam_time module
3 *
4 * Written by Andrew Morgan <morgan@linux.kernel.org> 1996/6/22
5 * (File syntax and much other inspiration from the shadow package
6 * shadow-960129)
7 * Field parsing rewritten by Tomas Mraz <tm@t8m.info>
8 */
9
10 #include "config.h"
11
12 #include <sys/file.h>
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <ctype.h>
16 #include <unistd.h>
17 #include <stdarg.h>
18 #include <time.h>
19 #include <syslog.h>
20 #include <string.h>
21 #include <sys/types.h>
22 #include <sys/stat.h>
23 #include <fcntl.h>
24 #include <netdb.h>
25
26 #include <security/_pam_macros.h>
27 #include <security/pam_modules.h>
28 #include <security/pam_ext.h>
29 #include <security/pam_modutil.h>
30 #include "pam_inline.h"
31
32 #ifdef HAVE_LIBAUDIT
33 #include <libaudit.h>
34 #endif
35
36 #define PAM_TIME_CONF (SCONFIGDIR "/time.conf")
37 #ifdef VENDOR_SCONFIGDIR
38 #define VENDOR_PAM_TIME_CONF (VENDOR_SCONFIGDIR "/time.conf")
39 #endif
40
41 #define PAM_TIME_BUFLEN 1000
42 #define FIELD_SEPARATOR ';' /* this is new as of .02 */
43
44 #define PAM_DEBUG_ARG 0x0001
45 #define PAM_NO_AUDIT 0x0002
46
47 #ifndef TRUE
48 # define TRUE 1
49 #endif
50 #ifndef FALSE
51 # define FALSE 0
52 #endif
53
54 typedef enum { AND, OR } operator;
55
56 static int
57 _pam_parse (const pam_handle_t *pamh, int argc, const char **argv, const char **conffile)
58 {
59 int ctrl = 0;
60
61 *conffile = NULL;
62 /* step through arguments */
63 for (; argc-- > 0; ++argv) {
64 const char *str;
65
66 /* generic options */
67
68 if (!strcmp(*argv, "debug")) {
69 ctrl |= PAM_DEBUG_ARG;
70 } else if (!strcmp(*argv, "noaudit")) {
71 ctrl |= PAM_NO_AUDIT;
72 } else if ((str = pam_str_skip_prefix(*argv, "conffile=")) != NULL) {
73 if (str[0] == '\0') {
74 pam_syslog(pamh, LOG_ERR,
75 "conffile= specification missing argument - ignored");
76 } else {
77 *conffile = str;
78 D(("new Configuration File: %s", *conffile));
79 }
80 } else {
81 pam_syslog(pamh, LOG_ERR, "unknown option: %s", *argv);
82 }
83 }
84
85 if (*conffile == NULL) {
86 *conffile = PAM_TIME_CONF;
87 #ifdef VENDOR_PAM_TIME_CONF
88 /*
89 * Check whether PAM_TIME_CONF file is available.
90 * If it does not exist, fall back to VENDOR_PAM_TIME_CONF file.
91 */
92 struct stat buffer;
93 if (stat(*conffile, &buffer) != 0 && errno == ENOENT) {
94 *conffile = VENDOR_PAM_TIME_CONF;
95 }
96 #endif
97 }
98
99 return ctrl;
100 }
101
102 /* --- static functions for checking whether the user should be let in --- */
103
104 static char *
105 shift_buf(char *mem, int from)
106 {
107 char *start = mem;
108 while ((*mem = mem[from]) != '\0')
109 ++mem;
110 pam_overwrite_n(mem, PAM_TIME_BUFLEN - (mem - start));
111 return mem;
112 }
113
114 static void
115 trim_spaces(char *buf, char *from)
116 {
117 while (from > buf) {
118 --from;
119 if (*from == ' ')
120 *from = '\0';
121 else
122 break;
123 }
124 }
125
126 #define STATE_NL 0 /* new line starting */
127 #define STATE_COMMENT 1 /* inside comment */
128 #define STATE_FIELD 2 /* field following */
129 #define STATE_EOF 3 /* end of file or error */
130
131 static int
132 read_field(const pam_handle_t *pamh, int fd, char **buf, int *from, int *state, const char *file)
133 {
134 char *to;
135 char *src;
136 int i;
137 char c;
138 int onspace;
139
140 /* is buf set ? */
141 if (! *buf) {
142 *buf = (char *) calloc(1, PAM_TIME_BUFLEN+1);
143 if (! *buf) {
144 pam_syslog(pamh, LOG_CRIT, "out of memory");
145 D(("no memory"));
146 *state = STATE_EOF;
147 return -1;
148 }
149 *from = 0;
150 *state = STATE_NL;
151 fd = open(file, O_RDONLY);
152 if (fd < 0) {
153 pam_syslog(pamh, LOG_ERR, "error opening %s: %m", file);
154 _pam_drop(*buf);
155 *state = STATE_EOF;
156 return -1;
157 }
158 }
159
160
161 if (*from > 0)
162 to = shift_buf(*buf, *from);
163 else
164 to = *buf;
165
166 while (fd != -1 && to - *buf < PAM_TIME_BUFLEN) {
167 i = pam_modutil_read(fd, to, PAM_TIME_BUFLEN - (to - *buf));
168 if (i < 0) {
169 pam_syslog(pamh, LOG_ERR, "error reading %s: %m", file);
170 close(fd);
171 pam_overwrite_n(*buf, PAM_TIME_BUFLEN);
172 _pam_drop(*buf);
173 *state = STATE_EOF;
174 return -1;
175 } else if (!i) {
176 close(fd);
177 fd = -1; /* end of file reached */
178 }
179
180 to += i;
181 }
182
183 if (to == *buf) {
184 /* nothing previously in buf, nothing read */
185 _pam_drop(*buf);
186 *state = STATE_EOF;
187 return -1;
188 }
189
190 pam_overwrite_n(to, PAM_TIME_BUFLEN - (to - *buf));
191
192 to = *buf;
193 onspace = 1; /* delete any leading spaces */
194
195 for (src = to; (c=*src) != '\0'; ++src) {
196 if (*state == STATE_COMMENT && c != '\n') {
197 continue;
198 }
199
200 switch (c) {
201 case '\n':
202 *state = STATE_NL;
203 *to = '\0';
204 *from = (src - *buf) + 1;
205 trim_spaces(*buf, to);
206 return fd;
207
208 case '\t':
209 case ' ':
210 if (!onspace) {
211 onspace = 1;
212 *to++ = ' ';
213 }
214 break;
215
216 case '!':
217 onspace = 1; /* ignore following spaces */
218 *to++ = '!';
219 break;
220
221 case '#':
222 *state = STATE_COMMENT;
223 break;
224
225 case FIELD_SEPARATOR:
226 *state = STATE_FIELD;
227 *to = '\0';
228 *from = (src - *buf) + 1;
229 trim_spaces(*buf, to);
230 return fd;
231
232 case '\\':
233 if (src[1] == '\n') {
234 ++src; /* skip it */
235 break;
236 }
237 /* fallthrough */
238 default:
239 *to++ = c;
240 onspace = 0;
241 }
242 if (src > to)
243 *src = '\0'; /* clearing */
244 }
245
246 if (*state != STATE_COMMENT) {
247 *state = STATE_COMMENT;
248 pam_syslog(pamh, LOG_ERR, "field too long - ignored");
249 **buf = '\0';
250 } else {
251 *to = '\0';
252 trim_spaces(*buf, to);
253 }
254
255 *from = 0;
256 return fd;
257 }
258
259 /* read a member from a field */
260
261 static int
262 logic_member(const char *string, int *at)
263 {
264 int c,to;
265 int done=0;
266 int token=0;
267
268 to=*at;
269 do {
270 c = string[to++];
271
272 switch (c) {
273
274 case '\0':
275 --to;
276 done = 1;
277 break;
278
279 case '&':
280 case '|':
281 case '!':
282 if (token) {
283 --to;
284 }
285 done = 1;
286 break;
287
288 default:
289 if (isalpha(c) || c == '*' || isdigit(c) || c == '_'
290 || c == '-' || c == '.' || c == '/' || c == ':') {
291 token = 1;
292 } else if (token) {
293 --to;
294 done = 1;
295 } else {
296 ++*at;
297 }
298 }
299 } while (!done);
300
301 return to - *at;
302 }
303
304 typedef enum { VAL, OP } expect;
305
306 static int
307 logic_field(pam_handle_t *pamh, const void *me, const char *x, int rule,
308 int (*agrees)(pam_handle_t *pamh,
309 const void *, const char *, int, int))
310 {
311 int left=FALSE, right, not=FALSE;
312 operator oper=OR;
313 int at=0, l;
314 expect next=VAL;
315
316 while ((l = logic_member(x,&at))) {
317 int c = x[at];
318
319 if (next == VAL) {
320 if (c == '!')
321 not = !not;
322 else if (isalpha(c) || c == '*' || isdigit(c) || c == '_'
323 || c == '-' || c == '.' || c == '/' || c == ':') {
324 right = not ^ agrees(pamh, me, x+at, l, rule);
325 if (oper == AND)
326 left &= right;
327 else
328 left |= right;
329 next = OP;
330 } else {
331 pam_syslog(pamh, LOG_ERR,
332 "garbled syntax; expected name (rule #%d)",
333 rule);
334 return FALSE;
335 }
336 } else { /* OP */
337 switch (c) {
338 case '&':
339 oper = AND;
340 break;
341 case '|':
342 oper = OR;
343 break;
344 default:
345 pam_syslog(pamh, LOG_ERR,
346 "garbled syntax; expected & or | (rule #%d)",
347 rule);
348 D(("%c at %d",c,at));
349 return FALSE;
350 }
351 next = VAL;
352 not = FALSE;
353 }
354 at += l;
355 }
356
357 return left;
358 }
359
360 static int
361 is_same(pam_handle_t *pamh UNUSED, const void *A, const char *b,
362 int len, int rule UNUSED)
363 {
364 int i;
365 const char *a;
366
367 a = A;
368 for (i=0; len > 0; ++i, --len) {
369 if (b[i] != a[i]) {
370 if (b[i++] == '*') {
371 return (!--len || !strncmp(b+i,a+strlen(a)-len,len));
372 } else
373 return FALSE;
374 }
375 }
376
377 /* Ok, we know that b is a substring from A and does not contain
378 wildcards, but now the length of both strings must be the same,
379 too. In this case it means, a[i] has to be the end of the string. */
380 if (a[i] != '\0')
381 return FALSE;
382
383 return ( !len );
384 }
385
386 typedef struct {
387 int day; /* array of 7 bits, one set for today */
388 int minute; /* integer, hour*100+minute for now */
389 } TIME;
390
391 static struct day {
392 const char *d;
393 int bit;
394 } const days[11] = {
395 { "su", 01 },
396 { "mo", 02 },
397 { "tu", 04 },
398 { "we", 010 },
399 { "th", 020 },
400 { "fr", 040 },
401 { "sa", 0100 },
402 { "wk", 076 },
403 { "wd", 0101 },
404 { "al", 0177 },
405 { NULL, 0 }
406 };
407
408 static TIME
409 time_now(void)
410 {
411 struct tm *local;
412 time_t the_time;
413 TIME this;
414
415 the_time = time((time_t *)0); /* get the current time */
416 local = localtime(&the_time);
417 this.day = days[local->tm_wday].bit;
418 this.minute = local->tm_hour*100 + local->tm_min;
419
420 D(("day: 0%o, time: %.4d", this.day, this.minute));
421 return this;
422 }
423
424 /* take the current date and see if the range "date" passes it */
425 static int
426 check_time(pam_handle_t *pamh, const void *AT, const char *times,
427 int len, int rule)
428 {
429 int not,pass;
430 int marked_day, time_start, time_end;
431 const TIME *at;
432 int i,j=0;
433
434 at = AT;
435 D(("chcking: 0%o/%.4d vs. %s", at->day, at->minute, times));
436
437 if (times == NULL) {
438 /* this should not happen */
439 pam_syslog(pamh, LOG_CRIT,
440 "internal error in file %s at line %d",
441 __FILE__, __LINE__);
442 return FALSE;
443 }
444
445 if (times[j] == '!') {
446 ++j;
447 not = TRUE;
448 } else {
449 not = FALSE;
450 }
451
452 for (marked_day = 0; len > 0 && isalpha(times[j]); --len) {
453 int this_day=-1;
454
455 D(("%c%c ?", times[j], times[j+1]));
456 for (i=0; days[i].d != NULL; ++i) {
457 if (tolower(times[j]) == days[i].d[0]
458 && tolower(times[j+1]) == days[i].d[1] ) {
459 this_day = days[i].bit;
460 break;
461 }
462 }
463 j += 2;
464 if (this_day == -1) {
465 pam_syslog(pamh, LOG_ERR, "bad day specified (rule #%d)", rule);
466 return FALSE;
467 }
468 marked_day ^= this_day;
469 }
470 if (marked_day == 0) {
471 pam_syslog(pamh, LOG_ERR, "no day specified");
472 return FALSE;
473 }
474 D(("day range = 0%o", marked_day));
475
476 time_start = 0;
477 for (i=0; len > 0 && i < 4 && isdigit(times[i+j]); ++i, --len) {
478 time_start *= 10;
479 time_start += times[i+j]-'0'; /* is this portable? */
480 }
481 j += i;
482
483 if (times[j] == '-') {
484 time_end = 0;
485 for (i=1; len > 0 && i < 5 && isdigit(times[i+j]); ++i, --len) {
486 time_end *= 10;
487 time_end += times[i+j]-'0'; /* is this portable */
488 }
489 j += i;
490 } else
491 time_end = -1;
492
493 D(("i=%d, time_end=%d, times[j]='%c'", i, time_end, times[j]));
494 if (i != 5 || time_end == -1) {
495 pam_syslog(pamh, LOG_ERR, "no/bad times specified (rule #%d)", rule);
496 return TRUE;
497 }
498 D(("times(%d to %d)", time_start,time_end));
499 D(("marked_day = 0%o", marked_day));
500
501 /* compare with the actual time now */
502
503 pass = FALSE;
504 if (time_start < time_end) { /* start < end ? --> same day */
505 if ((at->day & marked_day) && (at->minute >= time_start)
506 && (at->minute < time_end)) {
507 D(("time is listed"));
508 pass = TRUE;
509 }
510 } else { /* spans two days */
511 if ((at->day & marked_day) && (at->minute >= time_start)) {
512 D(("caught on first day"));
513 pass = TRUE;
514 } else {
515 marked_day <<= 1;
516 marked_day |= (marked_day & 0200) ? 1:0;
517 D(("next day = 0%o", marked_day));
518 if ((at->day & marked_day) && (at->minute <= time_end)) {
519 D(("caught on second day"));
520 pass = TRUE;
521 }
522 }
523 }
524
525 return (not ^ pass);
526 }
527
528 static int
529 check_account(pam_handle_t *pamh, const char *service,
530 const char *tty, const char *user, const char *file)
531 {
532 int from=0, state=STATE_NL, fd=-1;
533 char *buffer=NULL;
534 int count=0;
535 TIME here_and_now;
536 int retval=PAM_SUCCESS;
537
538 here_and_now = time_now(); /* find current time */
539 do {
540 int good=TRUE,intime;
541
542 /* here we get the service name field */
543
544 fd = read_field(pamh, fd, &buffer, &from, &state, file);
545 if (!buffer || !buffer[0]) {
546 /* empty line .. ? */
547 continue;
548 }
549 ++count;
550
551 if (state != STATE_FIELD) {
552 pam_syslog(pamh, LOG_ERR,
553 "%s: malformed rule #%d", file, count);
554 continue;
555 }
556
557 good = logic_field(pamh, service, buffer, count, is_same);
558 D(("with service: %s", good ? "passes":"fails" ));
559
560 /* here we get the terminal name field */
561
562 fd = read_field(pamh, fd, &buffer, &from, &state, file);
563 if (state != STATE_FIELD) {
564 pam_syslog(pamh, LOG_ERR,
565 "%s: malformed rule #%d", file, count);
566 continue;
567 }
568 good &= logic_field(pamh, tty, buffer, count, is_same);
569 D(("with tty: %s", good ? "passes":"fails" ));
570
571 /* here we get the username field */
572
573 fd = read_field(pamh, fd, &buffer, &from, &state, file);
574 if (state != STATE_FIELD) {
575 pam_syslog(pamh, LOG_ERR,
576 "%s: malformed rule #%d", file, count);
577 continue;
578 }
579 /* If buffer starts with @, we are using netgroups */
580 if (buffer[0] == '@')
581 #ifdef HAVE_INNETGR
582 good &= innetgr (&buffer[1], NULL, user, NULL);
583 #else
584 pam_syslog (pamh, LOG_ERR, "pam_time does not have netgroup support");
585 #endif
586 else
587 good &= logic_field(pamh, user, buffer, count, is_same);
588 D(("with user: %s", good ? "passes":"fails" ));
589
590 /* here we get the time field */
591
592 fd = read_field(pamh, fd, &buffer, &from, &state, file);
593 if (state == STATE_FIELD) {
594 pam_syslog(pamh, LOG_ERR,
595 "%s: poorly terminated rule #%d", file, count);
596 continue;
597 }
598
599 intime = logic_field(pamh, &here_and_now, buffer, count, check_time);
600 D(("with time: %s", intime ? "passes":"fails" ));
601
602 if (good && !intime) {
603 /*
604 * for security parse whole file.. also need to ensure
605 * that the buffer is free()'d and the file is closed.
606 */
607 retval = PAM_PERM_DENIED;
608 } else {
609 D(("rule passed"));
610 }
611 } while (state != STATE_EOF);
612
613 return retval;
614 }
615
616 /* --- public account management functions --- */
617
618 int
619 pam_sm_acct_mgmt(pam_handle_t *pamh, int flags UNUSED,
620 int argc, const char **argv)
621 {
622 const void *service=NULL, *void_tty=NULL;
623 const char *tty;
624 const char *user=NULL;
625 const char *conf_file = NULL;
626 int ctrl;
627 int rv;
628
629 ctrl = _pam_parse(pamh, argc, argv, &conf_file);
630
631 if (ctrl & PAM_DEBUG_ARG) {
632 pam_syslog(pamh, LOG_DEBUG, "conffile=%s", conf_file);
633 }
634
635 /* set service name */
636
637 if (pam_get_item(pamh, PAM_SERVICE, &service)
638 != PAM_SUCCESS || service == NULL) {
639 pam_syslog(pamh, LOG_ERR, "cannot find the current service name");
640 return PAM_ABORT;
641 }
642
643 /* set username */
644
645 if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || *user == '\0') {
646 pam_syslog(pamh, LOG_NOTICE, "cannot determine user name");
647 return PAM_USER_UNKNOWN;
648 }
649
650 /* set tty name */
651
652 if (pam_get_item(pamh, PAM_TTY, &void_tty) != PAM_SUCCESS
653 || void_tty == NULL) {
654 D(("PAM_TTY not set, probing stdin"));
655 tty = ttyname(STDIN_FILENO);
656 if (tty == NULL) {
657 tty = "";
658 }
659 if (pam_set_item(pamh, PAM_TTY, tty) != PAM_SUCCESS) {
660 pam_syslog(pamh, LOG_ERR, "couldn't set tty name");
661 return PAM_ABORT;
662 }
663 }
664 else
665 tty = void_tty;
666
667 if (tty[0] == '/') { /* full path */
668 const char *t;
669 tty++;
670 if ((t = strchr(tty, '/')) != NULL) {
671 tty = t + 1;
672 }
673 }
674
675 /* good, now we have the service name, the user and the terminal name */
676
677 D(("service=%s", service));
678 D(("user=%s", user));
679 D(("tty=%s", tty));
680
681 rv = check_account(pamh, service, tty, user, conf_file);
682 if (rv != PAM_SUCCESS) {
683 #ifdef HAVE_LIBAUDIT
684 if (!(ctrl & PAM_NO_AUDIT)) {
685 pam_modutil_audit_write(pamh, AUDIT_ANOM_LOGIN_TIME,
686 "pam_time", rv); /* ignore return value as we fail anyway */
687 }
688 #endif
689 if (ctrl & PAM_DEBUG_ARG) {
690 pam_syslog(pamh, LOG_DEBUG, "user %s rejected", user);
691 }
692 }
693 return rv;
694 }
695
696 /* end of module definition */