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