provide hint to sysadmin if memcached issue is preventing auth
[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
10 #define OILS_AUTH_CACHE_PRFX "oils_auth_"
11
12 #define MODULENAME "open-ils.auth"
13
14 #define OILS_AUTH_OPAC "opac"
15 #define OILS_AUTH_STAFF "staff"
16 #define OILS_AUTH_TEMP "temp"
17 #define OILS_AUTH_PERSIST "persist"
18
19 // Default time for extending a persistent session: ten minutes
20 #define DEFAULT_RESET_INTERVAL 10 * 60
21
22 int osrfAppInitialize();
23 int osrfAppChildInit();
24
25 static long _oilsAuthOPACTimeout = 0;
26 static long _oilsAuthStaffTimeout = 0;
27 static long _oilsAuthOverrideTimeout = 0;
28 static long _oilsAuthPersistTimeout = 0;
29
30
31 /**
32         @brief Initialize the application by registering functions for method calls.
33         @return Zero in all cases.
34 */
35 int osrfAppInitialize() {
36
37         osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");
38
39         /* load and parse the IDL */
40         if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
41
42         osrfAppRegisterMethod(
43                 MODULENAME,
44                 "open-ils.auth.authenticate.init",
45                 "oilsAuthInit",
46                 "Start the authentication process and returns the intermediate authentication seed"
47                 " PARAMS( username )", 1, 0 );
48
49         osrfAppRegisterMethod(
50                 MODULENAME,
51                 "open-ils.auth.authenticate.complete",
52                 "oilsAuthComplete",
53                 "Completes the authentication process.  Returns an object like so: "
54                 "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
55                 "token and authtime is the number of seconds the session will be active"
56                 "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
57                 "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
58                 "org_id is the location at which the login should be considered "
59                 "active for login timeout purposes", 1, 0 );
60
61         osrfAppRegisterMethod(
62                 MODULENAME,
63                 "open-ils.auth.session.retrieve",
64                 "oilsAuthSessionRetrieve",
65                 "Pass in the auth token and this retrieves the user object.  The auth "
66                 "timeout is reset when this call is made "
67                 "Returns the user object (password blanked) for the given login session "
68                 "PARAMS( authToken )", 1, 0 );
69
70         osrfAppRegisterMethod(
71                 MODULENAME,
72                 "open-ils.auth.session.delete",
73                 "oilsAuthSessionDelete",
74                 "Destroys the given login session "
75                 "PARAMS( authToken )",  1, 0 );
76
77         osrfAppRegisterMethod(
78                 MODULENAME,
79                 "open-ils.auth.session.reset_timeout",
80                 "oilsAuthResetTimeout",
81                 "Resets the login timeout for the given session "
82                 "Returns an ILS Event with payload = session_timeout of session "
83                 "if found, otherwise returns the NO_SESSION event"
84                 "PARAMS( authToken )", 1, 0 );
85
86         return 0;
87 }
88
89 /**
90         @brief Dummy placeholder for initializing a server drone.
91
92         There is nothing to do, so do nothing.
93 */
94 int osrfAppChildInit() {
95         return 0;
96 }
97
98 /**
99         @brief Implement the "init" method.
100         @param ctx The method context.
101         @return Zero if successful, or -1 if not.
102
103         Method parameters:
104         - username
105
106         Return to client: Intermediate authentication seed.
107
108         Combine the username with a timestamp and process ID, and take an md5 hash of the result.
109         Store the hash in memcache, with a key based on the username.  Then return the hash to
110         the client.
111
112         However: if the username includes one or more embedded blank spaces, return a dummy
113         hash without storing anything in memcache.  The dummy will never match a stored hash, so
114         any attempt to authenticate with it will fail.
115 */
116 int oilsAuthInit( osrfMethodContext* ctx ) {
117         OSRF_METHOD_VERIFY_CONTEXT(ctx);
118
119         char* username  = jsonObjectToSimpleString( jsonObjectGetIndex(ctx->params, 0) );
120         if( username ) {
121
122                 jsonObject* resp;
123
124                 if( strchr( username, ' ' ) ) {
125
126                         // Embedded spaces are not allowed in a username.  Use "x" as a dummy
127                         // seed.  It will never be a valid seed because 'x' is not a hex digit.
128                         resp = jsonNewObject( "x" );
129
130                 } else {
131
132                         // Build a key and a seed; store them in memcache.
133                         char* key  = va_list_to_string( "%s%s", OILS_AUTH_CACHE_PRFX, username );
134                         char* seed = md5sum( "%d.%ld.%s", (int) time(NULL), (long) getpid(), username );
135                         osrfCachePutString( key, seed, 30 );
136
137                         osrfLogDebug( OSRF_LOG_MARK, "oilsAuthInit(): has seed %s and key %s", seed, key );
138
139                         // Build a returnable object containing the seed.
140                         resp = jsonNewObject( seed );
141
142                         free( seed );
143                         free( key );
144                 }
145
146                 // Return the seed to the client.
147                 osrfAppRespondComplete( ctx, resp );
148
149                 jsonObjectFree(resp);
150                 free(username);
151                 return 0;
152         }
153
154         return -1;  // Error: no username parameter
155 }
156
157 /**
158         Verifies that the user has permission to login with the
159         given type.  If the permission fails, an oilsEvent is returned
160         to the caller.
161         @return -1 if the permission check failed, 0 if the permission
162         is granted
163 */
164 static int oilsAuthCheckLoginPerm(
165                 osrfMethodContext* ctx, const jsonObject* userObj, const char* type ) {
166
167         if(!(userObj && type)) return -1;
168         oilsEvent* perm = NULL;
169
170         if(!strcasecmp(type, OILS_AUTH_OPAC)) {
171                 char* permissions[] = { "OPAC_LOGIN" };
172                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
173
174         } else if(!strcasecmp(type, OILS_AUTH_STAFF)) {
175                 char* permissions[] = { "STAFF_LOGIN" };
176                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
177
178         } else if(!strcasecmp(type, OILS_AUTH_TEMP)) {
179                 char* permissions[] = { "STAFF_LOGIN" };
180                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
181         } else if(!strcasecmp(type, OILS_AUTH_PERSIST)) {
182                 char* permissions[] = { "PERSISTENT_LOGIN" };
183                 perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
184         }
185
186         if(perm) {
187                 osrfAppRespondComplete( ctx, oilsEventToJSON(perm) );
188                 oilsEventFree(perm);
189                 return -1;
190         }
191
192         return 0;
193 }
194
195 /**
196         Returns 1 if the password provided matches the user's real password
197         Returns 0 otherwise
198         Returns -1 on error
199 */
200 /**
201         @brief Verify the password received from the client.
202         @param ctx The method context.
203         @param userObj An object from the database, representing the user.
204         @param password An obfuscated password received from the client.
205         @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
206
207         (None of the so-called "passwords" used here are in plaintext.  All have been passed
208         through at least one layer of hashing to obfuscate them.)
209
210         Take the password from the user object.  Append it to the username seed from memcache,
211         as stored previously by a call to the init method.  Take an md5 hash of the result.
212         Then compare this hash to the password received from the client.
213
214         In order for the two to match, other than by dumb luck, the client had to construct
215         the password it passed in the same way.  That means it neded to know not only the
216         original password (either hashed or plaintext), but also the seed.  The latter requirement
217         means that the client process needs either to be the same process that called the init
218         method or to receive the seed from the process that did so.
219 */
220 static int oilsAuthVerifyPassword( const osrfMethodContext* ctx,
221                 const jsonObject* userObj, const char* uname, const char* password ) {
222
223         // Get the username seed, as stored previously in memcache by the init method
224         char* seed = osrfCacheGetString( "%s%s", OILS_AUTH_CACHE_PRFX, uname );
225         if(!seed) {
226                 return osrfAppRequestRespondException( ctx->session,
227                         ctx->request, "No authentication seed found. "
228                         "open-ils.auth.authenticate.init must be called first "
229                         " (check that memcached is running and can be connected to) "
230                 );
231         }
232
233         // Get the hashed password from the user object
234         char* realPassword = oilsFMGetString( userObj, "passwd" );
235
236         osrfLogInternal(OSRF_LOG_MARK, "oilsAuth retrieved real password: [%s]", realPassword);
237         osrfLogDebug(OSRF_LOG_MARK, "oilsAuth retrieved seed from cache: %s", seed );
238
239         // Concatenate them and take an MD5 hash of the result
240         char* maskedPw = md5sum( "%s%s", seed, realPassword );
241
242         free(realPassword);
243         free(seed);
244
245         if( !maskedPw ) {
246                 // This happens only if md5sum() runs out of memory
247                 free( maskedPw );
248                 return -1;  // md5sum() ran out of memory
249         }
250
251         osrfLogDebug(OSRF_LOG_MARK,  "oilsAuth generated masked password %s. "
252                         "Testing against provided password %s", maskedPw, password );
253
254         int ret = 0;
255         if( !strcmp( maskedPw, password ) )
256                 ret = 1;
257
258         free(maskedPw);
259
260         return ret;
261 }
262
263 /**
264         @brief Determine the login timeout.
265         @param userObj Pointer to an object describing the user.
266         @param type Pointer to one of four possible character strings identifying the login type.
267         @param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
268         @return The length of the timeout, in seconds.
269
270         The default timeout value comes from the configuration file, and depends on the
271         login type.
272
273         The default may be overridden by a corresponding org unit setting.  The @a orgloc
274         parameter says what org unit to use for the lookup.  If @a orgloc <= 0, or if the
275         lookup for @a orgloc yields no result, we look up the setting for the user's home org unit
276         instead (except that if it's the same as @a orgloc we don't bother repeating the lookup).
277
278         Whether defined in the config file or in an org unit setting, a timeout value may be
279         expressed as a raw number (i.e. all digits, possibly with leading and/or trailing white
280         space) or as an interval string to be translated into seconds by PostgreSQL.
281 */
282 static long oilsAuthGetTimeout( const jsonObject* userObj, const char* type, int orgloc ) {
283
284         if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */
285
286                 jsonObject* value_obj;
287
288                 value_obj = osrf_settings_host_value_object(
289                         "/apps/open-ils.auth/app_settings/default_timeout/opac" );
290                 _oilsAuthOPACTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
291                 jsonObjectFree(value_obj);
292                 if( -1 == _oilsAuthOPACTimeout ) {
293                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for OPAC logins" );
294                         _oilsAuthOPACTimeout = 0;
295                 }
296
297                 value_obj = osrf_settings_host_value_object(
298                         "/apps/open-ils.auth/app_settings/default_timeout/staff" );
299                 _oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
300                 jsonObjectFree(value_obj);
301                 if( -1 == _oilsAuthStaffTimeout ) {
302                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for staff logins" );
303                         _oilsAuthStaffTimeout = 0;
304                 }
305
306                 value_obj = osrf_settings_host_value_object(
307                         "/apps/open-ils.auth/app_settings/default_timeout/temp" );
308                 _oilsAuthOverrideTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
309                 jsonObjectFree(value_obj);
310                 if( -1 == _oilsAuthOverrideTimeout ) {
311                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for temp logins" );
312                         _oilsAuthOverrideTimeout = 0;
313                 }
314
315                 value_obj = osrf_settings_host_value_object(
316                         "/apps/open-ils.auth/app_settings/default_timeout/persist" );
317                 _oilsAuthPersistTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
318                 jsonObjectFree(value_obj);
319                 if( -1 == _oilsAuthPersistTimeout ) {
320                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for persist logins" );
321                         _oilsAuthPersistTimeout = 0;
322                 }
323
324                 osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: "
325                         "opac => %ld : staff => %ld : temp => %ld : persist => %ld",
326                         _oilsAuthOPACTimeout, _oilsAuthStaffTimeout,
327                         _oilsAuthOverrideTimeout, _oilsAuthPersistTimeout );
328         }
329
330         int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
331         if(orgloc < 1)
332                 orgloc = home_ou;
333
334         char* setting = NULL;
335         long default_timeout = 0;
336
337         if( !strcmp( type, OILS_AUTH_OPAC )) {
338                 setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
339                 default_timeout = _oilsAuthOPACTimeout;
340         } else if( !strcmp( type, OILS_AUTH_STAFF )) {
341                 setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
342                 default_timeout = _oilsAuthStaffTimeout;
343         } else if( !strcmp( type, OILS_AUTH_TEMP )) {
344                 setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
345                 default_timeout = _oilsAuthOverrideTimeout;
346         } else if( !strcmp( type, OILS_AUTH_PERSIST )) {
347                 setting = OILS_ORG_SETTING_PERSIST_TIMEOUT;
348                 default_timeout = _oilsAuthPersistTimeout;
349         }
350
351         // Get the org unit setting, if there is one.
352         char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
353         if(!timeout) {
354                 if( orgloc != home_ou ) {
355                         osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
356                                 "trying home_ou %d", orgloc, home_ou );
357                         timeout = oilsUtilsFetchOrgSetting( home_ou, setting );
358                 }
359         }
360
361         if(!timeout)
362                 return default_timeout;   // No override from org unit setting
363
364         // Translate the org unit setting to a number
365         long t;
366         if( !*timeout ) {
367                 osrfLogWarning( OSRF_LOG_MARK,
368                         "Timeout org unit setting is an empty string for %s login; using default",
369                         timeout, type );
370                 t = default_timeout;
371         } else {
372                 // Treat timeout string as an interval, and convert it to seconds
373                 t = oilsUtilsIntervalToSeconds( timeout );
374                 if( -1 == t ) {
375                         // Unable to convert; possibly an invalid interval string
376                         osrfLogError( OSRF_LOG_MARK,
377                                 "Unable to convert timeout interval \"%s\" for %s login; using default",
378                                 timeout, type );
379                         t = default_timeout;
380                 }
381         }
382
383         free(timeout);
384         return t;
385 }
386
387 /*
388         Adds the authentication token to the user cache.  The timeout for the
389         auth token is based on the type of login as well as (if type=='opac')
390         the org location id.
391         Returns the event that should be returned to the user.
392         Event must be freed
393 */
394 static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname,
395                 const char* type, int orgloc, const char* workstation ) {
396
397         oilsEvent* response;
398
399         long timeout;
400         char* wsorg = jsonObjectToSimpleString(oilsFMGetObject(userObj, "ws_ou"));
401         if(wsorg) { /* if there is a workstation, use it for the timeout */
402                 osrfLogDebug( OSRF_LOG_MARK,
403                                 "Auth session trying workstation id %d for auth timeout", atoi(wsorg));
404                 timeout = oilsAuthGetTimeout( userObj, type, atoi(wsorg) );
405                 free(wsorg);
406         } else {
407                 osrfLogDebug( OSRF_LOG_MARK,
408                                 "Auth session trying org from param [%d] for auth timeout", orgloc );
409                 timeout = oilsAuthGetTimeout( userObj, type, orgloc );
410         }
411         osrfLogDebug(OSRF_LOG_MARK, "Auth session timeout for %s: %ld", uname, timeout );
412
413         char* string = va_list_to_string(
414                         "%d.%ld.%s", (long) getpid(), time(NULL), uname );
415         char* authToken = md5sum(string);
416         char* authKey = va_list_to_string(
417                         "%s%s", OILS_AUTH_CACHE_PRFX, authToken );
418
419         const char* ws = (workstation) ? workstation : "";
420         osrfLogActivity(OSRF_LOG_MARK,
421                 "successful login: username=%s, authtoken=%s, workstation=%s", uname, authToken, ws );
422
423         oilsFMSetString( userObj, "passwd", "" );
424         jsonObject* cacheObj = jsonParseFmt( "{\"authtime\": %ld}", timeout );
425         jsonObjectSetKey( cacheObj, "userobj", jsonObjectClone(userObj));
426
427         if( !strcmp( type, OILS_AUTH_PERSIST )) {
428                 // Add entries for endtime and reset_interval, so that we can gracefully
429                 // extend the session a bit if the user is active toward the end of the 
430                 // timeout originally specified.
431                 time_t endtime = time( NULL ) + timeout;
432                 jsonObjectSetKey( cacheObj, "endtime", jsonNewNumberObject( (double) endtime ) );
433
434                 // Reset interval is hard-coded for now, but if we ever want to make it
435                 // configurable, this is the place to do it:
436                 jsonObjectSetKey( cacheObj, "reset_interval",
437                         jsonNewNumberObject( (double) DEFAULT_RESET_INTERVAL ));
438         }
439
440         osrfCachePutObject( authKey, cacheObj, (time_t) timeout );
441         jsonObjectFree(cacheObj);
442         osrfLogInternal(OSRF_LOG_MARK, "oilsAuthHandleLoginOK(): Placed user object into cache");
443         jsonObject* payload = jsonParseFmt(
444                 "{ \"authtoken\": \"%s\", \"authtime\": %ld }", authToken, timeout );
445
446         response = oilsNewEvent2( OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload );
447         free(string); free(authToken); free(authKey);
448         jsonObjectFree(payload);
449
450         return response;
451 }
452
453 static oilsEvent* oilsAuthVerifyWorkstation(
454                 const osrfMethodContext* ctx, jsonObject* userObj, const char* ws ) {
455         osrfLogInfo(OSRF_LOG_MARK, "Attaching workstation to user at login: %s", ws);
456         jsonObject* workstation = oilsUtilsFetchWorkstationByName(ws);
457         if(!workstation || workstation->type == JSON_NULL) {
458                 jsonObjectFree(workstation);
459                 return oilsNewEvent(OSRF_LOG_MARK, "WORKSTATION_NOT_FOUND");
460         }
461         long wsid = oilsFMGetObjectId(workstation);
462         LONG_TO_STRING(wsid);
463         char* orgid = oilsFMGetString(workstation, "owning_lib");
464         oilsFMSetString(userObj, "wsid", LONGSTR);
465         oilsFMSetString(userObj, "ws_ou", orgid);
466         free(orgid);
467         jsonObjectFree(workstation);
468         return NULL;
469 }
470
471
472
473 /**
474         @brief Implement the "complete" method.
475         @param ctx The method context.
476         @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
477         client to indicate completion; a positive integer if successful but no such STATUS
478         message has been sent.
479
480         Method parameters:
481         - a hash with some combination of the following elements:
482                 - "username"
483                 - "barcode"
484                 - "password" (hashed with the cached seed; not plaintext)
485                 - "type"
486                 - "org"
487                 - "workstation"
488
489         The password is required.  Either a username or a barcode must also be present.
490
491         Return to client: Intermediate authentication seed.
492
493         Validate the password, using the username if available, or the barcode if not.  The
494         user must be active, and not barred from logging on.  The barcode, if used for
495         authentication, must be active as well.  The workstation, if specified, must be valid.
496
497         Upon deciding whether to allow the logon, return a corresponding event to the client.
498 */
499 int oilsAuthComplete( osrfMethodContext* ctx ) {
500         OSRF_METHOD_VERIFY_CONTEXT(ctx);
501
502         const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
503
504         const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
505         const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
506         const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
507         int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
508         const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
509         const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
510
511         const char* ws = (workstation) ? workstation : "";
512
513         if( !type )
514                  type = OILS_AUTH_STAFF;
515
516         if( !( (uname || barcode) && password) ) {
517                 return osrfAppRequestRespondException( ctx->session, ctx->request,
518                         "username/barcode and password required for method: %s", ctx->method->name );
519         }
520
521         oilsEvent* response = NULL;
522         jsonObject* userObj = NULL;
523         int card_active     = 1;      // boolean; assume active until proven otherwise
524
525         // Fetch a row from the actor.usr table, by username if available,
526         // or by barcode if not.
527         if(uname) {
528                 userObj = oilsUtilsFetchUserByUsername( uname );
529                 if( userObj && JSON_NULL == userObj->type ) {
530                         jsonObjectFree( userObj );
531                         userObj = NULL;         // username not found
532                 }
533         }
534         else if(barcode) {
535                 // Read from actor.card by barcode
536
537                 osrfLogInfo( OSRF_LOG_MARK, "Fetching user by barcode %s", barcode );
538
539                 jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
540                 jsonObject* card = oilsUtilsQuickReq(
541                         "open-ils.cstore", "open-ils.cstore.direct.actor.card.search", params );
542                 jsonObjectFree( params );
543
544                 if( card && card->type != JSON_NULL ) {
545                         // Determine whether the card is active
546                         char* card_active_str = oilsFMGetString( card, "active" );
547                         card_active = oilsUtilsIsDBTrue( card_active_str );
548                         free( card_active_str );
549
550                         // Look up the user who owns the card
551                         char* userid = oilsFMGetString( card, "usr" );
552                         jsonObjectFree( card );
553                         params = jsonParseFmt( "[%s]", userid );
554                         free( userid );
555                         userObj = oilsUtilsQuickReq(
556                                         "open-ils.cstore", "open-ils.cstore.direct.actor.user.retrieve", params );
557                         jsonObjectFree( params );
558                         if( userObj && JSON_NULL == userObj->type ) {
559                                 // user not found (shouldn't happen, due to foreign key)
560                                 jsonObjectFree( userObj );
561                                 userObj = NULL;
562                         }
563                 }
564         }
565
566         if(!userObj) {
567                 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
568                 osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
569                                 uname, (barcode ? barcode : "(none)"), ws );
570                 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
571                 oilsEventFree(response);
572                 return 0;           // No such user
573         }
574
575         // Such a user exists.  Now see if he or she has the right credentials.
576         int passOK = -1;
577         if(uname)
578                 passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password );
579         else if (barcode)
580                 passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password );
581
582         if( passOK < 0 ) {
583                 jsonObjectFree(userObj);
584                 return passOK;
585         }
586
587         // See if the account is active
588         char* active = oilsFMGetString(userObj, "active");
589         if( !oilsUtilsIsDBTrue(active) ) {
590                 if( passOK )
591                         response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
592                 else
593                         response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
594
595                 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
596                 oilsEventFree(response);
597                 jsonObjectFree(userObj);
598                 free(active);
599                 return 0;
600         }
601         free(active);
602
603         osrfLogInfo( OSRF_LOG_MARK, "Fetching card by barcode %s", barcode );
604
605         if( !card_active ) {
606                 osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
607                 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
608                 osrfAppRespondComplete( ctx, oilsEventToJSON( response ) );
609                 oilsEventFree( response );
610                 jsonObjectFree( userObj );
611                 return 0;
612         }
613
614
615         // See if the user is even allowed to log in
616         if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
617                 jsonObjectFree(userObj);
618                 return 0;
619         }
620
621         // If a workstation is defined, add the workstation info
622         if( workstation != NULL ) {
623                 osrfLogDebug(OSRF_LOG_MARK, "Workstation is %s", workstation);
624                 response = oilsAuthVerifyWorkstation( ctx, userObj, workstation );
625                 if(response) {
626                         jsonObjectFree(userObj);
627                         osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
628                         oilsEventFree(response);
629                         return 0;
630                 }
631
632         } else {
633                 // Otherwise, use the home org as the workstation org on the user
634                 char* orgid = oilsFMGetString(userObj, "home_ou");
635                 oilsFMSetString(userObj, "ws_ou", orgid);
636                 free(orgid);
637         }
638
639         char* freeable_uname = NULL;
640         if(!uname) {
641                 uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
642         }
643
644         if( passOK ) {
645                 response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
646
647         } else {
648                 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
649                 osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
650                                 uname, (barcode ? barcode : "(none)"), ws );
651         }
652
653         jsonObjectFree(userObj);
654         osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
655         oilsEventFree(response);
656
657         if(freeable_uname)
658                 free(freeable_uname);
659
660         return 0;
661 }
662
663
664
665 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
666         OSRF_METHOD_VERIFY_CONTEXT(ctx);
667
668         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
669         jsonObject* resp = NULL;
670
671         if( authToken ) {
672                 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
673                 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
674                 osrfCacheRemove(key);
675                 resp = jsonNewObject(authToken); /**/
676                 free(key);
677         }
678
679         osrfAppRespondComplete( ctx, resp );
680         jsonObjectFree(resp);
681         return 0;
682 }
683
684 /**
685  * Fetches the user object from the database and updates the user object in 
686  * the cache object, which then has to be re-inserted into the cache.
687  * User object is retrieved inside a transaction to avoid replication issues.
688  */
689 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
690     int reqid, userId;
691     osrfAppSession* session;
692         osrfMessage* omsg;
693     jsonObject *param, *userObj, *newUserObj;
694
695     userObj = jsonObjectGetKey( cacheObj, "userobj" );
696     userId = oilsFMGetObjectId( userObj );
697
698     session = osrfAppSessionClientInit( "open-ils.cstore" );
699     osrfAppSessionConnect(session);
700
701     reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
702         omsg = osrfAppSessionRequestRecv(session, reqid, 60);
703
704     if(omsg) {
705
706         osrfMessageFree(omsg);
707         param = jsonNewNumberObject(userId);
708         reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
709             omsg = osrfAppSessionRequestRecv(session, reqid, 60);
710         jsonObjectFree(param);
711
712         if(omsg) {
713             newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
714             osrfMessageFree(omsg);
715             reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
716                 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
717             osrfMessageFree(omsg);
718         }
719     }
720
721     osrfAppSessionFree(session); // calls disconnect internally
722
723     if(newUserObj) {
724
725         // ws_ou and wsid are ephemeral and need to be manually propagated
726         // oilsFMSetString dupe()'s internally, no need to clone the string
727         oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
728         oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
729
730         jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
731         jsonObjectSetKey(cacheObj, "userobj", newUserObj);
732         return 1;
733     } 
734
735     osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
736     return 0;
737 }
738
739 /**
740         Resets the auth login timeout
741         @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
742 */
743 static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
744         if(!authToken) return NULL;
745
746         oilsEvent* evt = NULL;
747         time_t timeout;
748
749         osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
750         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
751         jsonObject* cacheObj = osrfCacheGetObject( key );
752
753         if(!cacheObj) {
754                 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
755                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
756
757         } else {
758
759         if(reloadUser) {
760             _oilsAuthReloadUser(cacheObj);
761         }
762
763                 // Determine a new timeout value
764                 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
765                 if( endtime_obj ) {
766                         // Extend the current endtime by a fixed amount
767                         time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
768                         int reset_interval = DEFAULT_RESET_INTERVAL;
769                         const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
770                                 cacheObj, "reset_interval" );
771                         if( reset_interval_obj ) {
772                                 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
773                                 if( reset_interval <= 0 )
774                                         reset_interval = DEFAULT_RESET_INTERVAL;
775                         }
776
777                         time_t now = time( NULL );
778                         time_t new_endtime = now + reset_interval;
779                         if( new_endtime > endtime ) {
780                                 // Keep the session alive a little longer
781                                 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
782                                 timeout = reset_interval;
783                                 osrfCachePutObject( key, cacheObj, timeout );
784                         } else {
785                                 // The session isn't close to expiring, so don't reset anything.
786                                 // Just report the time remaining.
787                                 timeout = endtime - now;
788                         }
789                 } else {
790                         // Reapply the existing timeout from the current time
791                         timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
792                         osrfCachePutObject( key, cacheObj, timeout );
793                 }
794
795                 jsonObject* payload = jsonNewNumberObject( (double) timeout );
796                 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
797                 jsonObjectFree(payload);
798                 jsonObjectFree(cacheObj);
799         }
800
801         free(key);
802         return evt;
803 }
804
805 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
806         OSRF_METHOD_VERIFY_CONTEXT(ctx);
807         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
808     double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
809         oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
810         osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
811         oilsEventFree(evt);
812         return 0;
813 }
814
815
816 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
817         OSRF_METHOD_VERIFY_CONTEXT(ctx);
818     bool returnFull = false;
819
820         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
821
822     if(ctx->params->size > 1) {
823         // caller wants full cached object, with authtime, etc.
824         const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
825         if(rt && strcmp(rt, "0") != 0) 
826             returnFull = true;
827     }
828
829         jsonObject* cacheObj = NULL;
830         oilsEvent* evt = NULL;
831
832         if( authToken ){
833
834                 // Reset the timeout to keep the session alive
835                 evt = _oilsAuthResetTimeout(authToken, 0);
836
837                 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
838                         osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
839
840                 } else {
841
842                         // Retrieve the cached session object
843                         osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
844                         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
845                         cacheObj = osrfCacheGetObject( key );
846                         if(cacheObj) {
847                                 // Return a copy of the cached user object
848                 if(returnFull)
849                                     osrfAppRespondComplete( ctx, cacheObj);
850                 else
851                                     osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
852                                 jsonObjectFree(cacheObj);
853                         } else {
854                                 // Auth token is invalid or expired
855                                 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
856                                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
857                                 oilsEventFree(evt2);
858                         }
859                         free(key);
860                 }
861
862         } else {
863
864                 // No session
865                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
866                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
867         }
868
869         if(evt)
870                 oilsEventFree(evt);
871
872         return 0;
873 }