Jeff Carneal <jeff@apex.net>
[freeradius.git] / src / modules / rlm_fastusers / rlm_fastusers.c
1 /*
2  * rlm_fastusers.c      authorization: Find a user in the hashed "users" file.
3  *              accounting:    Do nothing.  Auth module only.
4  *
5  */
6
7 #include        "autoconf.h"
8
9 #include        <sys/types.h>
10 #include        <sys/socket.h>
11 #include        <sys/time.h>
12 #include        <sys/stat.h>
13 #include        <netinet/in.h>
14
15 #include        <stdio.h>
16 #include        <stdlib.h>
17 #include        <string.h>
18 #include        <errno.h>
19 #include        <pwd.h>
20 #include        <grp.h>
21 #include        <time.h>
22 #include        <ctype.h>
23 #include        <fcntl.h>
24 #include        <unistd.h>
25 #include <limits.h>
26
27 #if HAVE_MALLOC_H
28 #  include      <malloc.h>
29 #endif
30
31 #include        "radiusd.h"
32 #include        "modules.h"
33
34 struct fastuser_instance {
35         char *compat_mode;
36         int      normal_defaults;
37         int      hash_reload;
38
39         /* hash table */
40         long hashsize;
41         PAIR_LIST **hashtable;
42         PAIR_LIST *default_entry;
43
44         char *usersfile;
45         time_t next_reload;
46 };
47
48 /* Function declarations */
49 static int fastuser_buildhash(struct fastuser_instance *inst);
50 static int fastuser_getfile(struct fastuser_instance *inst, const char *filename, 
51                                                                                                                 PAIR_LIST **default_list, PAIR_LIST **hashtable);
52 static int fastuser_hash(const char *s, long hashtablesize);
53 static int fastuser_store(PAIR_LIST **hashtable, PAIR_LIST *entry, int idx);
54 static PAIR_LIST *fastuser_find(PAIR_LIST **hashtable, const char *user,
55                                                                                                                         long hashsize);
56
57 /*
58  *      A temporary holding area for config values to be extracted
59  *      into, before they are copied into the instance data
60  */
61 static struct fastuser_instance config;
62
63 static CONF_PARSER module_config[] = {
64         { "usersfile",     PW_TYPE_STRING_PTR, &config.usersfile, "${raddbdir}/users_fast" },
65         { "hashsize",     PW_TYPE_INTEGER, &config.hashsize, "100000" },
66         { "compat",        PW_TYPE_STRING_PTR, &config.compat_mode, "cistron" },
67         { "normal_defaults", PW_TYPE_BOOLEAN, &config.normal_defaults, "yes" },
68         { "hash_reload",     PW_TYPE_INTEGER, &config.hash_reload, "600" },
69         { NULL, -1, NULL, NULL }
70 };
71
72 static int fastuser_buildhash(struct fastuser_instance *inst) {
73         long memsize=0;
74         int rcode, hashindex;
75         PAIR_LIST **newhash=NULL, **oldhash=NULL;
76         PAIR_LIST *newdefaults=NULL, *olddefaults=NULL, *cur=NULL;
77
78         /* 
79          * Allocate space for hash table here
80          */
81         memsize = sizeof(PAIR_LIST *) * inst->hashsize;
82         if( (newhash = (PAIR_LIST **)malloc(memsize)) == NULL) {
83                 radlog(L_ERR, "rlm_fastusers:  Can't build hashtable, out of memory!");
84                 return -1;
85         }
86         memset((PAIR_LIST *)newhash, 0, memsize);
87
88         rcode = fastuser_getfile(inst, inst->usersfile, &newdefaults, newhash);
89         if (rcode != 0) {
90                 radlog(L_ERR|L_CONS, "rlm_fastusers:  Errors reading %s", inst->usersfile);
91                 return -1;
92         }
93
94         /*
95          * We need to do this now so that users auths
96          * aren't blocked while we free the old table
97          * below
98          */
99         oldhash = inst->hashtable;
100         inst->hashtable = newhash;
101         olddefaults = inst->default_entry;
102         inst->default_entry = newdefaults;
103
104         /*
105          * When we get here, we assume the hash built properly.
106          * So we begin to tear down the old one
107          */
108         if(oldhash) {
109                 for(hashindex=0; hashindex<inst->hashsize; hashindex++) {
110                         if(oldhash[hashindex]) {
111                                 cur = oldhash[hashindex];
112                                 pairlist_free(&cur);
113                         }
114                 } 
115                 free(oldhash);
116                 pairlist_free(&olddefaults);
117         }
118         return 0;       
119 }
120
121 static int fastuser_getfile(struct fastuser_instance *inst, const char *filename, 
122                                                                                                                 PAIR_LIST **default_list, PAIR_LIST **hashtable) {
123         int rcode;
124         PAIR_LIST *users = NULL;
125         int compat_mode = FALSE;
126         PAIR_LIST *entry=NULL, *next=NULL, *cur=NULL, *defaults=NULL;
127         VALUE_PAIR *vp=NULL;
128         int hashindex = 0;
129         long numdefaults = 0, numusers=0;
130
131         rcode = pairlist_read(filename, &users, 1);
132         if (rcode < 0) {
133                 return -1;
134         }
135
136         if (strcmp(inst->compat_mode, "cistron") == 0) {
137                 compat_mode = TRUE;
138         }
139         
140         entry = users;
141         while (entry) {
142                 if (compat_mode) {
143                         DEBUG("[%s]:%d Cistron compatibility checks for entry %s ...",
144                                 filename, entry->lineno, entry->name);
145                 }
146
147                 /*
148                  *      Look for improper use of '=' in the
149                  *      check items.  They should be using
150                  *      '==' for on-the-wire RADIUS attributes,
151                  *      and probably ':=' for server
152                  *      configuration items.
153                  */
154                 for (vp = entry->check; vp != NULL; vp = vp->next) {
155                         /*
156                          *      Ignore attributes which are set
157                          *      properly.
158                          */
159                         if (vp->operator != T_OP_EQ) 
160                                 continue;
161                                 
162
163                         /*
164                          *      If it's a vendor attribute,
165                          *      or it's a wire protocol, 
166                          *      ensure it has '=='.
167                          */
168                         if (((vp->attribute & ~0xffff) != 0) ||
169                                 (vp->attribute < 0x100)) {
170                                 if (!compat_mode) {
171                                         DEBUG("[%s]:%d WARNING! Changing '%s =' to '%s =='\n\tfor comparing RADIUS attribute in check item list for user %s",
172                                         filename, entry->lineno, vp->name, vp->name, entry->name);
173                                 } else {
174                                         DEBUG("\tChanging '%s =' to '%s =='",
175                                                 vp->name, vp->name);
176                                 }
177                                 vp->operator = T_OP_CMP_EQ;
178                                 continue;
179                         }
180                                 
181                         /*
182                          *      Cistron Compatibility mode.
183                          *
184                          *      Re-write selected attributes
185                          *      to be '+=', instead of '='.
186                          *
187                          *      All others get set to '=='
188                          */
189                         if (compat_mode) {
190                                 /*
191                                  *      Non-wire attributes become +=
192                                  *
193                                  *      On the write attributes
194                                  *      become ==
195                                  */
196                                 if ((vp->attribute >= 0x100) &&
197                                         (vp->attribute <= 0xffff) &&
198                                         (vp->attribute != PW_HINT) &&
199                                         (vp->attribute != PW_HUNTGROUP_NAME)) {
200                                         DEBUG("\tChanging '%s =' to '%s +='",
201                                                 vp->name, vp->name);
202                                                 vp->operator = T_OP_ADD;
203                                 } else {
204                                         DEBUG("\tChanging '%s =' to '%s =='",
205                                                 vp->name, vp->name);
206                                         vp->operator = T_OP_CMP_EQ;
207                                 }
208                         }
209                                 
210                 } /* end of loop over check items */
211                 
212                 
213                 /*
214                  *      Look for server configuration items
215                  *      in the reply list.
216                  *
217                  *      It's a common enough mistake, that it's
218                  *      worth doing.
219                  */
220                 for (vp = entry->reply; vp != NULL; vp = vp->next) {
221                         /*
222                          *      If it's NOT a vendor attribute,
223                          *      and it's NOT a wire protocol
224                          *      and we ignore Fall-Through,
225                          *      then bitch about it, giving a
226                          *      good warning message.
227                          */
228                         if (!(vp->attribute & ~0xffff) &&
229                                 (vp->attribute > 0xff) &&
230                                 (vp->attribute > 1000)) {
231                                 log_debug("[%s]:%d WARNING! Check item \"%s\"\n"
232                                         "\tfound in reply item list for user \"%s\".\n"
233                                         "\tThis attribute MUST go on the first line"
234                                         " with the other check items", 
235                                         filename, entry->lineno, vp->name,
236                                         entry->name);
237                         }
238                 }
239
240                 /*
241                  * Ok, we've done all the same BS as
242                  * rlm_users, so here we tear apart the
243                  * linked list, and store our users in
244                  * the hashtable we've built instead
245                  */
246
247                 /* Save what was next */
248                 next = entry->next;
249
250                 /* Save the DEFAULT entry specially */
251                 if(strcmp(entry->name, "DEFAULT")==0) {
252                                 numdefaults++;
253                                 /* put it at the end of the list */
254                                 if(defaults) {
255                                         for(cur=defaults; cur->next; cur=cur->next);
256                                         cur->next = entry;
257                                         entry->next = NULL;
258                                 } else {
259                                         defaults = entry;
260                                         defaults->next = NULL; 
261                                 }
262
263                 } else {
264                         numusers++;
265
266                         /* Hash the username */
267                         hashindex = fastuser_hash(entry->name, inst->hashsize);
268
269                         /* Store user in the hash */
270                         fastuser_store(hashtable, entry, hashindex);
271
272                         /* Restore entry to next pair_list */
273                 }
274                 entry = next;
275
276         } /* while(entry) loop */
277
278         if(!inst->normal_defaults && (numdefaults>1)) {
279                 radlog(L_INFO, "Warning:  fastusers found multiple DEFAULT entries.  Using the first.");
280         }
281
282         *default_list = defaults;
283         radlog(L_INFO, "rlm_fastusers:  Loaded %ld users and %ld defaults",
284                                 numusers, numdefaults);
285
286         return 0;
287 }
288
289 /* Hashes the username sent to it and returns index into hashtable */
290 int fastuser_hash(const char *s, long hashtablesize) {
291      unsigned long hash = 0;
292
293      while (*s != '\0') {
294          hash = hash * 7907 + (unsigned char)*s++;
295       }
296
297      return (hash % hashtablesize);
298 }
299
300 /* Stores the username sent into the hashtable */
301 static int fastuser_store(PAIR_LIST **hashtable, PAIR_LIST *new, int idx) {
302
303    /* store new record at beginning of list */
304    new->next = hashtable[idx];
305    hashtable[idx] = new;
306
307    return 1;
308 }
309
310 /*
311  * Looks up user in hashtable.  If user can't be found, returns 0.
312  * Otherwise returns a pointer to the structure for the user
313  */
314 static PAIR_LIST *fastuser_find(PAIR_LIST **hashtable, 
315                 const char *user, long hashsize)
316 {
317
318    PAIR_LIST *cur;
319    int idx;
320
321    /* first hash the username and get the index into the hashtable */
322    idx = fastuser_hash(user, hashsize);
323
324    cur = hashtable[idx];
325
326    while((cur != NULL) && (strcmp(cur->name, user))) {
327       cur = cur->next;
328    }
329
330    if(cur) {
331       DEBUG2("  fastusers:  user %s found in hashtable bucket %d", user, idx);
332       return cur;
333    }
334
335    return (PAIR_LIST *)0;
336
337 }
338
339
340 /*
341  *      (Re-)read the "users" file into memory.
342  */
343 static int fastuser_instantiate(CONF_SECTION *conf, void **instance)
344 {
345         struct fastuser_instance *inst=0;
346
347         inst = malloc(sizeof *inst);
348         if (!inst) {
349                 radlog(L_ERR|L_CONS, "Out of memory\n");
350                 return -1;
351         }
352         memset(inst, 0, sizeof(inst));
353
354         if (cf_section_parse(conf, module_config) < 0) {
355                 free(inst);
356                 return -1;
357         }
358
359         inst->usersfile = config.usersfile;
360         inst->hashsize = config.hashsize;
361         inst->default_entry = config.default_entry;
362         inst->compat_mode = config.compat_mode;
363         inst->normal_defaults = config.normal_defaults;
364         inst->hash_reload = config.hash_reload;
365         inst->next_reload = time(NULL) + inst->hash_reload;
366         inst->hashtable = NULL;
367         if(fastuser_buildhash(inst) < 0) {
368                 radlog(L_ERR, "rlm_fastusers:  error building user hash.  aborting");
369                 return -1;
370         }
371
372         config.usersfile = NULL;
373         config.hashtable = NULL;
374         config.default_entry = NULL;
375         config.compat_mode = NULL;
376
377         *instance = inst;
378         return 0;
379 }
380
381 /*
382  *      Find the named user in the database.  Create the
383  *      set of attribute-value pairs to check and reply with
384  *      for this user from the database. The main code only
385  *      needs to check the password, the rest is done here.
386  */
387 static int fastuser_authorize(void *instance, REQUEST *request)
388 {
389
390         VALUE_PAIR      *namepair;
391         VALUE_PAIR      *request_pairs;
392         VALUE_PAIR      *check_tmp;
393         VALUE_PAIR      *reply_tmp;
394         VALUE_PAIR      **check_pairs;
395         VALUE_PAIR      **reply_pairs;
396         VALUE_PAIR      *check_save;
397         PAIR_LIST               *user;
398         const char      *name;
399         int                     found=0;
400         int                     checkdefault = 0;
401         struct fastuser_instance *inst = instance;
402
403         request_pairs = request->packet->vps;
404         check_pairs = &request->config_items;
405         reply_pairs = &request->reply->vps;
406
407         /*
408          * Do we need to reload the cache?
409          * Really we should spawn a thread to do this
410          */
411         if((inst->hash_reload) && (request->timestamp > inst->next_reload)) {
412                 inst->next_reload = request->timestamp + inst->hash_reload;
413                 radlog(L_INFO, "rlm_fastusers:  Reloading fastusers hash");
414                 if(fastuser_buildhash(inst) < 0) {
415                         radlog(L_ERR, "rlm_fastusers:  error building user hash.  aborting");
416                         exit(1);
417                 }
418         }
419
420         /*
421          *      Grab the canonical user name.
422          */
423         namepair = request->username;
424         name = namepair ? (char *) namepair->strvalue : "NONE";
425
426         /*
427          *      Find the entry for the user.
428          */
429         if((user=fastuser_find(inst->hashtable, name, inst->hashsize))==NULL) {
430                 if(inst->normal_defaults) {
431                         checkdefault = 1;
432                 } else {
433                         return RLM_MODULE_NOTFOUND;
434                 }
435         }
436
437         /*
438          * Usercollide means we have to compare check pairs
439          * _and_ the password
440          */
441         if(mainconfig.do_usercollide && !checkdefault) {
442                 /* Save the orginal config items */
443                 check_save = paircopy(request->config_items);
444
445                 while((user) && (!found) && (strcmp(user->name, name)==0)) {
446                         if(paircmp(request_pairs, user->check, reply_pairs) != 0) {
447                                 user = user->next;
448                                 continue;
449                         }
450                         DEBUG2("  fastusers(uc): Checking %s at %d", user->name, user->lineno);
451
452                         /* Copy this users check pairs to the request */
453                         check_tmp = paircopy(user->check);
454                         pairmove(check_pairs, &check_tmp);
455                         pairfree(check_tmp); 
456
457                         /* Check the req to see if we matched */
458                         if(rad_check_password(request)==0) {
459                                 found = 1;
460
461                         /* We didn't match here */
462                         } else {
463                                 /* Restore check items */
464                                 pairfree(request->config_items); 
465                                 request->config_items = paircopy(check_save);
466                                 check_pairs = &request->config_items;
467                                 user = user->next;
468                         }
469                 }
470
471                 /* Free our saved config items */
472                 pairfree(check_save);
473         }
474
475         /*
476          * No usercollide, just compare check pairs
477          */
478         if(!mainconfig.do_usercollide && !checkdefault) {
479                 while((user) && (!found) && (strcmp(user->name, name)==0)) {
480                         if(paircmp(request_pairs, user->check, reply_pairs) == 0) {
481                                 found = 1;
482                                 DEBUG2("  fastusers: Matched %s at %d", user->name, user->lineno);
483                         } else {
484                                 user = user->next;
485                         }       
486                 }
487         }
488
489         /* 
490          * When we get here, we've either found the user or not
491          * and we either do normal DEFAULTs or not.  
492          */
493         
494         /*
495          * We found the user & normal default 
496          * copy relevant pairs and return
497          */
498         if(found && inst->normal_defaults) {
499                 check_tmp = paircopy(user->check);
500                 pairmove(check_pairs, &check_tmp);
501                 pairfree(check_tmp); 
502                 reply_tmp = paircopy(user->reply);
503                 pairmove(reply_pairs, &reply_tmp);
504                 pairfree(reply_tmp);
505                 return RLM_MODULE_UPDATED;
506         }
507
508         /*
509          * We didn't find the user, and we aren't supposed to
510          * check defaults.  So just report not found.
511          */
512         if(!found && !inst->normal_defaults) {
513                 return RLM_MODULE_NOTFOUND;
514         }
515
516         /*
517          * We didn't find the user, but we should 
518          * check the defaults.  
519          */
520         if(!found && inst->normal_defaults) {
521                 user = inst->default_entry;
522                 while((user) && (!found)) {
523                         if(paircmp(request_pairs, user->check, reply_pairs) == 0) {
524                                 DEBUG2("  fastusers: Matched %s at %d", user->name, user->lineno);
525                                 found = 1;
526                         } else {
527                                 user = user->next;
528                         }
529                 }
530
531                 if(found) {
532                         check_tmp = paircopy(user->check);
533                         pairmove(check_pairs, &check_tmp);
534                         pairfree(check_tmp); 
535                         reply_tmp = paircopy(user->reply);
536                         pairmove(reply_pairs, &reply_tmp);
537                         pairfree(reply_tmp);
538                         return RLM_MODULE_UPDATED;
539
540                 } else {
541                         return RLM_MODULE_NOTFOUND;
542                 }
543         }
544
545         /*
546          * We found the user, and we don't use normal defaults.
547          * So copy the check and reply pairs from the default
548          * entry to the request
549          */
550         if(found && !inst->normal_defaults) {
551
552                 /* We've already done this above if(mainconfig.do_usercollide) */
553                 if(!mainconfig.do_usercollide) {
554                         check_tmp = paircopy(user->check);
555                         pairmove(check_pairs, &check_tmp);
556                         pairfree(check_tmp); 
557                 }
558                 reply_tmp = paircopy(user->reply);
559                 pairmove(reply_pairs, &reply_tmp);
560                 pairfree(reply_tmp);
561
562                 /* 
563                  * We also need to add the pairs from 
564                  * inst->default_entry if the vp is
565                  * not already present.
566                  */
567                 
568                 if(inst->default_entry) {
569                         check_tmp = paircopy(inst->default_entry->check);
570                         reply_tmp = paircopy(inst->default_entry->reply);
571                         pairmove(reply_pairs, &reply_tmp);
572                         pairmove(check_pairs, &check_tmp);
573                         pairfree(reply_tmp);
574                         pairfree(check_tmp); 
575                 }
576
577                 return RLM_MODULE_UPDATED;
578         }
579
580         return RLM_MODULE_NOOP;
581 }
582
583 /*
584  *      Authentication - unused.
585  */
586 static int fastuser_authenticate(void *instance, REQUEST *request)
587 {
588         instance = instance;
589         request = request;
590         return RLM_MODULE_OK;
591 }
592
593 /*
594  *  Clean up.
595  */
596 static int fastuser_detach(void *instance)
597 {
598         struct fastuser_instance *inst = instance;
599         int hashindex;
600         PAIR_LIST *cur;
601
602         /* Free hash table */
603         for(hashindex=0; hashindex<inst->hashsize; hashindex++) {
604                 if(inst->hashtable[hashindex]) {
605                         cur = inst->hashtable[hashindex];
606                         pairlist_free(&cur);
607                 }
608         } 
609
610         free(inst->hashtable);
611         pairlist_free(&inst->default_entry);
612         free(inst->usersfile);
613         free(inst->compat_mode);
614         free(inst);
615   return 0;
616 }
617
618 /*
619  *      This function is unused
620  */
621 static int fastuser_preacct(void *instance, REQUEST *request)
622 {
623         return RLM_MODULE_FAIL;
624 }
625
626 /*
627  *      This function is unused
628  */
629 static int fastuser_accounting(void *instance, REQUEST *request)
630 {
631         return RLM_MODULE_FAIL;
632 }
633
634 /* globally exported name */
635 module_t rlm_fastusers = {
636         "fastusers",
637         0,                              /* type: reserved */
638         NULL,                   /* initialization */
639         fastuser_instantiate,           /* instantiation */
640         fastuser_authorize,             /* authorization */
641         fastuser_authenticate,          /* authentication */
642         fastuser_preacct,                       /* preaccounting */
643         fastuser_accounting,            /* accounting */
644         NULL,                                                                   /* checksimul */
645         fastuser_detach,                        /* detach */
646         NULL                            /* destroy */
647 };
648