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