Package for stretch and buster
[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     grp = getgrnam("syscom");
203     for(i = 0; grp && grp->gr_mem[i]; i++) {
204         if(!strcmp(grp->gr_mem[i], username)) {
205             syscom = 1;
206             break;
207         }
208     }
209
210     /* check username */
211     for(i = 0; i < sizeof(allowed_usernames) / sizeof(char*); i++)
212     {
213         if(strcmp(allowed_usernames[i], username) == 0)
214         {
215             return PAM_SUCCESS;
216         }
217     }
218
219     /* escape username */
220     WARN_ZERO( username_escaped = pam_csc_escape_ldap_string(username) );
221
222     /* get term info and compute current and previous term */
223     WARN_NEG1( cur_time = time(NULL) )
224     WARN_ZERO( local_time = localtime(&cur_time) )
225     long_term = 3 * (1900 + local_time->tm_year) + (local_time->tm_mon / 4);
226     sprintf(cur_term, "%c%d", term_chars[long_term % 3], long_term / 3);
227     long_term--;
228     sprintf(prev_term, "%c%d", term_chars[long_term % 3], long_term / 3);
229     term_month = local_time->tm_mon % 4;
230
231     /* connect to CSC */
232     WARN_LDAP( ldap_create(&ld_csc) )
233     WARN_NEG1( ldap_simple_bind(ld_csc, NULL, NULL) )
234
235     /* check if we are logging in from a CSCF teaching thin client */
236     if(pam_get_item(pamh, PAM_RHOST, (const void**)&pam_rhost) && pam_rhost)
237     {
238         /* TODO: check if pam_rhost is tcNNN.student.cs */
239     }
240
241     /* create CSC request string */
242     WARN_ZERO( filter_csc = malloc(140 + strlen(username_escaped)) )
243     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);
244
245     /* issue CSC request */
246     WARN_NEG1( msg_csc = ldap_search(ld_csc, PAM_CSC_CSC_BASE_DN,
247         LDAP_SCOPE_SUBTREE, filter_csc, attrs_csc, 0) )
248
249     /* wait for CSC response */
250     WARN_NEG1( ldap_result(ld_csc, msg_csc, 1, &timeout, &res_csc) )
251
252     /* check if we received an entry from CSC */
253     if(ldap_count_entries(ld_csc, res_csc) == 0)
254     {
255         /* show notice and disallow login */
256         pam_csc_print_message(pamh, PAM_CSC_EXPIRED_MSG, PAM_ERROR_MSG);
257         syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_EXPIRED_NO_TERMS,
258             username);
259         retval = (syscom ? PAM_SUCCESS : PAM_AUTH_ERR);
260         goto cleanup;
261     }
262
263     /* get CSC entry */
264     WARN_ZERO( entry = ldap_first_entry(ld_csc, res_csc) )
265     values = ldap_get_values(ld_csc, entry, "term");
266     nmvalues = ldap_get_values(ld_csc, entry, "nonMemberTerm");
267
268     if(!values && !nmvalues)
269     {
270         syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_NOT_A_MEMBER,
271             username);
272         retval = PAM_SUCCESS;
273         goto cleanup;
274     }
275
276     /* iterate through term attributes */
277     expired = true;
278     if (values) {
279         values_iter = values;
280         while(*values_iter)
281         {
282             if(strcmp(*values_iter, cur_term) == 0)
283             {
284                 /* user is registered in current term */
285                 expired = false;
286                 break;
287             }
288             values_iter++;
289         }
290     }
291     if (nmvalues) {
292         values_iter = nmvalues;
293         while (*values_iter) {
294             if (strcmp(*values_iter, cur_term) == 0) {
295                 expired = false;
296                 break;
297             }
298             values_iter++;
299         }
300     }
301
302     /* check if account is expired */
303     if(expired)
304     {
305         /* we allow once month grace-period */
306         if(term_month == 0)
307         {
308             /* show notice and continue */
309             pam_csc_print_message(pamh, PAM_CSC_EXPIRED_MSG, PAM_TEXT_INFO);
310             syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_EXPIRED_LAST_TERM,
311                 username);
312         }
313         else
314         {
315             /* show notice and disallow login */
316             pam_csc_print_message(pamh, PAM_CSC_EXPIRED_MSG, PAM_ERROR_MSG);
317             syslog(LOG_AUTHPRIV | LOG_NOTICE, PAM_CSC_SYSLOG_EXPIRED_NO_TERMS,
318                 username);
319             retval = (syscom ? PAM_SUCCESS : PAM_AUTH_ERR);
320         }
321     }
322
323 cleanup:
324     if(values) ldap_value_free(values);
325     if(nmvalues) ldap_value_free(nmvalues);
326     if(res_csc) ldap_msgfree(res_csc);
327     if(ld_csc) ldap_unbind(ld_csc);
328     if(filter_csc) free(filter_csc);
329     if(username_escaped) free(username_escaped);
330     return retval;
331 }
332