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