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