libpam-csc 1.7
[mspang/libpam-csc.git] / pam_csc.c
1 #define PAM_SM_ACCOUNT
2 #include <unistd.h>
3 #include <sys/types.h>
4 #include <sys/time.h>
5 #include <time.h>
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <stdbool.h>
9 #include <string.h>
10 #include <security/pam_modules.h>
11 #include <security/pam_appl.h>
12 #include <ldap.h>
13 #include <sasl/sasl.h>
14 #include <syslog.h>
15 #include <pwd.h>
16
17 #define PAM_CSC_CSC_BASE_DN         "ou=People,dc=csclub,dc=uwaterloo,dc=ca"
18 #define PAM_CSC_CSCF_URI \
19     "ldaps://eponina.student.cs.uwaterloo.ca" \
20     "ldaps://canadenis.student.cs.uwaterloo.ca"
21 #define PAM_CSC_CSCF_BASE_DN        "dc=student,dc=cs,dc=uwateloo,dc=ca"
22 #define PAM_CSC_CSCF_BIND_DN \
23     "uid=TODO,dc=student,dc=cs,dc=uwaterloo,dc=ca"
24 #define PAM_CSC_CSCF_SASL_USER \
25     "dn:uid=TODO,cn=STUDENT.CS.UWATERLOO.CA,cn=DIGEST-MD5,cn=auth"
26 #define PAM_CSC_CSCF_PASSWORD_FILE  "/etc/security/pam_csc_cscf_password"
27 #define PAM_CSC_CSCF_SASL_REALM     "STUDENT.CS.UWATERLOO.CA"
28 #define PAM_CSC_LDAP_TIMEOUT        5
29 #define PAM_CSC_ALLOWED_USERNAMES   {"nobody"}
30 #define PAM_CSC_EXPIRED_MSG \
31     "*****************************************************************************\n" \
32     "*                                                                           *\n" \
33     "*    Your account has expired - please contact the Computer Science Club    *\n" \
34     "*                                                                           *\n" \
35     "*****************************************************************************\n"
36 #define PAM_CSC_CSCF_DISALLOWED_MSG \
37     "You are not registered as a CS student - login denied."
38
39 #define PAM_CSC_SYSLOG_EXPIRED_WARNING \
40     "(pam_csc): %s was not registered for current term or previous term - denying login\n"
41 #define PAM_CSC_SYSLOG_EXPIRED_ERROR \
42     "(pam_csc): %s was not registered for current term but was registered for previous term - permitting login\n"
43 #define PAM_CSC_SYSLOG_NOT_A_MEMBER \
44     "(pam_csc): %s is not a member account - permitting login\n"
45 #define PAM_CSC_SYSLOG_CSCF_DISALLOWED \
46     "(pam_csc): %s is using a CSCF machine but is not enrolled in CS - denying login\n"
47 #define PAM_CSC_SYSLOG_SASL_UNRECOGNIZED_CALLBACK \
48     "(pam_csc): %ld is not a recognized SASL callback option\n"
49
50 /*
51  * User terms are defined as (3 * year + term) where term is:
52  *   0 = Winter, 1 = Spring, 2 = Fall
53  * Term is a string in the form [f|w|s][year]
54  */
55
56 #define HANDLE_WARN \
57 { \
58     syslog(LOG_AUTHPRIV | LOG_WARNING, "pam_csc generated a warning on line %d of %s\n", __LINE__, __FILE__); \
59     retval = PAM_SUCCESS; \
60     goto cleanup; \
61 }
62
63 #define WARN_ZERO(x) \
64     if( (x) == 0 ) HANDLE_WARN
65
66 #define WARN_NEG1(x) \
67     if( (x) == -1 ) HANDLE_WARN
68
69 #define WARN_PAM(x) \
70     if( (x) != PAM_SUCCESS ) HANDLE_WARN
71
72 #define WARN_LDAP(x) \
73     if( (x) != LDAP_SUCCESS ) HANDLE_WARN
74
75 struct pam_csc_sasl_interact_param
76 {
77     const char* realm;
78     const char* user;
79     char pass[100];
80 };
81 typedef struct pam_csc_sasl_interact_param pam_csc_sasl_interact_param_t;
82
83 int pam_csc_sasl_interact(LDAP* ld, unsigned flags, void* def, void* inter)
84 {
85     pam_csc_sasl_interact_param_t* param = (pam_csc_sasl_interact_param_t*)def;
86     sasl_interact_t* interact = (sasl_interact_t*)interact;
87     while(interact->id != SASL_CB_LIST_END)
88     {
89         switch(interact->id)
90         {
91         case SASL_CB_GETREALM:
92             interact->result = param->realm;
93             break;
94         case SASL_CB_USER:
95             interact->result = param->user;
96             break;
97         case SASL_CB_PASS:
98             interact->result = param->pass;
99             break;
100         default:
101             syslog(LOG_AUTHPRIV | LOG_NOTICE,
102                 PAM_CSC_SYSLOG_SASL_UNRECOGNIZED_CALLBACK, interact->id);
103             interact->result = "";
104             break;
105         }
106         interact->len = strlen(interact->result);
107     }
108
109     return LDAP_SUCCESS;
110 }
111
112 char* pam_csc_escape_ldap_string(const char* src)
113 {
114     char *dst, *dst_ptr;
115     int i;
116
117     if(!(dst = malloc(2 * strlen(src) + 1)))
118         return NULL;
119     dst_ptr = dst;
120
121     for(i = 0; i < strlen(src); i++)
122     {
123         if(src[i] == '*' || src[i] == '(' || src[i] == ')' || src[i] == '\\')
124         {
125             dst_ptr[0] = '\\';
126             dst_ptr++;
127         }
128         dst_ptr[0] = src[i];
129         dst_ptr++;
130     }
131     dst_ptr[0] = '\0';
132
133     return dst;
134 }
135
136 int pam_csc_print_message(pam_handle_t* pamh, char* msg, int style)
137 {
138     int retval = PAM_SUCCESS;
139     const struct pam_conv* conv;
140     struct pam_message message;
141     struct pam_message* messages[1];
142     struct pam_response* response;
143
144     /* output message */
145     WARN_PAM( pam_get_item(pamh, PAM_CONV, (const void**)&conv) )
146     if(!conv || !conv->conv)
147         goto cleanup;
148     messages[0] = &message;
149     message.msg_style = style;
150     message.msg = msg;
151     WARN_PAM( conv->conv(1, (const struct pam_message**)messages, 
152         &response, conv->appdata_ptr) )
153
154 cleanup:
155
156     return retval;
157 }
158
159 PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t* pamh, int flags, int argc, const char* argv[])
160 {
161     int retval = PAM_SUCCESS;
162     const char* username;
163     struct passwd* pwd;
164     const char* allowed_usernames[] = PAM_CSC_ALLOWED_USERNAMES;
165     int i;
166     time_t cur_time;
167     struct tm* local_time;
168     int long_term, term_month;
169     static const char term_chars[] = {'w', 's', 'f'};
170     char cur_term[6], prev_term[6];
171     LDAP *ld_csc = NULL, *ld_cscf = NULL;
172     bool cscf;
173     FILE* pass_file = NULL;
174     char* username_escaped = NULL;
175     char *filter_csc = NULL, *filter_cscf = NULL;
176     char *attrs_csc[] = {"objectClass", "term", "nonMemberTerm", NULL},
177         *attrs_cscf[] = {"objectClass", NULL};
178     bool expired;
179     const char* pam_rhost;
180     int msg_csc, msg_cscf;
181     LDAPMessage *res_csc = NULL, *res_cscf = NULL;
182     struct timeval timeout = {PAM_CSC_LDAP_TIMEOUT, 0};
183     LDAPMessage* entry = NULL;
184     char **values = NULL, **nmvalues = NULL, **values_iter = NULL;
185
186     /* determine username */
187     if((pam_get_user(pamh, &username, NULL) != PAM_SUCCESS) || !username)
188     {
189         return PAM_USER_UNKNOWN;
190     }
191
192     /* check uid range */
193     pwd = getpwnam(username);
194     if(pwd)
195     {
196         /* these ranges are taken from puppet/documents/id-range */
197         if(pwd->pw_uid < 500 || (pwd->pw_uid >= 1000 && pwd->pw_uid < 10000))
198         {
199             return PAM_SUCCESS;
200         }
201     }
202
203     /* check username */
204     for(i = 0; i < sizeof(allowed_usernames) / sizeof(char*); i++)
205     {
206         if(strcmp(allowed_usernames[i], username) == 0)
207         {
208             return PAM_SUCCESS;
209         }
210     }
211
212     /* escape username */
213     WARN_ZERO( username_escaped = pam_csc_escape_ldap_string(username) );
214
215     /* get term info and compute current and previous term */
216     WARN_NEG1( cur_time = time(NULL) )
217     WARN_ZERO( local_time = localtime(&cur_time) )
218     long_term = 3 * (1900 + local_time->tm_year) + (local_time->tm_mon / 4);
219     sprintf(cur_term, "%c%d", term_chars[long_term % 3], long_term / 3);
220     long_term--;
221     sprintf(prev_term, "%c%d", term_chars[long_term % 3], long_term / 3);
222     term_month = local_time->tm_mon % 4;
223
224     /* connect to CSC */
225     WARN_LDAP( ldap_create(&ld_csc) )
226     WARN_NEG1( ldap_simple_bind(ld_csc, NULL, NULL) )
227
228     /* check if we are logging in from a CSCF teaching thin client */
229     cscf = false;
230     if(pam_get_item(pamh, PAM_RHOST, (const void**)&pam_rhost) && pam_rhost)
231     {
232         /* TODO: check if pam_rhost is tcNNN.student.cs */
233     }
234
235     if(cscf)
236     {
237         pam_csc_sasl_interact_param_t interact_param = {
238             PAM_CSC_CSCF_SASL_REALM,
239             PAM_CSC_CSCF_SASL_USER
240         };
241         int ret;
242
243         /* read password file */
244         WARN_ZERO( pass_file = fopen(PAM_CSC_CSCF_PASSWORD_FILE, "r") )
245         ret = fread(interact_param.pass, sizeof(char), 
246             sizeof(interact_param.pass) - 1, pass_file);
247         interact_param.pass[ret] = '\0';
248         if(ret && interact_param.pass[ret - 1] == '\n')
249             interact_param.pass[ret - 1] = '\0';
250         fclose(pass_file); pass_file = NULL;
251
252         /* connect to CSCF */
253         WARN_LDAP( ldap_initialize(&ld_cscf, PAM_CSC_CSCF_URI) )
254         WARN_NEG1( ldap_sasl_interactive_bind_s(ld_cscf, PAM_CSC_CSCF_BIND_DN,
255             "DIGEST-MD5", NULL, NULL, LDAP_SASL_INTERACTIVE | LDAP_SASL_QUIET,
256             pam_csc_sasl_interact, &interact_param) )
257     }
258
259     /* create CSC request string */
260     WARN_ZERO( filter_csc = malloc(140 + strlen(username_escaped)) )
261     sprintf(filter_csc, "(&(uid=%s)(|(&(objectClass=member)(|(term=%s)(term=%s)(nonMemberTerm=%s)(nonMemberTerm=%s)))(!(objectClass=member))))", username_escaped, cur_term, prev_term, cur_term, prev_term);
262
263     /* issue CSC request */
264     WARN_NEG1( msg_csc = ldap_search(ld_csc, PAM_CSC_CSC_BASE_DN, 
265         LDAP_SCOPE_SUBTREE, filter_csc, attrs_csc, 0) )
266
267     if(cscf)
268     {
269         /* create CSCF request string */
270         WARN_ZERO( filter_cscf = malloc(100 + strlen(username_escaped)) )
271         sprintf(filter_csc, "TODO %s", username_escaped);
272
273         /* issue CSCF request */
274         WARN_NEG1( msg_cscf = ldap_search(ld_cscf, PAM_CSC_CSCF_BASE_DN, 
275             LDAP_SCOPE_SUBTREE, filter_cscf, attrs_cscf, 1) )
276     }
277
278     /* wait for CSC response */
279     WARN_NEG1( ldap_result(ld_csc, msg_csc, 1, &timeout, &res_csc) )
280
281     /* check if we received an entry from CSC */
282     if(ldap_count_entries(ld_csc, res_csc) == 0)
283     {
284         /* show notice and disallow login */
285         pam_csc_print_message(pamh, PAM_CSC_EXPIRED_MSG, PAM_ERROR_MSG);
286         syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_EXPIRED_WARNING, 
287             username);
288         retval = PAM_AUTH_ERR;
289         goto cleanup;
290     }
291
292     /* get CSC entry */
293     WARN_ZERO( entry = ldap_first_entry(ld_csc, res_csc) )
294     values = ldap_get_values(ld_csc, entry, "term");
295     nmvalues = ldap_get_values(ld_csc, entry, "nonMemberTerm");
296
297     if(!values && !nmvalues)
298     {
299         syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_NOT_A_MEMBER, 
300             username);
301         retval = PAM_SUCCESS;
302         goto cleanup;
303     }
304
305     /* iterate through term attributes */
306     expired = true;
307     if (values) {
308         values_iter = values;
309         while(*values_iter)
310         {
311             if(strcmp(*values_iter, cur_term) == 0)
312             {
313                 /* user is registered in current term */
314                 expired = false;
315                 break;
316             }
317             values_iter++;
318         }
319     }
320     if (nmvalues) {
321         values_iter = nmvalues;
322         while (*values_iter) {
323             if (strcmp(*values_iter, cur_term) == 0) {
324                 expired = false;
325                 break;
326             }
327             values_iter++;
328         }
329     }
330
331     /* check if account is expired */
332     if(expired)
333     {
334         /* we allow once month grace-period */
335         if(term_month == 0)
336         {
337             /* show notice and continue */
338             pam_csc_print_message(pamh, PAM_CSC_EXPIRED_MSG, PAM_TEXT_INFO);
339             syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_EXPIRED_ERROR, 
340                 username);
341         }
342         else
343         {
344             /* show notice and disallow login */
345             pam_csc_print_message(pamh, PAM_CSC_EXPIRED_MSG, PAM_ERROR_MSG);
346             syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_EXPIRED_WARNING, 
347                 username);
348             retval = PAM_AUTH_ERR;
349             goto cleanup;
350         }
351     }
352
353     if(cscf)
354     {
355         /* wait for CSCF response */
356         WARN_NEG1( ldap_result(ld_cscf, msg_cscf, 1, &timeout, &res_cscf) )
357
358         /* check if we got an entry back from CSCF */
359         if(ldap_count_entries(ld_cscf, res_cscf) == 0)
360         {
361             /* output CSCF disallowed message */
362             pam_csc_print_message(pamh, PAM_CSC_CSCF_DISALLOWED_MSG, 
363                 PAM_ERROR_MSG);
364             syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_CSCF_DISALLOWED, 
365                 username);
366             retval = PAM_AUTH_ERR;
367             goto cleanup;
368         }
369     }
370
371 cleanup:
372
373     if(values) ldap_value_free(values);
374     if(nmvalues) ldap_value_free(nmvalues);
375     if(res_csc) ldap_msgfree(res_csc);
376     if(res_cscf) ldap_msgfree(res_cscf);
377     if(ld_csc) ldap_unbind(ld_csc);
378     if(ld_cscf) ldap_unbind(ld_cscf);
379     if(filter_csc) free(filter_csc);
380     if(filter_cscf) free(filter_cscf);
381     if(username_escaped) free(username_escaped);
382
383     return retval;
384 }