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