]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/c-apps/oils_auth_internal.c
LP#1474029: teach Evergreen how to prevent expired staff from logging in
[Evergreen.git] / Open-ILS / src / c-apps / oils_auth_internal.c
1 #define _XOPEN_SOURCE
2 #include <time.h>
3 #include <string.h>
4 #include <strings.h>
5 #include "opensrf/osrf_app_session.h"
6 #include "opensrf/osrf_application.h"
7 #include "opensrf/osrf_settings.h"
8 #include "opensrf/osrf_json.h"
9 #include "opensrf/log.h"
10 #include "openils/oils_utils.h"
11 #include "openils/oils_constants.h"
12 #include "openils/oils_event.h"
13
14 #define OILS_AUTH_CACHE_PRFX "oils_auth_"
15 #define OILS_AUTH_COUNT_SFFX "_count"
16
17 #define MODULENAME "open-ils.auth_internal"
18
19 #define OILS_AUTH_OPAC "opac"
20 #define OILS_AUTH_STAFF "staff"
21 #define OILS_AUTH_TEMP "temp"
22 #define OILS_AUTH_PERSIST "persist"
23
24 #define BLOCK_EXPIRED_STAFF_LOGIN_FLAG "auth.block_expired_staff_login"
25
26 // Default time for extending a persistent session: ten minutes
27 #define DEFAULT_RESET_INTERVAL 10 * 60
28
29 int safe_line = __LINE__;
30 #define OILS_LOG_MARK_SAFE __FILE__,safe_line
31
32 int osrfAppInitialize();
33 int osrfAppChildInit();
34
35 static long _oilsAuthOPACTimeout = 0;
36 static long _oilsAuthStaffTimeout = 0;
37 static long _oilsAuthOverrideTimeout = 0;
38 static long _oilsAuthPersistTimeout = 0;
39
40 /**
41     @brief Initialize the application by registering functions for method calls.
42     @return Zero on success, 1 on error.
43 */
44 int osrfAppInitialize() {
45
46     osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Internal Server...");
47
48     /* load and parse the IDL */
49     /* return non-zero to indicate error */
50     if (!oilsInitIDL(NULL)) return 1; 
51
52     osrfAppRegisterMethod(
53         MODULENAME,
54         "open-ils.auth_internal.session.create",
55         "oilsAuthInternalCreateSession",
56         "Adds a user to the authentication cache to indicate "
57         "the user is authenticated", 1, 0 
58     );
59
60     osrfAppRegisterMethod(
61         MODULENAME,
62         "open-ils.auth_internal.user.validate",
63         "oilsAuthInternalValidate",
64         "Determines whether a user should be allowed to login.  " 
65         "Returns SUCCESS oilsEvent when the user is valid, otherwise "
66         "returns a non-SUCCESS oilsEvent object", 1, 0
67     );
68
69     return 0;
70 }
71
72 /**
73     @brief Dummy placeholder for initializing a server drone.
74
75     There is nothing to do, so do nothing.
76 */
77 int osrfAppChildInit() {
78     return 0;
79 }
80
81
82 /**
83     @brief Determine the login timeout.
84     @param userObj Pointer to an object describing the user.
85     @param type Pointer to one of four possible character strings identifying the login type.
86     @param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
87     @return The length of the timeout, in seconds.
88
89     The default timeout value comes from the configuration file, and
90     depends on the login type.
91
92     The default may be overridden by a corresponding org unit setting.
93     The @a orgloc parameter says what org unit to use for the lookup.
94     If @a orgloc <= 0, or if the lookup for @a orgloc yields no result,
95     we look up the setting for the user's home org unit instead (except
96     that if it's the same as @a orgloc we don't bother repeating the
97     lookup).
98
99     Whether defined in the config file or in an org unit setting, a
100     timeout value may be expressed as a raw number (i.e. all digits,
101     possibly with leading and/or trailing white space) or as an interval
102     string to be translated into seconds by PostgreSQL.
103 */
104 static long oilsAuthGetTimeout(
105     const jsonObject* userObj, const char* type, int orgloc) {
106
107     if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */
108
109         jsonObject* value_obj;
110
111         value_obj = osrf_settings_host_value_object(
112             "/apps/open-ils.auth_internal/app_settings/default_timeout/opac" );
113         _oilsAuthOPACTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
114         jsonObjectFree(value_obj);
115         if( -1 == _oilsAuthOPACTimeout ) {
116             osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for OPAC logins" );
117             _oilsAuthOPACTimeout = 0;
118         }
119
120         value_obj = osrf_settings_host_value_object(
121             "/apps/open-ils.auth_internal/app_settings/default_timeout/staff" );
122         _oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
123         jsonObjectFree(value_obj);
124         if( -1 == _oilsAuthStaffTimeout ) {
125             osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for staff logins" );
126             _oilsAuthStaffTimeout = 0;
127         }
128
129         value_obj = osrf_settings_host_value_object(
130             "/apps/open-ils.auth_internal/app_settings/default_timeout/temp" );
131         _oilsAuthOverrideTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
132         jsonObjectFree(value_obj);
133         if( -1 == _oilsAuthOverrideTimeout ) {
134             osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for temp logins" );
135             _oilsAuthOverrideTimeout = 0;
136         }
137
138         value_obj = osrf_settings_host_value_object(
139             "/apps/open-ils.auth_internal/app_settings/default_timeout/persist" );
140         _oilsAuthPersistTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
141         jsonObjectFree(value_obj);
142         if( -1 == _oilsAuthPersistTimeout ) {
143             osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for persist logins" );
144             _oilsAuthPersistTimeout = 0;
145         }
146
147         osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: "
148             "opac => %ld : staff => %ld : temp => %ld : persist => %ld",
149             _oilsAuthOPACTimeout, _oilsAuthStaffTimeout,
150             _oilsAuthOverrideTimeout, _oilsAuthPersistTimeout );
151     }
152
153     int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
154     if(orgloc < 1)
155         orgloc = home_ou;
156
157     char* setting = NULL;
158     long default_timeout = 0;
159
160     if( !strcmp( type, OILS_AUTH_OPAC )) {
161         setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
162         default_timeout = _oilsAuthOPACTimeout;
163     } else if( !strcmp( type, OILS_AUTH_STAFF )) {
164         setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
165         default_timeout = _oilsAuthStaffTimeout;
166     } else if( !strcmp( type, OILS_AUTH_TEMP )) {
167         setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
168         default_timeout = _oilsAuthOverrideTimeout;
169     } else if( !strcmp( type, OILS_AUTH_PERSIST )) {
170         setting = OILS_ORG_SETTING_PERSIST_TIMEOUT;
171         default_timeout = _oilsAuthPersistTimeout;
172     }
173
174     // Get the org unit setting, if there is one.
175     char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
176     if(!timeout) {
177         if( orgloc != home_ou ) {
178             osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
179                 "trying home_ou %d", orgloc, home_ou );
180             timeout = oilsUtilsFetchOrgSetting( home_ou, setting );
181         }
182     }
183
184     if(!timeout)
185         return default_timeout;   // No override from org unit setting
186
187     // Translate the org unit setting to a number
188     long t;
189     if( !*timeout ) {
190         osrfLogWarning( OSRF_LOG_MARK,
191             "Timeout org unit setting is an empty string for %s login; using default",
192             timeout, type );
193         t = default_timeout;
194     } else {
195         // Treat timeout string as an interval, and convert it to seconds
196         t = oilsUtilsIntervalToSeconds( timeout );
197         if( -1 == t ) {
198             // Unable to convert; possibly an invalid interval string
199             osrfLogError( OSRF_LOG_MARK,
200                 "Unable to convert timeout interval \"%s\" for %s login; using default",
201                 timeout, type );
202             t = default_timeout;
203         }
204     }
205
206     free(timeout);
207     return t;
208 }
209
210 /**
211  * Verify workstation exists and stuff it into the user object to be cached
212  */
213 static oilsEvent* oilsAuthVerifyWorkstation(
214         const osrfMethodContext* ctx, jsonObject* userObj, const char* ws ) {
215
216     jsonObject* workstation = oilsUtilsFetchWorkstationByName(ws);
217
218     if(!workstation || workstation->type == JSON_NULL) {
219         jsonObjectFree(workstation);
220         return oilsNewEvent(OSRF_LOG_MARK, "WORKSTATION_NOT_FOUND");
221     }
222
223     long wsid = oilsFMGetObjectId(workstation);
224     LONG_TO_STRING(wsid);
225     char* orgid = oilsFMGetString(workstation, "owning_lib");
226     oilsFMSetString(userObj, "wsid", LONGSTR);
227     oilsFMSetString(userObj, "ws_ou", orgid);
228     free(orgid);
229     jsonObjectFree(workstation);
230     return NULL;
231 }
232
233 /**
234     Verifies that the user has permission to login with the given type.  
235     Caller is responsible for freeing returned oilsEvent.
236     @return oilsEvent* if the permission check failed, NULL otherwise.
237 */
238 static oilsEvent* oilsAuthCheckLoginPerm(osrfMethodContext* ctx, 
239     int user_id, int org_id, const char* type ) {
240
241     // For backwards compatibility, check all login permissions 
242     // using the root org unit as the context org unit.
243     org_id = -1;
244
245     char* perms[1];
246
247     if (!strcasecmp(type, OILS_AUTH_OPAC)) {
248         perms[0] = "OPAC_LOGIN";
249
250     } else if (!strcasecmp(type, OILS_AUTH_STAFF)) {
251         perms[0] = "STAFF_LOGIN";
252
253     } else if (!strcasecmp(type, OILS_AUTH_TEMP)) {
254         perms[0] = "STAFF_LOGIN";
255
256     } else if (!strcasecmp(type, OILS_AUTH_PERSIST)) {
257         perms[0] = "PERSISTENT_LOGIN";
258     }
259
260     return oilsUtilsCheckPerms(user_id, org_id, perms, 1);
261 }
262
263
264
265 /**
266     @brief Implement the session create method
267     @param ctx The method context.
268     @return -1 upon error; zero if successful, and if a STATUS message has 
269     been sent to the client to indicate completion; a positive integer if 
270     successful but no such STATUS message has been sent.
271
272     Method parameters:
273     - a hash with some combination of the following elements:
274         - "user_id"     -- actor.usr (au) ID for the user to cache.
275         - "org_unit"    -- actor.org_unit (aou) ID representing the physical 
276                            location / context used for timeout, etc. settings.
277         - "login_type"  -- login type (opac, staff, temp, persist)
278         - "workstation" -- workstation name
279
280 */
281 int oilsAuthInternalCreateSession(osrfMethodContext* ctx) {
282     OSRF_METHOD_VERIFY_CONTEXT(ctx);
283
284     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
285
286     const char* user_id     = jsonObjectGetString(jsonObjectGetKeyConst(args, "user_id"));
287     const char* login_type  = jsonObjectGetString(jsonObjectGetKeyConst(args, "login_type"));
288     const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
289     int org_unit            = jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org_unit"));
290
291     if ( !(user_id && login_type) ) {
292         return osrfAppRequestRespondException( ctx->session, ctx->request,
293             "Missing parameters for method: %s", ctx->method->name );
294     }
295
296     oilsEvent* response = NULL;
297
298     // fetch the user object
299     jsonObject* idParam = jsonNewNumberStringObject(user_id);
300     jsonObject* userObj = oilsUtilsCStoreReqCtx(
301         ctx, "open-ils.cstore.direct.actor.user.retrieve", idParam);
302     jsonObjectFree(idParam);
303
304     if (!userObj) {
305         return osrfAppRequestRespondException(ctx->session, 
306             ctx->request, "No user found with ID %s", user_id);
307     }
308
309     // If a workstation is defined, add the workstation info
310     if (workstation) {
311         response = oilsAuthVerifyWorkstation(ctx, userObj, workstation);
312
313         if (response) { // invalid workstation.
314             jsonObjectFree(userObj);
315             osrfAppRespondComplete(ctx, oilsEventToJSON(response));
316             oilsEventFree(response);
317             return 0;
318
319         } else { // workstation OK.  
320
321             // The worksation org unit supersedes any org unit value 
322             // provided via the API.  oilsAuthVerifyWorkstation() sets the 
323             // ws_ou value to the WS owning lib.  A value is guaranteed.
324             org_unit = atoi(oilsFMGetStringConst(userObj, "ws_ou"));
325         }
326
327     } else { // no workstation
328
329         // For backwards compatibility, when no workstation is provided, use 
330         // the users's home org as its workstation org unit, regardless of 
331         // any API-level org unit value provided.
332         const char* orgid = oilsFMGetStringConst(userObj, "home_ou");
333         oilsFMSetString(userObj, "ws_ou", orgid);
334
335         // The context org unit defaults to the user's home library when
336         // no workstation is used and no API-level value is provided.
337         if (org_unit < 1) org_unit = atoi(orgid);
338     }
339
340     // determine the auth/cache timeout
341     long timeout = oilsAuthGetTimeout(userObj, login_type, org_unit);
342
343     char* string = va_list_to_string("%d.%ld.%ld", 
344         (long) getpid(), time(NULL), oilsFMGetObjectId(userObj));
345     char* authToken = md5sum(string);
346     char* authKey = va_list_to_string(
347         "%s%s", OILS_AUTH_CACHE_PRFX, authToken);
348
349     oilsFMSetString(userObj, "passwd", "");
350     jsonObject* cacheObj = jsonParseFmt("{\"authtime\": %ld}", timeout);
351     jsonObjectSetKey(cacheObj, "userobj", jsonObjectClone(userObj));
352
353     if( !strcmp(login_type, OILS_AUTH_PERSIST)) {
354         // Add entries for endtime and reset_interval, so that we can gracefully
355         // extend the session a bit if the user is active toward the end of the 
356         // timeout originally specified.
357         time_t endtime = time( NULL ) + timeout;
358         jsonObjectSetKey(cacheObj, "endtime", 
359             jsonNewNumberObject( (double) endtime ));
360
361         // Reset interval is hard-coded for now, but if we ever want to make it
362         // configurable, this is the place to do it:
363         jsonObjectSetKey(cacheObj, "reset_interval",
364             jsonNewNumberObject( (double) DEFAULT_RESET_INTERVAL));
365     }
366
367     osrfCachePutObject(authKey, cacheObj, (time_t) timeout);
368     jsonObjectFree(cacheObj);
369     jsonObject* payload = jsonParseFmt(
370         "{\"authtoken\": \"%s\", \"authtime\": %ld}", authToken, timeout);
371
372     response = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
373     free(string); free(authToken); free(authKey);
374     jsonObjectFree(payload);
375
376     jsonObjectFree(userObj);
377     osrfAppRespondComplete(ctx, oilsEventToJSON(response));
378     oilsEventFree(response);
379
380     return 0;
381 }
382
383 int _checkIfExpiryDatePassed(const char *expire_date) {
384
385     struct tm expire_tm;
386     memset(&expire_tm, 0, sizeof(expire_tm));
387     strptime(expire_date, "%FT%T%z", &expire_tm);
388     time_t now = time(NULL);
389     time_t expire_time_t = mktime(&expire_tm);
390     if (now > expire_time_t) {
391         return 1;
392     } else {
393         return 0;
394     }
395 }
396
397 int _blockExpiredStaffLogin(osrfMethodContext* ctx, int user_id) {
398     // check global flag whether we're supposed to block or not
399     jsonObject *cgfObj = NULL, *params = NULL;
400     params = jsonNewObject(BLOCK_EXPIRED_STAFF_LOGIN_FLAG);
401     cgfObj = oilsUtilsCStoreReqCtx(
402         ctx, "open-ils.cstore.direct.config.global_flag.retrieve", params);
403     jsonObjectFree(params);
404
405     int may_block_login = 0;
406     char* tmp_str = NULL;
407     if (cgfObj && cgfObj->type != JSON_NULL) {
408         tmp_str = oilsFMGetString(cgfObj, "enabled");
409         if (oilsUtilsIsDBTrue(tmp_str)) {
410             may_block_login = 1;
411         }
412         free(tmp_str);
413     }
414     jsonObjectFree(cgfObj);
415
416     if (!may_block_login) {
417         return 0;
418     }
419
420     // OK, we're supposed to block logins by expired staff accounts,
421     // so let's see if the account is one. We'll do so by seeing
422     // if the account has the STAFF_LOGIN permission anywhere. We
423     // are _not_ checking the login_type, as blocking 'staff' and
424     // 'temp' logins still leaves open the possibility of constructing
425     // an 'opac'-type login that _also_ sets a workstation, which
426     // in turn could be used to set an authtoken cookie that works
427     // in the staff interface. This means, that unlike ordinary patrons,
428     // a staff account that expires will not be able to log into
429     // the public catalog... but then, staff members really ought
430     // to be using a separate account when acting as a library patron
431     // anyway.
432
433     int block_login = 0;
434
435     // using the root org unit as the context org unit.
436     int org_id = -1;
437     char* perms[1];
438     perms[0] = "STAFF_LOGIN";
439     oilsEvent* response = oilsUtilsCheckPerms(user_id, org_id, perms, 1);
440
441     if (!response) {
442         // user has STAFF_LOGIN, so should be blocked
443         block_login = 1;
444     } else {
445         oilsEventFree(response);
446     }
447
448     return block_login;
449 }
450
451 int oilsAuthInternalValidate(osrfMethodContext* ctx) {
452     OSRF_METHOD_VERIFY_CONTEXT(ctx);
453
454     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
455
456     const char* user_id     = jsonObjectGetString(jsonObjectGetKeyConst(args, "user_id"));
457     const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
458     const char* login_type  = jsonObjectGetString(jsonObjectGetKeyConst(args, "login_type"));
459     int org_unit            = jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org_unit"));
460
461     if ( !(user_id && login_type) ) {
462         return osrfAppRequestRespondException( ctx->session, ctx->request,
463             "Missing parameters for method: %s", ctx->method->name );
464     }
465
466     oilsEvent* response = NULL;
467     jsonObject *userObj = NULL, *params = NULL;
468     char* tmp_str = NULL;
469     int user_exists = 0, user_active = 0, 
470         user_barred = 0, user_deleted = 0,
471         expired = 0;
472
473     // Confirm user exists, active=true, barred=false, deleted=false
474     params = jsonNewNumberStringObject(user_id);
475     userObj = oilsUtilsCStoreReqCtx(
476         ctx, "open-ils.cstore.direct.actor.user.retrieve", params);
477     jsonObjectFree(params);
478
479     if (userObj && userObj->type != JSON_NULL) {
480         user_exists = 1;
481
482         tmp_str = oilsFMGetString(userObj, "active");
483         user_active = oilsUtilsIsDBTrue(tmp_str);
484         free(tmp_str);
485
486         tmp_str = oilsFMGetString(userObj, "barred");
487         user_barred = oilsUtilsIsDBTrue(tmp_str);
488         free(tmp_str);
489
490         tmp_str = oilsFMGetString(userObj, "deleted");
491         user_deleted = oilsUtilsIsDBTrue(tmp_str);
492         free(tmp_str);
493
494         tmp_str = oilsFMGetString(userObj, "expire_date");
495         expired = _checkIfExpiryDatePassed(tmp_str);
496         free(tmp_str);
497     }
498
499     if (!user_exists || user_barred || user_deleted) {
500         response = oilsNewEvent(OILS_LOG_MARK_SAFE, OILS_EVENT_AUTH_FAILED);
501     }
502
503     if (!response && expired) {
504         if (_blockExpiredStaffLogin(ctx, atoi(user_id))) {
505             tmp_str = oilsFMGetString(userObj, "usrname");
506             osrfLogWarning( OSRF_LOG_MARK, "Blocked login for expired staff user %s", tmp_str );
507             free(tmp_str);
508             response = oilsNewEvent(OILS_LOG_MARK_SAFE, OILS_EVENT_AUTH_FAILED);
509         }
510     }
511
512     if (!response && !user_active) {
513         // In some cases, it's useful for the caller to know if the
514         // patron was unable to login becuase the account is inactive.
515         // Return a specific event for this.
516         response = oilsNewEvent(OILS_LOG_MARK_SAFE, "PATRON_INACTIVE");
517     }
518
519     if (!response && barcode) {
520         // Caller provided a barcode.  Ensure it exists and is active.
521
522         int card_ok = 0;
523         params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
524         jsonObject* card = oilsUtilsCStoreReqCtx(
525             ctx, "open-ils.cstore.direct.actor.card.search", params);
526         jsonObjectFree(params);
527
528         if (card && card->type != JSON_NULL) {
529             tmp_str = oilsFMGetString(card, "active");
530             card_ok = oilsUtilsIsDBTrue(tmp_str);
531             free(tmp_str);
532         }
533
534         jsonObjectFree(card); // card=NULL OK here.
535
536         if (!card_ok) {
537             response = oilsNewEvent(
538                 OILS_LOG_MARK_SAFE, "PATRON_CARD_INACTIVE");
539         }
540     }
541
542     // XXX: login permission checks are always global (see 
543     // oilsAuthCheckLoginPerm()).  No need to extract the 
544     // workstation org unit here.
545
546     if (!response) { // Still OK
547         // Confirm user has permission to login w/ the requested type.
548         response = oilsAuthCheckLoginPerm(
549             ctx, atoi(user_id), org_unit, login_type);
550     }
551
552
553     if (!response) {
554         // No tests failed.  Return SUCCESS.
555         response = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_SUCCESS);
556     }
557
558
559     jsonObjectFree(userObj); // userObj=NULL OK here.
560     osrfAppRespondComplete(ctx, oilsEventToJSON(response));
561     oilsEventFree(response);
562
563     return 0;
564 }
565