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