Made it so that syscom is checked in pam_csc.so instead of earlier in the PAM stack.
[public/libpam-csc.git] / pam_csc.c
1 #define PAM_SM_ACCOUNT
2 #define LDAP_DEPRECATED 1
3 #include <unistd.h>
4 #include <sys/types.h>
5 #include <sys/time.h>
6 #include <time.h>
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <stdbool.h>
10 #include <string.h>
11 #include <security/pam_appl.h>
12 #include <security/pam_modules.h>
13 #include <ldap.h>
14 #include <sasl/sasl.h>
15 #include <syslog.h>
16 #include <pwd.h>
17 #include <grp.h>
18
19 #ifndef LDAP_SASL_QUIET
20 #  define LDAP_SASL_QUIET 0
21 #endif
22
23 #ifndef LOG_AUTHPRIV
24 #  define LOG_AUTHPRIV LOG_AUTH
25 #endif
26
27 #ifndef PAM_EXTERN
28 #  define PAM_EXTERN extern
29 #endif
30
31 #define PAM_CSC_CSC_BASE_DN         "ou=People,dc=csclub,dc=uwaterloo,dc=ca"
32 #define PAM_CSC_LDAP_TIMEOUT        5
33 #define PAM_CSC_ALLOWED_USERNAMES   {"nobody"}
34 #define PAM_CSC_EXPIRED_MSG \
35     "*****************************************************************************\n" \
36     "*                                                                           *\n" \
37     "*    Your account has expired - please contact the Computer Science Club    *\n" \
38     "*                                                                           *\n" \
39     "*****************************************************************************\n"
40
41 #define PAM_CSC_SYSLOG_EXPIRED_NO_TERMS \
42     "(pam_csc): %s was not registered for current term or previous term - denying login\n"
43 #define PAM_CSC_SYSLOG_EXPIRED_LAST_TERM \
44     "(pam_csc): %s was not registered for current term but was registered for previous term - permitting login\n"
45 #define PAM_CSC_SYSLOG_NOT_A_MEMBER \
46     "(pam_csc): %s is not a member account - permitting 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     struct group *grp;
165     const char* allowed_usernames[] = PAM_CSC_ALLOWED_USERNAMES;
166     unsigned int i;
167     time_t cur_time;
168     struct tm* local_time;
169     int long_term, term_month;
170     static const char term_chars[] = {'w', 's', 'f'};
171     char cur_term[6], prev_term[6];
172     LDAP *ld_csc = NULL;
173     char* username_escaped = NULL;
174     char *filter_csc = NULL;
175     char *attrs_csc[] = {"objectClass", "term", "nonMemberTerm", NULL};
176     bool expired, syscom = 0; 
177     const char* pam_rhost;
178     int msg_csc;
179     LDAPMessage *res_csc = NULL;
180     struct timeval timeout = {PAM_CSC_LDAP_TIMEOUT, 0};
181     LDAPMessage* entry = NULL;
182     char **values = NULL, **nmvalues = 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 range */
191     pwd = getpwnam(username);
192     if(pwd)
193     {
194         /* these ranges are taken from puppet/documents/id-range */
195         if(pwd->pw_uid < 500 || (pwd->pw_uid >= 1000 && pwd->pw_uid < 10000))
196         {
197             return PAM_SUCCESS;
198         }
199     }
200
201     /* check to see if user is in group syscom, if yes, still print message but allow login even if user expired */
202     i = 0;
203     grp = getgrnam("syscom");
204     while(grp->gr_mem[i] != NULL) {
205         if(!strcmp(grp->gr_mem[i], username)) {
206             syscom = 1;
207             break;
208         }
209         i++;
210     }
211
212     /* check username */
213     for(i = 0; i < sizeof(allowed_usernames) / sizeof(char*); i++)
214     {
215         if(strcmp(allowed_usernames[i], username) == 0)
216         {
217             return PAM_SUCCESS;
218         }
219     }
220
221     /* escape username */
222     WARN_ZERO( username_escaped = pam_csc_escape_ldap_string(username) );
223
224     /* get term info and compute current and previous term */
225     WARN_NEG1( cur_time = time(NULL) )
226     WARN_ZERO( local_time = localtime(&cur_time) )
227     long_term = 3 * (1900 + local_time->tm_year) + (local_time->tm_mon / 4);
228     sprintf(cur_term, "%c%d", term_chars[long_term % 3], long_term / 3);
229     long_term--;
230     sprintf(prev_term, "%c%d", term_chars[long_term % 3], long_term / 3);
231     term_month = local_time->tm_mon % 4;
232
233     /* connect to CSC */
234     WARN_LDAP( ldap_create(&ld_csc) )
235     WARN_NEG1( ldap_simple_bind(ld_csc, NULL, NULL) )
236
237     /* check if we are logging in from a CSCF teaching thin client */
238     if(pam_get_item(pamh, PAM_RHOST, (const void**)&pam_rhost) && pam_rhost)
239     {
240         /* TODO: check if pam_rhost is tcNNN.student.cs */
241     }
242
243     /* create CSC request string */
244     WARN_ZERO( filter_csc = malloc(140 + strlen(username_escaped)) )
245     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);
246
247     /* issue CSC request */
248     WARN_NEG1( msg_csc = ldap_search(ld_csc, PAM_CSC_CSC_BASE_DN,
249         LDAP_SCOPE_SUBTREE, filter_csc, attrs_csc, 0) )
250
251     /* wait for CSC response */
252     WARN_NEG1( ldap_result(ld_csc, msg_csc, 1, &timeout, &res_csc) )
253
254     /* check if we received an entry from CSC */
255     if(ldap_count_entries(ld_csc, res_csc) == 0)
256     {
257         /* show notice and disallow login */
258         pam_csc_print_message(pamh, PAM_CSC_EXPIRED_MSG, PAM_ERROR_MSG);
259         syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_EXPIRED_NO_TERMS,
260             username);
261         retval = (syscom ? PAM_SUCCESS : PAM_AUTH_ERR);
262         goto cleanup;
263     }
264
265     /* get CSC entry */
266     WARN_ZERO( entry = ldap_first_entry(ld_csc, res_csc) )
267     values = ldap_get_values(ld_csc, entry, "term");
268     nmvalues = ldap_get_values(ld_csc, entry, "nonMemberTerm");
269
270     if(!values && !nmvalues)
271     {
272         syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_NOT_A_MEMBER,
273             username);
274         retval = PAM_SUCCESS;
275         goto cleanup;
276     }
277
278     /* iterate through term attributes */
279     expired = true;
280     if (values) {
281         values_iter = values;
282         while(*values_iter)
283         {
284             if(strcmp(*values_iter, cur_term) == 0)
285             {
286                 /* user is registered in current term */
287                 expired = false;
288                 break;
289             }
290             values_iter++;
291         }
292     }
293     if (nmvalues) {
294         values_iter = nmvalues;
295         while (*values_iter) {
296             if (strcmp(*values_iter, cur_term) == 0) {
297                 expired = false;
298                 break;
299             }
300             values_iter++;
301         }
302     }
303
304     /* check if account is expired */
305     if(expired)
306     {
307         /* we allow once month grace-period */
308         if(term_month == 0)
309         {
310             /* show notice and continue */
311             pam_csc_print_message(pamh, PAM_CSC_EXPIRED_MSG, PAM_TEXT_INFO);
312             syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_EXPIRED_LAST_TERM,
313                 username);
314         }
315         else
316         {
317             /* show notice and disallow login */
318             pam_csc_print_message(pamh, PAM_CSC_EXPIRED_MSG, PAM_ERROR_MSG);
319             syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_EXPIRED_NO_TERMS,
320                 username);
321             retval = (syscom ? PAM_SUCCESS : PAM_AUTH_ERR);
322         }
323     }
324
325 cleanup:
326     if(values) ldap_value_free(values);
327     if(nmvalues) ldap_value_free(nmvalues);
328     if(res_csc) ldap_msgfree(res_csc);
329     if(ld_csc) ldap_unbind(ld_csc);
330     if(filter_csc) free(filter_csc);
331     if(username_escaped) free(username_escaped);
332     return retval;
333 }
334