962977abab9aea77c323d55b7aab13ec69a2690d
[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         }
230
231         // Get the hashed password from the user object
232         char* realPassword = oilsFMGetString( userObj, "passwd" );
233
234         osrfLogInternal(OSRF_LOG_MARK, "oilsAuth retrieved real password: [%s]", realPassword);
235         osrfLogDebug(OSRF_LOG_MARK, "oilsAuth retrieved seed from cache: %s", seed );
236
237         // Concatenate them and take an MD5 hash of the result
238         char* maskedPw = md5sum( "%s%s", seed, realPassword );
239
240         free(realPassword);
241         free(seed);
242
243         if( !maskedPw ) {
244                 // This happens only if md5sum() runs out of memory
245                 free( maskedPw );
246                 return -1;  // md5sum() ran out of memory
247         }
248
249         osrfLogDebug(OSRF_LOG_MARK,  "oilsAuth generated masked password %s. "
250                         "Testing against provided password %s", maskedPw, password );
251
252         int ret = 0;
253         if( !strcmp( maskedPw, password ) )
254                 ret = 1;
255
256         free(maskedPw);
257
258         return ret;
259 }
260
261 /**
262         @brief Determine the login timeout.
263         @param userObj Pointer to an object describing the user.
264         @param type Pointer to one of four possible character strings identifying the login type.
265         @param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
266         @return The length of the timeout, in seconds.
267
268         The default timeout value comes from the configuration file, and depends on the
269         login type.
270
271         The default may be overridden by a corresponding org unit setting.  The @a orgloc
272         parameter says what org unit to use for the lookup.  If @a orgloc <= 0, or if the
273         lookup for @a orgloc yields no result, we look up the setting for the user's home org unit
274         instead (except that if it's the same as @a orgloc we don't bother repeating the lookup).
275
276         Whether defined in the config file or in an org unit setting, a timeout value may be
277         expressed as a raw number (i.e. all digits, possibly with leading and/or trailing white
278         space) or as an interval string to be translated into seconds by PostgreSQL.
279 */
280 static long oilsAuthGetTimeout( const jsonObject* userObj, const char* type, int orgloc ) {
281
282         if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */
283
284                 jsonObject* value_obj;
285
286                 value_obj = osrf_settings_host_value_object(
287                         "/apps/open-ils.auth/app_settings/default_timeout/opac" );
288                 _oilsAuthOPACTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
289                 jsonObjectFree(value_obj);
290                 if( -1 == _oilsAuthOPACTimeout ) {
291                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for OPAC logins" );
292                         _oilsAuthOPACTimeout = 0;
293                 }
294
295                 value_obj = osrf_settings_host_value_object(
296                         "/apps/open-ils.auth/app_settings/default_timeout/staff" );
297                 _oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
298                 jsonObjectFree(value_obj);
299                 if( -1 == _oilsAuthStaffTimeout ) {
300                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for staff logins" );
301                         _oilsAuthStaffTimeout = 0;
302                 }
303
304                 value_obj = osrf_settings_host_value_object(
305                         "/apps/open-ils.auth/app_settings/default_timeout/temp" );
306                 _oilsAuthOverrideTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
307                 jsonObjectFree(value_obj);
308                 if( -1 == _oilsAuthOverrideTimeout ) {
309                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for temp logins" );
310                         _oilsAuthOverrideTimeout = 0;
311                 }
312
313                 value_obj = osrf_settings_host_value_object(
314                         "/apps/open-ils.auth/app_settings/default_timeout/persist" );
315                 _oilsAuthPersistTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
316                 jsonObjectFree(value_obj);
317                 if( -1 == _oilsAuthPersistTimeout ) {
318                         osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for persist logins" );
319                         _oilsAuthPersistTimeout = 0;
320                 }
321
322                 osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: "
323                         "opac => %ld : staff => %ld : temp => %ld : persist => %ld",
324                         _oilsAuthOPACTimeout, _oilsAuthStaffTimeout,
325                         _oilsAuthOverrideTimeout, _oilsAuthPersistTimeout );
326         }
327
328         int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
329         if(orgloc < 1)
330                 orgloc = home_ou;
331
332         char* setting = NULL;
333         long default_timeout = 0;
334
335         if( !strcmp( type, OILS_AUTH_OPAC )) {
336                 setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
337                 default_timeout = _oilsAuthOPACTimeout;
338         } else if( !strcmp( type, OILS_AUTH_STAFF )) {
339                 setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
340                 default_timeout = _oilsAuthStaffTimeout;
341         } else if( !strcmp( type, OILS_AUTH_TEMP )) {
342                 setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
343                 default_timeout = _oilsAuthOverrideTimeout;
344         } else if( !strcmp( type, OILS_AUTH_PERSIST )) {
345                 setting = OILS_ORG_SETTING_PERSIST_TIMEOUT;
346                 default_timeout = _oilsAuthPersistTimeout;
347         }
348
349         // Get the org unit setting, if there is one.
350         char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
351         if(!timeout) {
352                 if( orgloc != home_ou ) {
353                         osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
354                                 "trying home_ou %d", orgloc, home_ou );
355                         timeout = oilsUtilsFetchOrgSetting( home_ou, setting );
356                 }
357         }
358
359         if(!timeout)
360                 return default_timeout;   // No override from org unit setting
361
362         // Translate the org unit setting to a number
363         long t;
364         if( !*timeout ) {
365                 osrfLogWarning( OSRF_LOG_MARK,
366                         "Timeout org unit setting is an empty string for %s login; using default",
367                         timeout, type );
368                 t = default_timeout;
369         } else {
370                 // Treat timeout string as an interval, and convert it to seconds
371                 t = oilsUtilsIntervalToSeconds( timeout );
372                 if( -1 == t ) {
373                         // Unable to convert; possibly an invalid interval string
374                         osrfLogError( OSRF_LOG_MARK,
375                                 "Unable to convert timeout interval \"%s\" for %s login; using default",
376                                 timeout, type );
377                         t = default_timeout;
378                 }
379         }
380
381         free(timeout);
382         return t;
383 }
384
385 /*
386         Adds the authentication token to the user cache.  The timeout for the
387         auth token is based on the type of login as well as (if type=='opac')
388         the org location id.
389         Returns the event that should be returned to the user.
390         Event must be freed
391 */
392 static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname,
393                 const char* type, int orgloc, const char* workstation ) {
394
395         oilsEvent* response;
396
397         long timeout;
398         char* wsorg = jsonObjectToSimpleString(oilsFMGetObject(userObj, "ws_ou"));
399         if(wsorg) { /* if there is a workstation, use it for the timeout */
400                 osrfLogDebug( OSRF_LOG_MARK,
401                                 "Auth session trying workstation id %d for auth timeout", atoi(wsorg));
402                 timeout = oilsAuthGetTimeout( userObj, type, atoi(wsorg) );
403                 free(wsorg);
404         } else {
405                 osrfLogDebug( OSRF_LOG_MARK,
406                                 "Auth session trying org from param [%d] for auth timeout", orgloc );
407                 timeout = oilsAuthGetTimeout( userObj, type, orgloc );
408         }
409         osrfLogDebug(OSRF_LOG_MARK, "Auth session timeout for %s: %ld", uname, timeout );
410
411         char* string = va_list_to_string(
412                         "%d.%ld.%s", (long) getpid(), time(NULL), uname );
413         char* authToken = md5sum(string);
414         char* authKey = va_list_to_string(
415                         "%s%s", OILS_AUTH_CACHE_PRFX, authToken );
416
417         const char* ws = (workstation) ? workstation : "";
418         osrfLogActivity(OSRF_LOG_MARK,
419                 "successful login: username=%s, authtoken=%s, workstation=%s", uname, authToken, ws );
420
421         oilsFMSetString( userObj, "passwd", "" );
422         jsonObject* cacheObj = jsonParseFmt( "{\"authtime\": %ld}", timeout );
423         jsonObjectSetKey( cacheObj, "userobj", jsonObjectClone(userObj));
424
425         if( !strcmp( type, OILS_AUTH_PERSIST )) {
426                 // Add entries for endtime and reset_interval, so that we can gracefully
427                 // extend the session a bit if the user is active toward the end of the 
428                 // timeout originally specified.
429                 time_t endtime = time( NULL ) + timeout;
430                 jsonObjectSetKey( cacheObj, "endtime", jsonNewNumberObject( (double) endtime ) );
431
432                 // Reset interval is hard-coded for now, but if we ever want to make it
433                 // configurable, this is the place to do it:
434                 jsonObjectSetKey( cacheObj, "reset_interval",
435                         jsonNewNumberObject( (double) DEFAULT_RESET_INTERVAL ));
436         }
437
438         osrfCachePutObject( authKey, cacheObj, (time_t) timeout );
439         jsonObjectFree(cacheObj);
440         osrfLogInternal(OSRF_LOG_MARK, "oilsAuthHandleLoginOK(): Placed user object into cache");
441         jsonObject* payload = jsonParseFmt(
442                 "{ \"authtoken\": \"%s\", \"authtime\": %ld }", authToken, timeout );
443
444         response = oilsNewEvent2( OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload );
445         free(string); free(authToken); free(authKey);
446         jsonObjectFree(payload);
447
448         return response;
449 }
450
451 static oilsEvent* oilsAuthVerifyWorkstation(
452                 const osrfMethodContext* ctx, jsonObject* userObj, const char* ws ) {
453         osrfLogInfo(OSRF_LOG_MARK, "Attaching workstation to user at login: %s", ws);
454         jsonObject* workstation = oilsUtilsFetchWorkstationByName(ws);
455         if(!workstation || workstation->type == JSON_NULL) {
456                 jsonObjectFree(workstation);
457                 return oilsNewEvent(OSRF_LOG_MARK, "WORKSTATION_NOT_FOUND");
458         }
459         long wsid = oilsFMGetObjectId(workstation);
460         LONG_TO_STRING(wsid);
461         char* orgid = oilsFMGetString(workstation, "owning_lib");
462         oilsFMSetString(userObj, "wsid", LONGSTR);
463         oilsFMSetString(userObj, "ws_ou", orgid);
464         free(orgid);
465         jsonObjectFree(workstation);
466         return NULL;
467 }
468
469
470
471 /**
472         @brief Implement the "complete" method.
473         @param ctx The method context.
474         @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
475         client to indicate completion; a positive integer if successful but no such STATUS
476         message has been sent.
477
478         Method parameters:
479         - a hash with some combination of the following elements:
480                 - "username"
481                 - "barcode"
482                 - "password" (hashed with the cached seed; not plaintext)
483                 - "type"
484                 - "org"
485                 - "workstation"
486
487         The password is required.  Either a username or a barcode must also be present.
488
489         Return to client: Intermediate authentication seed.
490
491         Validate the password, using the username if available, or the barcode if not.  The
492         user must be active, and not barred from logging on.  The barcode, if used for
493         authentication, must be active as well.  The workstation, if specified, must be valid.
494
495         Upon deciding whether to allow the logon, return a corresponding event to the client.
496 */
497 int oilsAuthComplete( osrfMethodContext* ctx ) {
498         OSRF_METHOD_VERIFY_CONTEXT(ctx);
499
500         const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
501
502         const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
503         const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
504         const char* type        = jsonObjectGetString(jsonObjectGetKeyConst(args, "type"));
505         int orgloc        = (int) jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org"));
506         const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
507         const char* barcode     = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
508
509         const char* ws = (workstation) ? workstation : "";
510
511         if( !type )
512                  type = OILS_AUTH_STAFF;
513
514         if( !( (uname || barcode) && password) ) {
515                 return osrfAppRequestRespondException( ctx->session, ctx->request,
516                         "username/barcode and password required for method: %s", ctx->method->name );
517         }
518
519         oilsEvent* response = NULL;
520         jsonObject* userObj = NULL;
521         int card_active     = 1;      // boolean; assume active until proven otherwise
522
523         // Fetch a row from the actor.usr table, by username if available,
524         // or by barcode if not.
525         if(uname) {
526                 userObj = oilsUtilsFetchUserByUsername( uname );
527                 if( userObj && JSON_NULL == userObj->type ) {
528                         jsonObjectFree( userObj );
529                         userObj = NULL;         // username not found
530                 }
531         }
532         else if(barcode) {
533                 // Read from actor.card by barcode
534
535                 osrfLogInfo( OSRF_LOG_MARK, "Fetching user by barcode %s", barcode );
536
537                 jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
538                 jsonObject* card = oilsUtilsQuickReq(
539                         "open-ils.cstore", "open-ils.cstore.direct.actor.card.search", params );
540                 jsonObjectFree( params );
541
542                 if( card && card->type != JSON_NULL ) {
543                         // Determine whether the card is active
544                         char* card_active_str = oilsFMGetString( card, "active" );
545                         card_active = oilsUtilsIsDBTrue( card_active_str );
546                         free( card_active_str );
547
548                         // Look up the user who owns the card
549                         char* userid = oilsFMGetString( card, "usr" );
550                         jsonObjectFree( card );
551                         params = jsonParseFmt( "[%s]", userid );
552                         free( userid );
553                         userObj = oilsUtilsQuickReq(
554                                         "open-ils.cstore", "open-ils.cstore.direct.actor.user.retrieve", params );
555                         jsonObjectFree( params );
556                         if( userObj && JSON_NULL == userObj->type ) {
557                                 // user not found (shouldn't happen, due to foreign key)
558                                 jsonObjectFree( userObj );
559                                 userObj = NULL;
560                         }
561                 }
562         }
563
564         if(!userObj) {
565                 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
566                 osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
567                                 uname, (barcode ? barcode : "(none)"), ws );
568                 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
569                 oilsEventFree(response);
570                 return 0;           // No such user
571         }
572
573         // Such a user exists.  Now see if he or she has the right credentials.
574         int passOK = -1;
575         if(uname)
576                 passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password );
577         else if (barcode)
578                 passOK = oilsAuthVerifyPassword( ctx, userObj, barcode, password );
579
580         if( passOK < 0 ) {
581                 jsonObjectFree(userObj);
582                 return passOK;
583         }
584
585         // See if the account is active
586         char* active = oilsFMGetString(userObj, "active");
587         if( !oilsUtilsIsDBTrue(active) ) {
588                 if( passOK )
589                         response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
590                 else
591                         response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
592
593                 osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
594                 oilsEventFree(response);
595                 jsonObjectFree(userObj);
596                 free(active);
597                 return 0;
598         }
599         free(active);
600
601         osrfLogInfo( OSRF_LOG_MARK, "Fetching card by barcode %s", barcode );
602
603         if( !card_active ) {
604                 osrfLogInfo( OSRF_LOG_MARK, "barcode %s is not active, returning event", barcode );
605                 response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_CARD_INACTIVE" );
606                 osrfAppRespondComplete( ctx, oilsEventToJSON( response ) );
607                 oilsEventFree( response );
608                 jsonObjectFree( userObj );
609                 return 0;
610         }
611
612
613         // See if the user is even allowed to log in
614         if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
615                 jsonObjectFree(userObj);
616                 return 0;
617         }
618
619         // If a workstation is defined, add the workstation info
620         if( workstation != NULL ) {
621                 osrfLogDebug(OSRF_LOG_MARK, "Workstation is %s", workstation);
622                 response = oilsAuthVerifyWorkstation( ctx, userObj, workstation );
623                 if(response) {
624                         jsonObjectFree(userObj);
625                         osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
626                         oilsEventFree(response);
627                         return 0;
628                 }
629
630         } else {
631                 // Otherwise, use the home org as the workstation org on the user
632                 char* orgid = oilsFMGetString(userObj, "home_ou");
633                 oilsFMSetString(userObj, "ws_ou", orgid);
634                 free(orgid);
635         }
636
637         char* freeable_uname = NULL;
638         if(!uname) {
639                 uname = freeable_uname = oilsFMGetString( userObj, "usrname" );
640         }
641
642         if( passOK ) {
643                 response = oilsAuthHandleLoginOK( userObj, uname, type, orgloc, workstation );
644
645         } else {
646                 response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_AUTH_FAILED );
647                 osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s, barcode=%s, workstation=%s",
648                                 uname, (barcode ? barcode : "(none)"), ws );
649         }
650
651         jsonObjectFree(userObj);
652         osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
653         oilsEventFree(response);
654
655         if(freeable_uname)
656                 free(freeable_uname);
657
658         return 0;
659 }
660
661
662
663 int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
664         OSRF_METHOD_VERIFY_CONTEXT(ctx);
665
666         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
667         jsonObject* resp = NULL;
668
669         if( authToken ) {
670                 osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
671                 char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
672                 osrfCacheRemove(key);
673                 resp = jsonNewObject(authToken); /**/
674                 free(key);
675         }
676
677         osrfAppRespondComplete( ctx, resp );
678         jsonObjectFree(resp);
679         return 0;
680 }
681
682 /**
683  * Fetches the user object from the database and updates the user object in 
684  * the cache object, which then has to be re-inserted into the cache.
685  * User object is retrieved inside a transaction to avoid replication issues.
686  */
687 static int _oilsAuthReloadUser(jsonObject* cacheObj) {
688     int reqid, userId;
689     osrfAppSession* session;
690         osrfMessage* omsg;
691     jsonObject *param, *userObj, *newUserObj;
692
693     userObj = jsonObjectGetKey( cacheObj, "userobj" );
694     userId = oilsFMGetObjectId( userObj );
695
696     session = osrfAppSessionClientInit( "open-ils.cstore" );
697     osrfAppSessionConnect(session);
698
699     reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
700         omsg = osrfAppSessionRequestRecv(session, reqid, 60);
701
702     if(omsg) {
703
704         osrfMessageFree(omsg);
705         param = jsonNewNumberObject(userId);
706         reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
707             omsg = osrfAppSessionRequestRecv(session, reqid, 60);
708         jsonObjectFree(param);
709
710         if(omsg) {
711             newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
712             osrfMessageFree(omsg);
713             reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
714                 omsg = osrfAppSessionRequestRecv(session, reqid, 60);
715             osrfMessageFree(omsg);
716         }
717     }
718
719     osrfAppSessionFree(session); // calls disconnect internally
720
721     if(newUserObj) {
722
723         // ws_ou and wsid are ephemeral and need to be manually propagated
724         // oilsFMSetString dupe()'s internally, no need to clone the string
725         oilsFMSetString(newUserObj, "wsid", oilsFMGetStringConst(userObj, "wsid"));
726         oilsFMSetString(newUserObj, "ws_ou", oilsFMGetStringConst(userObj, "ws_ou"));
727
728         jsonObjectRemoveKey(cacheObj, "userobj"); // this also frees the old user object
729         jsonObjectSetKey(cacheObj, "userobj", newUserObj);
730         return 1;
731     } 
732
733     osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
734     return 0;
735 }
736
737 /**
738         Resets the auth login timeout
739         @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
740 */
741 static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
742         if(!authToken) return NULL;
743
744         oilsEvent* evt = NULL;
745         time_t timeout;
746
747         osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
748         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
749         jsonObject* cacheObj = osrfCacheGetObject( key );
750
751         if(!cacheObj) {
752                 osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
753                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
754
755         } else {
756
757         if(reloadUser) {
758             _oilsAuthReloadUser(cacheObj);
759         }
760
761                 // Determine a new timeout value
762                 jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
763                 if( endtime_obj ) {
764                         // Extend the current endtime by a fixed amount
765                         time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
766                         int reset_interval = DEFAULT_RESET_INTERVAL;
767                         const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
768                                 cacheObj, "reset_interval" );
769                         if( reset_interval_obj ) {
770                                 reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
771                                 if( reset_interval <= 0 )
772                                         reset_interval = DEFAULT_RESET_INTERVAL;
773                         }
774
775                         time_t now = time( NULL );
776                         time_t new_endtime = now + reset_interval;
777                         if( new_endtime > endtime ) {
778                                 // Keep the session alive a little longer
779                                 jsonObjectSetNumber( endtime_obj, (double) new_endtime );
780                                 timeout = reset_interval;
781                                 osrfCachePutObject( key, cacheObj, timeout );
782                         } else {
783                                 // The session isn't close to expiring, so don't reset anything.
784                                 // Just report the time remaining.
785                                 timeout = endtime - now;
786                         }
787                 } else {
788                         // Reapply the existing timeout from the current time
789                         timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
790                         osrfCachePutObject( key, cacheObj, timeout );
791                 }
792
793                 jsonObject* payload = jsonNewNumberObject( (double) timeout );
794                 evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
795                 jsonObjectFree(payload);
796                 jsonObjectFree(cacheObj);
797         }
798
799         free(key);
800         return evt;
801 }
802
803 int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
804         OSRF_METHOD_VERIFY_CONTEXT(ctx);
805         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
806     double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
807         oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
808         osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
809         oilsEventFree(evt);
810         return 0;
811 }
812
813
814 int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
815         OSRF_METHOD_VERIFY_CONTEXT(ctx);
816     bool returnFull = false;
817
818         const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
819
820     if(ctx->params->size > 1) {
821         // caller wants full cached object, with authtime, etc.
822         const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
823         if(rt && strcmp(rt, "0") != 0) 
824             returnFull = true;
825     }
826
827         jsonObject* cacheObj = NULL;
828         oilsEvent* evt = NULL;
829
830         if( authToken ){
831
832                 // Reset the timeout to keep the session alive
833                 evt = _oilsAuthResetTimeout(authToken, 0);
834
835                 if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
836                         osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
837
838                 } else {
839
840                         // Retrieve the cached session object
841                         osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
842                         char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
843                         cacheObj = osrfCacheGetObject( key );
844                         if(cacheObj) {
845                                 // Return a copy of the cached user object
846                 if(returnFull)
847                                     osrfAppRespondComplete( ctx, cacheObj);
848                 else
849                                     osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
850                                 jsonObjectFree(cacheObj);
851                         } else {
852                                 // Auth token is invalid or expired
853                                 oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
854                                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
855                                 oilsEventFree(evt2);
856                         }
857                         free(key);
858                 }
859
860         } else {
861
862                 // No session
863                 evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
864                 osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
865         }
866
867         if(evt)
868                 oilsEventFree(evt);
869
870         return 0;
871 }