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