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