1 /*
2 * pam_group module
3 *
4 * Written by Andrew Morgan <morgan@linux.kernel.org> 1996/7/6
5 * Field parsing rewritten by Tomas Mraz <tm@t8m.info>
6 */
7
8 #include "config.h"
9
10 #include <sys/file.h>
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <ctype.h>
14 #include <unistd.h>
15 #include <stdarg.h>
16 #include <time.h>
17 #include <syslog.h>
18 #include <string.h>
19 #include <errno.h>
20
21 #include <grp.h>
22 #include <sys/types.h>
23 #include <sys/stat.h>
24 #include <fcntl.h>
25 #include <netdb.h>
26
27 #define PAM_GROUP_CONF SCONFIGDIR "/group.conf"
28 #ifdef VENDOR_SCONFIGDIR
29 # define VENDOR_PAM_GROUP_CONF VENDOR_SCONFIGDIR "/group.conf"
30 #endif
31 #define PAM_GROUP_BUFLEN 1000
32 #define FIELD_SEPARATOR ';' /* this is new as of .02 */
33
34 #ifndef TRUE
35 # define TRUE 1
36 #endif
37 #ifndef FALSE
38 # define FALSE 0
39 #endif
40
41 typedef enum { AND, OR } operator;
42
43 #include <security/pam_modules.h>
44 #include <security/_pam_macros.h>
45 #include <security/pam_modutil.h>
46 #include <security/pam_ext.h>
47 #include "pam_inline.h"
48
49 /* --- static functions for checking whether the user should be let in --- */
50
51 static char *
52 shift_buf(char *mem, int from)
53 {
54 char *start = mem;
55 while ((*mem = mem[from]) != '\0')
56 ++mem;
57 pam_overwrite_n(mem, PAM_GROUP_BUFLEN - (mem - start));
58 return mem;
59 }
60
61 static void
62 trim_spaces(char *buf, char *from)
63 {
64 while (from > buf) {
65 --from;
66 if (*from == ' ')
67 *from = '\0';
68 else
69 break;
70 }
71 }
72
73 #define STATE_NL 0 /* new line starting */
74 #define STATE_COMMENT 1 /* inside comment */
75 #define STATE_FIELD 2 /* field following */
76 #define STATE_EOF 3 /* end of file or error */
77
78 static int
79 read_field(const pam_handle_t *pamh, int fd, char **buf, int *from, int *state,
80 const char *conf_filename)
81 {
82 char *to;
83 char *src;
84 int i;
85 char c;
86 int onspace;
87
88 /* is buf set ? */
89 if (! *buf) {
90 *buf = (char *) calloc(1, PAM_GROUP_BUFLEN+1);
91 if (! *buf) {
92 pam_syslog(pamh, LOG_CRIT, "out of memory");
93 D(("no memory"));
94 *state = STATE_EOF;
95 return -1;
96 }
97 *from = 0;
98 *state = STATE_NL;
99 fd = open(conf_filename, O_RDONLY);
100 if (fd < 0) {
101 pam_syslog(pamh, LOG_ERR, "error opening %s: %m", conf_filename);
102 _pam_drop(*buf);
103 *state = STATE_EOF;
104 return -1;
105 }
106 }
107
108 if (*from > 0)
109 to = shift_buf(*buf, *from);
110 else
111 to = *buf;
112
113 while (fd != -1 && to - *buf < PAM_GROUP_BUFLEN) {
114 i = pam_modutil_read(fd, to, PAM_GROUP_BUFLEN - (to - *buf));
115 if (i < 0) {
116 pam_syslog(pamh, LOG_ERR, "error reading %s: %m", conf_filename);
117 close(fd);
118 pam_overwrite_n(*buf, PAM_GROUP_BUFLEN);
119 _pam_drop(*buf);
120 *state = STATE_EOF;
121 return -1;
122 } else if (!i) {
123 close(fd);
124 fd = -1; /* end of file reached */
125 }
126
127 to += i;
128 }
129
130 if (to == *buf) {
131 /* nothing previously in buf, nothing read */
132 _pam_drop(*buf);
133 *state = STATE_EOF;
134 return -1;
135 }
136
137 pam_overwrite_n(to, PAM_GROUP_BUFLEN - (to - *buf));
138
139 to = *buf;
140 onspace = 1; /* delete any leading spaces */
141
142 for (src = to; (c=*src) != '\0'; ++src) {
143 if (*state == STATE_COMMENT && c != '\n') {
144 continue;
145 }
146
147 switch (c) {
148 case '\n':
149 *state = STATE_NL;
150 *to = '\0';
151 *from = (src - *buf) + 1;
152 trim_spaces(*buf, to);
153 return fd;
154
155 case '\t':
156 case ' ':
157 if (!onspace) {
158 onspace = 1;
159 *to++ = ' ';
160 }
161 break;
162
163 case '!':
164 onspace = 1; /* ignore following spaces */
165 *to++ = '!';
166 break;
167
168 case '#':
169 *state = STATE_COMMENT;
170 break;
171
172 case FIELD_SEPARATOR:
173 *state = STATE_FIELD;
174 *to = '\0';
175 *from = (src - *buf) + 1;
176 trim_spaces(*buf, to);
177 return fd;
178
179 case '\\':
180 if (src[1] == '\n') {
181 ++src; /* skip it */
182 break;
183 }
184 /* fallthrough */
185 default:
186 *to++ = c;
187 onspace = 0;
188 }
189 if (src > to)
190 *src = '\0'; /* clearing */
191 }
192
193 if (*state != STATE_COMMENT) {
194 *state = STATE_COMMENT;
195 pam_syslog(pamh, LOG_ERR, "field too long - ignored");
196 **buf = '\0';
197 } else {
198 *to = '\0';
199 trim_spaces(*buf, to);
200 }
201
202 *from = 0;
203 return fd;
204 }
205
206 /* read a member from a field */
207
208 static int logic_member(const char *string, int *at)
209 {
210 int c,to;
211 int done=0;
212 int token=0;
213
214 to=*at;
215 do {
216 c = string[to++];
217
218 switch (c) {
219
220 case '\0':
221 --to;
222 done = 1;
223 break;
224
225 case '&':
226 case '|':
227 case '!':
228 if (token) {
229 --to;
230 }
231 done = 1;
232 break;
233
234 default:
235 if (isalpha(c) || c == '*' || isdigit(c) || c == '_'
236 || c == '-' || c == '.' || c == '/' || c == ':') {
237 token = 1;
238 } else if (token) {
239 --to;
240 done = 1;
241 } else {
242 ++*at;
243 }
244 }
245 } while (!done);
246
247 return to - *at;
248 }
249
250 typedef enum { VAL, OP } expect;
251
252 static int
253 logic_field (const pam_handle_t *pamh, const void *me,
254 const char *x, int rule,
255 int (*agrees)(const pam_handle_t *pamh, const void *,
256 const char *, int, int))
257 {
258 int left=FALSE, right, not=FALSE;
259 operator oper=OR;
260 int at=0, l;
261 expect next=VAL;
262
263 while ((l = logic_member(x,&at))) {
264 int c = x[at];
265
266 if (next == VAL) {
267 if (c == '!')
268 not = !not;
269 else if (isalpha(c) || c == '*' || isdigit(c) || c == '_'
270 || c == '-' || c == '.' || c == '/' || c == ':') {
271 right = not ^ agrees(pamh, me, x+at, l, rule);
272 if (oper == AND)
273 left &= right;
274 else
275 left |= right;
276 next = OP;
277 } else {
278 pam_syslog(pamh, LOG_ERR,
279 "garbled syntax; expected name (rule #%d)",
280 rule);
281 return FALSE;
282 }
283 } else { /* OP */
284 switch (c) {
285 case '&':
286 oper = AND;
287 break;
288 case '|':
289 oper = OR;
290 break;
291 default:
292 pam_syslog(pamh, LOG_ERR,
293 "garbled syntax; expected & or | (rule #%d)",
294 rule);
295 D(("%c at %d",c,at));
296 return FALSE;
297 }
298 next = VAL;
299 not = FALSE;
300 }
301 at += l;
302 }
303
304 return left;
305 }
306
307 static int
308 is_same (const pam_handle_t *pamh UNUSED,
309 const void *A, const char *b, int len, int rule UNUSED)
310 {
311 int i;
312 const char *a;
313
314 a = A;
315 for (i=0; len > 0; ++i, --len) {
316 if (b[i] != a[i]) {
317 if (b[i++] == '*') {
318 return (!--len || !strncmp(b+i,a+strlen(a)-len,len));
319 } else
320 return FALSE;
321 }
322 }
323
324 /* Ok, we know that b is a substring from A and does not contain
325 wildcards, but now the length of both strings must be the same,
326 too. In this case it means, a[i] has to be the end of the string. */
327 if (a[i] != '\0')
328 return FALSE;
329
330 return ( !len );
331 }
332
333 typedef struct {
334 int day; /* array of 7 bits, one set for today */
335 int minute; /* integer, hour*100+minute for now */
336 } TIME;
337
338 static struct day {
339 const char *d;
340 int bit;
341 } const days[11] = {
342 { "su", 01 },
343 { "mo", 02 },
344 { "tu", 04 },
345 { "we", 010 },
346 { "th", 020 },
347 { "fr", 040 },
348 { "sa", 0100 },
349 { "wk", 076 },
350 { "wd", 0101 },
351 { "al", 0177 },
352 { NULL, 0 }
353 };
354
355 static TIME time_now(void)
356 {
357 struct tm *local;
358 time_t the_time;
359 TIME this;
360
361 the_time = time((time_t *)0); /* get the current time */
362 local = localtime(&the_time);
363 this.day = days[local->tm_wday].bit;
364 this.minute = local->tm_hour*100 + local->tm_min;
365
366 D(("day: 0%o, time: %.4d", this.day, this.minute));
367 return this;
368 }
369
370 /* take the current date and see if the range "date" passes it */
371 static int
372 check_time (const pam_handle_t *pamh, const void *AT,
373 const char *times, int len, int rule)
374 {
375 int not,pass;
376 int marked_day, time_start, time_end;
377 const TIME *at;
378 int i,j=0;
379
380 at = AT;
381 D(("checking: 0%o/%.4d vs. %s", at->day, at->minute, times));
382
383 if (times == NULL) {
384 /* this should not happen */
385 pam_syslog(pamh, LOG_CRIT, "internal error in file %s at line %d",
386 __FILE__, __LINE__);
387 return FALSE;
388 }
389
390 if (times[j] == '!') {
391 ++j;
392 not = TRUE;
393 } else {
394 not = FALSE;
395 }
396
397 for (marked_day = 0; len > 0 && isalpha(times[j]); --len) {
398 int this_day=-1;
399
400 D(("%c%c ?", times[j], times[j+1]));
401 for (i=0; days[i].d != NULL; ++i) {
402 if (tolower(times[j]) == days[i].d[0]
403 && tolower(times[j+1]) == days[i].d[1] ) {
404 this_day = days[i].bit;
405 break;
406 }
407 }
408 j += 2;
409 if (this_day == -1) {
410 pam_syslog(pamh, LOG_ERR, "bad day specified (rule #%d)", rule);
411 return FALSE;
412 }
413 marked_day ^= this_day;
414 }
415 if (marked_day == 0) {
416 pam_syslog(pamh, LOG_ERR, "no day specified");
417 return FALSE;
418 }
419 D(("day range = 0%o", marked_day));
420
421 time_start = 0;
422 for (i=0; len > 0 && i < 4 && isdigit(times[i+j]); ++i, --len) {
423 time_start *= 10;
424 time_start += times[i+j]-'0'; /* is this portable? */
425 }
426 j += i;
427
428 if (times[j] == '-') {
429 time_end = 0;
430 for (i=1; len > 0 && i < 5 && isdigit(times[i+j]); ++i, --len) {
431 time_end *= 10;
432 time_end += times[i+j]-'0'; /* is this portable? */
433 }
434 j += i;
435 } else
436 time_end = -1;
437
438 D(("i=%d, time_end=%d, times[j]='%c'", i, time_end, times[j]));
439 if (i != 5 || time_end == -1) {
440 pam_syslog(pamh, LOG_ERR, "no/bad times specified (rule #%d)", rule);
441 return TRUE;
442 }
443 D(("times(%d to %d)", time_start,time_end));
444 D(("marked_day = 0%o", marked_day));
445
446 /* compare with the actual time now */
447
448 pass = FALSE;
449 if (time_start < time_end) { /* start < end ? --> same day */
450 if ((at->day & marked_day) && (at->minute >= time_start)
451 && (at->minute < time_end)) {
452 D(("time is listed"));
453 pass = TRUE;
454 }
455 } else { /* spans two days */
456 if ((at->day & marked_day) && (at->minute >= time_start)) {
457 D(("caught on first day"));
458 pass = TRUE;
459 } else {
460 marked_day <<= 1;
461 marked_day |= (marked_day & 0200) ? 1:0;
462 D(("next day = 0%o", marked_day));
463 if ((at->day & marked_day) && (at->minute <= time_end)) {
464 D(("caught on second day"));
465 pass = TRUE;
466 }
467 }
468 }
469
470 return (not ^ pass);
471 }
472
473 static int find_member(const char *string, int *at)
474 {
475 int c,to;
476 int done=0;
477 int token=0;
478
479 to=*at;
480 do {
481 c = string[to++];
482
483 switch (c) {
484
485 case '\0':
486 --to;
487 done = 1;
488 break;
489
490 case '&':
491 case '|':
492 case '!':
493 if (token) {
494 --to;
495 }
496 done = 1;
497 break;
498
499 default:
500 if (isalpha(c) || isdigit(c) || c == '_' || c == '*'
501 || c == '-') {
502 token = 1;
503 } else if (token) {
504 --to;
505 done = 1;
506 } else {
507 ++*at;
508 }
509 }
510 } while (!done);
511
512 return to - *at;
513 }
514
515 #define GROUP_BLK 10
516 #define blk_size(len) (((len-1 + GROUP_BLK)/GROUP_BLK)*GROUP_BLK)
517
518 static int mkgrplist(pam_handle_t *pamh, char *buf, gid_t **list, int len)
519 {
520 int l,at=0;
521 int blks;
522
523 blks = blk_size(len);
524 D(("cf. blks=%d and len=%d", blks,len));
525
526 while ((l = find_member(buf,&at))) {
527 int edge;
528
529 if (len >= blks) {
530 gid_t *tmp;
531
532 D(("allocating new block"));
533 tmp = (gid_t *) realloc((*list)
534 , sizeof(gid_t) * (blks += GROUP_BLK));
535 if (tmp != NULL) {
536 (*list) = tmp;
537 } else {
538 pam_syslog(pamh, LOG_ERR, "out of memory for group list");
539 free(*list);
540 (*list) = NULL;
541 return -1;
542 }
543 }
544
545 /* '\0' terminate the entry */
546
547 edge = (buf[at+l]) ? 1:0;
548 buf[at+l] = '\0';
549 D(("found group: %s",buf+at));
550
551 /* this is where we convert a group name to a gid_t */
552 {
553 const struct group *grp;
554
555 grp = pam_modutil_getgrnam(pamh, buf+at);
556 if (grp == NULL) {
557 pam_syslog(pamh, LOG_ERR, "bad group: %s", buf+at);
558 } else {
559 D(("group %s exists", buf+at));
560 (*list)[len++] = grp->gr_gid;
561 }
562 }
563
564 /* next entry along */
565
566 at += l + edge;
567 }
568 D(("returning with [%p/len=%d]->%p",list,len,*list));
569 return len;
570 }
571
572
573 static int check_account(pam_handle_t *pamh, const char *service,
574 const char *tty, const char *user)
575 {
576 int from=0, state=STATE_NL, fd=-1;
577 char *buffer=NULL;
578 int count=0;
579 TIME here_and_now;
580 int retval=PAM_SUCCESS;
581 gid_t *grps;
582 int no_grps;
583 const char *conf_filename = PAM_GROUP_CONF;
584
585 #ifdef VENDOR_PAM_GROUP_CONF
586 /*
587 * Check whether PAM_GROUP_CONF file is available.
588 * If it does not exist, fall back to VENDOR_PAM_GROUP_CONF file.
589 */
590 struct stat stat_buffer;
591 if (stat(conf_filename, &stat_buffer) != 0 && errno == ENOENT) {
592 conf_filename = VENDOR_PAM_GROUP_CONF;
593 }
594 #endif
595
596 /*
597 * first we get the current list of groups - the application
598 * will have previously done an initgroups(), or equivalent.
599 */
600
601 D(("counting supplementary groups"));
602 no_grps = getgroups(0, NULL); /* find the current number of groups */
603 if (no_grps > 0) {
604 grps = calloc( blk_size(no_grps) , sizeof(gid_t) );
605 D(("copying current list into grps [%d big]",blk_size(no_grps)));
606 if (getgroups(no_grps, grps) < 0) {
607 D(("getgroups call failed"));
608 no_grps = 0;
609 _pam_drop(grps);
610 }
611 #ifdef PAM_DEBUG
612 {
613 int z;
614 for (z=0; z<no_grps; ++z) {
615 D(("gid[%d]=%d", z, grps[z]));
616 }
617 }
618 #endif
619 } else {
620 D(("no supplementary groups known"));
621 no_grps = 0;
622 grps = NULL;
623 }
624
625 here_and_now = time_now(); /* find current time */
626
627 /* parse the rules in the configuration file */
628 do {
629 int good=TRUE;
630
631 /* here we get the service name field */
632
633 fd = read_field(pamh, fd, &buffer, &from, &state, conf_filename);
634 if (!buffer || !buffer[0]) {
635 /* empty line .. ? */
636 continue;
637 }
638 ++count;
639 D(("working on rule #%d",count));
640
641 if (state != STATE_FIELD) {
642 pam_syslog(pamh, LOG_ERR,
643 "%s: malformed rule #%d", conf_filename, count);
644 continue;
645 }
646
647 good = logic_field(pamh,service, buffer, count, is_same);
648 D(("with service: %s", good ? "passes":"fails" ));
649
650 /* here we get the terminal name field */
651
652 fd = read_field(pamh, fd, &buffer, &from, &state, conf_filename);
653 if (state != STATE_FIELD) {
654 pam_syslog(pamh, LOG_ERR,
655 "%s: malformed rule #%d", conf_filename, count);
656 continue;
657 }
658 good &= logic_field(pamh,tty, buffer, count, is_same);
659 D(("with tty: %s", good ? "passes":"fails" ));
660
661 /* here we get the username field */
662
663 fd = read_field(pamh, fd, &buffer, &from, &state, conf_filename);
664 if (state != STATE_FIELD) {
665 pam_syslog(pamh, LOG_ERR,
666 "%s: malformed rule #%d", conf_filename, count);
667 continue;
668 }
669 /* If buffer starts with @, we are using netgroups */
670 if (buffer[0] == '@')
671 #ifdef HAVE_INNETGR
672 good &= innetgr (&buffer[1], NULL, user, NULL);
673 #else
674 pam_syslog (pamh, LOG_ERR, "pam_group does not have netgroup support");
675 #endif
676 /* otherwise, if the buffer starts with %, it's a UNIX group */
677 else if (buffer[0] == '%')
678 good &= pam_modutil_user_in_group_nam_nam(pamh, user, &buffer[1]);
679 else
680 good &= logic_field(pamh,user, buffer, count, is_same);
681 D(("with user: %s", good ? "passes":"fails" ));
682
683 /* here we get the time field */
684
685 fd = read_field(pamh, fd, &buffer, &from, &state, conf_filename);
686 if (state != STATE_FIELD) {
687 pam_syslog(pamh, LOG_ERR,
688 "%s: malformed rule #%d", conf_filename, count);
689 continue;
690 }
691
692 good &= logic_field(pamh,&here_and_now, buffer, count, check_time);
693 D(("with time: %s", good ? "passes":"fails" ));
694
695 fd = read_field(pamh, fd, &buffer, &from, &state, conf_filename);
696 if (state == STATE_FIELD) {
697 pam_syslog(pamh, LOG_ERR,
698 "%s: poorly terminated rule #%d", conf_filename, count);
699 continue;
700 }
701
702 /*
703 * so we have a list of groups, we need to turn it into
704 * something to send to setgroups(2)
705 */
706
707 if (good) {
708 D(("adding %s to gid list", buffer));
709 good = mkgrplist(pamh, buffer, &grps, no_grps);
710 if (good < 0) {
711 no_grps = 0;
712 } else {
713 no_grps = good;
714 }
715 }
716
717 if (good > 0) {
718 D(("rule #%d passed, added %d groups", count, good));
719 } else if (good < 0) {
720 retval = PAM_BUF_ERR;
721 } else {
722 D(("rule #%d failed", count));
723 }
724
725 } while (state != STATE_EOF);
726
727 /* now set the groups for the user */
728
729 if (no_grps > 0) {
730 #ifdef PAM_DEBUG
731 int err;
732 #endif
733 D(("trying to set %d groups", no_grps));
734 #ifdef PAM_DEBUG
735 for (err=0; err<no_grps; ++err) {
736 D(("gid[%d]=%d", err, grps[err]));
737 }
738 #endif
739 if (setgroups(no_grps, grps) != 0) {
740 D(("but couldn't set groups %m"));
741 pam_syslog(pamh, LOG_ERR,
742 "unable to set the group membership for user: %m");
743 retval = PAM_CRED_ERR;
744 }
745 }
746
747 if (grps) { /* tidy up */
748 pam_overwrite_n(grps, sizeof(gid_t) * blk_size(no_grps));
749 _pam_drop(grps);
750 no_grps = 0;
751 }
752
753 return retval;
754 }
755
756 /* --- public authentication management functions --- */
757
758 int
759 pam_sm_authenticate (pam_handle_t *pamh UNUSED, int flags UNUSED,
760 int argc UNUSED, const char **argv UNUSED)
761 {
762 return PAM_IGNORE;
763 }
764
765 int
766 pam_sm_setcred (pam_handle_t *pamh, int flags,
767 int argc UNUSED, const char **argv UNUSED)
768 {
769 const void *service=NULL, *void_tty=NULL;
770 const char *user=NULL;
771 const char *tty;
772 int retval;
773 unsigned setting;
774
775 /* only interested in establishing credentials */
776
777 setting = flags;
778 if (!(setting & (PAM_ESTABLISH_CRED | PAM_REINITIALIZE_CRED))) {
779 D(("ignoring call - not for establishing credentials"));
780 return PAM_SUCCESS; /* don't fail because of this */
781 }
782
783 /* set service name */
784
785 if (pam_get_item(pamh, PAM_SERVICE, &service)
786 != PAM_SUCCESS || service == NULL) {
787 pam_syslog(pamh, LOG_ERR, "cannot find the current service name");
788 return PAM_ABORT;
789 }
790
791 /* set username */
792
793 if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || *user == '\0') {
794 pam_syslog(pamh, LOG_NOTICE, "cannot determine user name");
795 return PAM_USER_UNKNOWN;
796 }
797
798 /* set tty name */
799
800 if (pam_get_item(pamh, PAM_TTY, &void_tty) != PAM_SUCCESS
801 || void_tty == NULL) {
802 D(("PAM_TTY not set, probing stdin"));
803 tty = ttyname(STDIN_FILENO);
804 if (tty == NULL) {
805 tty = "";
806 }
807 if (pam_set_item(pamh, PAM_TTY, tty) != PAM_SUCCESS) {
808 pam_syslog(pamh, LOG_ERR, "couldn't set tty name");
809 return PAM_ABORT;
810 }
811 }
812 else
813 tty = (const char *) void_tty;
814
815 if (tty[0] == '/') { /* full path */
816 const char *t;
817 tty++;
818 if ((t = strchr(tty, '/')) != NULL) {
819 tty = t + 1;
820 }
821 }
822
823 /* good, now we have the service name, the user and the terminal name */
824
825 D(("service=%s", service));
826 D(("user=%s", user));
827 D(("tty=%s", tty));
828
829 retval = check_account(pamh,service,tty,user); /* get groups */
830
831 return retval;
832 }
833
834 /* end of module definition */