]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/c-apps/oils_auth.c
LP#1485374: add way for C code to make TZ-aware subrequests
[Evergreen.git] / Open-ILS / src / c-apps / oils_auth.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 #include <pcre.h>
10
11 #define OILS_AUTH_CACHE_PRFX "oils_auth_"
12 #define OILS_AUTH_COUNT_SFFX "_count"
13
14 #define MODULENAME "open-ils.auth"
15
16 #define OILS_AUTH_OPAC "opac"
17 #define OILS_AUTH_STAFF "staff"
18 #define OILS_AUTH_TEMP "temp"
19 #define OILS_AUTH_PERSIST "persist"
20
21 // Default time for extending a persistent session: ten minutes
22 #define DEFAULT_RESET_INTERVAL 10 * 60
23
24 int osrfAppInitialize();
25 int osrfAppChildInit();
26
27 static long _oilsAuthSeedTimeout = 0;
28 static long _oilsAuthBlockTimeout = 0;
29 static long _oilsAuthBlockCount = 0;
30
31
32 /**
33         @brief Initialize the application by registering functions for method calls.
34         @return Zero in all cases.
35 */
36 int osrfAppInitialize() {
37
38         osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");
39
40         /* load and parse the IDL */
41         if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
42
43         osrfAppRegisterMethod(
44                 MODULENAME,
45                 "open-ils.auth.authenticate.init",
46                 "oilsAuthInit",
47                 "Start the authentication process and returns the intermediate authentication seed"
48                 " PARAMS( username )", 1, 0 );
49
50     osrfAppRegisterMethod(
51         MODULENAME,
52         "open-ils.auth.authenticate.init.barcode",
53         "oilsAuthInitBarcode",
54         "Start the authentication process using a patron barcode and return "
55         "the intermediate authentication seed. PARAMS(barcode)", 1, 0);
56
57     osrfAppRegisterMethod(
58         MODULENAME,
59         "open-ils.auth.authenticate.init.username",
60         "oilsAuthInitUsername",
61         "Start the authentication process using a patron username and return "
62         "the intermediate authentication seed. PARAMS(username)", 1, 0);
63
64         osrfAppRegisterMethod(
65                 MODULENAME,
66                 "open-ils.auth.authenticate.complete",
67                 "oilsAuthComplete",
68                 "Completes the authentication process.  Returns an object like so: "
69                 "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
70                 "token and authtime is the number of seconds the session will be active"
71                 "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
72                 "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
73                 "org_id is the location at which the login should be considered "
74                 "active for login timeout purposes", 1, 0 );
75
76         osrfAppRegisterMethod(
77                 MODULENAME,
78                 "open-ils.auth.authenticate.verify",
79                 "oilsAuthComplete",
80                 "Verifies the user provided a valid username and password."
81                 "Params and are the same as open-ils.auth.authenticate.complete."
82                 "Returns SUCCESS event on success, failure event on failure", 1, 0);
83
84
85         osrfAppRegisterMethod(
86                 MODULENAME,
87                 "open-ils.auth.session.retrieve",
88                 "oilsAuthSessionRetrieve",
89                 "Pass in the auth token and this retrieves the user object.  The auth "
90                 "timeout is reset when this call is made "
91                 "Returns the user object (password blanked) for the given login session "
92                 "PARAMS( authToken )", 1, 0 );
93
94         osrfAppRegisterMethod(
95                 MODULENAME,
96                 "open-ils.auth.session.delete",
97                 "oilsAuthSessionDelete",
98                 "Destroys the given login session "
99                 "PARAMS( authToken )",  1, 0 );
100
101         osrfAppRegisterMethod(
102                 MODULENAME,
103                 "open-ils.auth.session.reset_timeout",
104                 "oilsAuthResetTimeout",
105                 "Resets the login timeout for the given session "
106                 "Returns an ILS Event with payload = session_timeout of session "
107                 "if found, otherwise returns the NO_SESSION event"
108                 "PARAMS( authToken )", 1, 0 );
109
110         if(!_oilsAuthSeedTimeout) { /* Load the default timeouts */
111
112                 jsonObject* value_obj;
113
114                 value_obj = osrf_settings_host_value_object(
115                         "/apps/open-ils.auth/app_settings/auth_limits/seed" );
116                 _oilsAuthSeedTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
117                 jsonObjectFree(value_obj);
118                 if( -1 == _oilsAuthSeedTimeout ) {
119                         osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Auth Seeds - Using 30 seconds" );
120                         _oilsAuthSeedTimeout = 30;
121                 }
122
123                 value_obj = osrf_settings_host_value_object(
124                         "/apps/open-ils.auth/app_settings/auth_limits/block_time" );
125                 _oilsAuthBlockTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
126                 jsonObjectFree(value_obj);
127                 if( -1 == _oilsAuthBlockTimeout ) {
128                         osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Blocking Timeout - Using 3x Seed" );
129                         _oilsAuthBlockTimeout = _oilsAuthSeedTimeout * 3;
130                 }
131
132                 value_obj = osrf_settings_host_value_object(
133                         "/apps/open-ils.auth/app_settings/auth_limits/block_count" );
134                 _oilsAuthBlockCount = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
135                 jsonObjectFree(value_obj);
136                 if( -1 == _oilsAuthBlockCount ) {
137                         osrfLogWarning( OSRF_LOG_MARK, "Invalid count for Blocking - Using 10" );
138                         _oilsAuthBlockCount = 10;
139                 }
140
141                 osrfLogInfo(OSRF_LOG_MARK, "Set auth limits: "
142                         "seed => %ld : block_timeout => %ld : block_count => %ld",
143                         _oilsAuthSeedTimeout, _oilsAuthBlockTimeout, _oilsAuthBlockCount );
144         }
145
146         return 0;
147 }
148
149 /**
150         @brief Dummy placeholder for initializing a server drone.
151
152         There is nothing to do, so do nothing.
153 */
154 int osrfAppChildInit() {
155         return 0;
156 }
157
158 // free() response
159 static char* oilsAuthGetSalt(int user_id) {
160     char* salt_str = NULL;
161
162     jsonObject* params = jsonParseFmt( // free
163         "{\"from\":[\"actor.get_salt\",%d,\"%s\"]}", user_id, "main");
164
165     jsonObject* salt_obj = // free
166         oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
167
168     jsonObjectFree(params);
169
170     if (salt_obj) {
171
172         if (salt_obj->type != JSON_NULL) {
173
174             const char* salt_val = jsonObjectGetString(
175                 jsonObjectGetKeyConst(salt_obj, "actor.get_salt"));
176
177             // caller expects a free-able string, could be NULL.
178             if (salt_val) { salt_str = strdup(salt_val); } 
179         }
180
181         jsonObjectFree(salt_obj);
182     }
183
184     return salt_str;
185 }
186
187 // ident is either a username or barcode
188 // Returns the init seed -> requires free();
189 static char* oilsAuthBuildInitCache(
190     int user_id, const char* ident, const char* ident_type, const char* nonce) {
191
192     char* cache_key  = va_list_to_string(
193         "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, nonce);
194
195     char* count_key = va_list_to_string(
196         "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, OILS_AUTH_COUNT_SFFX);
197
198     char* auth_seed;
199     if (user_id == -1) {
200         // user does not exist.  Use a dummy seed
201         auth_seed = strdup("x");
202     } else {
203         auth_seed = oilsAuthGetSalt(user_id);
204     }
205
206     jsonObject* seed_object = jsonParseFmt(
207         "{\"%s\":\"%s\",\"user_id\":%d,\"seed\":\"%s\"}",
208         ident_type, ident, user_id, auth_seed);
209
210     jsonObject* count_object = osrfCacheGetObject(count_key);
211     if(!count_object) {
212         count_object = jsonNewNumberObject((double) 0);
213     }
214
215     osrfCachePutObject(cache_key, seed_object, _oilsAuthSeedTimeout);
216
217     if (user_id != -1) {
218         // Only track login counts for existing users, since a 
219         // login for a nonexistent user will never succeed anyway.
220         osrfCachePutObject(count_key, count_object, _oilsAuthBlockTimeout);
221     }
222
223     osrfLogDebug(OSRF_LOG_MARK, 
224         "oilsAuthInit(): has seed %s and key %s", auth_seed, cache_key);
225
226     free(cache_key);
227     free(count_key);
228     jsonObjectFree(count_object);
229     jsonObjectFree(seed_object);
230
231     return auth_seed;
232 }
233
234 static int oilsAuthInitUsernameHandler(
235     osrfMethodContext* ctx, const char* username, const char* nonce) {
236
237     osrfLogInfo(OSRF_LOG_MARK, 
238         "User logging in with username %s", username);
239
240     int user_id = -1;
241     jsonObject* resp = NULL; // free
242     jsonObject* user_obj = oilsUtilsFetchUserByUsername(ctx, username); // free
243
244     if (user_obj && user_obj->type != JSON_NULL) 
245         user_id = oilsFMGetObjectId(user_obj);
246
247     jsonObjectFree(user_obj); // NULL OK
248
249     char* seed = oilsAuthBuildInitCache(user_id, username, "username", nonce);
250     resp = jsonNewObject(seed);
251     free(seed);
252
253     osrfAppRespondComplete(ctx, resp);
254     jsonObjectFree(resp);
255     return 0;
256 }
257
258 // open-ils.auth.authenticate.init.username
259 int oilsAuthInitUsername(osrfMethodContext* ctx) {
260     OSRF_METHOD_VERIFY_CONTEXT(ctx);
261
262     char* username =  // free
263         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
264     const char* nonce = 
265         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
266
267     if (!nonce) nonce = "";
268     if (!username) return -1;
269
270     int resp = oilsAuthInitUsernameHandler(ctx, username, nonce);
271
272     free(username);
273     return resp;
274 }
275
276 static int oilsAuthInitBarcodeHandler(
277     osrfMethodContext* ctx, const char* barcode, const char* nonce) {
278
279     osrfLogInfo(OSRF_LOG_MARK, 
280         "User logging in with barcode %s", barcode);
281
282     int user_id = -1;
283     jsonObject* resp = NULL; // free
284     jsonObject* user_obj = oilsUtilsFetchUserByBarcode(ctx, barcode); // free
285
286     if (user_obj && user_obj->type != JSON_NULL) 
287         user_id = oilsFMGetObjectId(user_obj);
288
289     jsonObjectFree(user_obj); // NULL OK
290
291     char* seed = oilsAuthBuildInitCache(user_id, barcode, "barcode", nonce);
292     resp = jsonNewObject(seed);
293     free(seed);
294
295     osrfAppRespondComplete(ctx, resp);
296     jsonObjectFree(resp);
297     return 0;
298 }
299
300
301 // open-ils.auth.authenticate.init.barcode
302 int oilsAuthInitBarcode(osrfMethodContext* ctx) {
303     OSRF_METHOD_VERIFY_CONTEXT(ctx);
304
305     char* barcode = // free
306         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
307     const char* nonce = 
308         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
309
310     if (!nonce) nonce = "";
311     if (!barcode) return -1;
312
313     int resp = oilsAuthInitBarcodeHandler(ctx, barcode, nonce);
314
315     free(barcode);
316     return resp;
317 }
318
319 // returns true if the provided identifier matches the barcode regex.
320 static int oilsAuthIdentIsBarcode(const char* identifier) {
321
322     // Assumes barcode regex is a global setting.
323     // TODO: add an org_unit param to the .init API for future use?
324     char* bc_regex = oilsUtilsFetchOrgSetting(
325         oilsUtilsGetRootOrgId(), "opac.barcode_regex");
326
327     if (!bc_regex) {
328         // if no regex is set, assume any identifier starting
329         // with a number is a barcode.
330         bc_regex = strdup("^\\d"); // dupe for later free'ing
331     }
332
333     const char *err_str;
334     int err_offset, match_ret;
335
336     pcre *compiled = pcre_compile(
337         bc_regex, 0, &err_str, &err_offset, NULL);
338
339     if (compiled == NULL) {
340         osrfLogError(OSRF_LOG_MARK,
341             "Could not compile '%s': %s", bc_regex, err_str);
342         free(bc_regex);
343         pcre_free(compiled);
344         return 0;
345     }
346
347     pcre_extra *extra = pcre_study(compiled, 0, &err_str);
348
349     if(err_str != NULL) {
350         osrfLogError(OSRF_LOG_MARK,
351             "Could not study regex '%s': %s", bc_regex, err_str);
352         free(bc_regex);
353         pcre_free(compiled);
354         return 0;
355     } 
356
357     match_ret = pcre_exec(
358         compiled, extra, identifier, strlen(identifier), 0, 0, NULL, 0);       
359
360     free(bc_regex);
361     pcre_free(compiled);
362     if (extra) pcre_free(extra);
363
364     if (match_ret >= 0) return 1; // regex matched
365
366     if (match_ret != PCRE_ERROR_NOMATCH) 
367         osrfLogError(OSRF_LOG_MARK, "Unknown error processing barcode regex");
368
369     return 0; // regex did not match
370 }
371
372
373 /**
374         @brief Implement the "init" method.
375         @param ctx The method context.
376         @return Zero if successful, or -1 if not.
377
378         Method parameters:
379         - username
380         - nonce : optional login seed (string) provided by the caller which
381                 is added to the auth init cache to differentiate between logins
382                 using the same username and thus avoiding cache collisions for
383                 near-simultaneous logins.
384
385         Return to client: Intermediate authentication seed.
386 */
387 int oilsAuthInit(osrfMethodContext* ctx) {
388     OSRF_METHOD_VERIFY_CONTEXT(ctx);
389     int resp = 0;
390
391     char* identifier = // free
392         jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0));
393     const char* nonce = 
394         jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
395
396     if (!nonce) nonce = "";
397     if (!identifier) return -1;  // we need an identifier
398
399     if (oilsAuthIdentIsBarcode(identifier)) {
400         resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce);
401     } else {
402         resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce);
403     }
404
405     free(identifier);
406     return resp;
407 }
408
409 /**
410         Returns 1 if the password provided matches the user's real password
411         Returns 0 otherwise
412         Returns -1 on error
413 */
414 /**
415         @brief Verify the password received from the client.
416         @param ctx The method context.
417         @param userObj An object from the database, representing the user.
418         @param password An obfuscated password received from the client.
419         @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
420
421         (None of the so-called "passwords" used here are in plaintext.  All have been passed
422         through at least one layer of hashing to obfuscate them.)
423
424         Take the password from the user object.  Append it to the username seed from memcache,
425         as stored previously by a call to the init method.  Take an md5 hash of the result.
426         Then compare this hash to the password received from the client.
427
428         In order for the two to match, other than by dumb luck, the client had to construct
429         the password it passed in the same way.  That means it neded to know not only the
430         original password (either hashed or plaintext), but also the seed.  The latter requirement
431         means that the client process needs either to be the same process that called the init
432         method or to receive the seed from the process that did so.
433 */
434 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id, 
435         const char* identifier, const char* password, const char* nonce) {
436
437     int verified = 0;
438
439     // We won't be needing the seed again, remove it
440     osrfCacheRemove("%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
441
442     // Ask the DB to verify the user's password.
443     // Here, the password is md5(md5(password) + salt)
444
445     jsonObject* params = jsonParseFmt( // free
446         "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}", 
447         user_id, password);
448
449     jsonObject* verify_obj = // free 
450         oilsUtilsCStoreReq("open-ils.cstore.json_query", params);
451
452     jsonObjectFree(params);
453
454     if (verify_obj) {
455         verified = oilsUtilsIsDBTrue(
456             jsonObjectGetString(
457                 jsonObjectGetKeyConst(
458                     verify_obj, "actor.verify_passwd")));
459
460         jsonObjectFree(verify_obj);
461     }
462
463     char* countkey = va_list_to_string("%s%s%s", 
464         OILS_AUTH_CACHE_PRFX, identifier, OILS_AUTH_COUNT_SFFX );
465     jsonObject* countobject = osrfCacheGetObject( countkey );
466     if(countobject) {
467         long failcount = (long) jsonObjectGetNumber( countobject );
468         if(failcount >= _oilsAuthBlockCount) {
469             verified = 0;
470             osrfLogInfo(OSRF_LOG_MARK, 
471                 "oilsAuth found too many recent failures for '%s' : %i, "
472                 "forcing failure state.", identifier, failcount);
473         }
474         if(verified == 0) {
475             failcount += 1;
476         }
477         jsonObjectSetNumber( countobject, failcount );
478         osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
479         jsonObjectFree(countobject);
480     }
481     free(countkey);
482
483     return verified;
484 }
485
486 /*
487         Adds the authentication token to the user cache.  The timeout for the
488         auth token is based on the type of login as well as (if type=='opac')
489         the org location id.
490         Returns the event that should be returned to the user.
491         Event must be freed
492 */
493 static oilsEvent* oilsAuthHandleLoginOK( osrfMethodContext* ctx, jsonObject* userObj, const char* uname,
494                 const char* type, int orgloc, const char* workstation ) {
495
496         oilsEvent* response = NULL;
497
498     jsonObject* params = jsonNewObject(NULL);
499     jsonObjectSetKey(params, "user_id", 
500         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
501     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
502     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
503     if (workstation) 
504         jsonObjectSetKey(params, "workstation", jsonNewObject(workstation));
505
506     jsonObject* authEvt = oilsUtilsQuickReqCtx(
507         ctx,
508         "open-ils.auth_internal",
509         "open-ils.auth_internal.session.create", params);
510     jsonObjectFree(params);
511
512     if (authEvt) {
513
514         response = oilsNewEvent2(
515             OSRF_LOG_MARK, 
516             jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode")),
517             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
518         );
519
520         osrfLogActivity(OSRF_LOG_MARK,
521             "successful login: username=%s, authtoken=%s, workstation=%s",
522             uname,
523             jsonObjectGetString(
524                 jsonObjectGetKeyConst(
525                     jsonObjectGetKeyConst(authEvt, "payload"),
526                     "authtoken"
527                 )
528             ),
529             workstation ? workstation : ""
530         );
531
532         jsonObjectFree(authEvt);
533
534     } else {
535         osrfLogError(OSRF_LOG_MARK, 
536             "Error caching auth session in open-ils.auth_internal");
537     }
538
539     return response;
540 }
541
542
543 /**
544         @brief Implement the "complete" method.
545         @param ctx The method context.
546         @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
547         client to indicate completion; a positive integer if successful but no such STATUS
548         message has been sent.
549
550         Method parameters:
551         - a hash with some combination of the following elements:
552                 - "username"
553                 - "barcode"
554                 - "password" (hashed with the cached seed; not plaintext)
555                 - "type"
556                 - "org"
557                 - "workstation"
558                 - "agent" (what software/interface/3rd-party is making the request)
559                 - "nonce" optional login seed to differentiate logins using the same username.
560
561         The password is required.  Either a username or a barcode must also be present.
562
563         Return to client: Intermediate authentication seed.
564
565         Validate the password, using the username if available, or the barcode if not.  The
566         user must be active, and not barred from logging on.  The barcode, if used for
567         authentication, must be active as well.  The workstation, if specified, must be valid.
568
569         Upon deciding whether to allow the logon, return a corresponding event to the client.
570 */
571 int oilsAuthComplete( osrfMethodContext* ctx ) {
572     OSRF_METHOD_VERIFY_CONTEXT(ctx);
573
574     const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
575
576     const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
577     const char* identifier  = jsonObjectGetString(jsonObjectGetKeyConst(args, "identifier"));
578     const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
579     const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
580     int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
581     const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
582     const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
583     const char* ewho        = jsonObjectGetString(jsonObjectGetKeyConst(args, "agent"));
584     const char* nonce       = jsonObjectGetString(jsonObjectGetKeyConst(args, "nonce"));
585
586     const char* ws = (workstation) ? workstation : "";
587     if (!nonce) nonce = "";
588
589     // we no longer care how the identifier reaches us, 
590     // as long as we have one.
591     if (!identifier) {
592         if (uname) {
593             identifier = uname;
594         } else if (barcode) {
595             identifier = barcode;
596         }
597     }
598
599     if (!identifier) {
600         return osrfAppRequestRespondException(ctx->session, ctx->request,
601             "username/barcode and password required for method: %s", 
602             ctx->method->name);
603     }
604
605     osrfLogInfo(OSRF_LOG_MARK, 
606         "Patron completing authentication with identifer %s", identifier);
607
608     /* Use __FILE__, harmless_line_number for creating
609      * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
610      * giving away information about why an authentication attempt failed.
611      */
612     int harmless_line_number = __LINE__;
613
614     if( !type )
615          type = OILS_AUTH_STAFF;
616
617     oilsEvent* response = NULL; // free
618     jsonObject* userObj = NULL; // free
619     int card_active = 1; // boolean; assume active until proven otherwise
620
621     char* cache_key = va_list_to_string(
622         "%s%s%s", OILS_AUTH_CACHE_PRFX, identifier, nonce);
623     jsonObject* cacheObj = osrfCacheGetObject(cache_key); // free
624
625     if (!cacheObj) {
626         return osrfAppRequestRespondException(ctx->session,
627             ctx->request, "No authentication seed found. "
628             "open-ils.auth.authenticate.init must be called first "
629             " (check that memcached is running and can be connected to) "
630         );
631     }
632
633     int user_id = jsonObjectGetNumber(
634         jsonObjectGetKeyConst(cacheObj, "user_id"));
635
636     if (user_id == -1) {
637         // User was not found during init.  Clean up and exit early.
638         response = oilsNewEvent(
639             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
640         osrfAppRespondComplete(ctx, oilsEventToJSON(response));
641         oilsEventFree(response); // frees event JSON
642         osrfCacheRemove(cache_key);
643         jsonObjectFree(cacheObj);
644         return 0;
645     }
646
647     jsonObject* param = jsonNewNumberObject(user_id); // free
648     userObj = oilsUtilsCStoreReqCtx(
649         ctx, "open-ils.cstore.direct.actor.user.retrieve", param);
650     jsonObjectFree(param);
651
652     char* freeable_uname = NULL;
653     if (!uname) {
654         uname = freeable_uname = oilsFMGetString(userObj, "usrname");
655     }
656
657     // See if the user is allowed to login.
658
659     jsonObject* params = jsonNewObject(NULL);
660     jsonObjectSetKey(params, "user_id", 
661         jsonNewNumberObject(oilsFMGetObjectId(userObj)));
662     jsonObjectSetKey(params,"org_unit", jsonNewNumberObject(orgloc));
663     jsonObjectSetKey(params, "login_type", jsonNewObject(type));
664     if (barcode) jsonObjectSetKey(params, "barcode", jsonNewObject(barcode));
665
666     jsonObject* authEvt = oilsUtilsQuickReqCtx( // freed after password test
667         ctx
668         "open-ils.auth_internal",
669         "open-ils.auth_internal.user.validate", params);
670     jsonObjectFree(params);
671
672     if (!authEvt) {
673         // Something went seriously wrong.  Get outta here before 
674         // we start segfaulting.
675         jsonObjectFree(userObj);
676         if(freeable_uname) free(freeable_uname);
677         return -1;
678     }
679
680     const char* authEvtCode = 
681         jsonObjectGetString(jsonObjectGetKey(authEvt, "textcode"));
682
683     if (!strcmp(authEvtCode, OILS_EVENT_AUTH_FAILED)) {
684         // Received the generic login failure event.
685
686         osrfLogInfo(OSRF_LOG_MARK,  
687             "failed login: username=%s, barcode=%s, workstation=%s",
688             uname, (barcode ? barcode : "(none)"), ws);
689
690         response = oilsNewEvent(
691             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
692     }
693
694     int passOK = 0;
695     
696     if (!response) {
697         // User exists and is not barred, etc.  Test the password.
698
699         passOK = oilsAuthVerifyPassword(
700             ctx, user_id, identifier, password, nonce);
701
702         if (!passOK) {
703             // Password check failed. Return generic login failure.
704
705             response = oilsNewEvent(
706                 __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
707
708             osrfLogInfo(OSRF_LOG_MARK,  
709                 "failed login: username=%s, barcode=%s, workstation=%s",
710                     uname, (barcode ? barcode : "(none)"), ws );
711         }
712     }
713
714
715     // Below here, we know the password check succeeded if no response
716     // object is present.
717
718     if (!response && (
719         !strcmp(authEvtCode, "PATRON_INACTIVE") ||
720         !strcmp(authEvtCode, "PATRON_CARD_INACTIVE"))) {
721         // Patron and/or card is inactive but the correct password 
722         // was provided.  Alert the caller to the inactive-ness.
723         response = oilsNewEvent2(
724             OSRF_LOG_MARK, authEvtCode,
725             jsonObjectGetKey(authEvt, "payload")   // cloned within Event
726         );
727     }
728
729     if (!response && strcmp(authEvtCode, OILS_EVENT_SUCCESS)) {
730         // Validate API returned an unexpected non-success event.
731         // To be safe, treat this as a generic login failure.
732
733         response = oilsNewEvent(
734             __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED);
735     }
736
737     if (!response) {
738         // password OK and no other events have prevented login completion.
739
740         char* ewhat = "login";
741
742         if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
743             response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
744             ewhat = "verify";
745
746         } else {
747             response = oilsAuthHandleLoginOK(
748                 ctx, userObj, uname, type, orgloc, workstation);
749         }
750
751         oilsUtilsTrackUserActivity(
752             ctx,
753             oilsFMGetObjectId(userObj), 
754             ewho, ewhat, 
755             osrfAppSessionGetIngress()
756         );
757     }
758
759     // reply
760     osrfAppRespondComplete(ctx, oilsEventToJSON(response));
761
762     // clean up
763     oilsEventFree(response);
764     jsonObjectFree(userObj);
765     jsonObjectFree(authEvt);
766     jsonObjectFree(cacheObj);
767     if(freeable_uname)
768         free(freeable_uname);
769
770         return 0;
771 }
772
773
774
775 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
776         OSRF_METHOD_VERIFY_CONTEXT(ctx);
777
778         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
779         jsonObject* resp = NULL;
780
781         if( authToken ) {
782                 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
783                 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
784                 osrfCacheRemove(key);
785                 resp = jsonNewObject(authToken); /**/
786                 free(key);
787         }
788
789         osrfAppRespondComplete( ctx, resp );
790         jsonObjectFree(resp);
791         return 0;
792 }
793
794 /**
795  * Fetches the user object from the database and updates the user object in 
796  * the cache object, which then has to be re-inserted into the cache.
797  * User object is retrieved inside a transaction to avoid replication issues.
798  */
799 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
800     int reqid, userId;
801     osrfAppSession* session;
802         osrfMessage* omsg;
803     jsonObject *param, *userObj, *newUserObj = NULL;
804
805     userObj = jsonObjectGetKey( cacheObj, "userobj" );
806     userId = oilsFMGetObjectId( userObj );
807
808     session = osrfAppSessionClientInit( "open-ils.cstore" );
809     osrfAppSessionConnect(session);
810
811     reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
812         omsg = osrfAppSessionRequestRecv(session, reqid, 60);
813
814     if(omsg) {
815
816         osrfMessageFree(omsg);
817         param = jsonNewNumberObject(userId);
818         reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
819             omsg = osrfAppSessionRequestRecv(session, reqid, 60);
820         jsonObjectFree(param);
821
822         if(omsg) {
823             newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
824             osrfMessageFree(omsg);
825             reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
826                 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
827             osrfMessageFree(omsg);
828         }
829     }
830
831     osrfAppSessionFree(session); // calls disconnect internally
832
833     if(newUserObj) {
834
835         // ws_ou and wsid are ephemeral and need to be manually propagated
836         // oilsFMSetString dupe()'s internally, no need to clone the string
837         oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
838         oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
839
840         jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
841         jsonObjectSetKey(cacheObj, "userobj", newUserObj);
842         return 1;
843     } 
844
845     osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
846     return 0;
847 }
848
849 /**
850         Resets the auth login timeout
851         @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
852 */
853 static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
854         if(!authToken) return NULL;
855
856         oilsEvent* evt = NULL;
857         time_t timeout;
858
859         osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
860         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
861         jsonObject* cacheObj = osrfCacheGetObject( key );
862
863         if(!cacheObj) {
864                 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
865                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
866
867         } else {
868
869         if(reloadUser) {
870             _oilsAuthReloadUser(cacheObj);
871         }
872
873                 // Determine a new timeout value
874                 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
875                 if( endtime_obj ) {
876                         // Extend the current endtime by a fixed amount
877                         time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
878                         int reset_interval = DEFAULT_RESET_INTERVAL;
879                         const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
880                                 cacheObj, "reset_interval" );
881                         if( reset_interval_obj ) {
882                                 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
883                                 if( reset_interval <= 0 )
884                                         reset_interval = DEFAULT_RESET_INTERVAL;
885                         }
886
887                         time_t now = time( NULL );
888                         time_t new_endtime = now + reset_interval;
889                         if( new_endtime > endtime ) {
890                                 // Keep the session alive a little longer
891                                 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
892                                 timeout = reset_interval;
893                                 osrfCachePutObject( key, cacheObj, timeout );
894                         } else {
895                                 // The session isn't close to expiring, so don't reset anything.
896                                 // Just report the time remaining.
897                                 timeout = endtime - now;
898                         }
899                 } else {
900                         // Reapply the existing timeout from the current time
901                         timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
902                         osrfCachePutObject( key, cacheObj, timeout );
903                 }
904
905                 jsonObject* payload = jsonNewNumberObject( (double) timeout );
906                 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
907                 jsonObjectFree(payload);
908                 jsonObjectFree(cacheObj);
909         }
910
911         free(key);
912         return evt;
913 }
914
915 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
916         OSRF_METHOD_VERIFY_CONTEXT(ctx);
917         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
918     double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
919         oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
920         osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
921         oilsEventFree(evt);
922         return 0;
923 }
924
925
926 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
927         OSRF_METHOD_VERIFY_CONTEXT(ctx);
928     bool returnFull = false;
929
930         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
931
932     if(ctx->params->size > 1) {
933         // caller wants full cached object, with authtime, etc.
934         const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
935         if(rt && strcmp(rt, "0") != 0) 
936             returnFull = true;
937     }
938
939         jsonObject* cacheObj = NULL;
940         oilsEvent* evt = NULL;
941
942         if( authToken ){
943
944                 // Reset the timeout to keep the session alive
945                 evt = _oilsAuthResetTimeout(authToken, 0);
946
947                 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
948                         osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
949
950                 } else {
951
952                         // Retrieve the cached session object
953                         osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
954                         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
955                         cacheObj = osrfCacheGetObject( key );
956                         if(cacheObj) {
957                                 // Return a copy of the cached user object
958                 if(returnFull)
959                                     osrfAppRespondComplete( ctx, cacheObj);
960                 else
961                                     osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
962                                 jsonObjectFree(cacheObj);
963                         } else {
964                                 // Auth token is invalid or expired
965                                 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
966                                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
967                                 oilsEventFree(evt2);
968                         }
969                         free(key);
970                 }
971
972         } else {
973
974                 // No session
975                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
976                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
977         }
978
979         if(evt)
980                 oilsEventFree(evt);
981
982         return 0;
983 }