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