5 #include "opensrf/osrf_app_session.h"
6 #include "opensrf/osrf_application.h"
7 #include "opensrf/osrf_settings.h"
8 #include "opensrf/osrf_json.h"
9 #include "opensrf/log.h"
10 #include "openils/oils_utils.h"
11 #include "openils/oils_constants.h"
12 #include "openils/oils_event.h"
14 #define OILS_AUTH_CACHE_PRFX "oils_auth_"
15 #define OILS_AUTH_COUNT_SFFX "_count"
17 #define MODULENAME "open-ils.auth_internal"
19 #define OILS_AUTH_OPAC "opac"
20 #define OILS_AUTH_STAFF "staff"
21 #define OILS_AUTH_TEMP "temp"
22 #define OILS_AUTH_PERSIST "persist"
24 #define BLOCK_EXPIRED_STAFF_LOGIN_FLAG "auth.block_expired_staff_login"
26 // Default time for extending a persistent session: ten minutes
27 #define DEFAULT_RESET_INTERVAL 10 * 60
29 int safe_line = __LINE__;
30 #define OILS_LOG_MARK_SAFE __FILE__,safe_line
32 int osrfAppInitialize();
33 int osrfAppChildInit();
35 static long _oilsAuthOPACTimeout = 0;
36 static long _oilsAuthStaffTimeout = 0;
37 static long _oilsAuthOverrideTimeout = 0;
38 static long _oilsAuthPersistTimeout = 0;
41 @brief Initialize the application by registering functions for method calls.
42 @return Zero on success, 1 on error.
44 int osrfAppInitialize() {
46 osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Internal Server...");
48 /* load and parse the IDL */
49 /* return non-zero to indicate error */
50 if (!oilsInitIDL(NULL)) return 1;
52 osrfAppRegisterMethod(
54 "open-ils.auth_internal.session.create",
55 "oilsAuthInternalCreateSession",
56 "Adds a user to the authentication cache to indicate "
57 "the user is authenticated", 1, 0
60 osrfAppRegisterMethod(
62 "open-ils.auth_internal.user.validate",
63 "oilsAuthInternalValidate",
64 "Determines whether a user should be allowed to login. "
65 "Returns SUCCESS oilsEvent when the user is valid, otherwise "
66 "returns a non-SUCCESS oilsEvent object", 1, 0
73 @brief Dummy placeholder for initializing a server drone.
75 There is nothing to do, so do nothing.
77 int osrfAppChildInit() {
83 @brief Determine the login timeout.
84 @param userObj Pointer to an object describing the user.
85 @param type Pointer to one of four possible character strings identifying the login type.
86 @param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
87 @return The length of the timeout, in seconds.
89 The default timeout value comes from the configuration file, and
90 depends on the login type.
92 The default may be overridden by a corresponding org unit setting.
93 The @a orgloc parameter says what org unit to use for the lookup.
94 If @a orgloc <= 0, or if the lookup for @a orgloc yields no result,
95 we look up the setting for the user's home org unit instead (except
96 that if it's the same as @a orgloc we don't bother repeating the
99 Whether defined in the config file or in an org unit setting, a
100 timeout value may be expressed as a raw number (i.e. all digits,
101 possibly with leading and/or trailing white space) or as an interval
102 string to be translated into seconds by PostgreSQL.
104 static long oilsAuthGetTimeout(
105 const jsonObject* userObj, const char* type, int orgloc) {
107 if(!_oilsAuthOPACTimeout) { /* Load the default timeouts */
109 jsonObject* value_obj;
111 value_obj = osrf_settings_host_value_object(
112 "/apps/open-ils.auth_internal/app_settings/default_timeout/opac" );
113 _oilsAuthOPACTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
114 jsonObjectFree(value_obj);
115 if( -1 == _oilsAuthOPACTimeout ) {
116 osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for OPAC logins" );
117 _oilsAuthOPACTimeout = 0;
120 value_obj = osrf_settings_host_value_object(
121 "/apps/open-ils.auth_internal/app_settings/default_timeout/staff" );
122 _oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
123 jsonObjectFree(value_obj);
124 if( -1 == _oilsAuthStaffTimeout ) {
125 osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for staff logins" );
126 _oilsAuthStaffTimeout = 0;
129 value_obj = osrf_settings_host_value_object(
130 "/apps/open-ils.auth_internal/app_settings/default_timeout/temp" );
131 _oilsAuthOverrideTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
132 jsonObjectFree(value_obj);
133 if( -1 == _oilsAuthOverrideTimeout ) {
134 osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for temp logins" );
135 _oilsAuthOverrideTimeout = 0;
138 value_obj = osrf_settings_host_value_object(
139 "/apps/open-ils.auth_internal/app_settings/default_timeout/persist" );
140 _oilsAuthPersistTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
141 jsonObjectFree(value_obj);
142 if( -1 == _oilsAuthPersistTimeout ) {
143 osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for persist logins" );
144 _oilsAuthPersistTimeout = 0;
147 osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: "
148 "opac => %ld : staff => %ld : temp => %ld : persist => %ld",
149 _oilsAuthOPACTimeout, _oilsAuthStaffTimeout,
150 _oilsAuthOverrideTimeout, _oilsAuthPersistTimeout );
153 int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
157 char* setting = NULL;
158 long default_timeout = 0;
160 if( !strcmp( type, OILS_AUTH_OPAC )) {
161 setting = OILS_ORG_SETTING_OPAC_TIMEOUT;
162 default_timeout = _oilsAuthOPACTimeout;
163 } else if( !strcmp( type, OILS_AUTH_STAFF )) {
164 setting = OILS_ORG_SETTING_STAFF_TIMEOUT;
165 default_timeout = _oilsAuthStaffTimeout;
166 } else if( !strcmp( type, OILS_AUTH_TEMP )) {
167 setting = OILS_ORG_SETTING_TEMP_TIMEOUT;
168 default_timeout = _oilsAuthOverrideTimeout;
169 } else if( !strcmp( type, OILS_AUTH_PERSIST )) {
170 setting = OILS_ORG_SETTING_PERSIST_TIMEOUT;
171 default_timeout = _oilsAuthPersistTimeout;
174 // Get the org unit setting, if there is one.
175 char* timeout = oilsUtilsFetchOrgSetting( orgloc, setting );
177 if( orgloc != home_ou ) {
178 osrfLogDebug(OSRF_LOG_MARK, "Auth timeout not defined for org %d, "
179 "trying home_ou %d", orgloc, home_ou );
180 timeout = oilsUtilsFetchOrgSetting( home_ou, setting );
185 return default_timeout; // No override from org unit setting
187 // Translate the org unit setting to a number
190 osrfLogWarning( OSRF_LOG_MARK,
191 "Timeout org unit setting is an empty string for %s login; using default",
195 // Treat timeout string as an interval, and convert it to seconds
196 t = oilsUtilsIntervalToSeconds( timeout );
198 // Unable to convert; possibly an invalid interval string
199 osrfLogError( OSRF_LOG_MARK,
200 "Unable to convert timeout interval \"%s\" for %s login; using default",
211 * Verify workstation exists and stuff it into the user object to be cached
213 static oilsEvent* oilsAuthVerifyWorkstation(
214 const osrfMethodContext* ctx, jsonObject* userObj, const char* ws ) {
216 jsonObject* workstation = oilsUtilsFetchWorkstationByName(ws);
218 if(!workstation || workstation->type == JSON_NULL) {
219 jsonObjectFree(workstation);
220 return oilsNewEvent(OSRF_LOG_MARK, "WORKSTATION_NOT_FOUND");
223 long wsid = oilsFMGetObjectId(workstation);
224 LONG_TO_STRING(wsid);
225 char* orgid = oilsFMGetString(workstation, "owning_lib");
226 oilsFMSetString(userObj, "wsid", LONGSTR);
227 oilsFMSetString(userObj, "ws_ou", orgid);
229 jsonObjectFree(workstation);
234 Verifies that the user has permission to login with the given type.
235 Caller is responsible for freeing returned oilsEvent.
236 @return oilsEvent* if the permission check failed, NULL otherwise.
238 static oilsEvent* oilsAuthCheckLoginPerm(osrfMethodContext* ctx,
239 int user_id, int org_id, const char* type ) {
241 // For backwards compatibility, check all login permissions
242 // using the root org unit as the context org unit.
247 if (!strcasecmp(type, OILS_AUTH_OPAC)) {
248 perms[0] = "OPAC_LOGIN";
250 } else if (!strcasecmp(type, OILS_AUTH_STAFF)) {
251 perms[0] = "STAFF_LOGIN";
253 } else if (!strcasecmp(type, OILS_AUTH_TEMP)) {
254 perms[0] = "STAFF_LOGIN";
256 } else if (!strcasecmp(type, OILS_AUTH_PERSIST)) {
257 perms[0] = "PERSISTENT_LOGIN";
260 return oilsUtilsCheckPerms(user_id, org_id, perms, 1);
266 @brief Implement the session create method
267 @param ctx The method context.
268 @return -1 upon error; zero if successful, and if a STATUS message has
269 been sent to the client to indicate completion; a positive integer if
270 successful but no such STATUS message has been sent.
273 - a hash with some combination of the following elements:
274 - "user_id" -- actor.usr (au) ID for the user to cache.
275 - "org_unit" -- actor.org_unit (aou) ID representing the physical
276 location / context used for timeout, etc. settings.
277 - "login_type" -- login type (opac, staff, temp, persist)
278 - "workstation" -- workstation name
281 int oilsAuthInternalCreateSession(osrfMethodContext* ctx) {
282 OSRF_METHOD_VERIFY_CONTEXT(ctx);
284 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
286 const char* user_id = jsonObjectGetString(jsonObjectGetKeyConst(args, "user_id"));
287 const char* login_type = jsonObjectGetString(jsonObjectGetKeyConst(args, "login_type"));
288 const char* workstation = jsonObjectGetString(jsonObjectGetKeyConst(args, "workstation"));
289 int org_unit = jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org_unit"));
291 if ( !(user_id && login_type) ) {
292 return osrfAppRequestRespondException( ctx->session, ctx->request,
293 "Missing parameters for method: %s", ctx->method->name );
296 oilsEvent* response = NULL;
298 // fetch the user object
299 jsonObject* idParam = jsonNewNumberStringObject(user_id);
300 jsonObject* userObj = oilsUtilsCStoreReqCtx(
301 ctx, "open-ils.cstore.direct.actor.user.retrieve", idParam);
302 jsonObjectFree(idParam);
305 return osrfAppRequestRespondException(ctx->session,
306 ctx->request, "No user found with ID %s", user_id);
309 // If a workstation is defined, add the workstation info
311 response = oilsAuthVerifyWorkstation(ctx, userObj, workstation);
313 if (response) { // invalid workstation.
314 jsonObjectFree(userObj);
315 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
316 oilsEventFree(response);
319 } else { // workstation OK.
321 // The worksation org unit supersedes any org unit value
322 // provided via the API. oilsAuthVerifyWorkstation() sets the
323 // ws_ou value to the WS owning lib. A value is guaranteed.
324 org_unit = atoi(oilsFMGetStringConst(userObj, "ws_ou"));
327 } else { // no workstation
329 // For backwards compatibility, when no workstation is provided, use
330 // the users's home org as its workstation org unit, regardless of
331 // any API-level org unit value provided.
332 const char* orgid = oilsFMGetStringConst(userObj, "home_ou");
333 oilsFMSetString(userObj, "ws_ou", orgid);
335 // The context org unit defaults to the user's home library when
336 // no workstation is used and no API-level value is provided.
337 if (org_unit < 1) org_unit = atoi(orgid);
340 // determine the auth/cache timeout
341 long timeout = oilsAuthGetTimeout(userObj, login_type, org_unit);
343 char* string = va_list_to_string("%d.%ld.%ld",
344 (long) getpid(), time(NULL), oilsFMGetObjectId(userObj));
345 char* authToken = md5sum(string);
346 char* authKey = va_list_to_string(
347 "%s%s", OILS_AUTH_CACHE_PRFX, authToken);
349 oilsFMSetString(userObj, "passwd", "");
350 jsonObject* cacheObj = jsonParseFmt("{\"authtime\": %ld}", timeout);
351 jsonObjectSetKey(cacheObj, "userobj", jsonObjectClone(userObj));
353 if( !strcmp(login_type, OILS_AUTH_PERSIST)) {
354 // Add entries for endtime and reset_interval, so that we can gracefully
355 // extend the session a bit if the user is active toward the end of the
356 // timeout originally specified.
357 time_t endtime = time( NULL ) + timeout;
358 jsonObjectSetKey(cacheObj, "endtime",
359 jsonNewNumberObject( (double) endtime ));
361 // Reset interval is hard-coded for now, but if we ever want to make it
362 // configurable, this is the place to do it:
363 jsonObjectSetKey(cacheObj, "reset_interval",
364 jsonNewNumberObject( (double) DEFAULT_RESET_INTERVAL));
367 osrfCachePutObject(authKey, cacheObj, (time_t) timeout);
368 jsonObjectFree(cacheObj);
369 jsonObject* payload = jsonParseFmt(
370 "{\"authtoken\": \"%s\", \"authtime\": %ld}", authToken, timeout);
372 response = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
373 free(string); free(authToken); free(authKey);
374 jsonObjectFree(payload);
376 jsonObjectFree(userObj);
377 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
378 oilsEventFree(response);
383 int _checkIfExpiryDatePassed(const char *expire_date) {
386 memset(&expire_tm, 0, sizeof(expire_tm));
387 strptime(expire_date, "%FT%T%z", &expire_tm);
388 time_t now = time(NULL);
389 time_t expire_time_t = mktime(&expire_tm);
390 if (now > expire_time_t) {
397 int _blockExpiredStaffLogin(osrfMethodContext* ctx, int user_id) {
398 // check global flag whether we're supposed to block or not
399 jsonObject *cgfObj = NULL, *params = NULL;
400 params = jsonNewObject(BLOCK_EXPIRED_STAFF_LOGIN_FLAG);
401 cgfObj = oilsUtilsCStoreReqCtx(
402 ctx, "open-ils.cstore.direct.config.global_flag.retrieve", params);
403 jsonObjectFree(params);
405 int may_block_login = 0;
406 char* tmp_str = NULL;
407 if (cgfObj && cgfObj->type != JSON_NULL) {
408 tmp_str = oilsFMGetString(cgfObj, "enabled");
409 if (oilsUtilsIsDBTrue(tmp_str)) {
414 jsonObjectFree(cgfObj);
416 if (!may_block_login) {
420 // OK, we're supposed to block logins by expired staff accounts,
421 // so let's see if the account is one. We'll do so by seeing
422 // if the account has the STAFF_LOGIN permission anywhere. We
423 // are _not_ checking the login_type, as blocking 'staff' and
424 // 'temp' logins still leaves open the possibility of constructing
425 // an 'opac'-type login that _also_ sets a workstation, which
426 // in turn could be used to set an authtoken cookie that works
427 // in the staff interface. This means, that unlike ordinary patrons,
428 // a staff account that expires will not be able to log into
429 // the public catalog... but then, staff members really ought
430 // to be using a separate account when acting as a library patron
435 // using the root org unit as the context org unit.
438 perms[0] = "STAFF_LOGIN";
439 oilsEvent* response = oilsUtilsCheckPerms(user_id, org_id, perms, 1);
442 // user has STAFF_LOGIN, so should be blocked
445 oilsEventFree(response);
451 int oilsAuthInternalValidate(osrfMethodContext* ctx) {
452 OSRF_METHOD_VERIFY_CONTEXT(ctx);
454 const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
456 const char* user_id = jsonObjectGetString(jsonObjectGetKeyConst(args, "user_id"));
457 const char* barcode = jsonObjectGetString(jsonObjectGetKeyConst(args, "barcode"));
458 const char* login_type = jsonObjectGetString(jsonObjectGetKeyConst(args, "login_type"));
459 int org_unit = jsonObjectGetNumber(jsonObjectGetKeyConst(args, "org_unit"));
461 if ( !(user_id && login_type) ) {
462 return osrfAppRequestRespondException( ctx->session, ctx->request,
463 "Missing parameters for method: %s", ctx->method->name );
466 oilsEvent* response = NULL;
467 jsonObject *userObj = NULL, *params = NULL;
468 char* tmp_str = NULL;
469 int user_exists = 0, user_active = 0,
470 user_barred = 0, user_deleted = 0,
473 // Confirm user exists, active=true, barred=false, deleted=false
474 params = jsonNewNumberStringObject(user_id);
475 userObj = oilsUtilsCStoreReqCtx(
476 ctx, "open-ils.cstore.direct.actor.user.retrieve", params);
477 jsonObjectFree(params);
479 if (userObj && userObj->type != JSON_NULL) {
482 tmp_str = oilsFMGetString(userObj, "active");
483 user_active = oilsUtilsIsDBTrue(tmp_str);
486 tmp_str = oilsFMGetString(userObj, "barred");
487 user_barred = oilsUtilsIsDBTrue(tmp_str);
490 tmp_str = oilsFMGetString(userObj, "deleted");
491 user_deleted = oilsUtilsIsDBTrue(tmp_str);
494 tmp_str = oilsFMGetString(userObj, "expire_date");
495 expired = _checkIfExpiryDatePassed(tmp_str);
499 if (!user_exists || user_barred || user_deleted) {
500 response = oilsNewEvent(OILS_LOG_MARK_SAFE, OILS_EVENT_AUTH_FAILED);
503 if (!response && expired) {
504 if (_blockExpiredStaffLogin(ctx, atoi(user_id))) {
505 tmp_str = oilsFMGetString(userObj, "usrname");
506 osrfLogWarning( OSRF_LOG_MARK, "Blocked login for expired staff user %s", tmp_str );
508 response = oilsNewEvent(OILS_LOG_MARK_SAFE, OILS_EVENT_AUTH_FAILED);
512 if (!response && !user_active) {
513 // In some cases, it's useful for the caller to know if the
514 // patron was unable to login becuase the account is inactive.
515 // Return a specific event for this.
516 response = oilsNewEvent(OILS_LOG_MARK_SAFE, "PATRON_INACTIVE");
519 if (!response && barcode) {
520 // Caller provided a barcode. Ensure it exists and is active.
523 params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
524 jsonObject* card = oilsUtilsCStoreReqCtx(
525 ctx, "open-ils.cstore.direct.actor.card.search", params);
526 jsonObjectFree(params);
528 if (card && card->type != JSON_NULL) {
529 tmp_str = oilsFMGetString(card, "active");
530 card_ok = oilsUtilsIsDBTrue(tmp_str);
534 jsonObjectFree(card); // card=NULL OK here.
537 response = oilsNewEvent(
538 OILS_LOG_MARK_SAFE, "PATRON_CARD_INACTIVE");
542 // XXX: login permission checks are always global (see
543 // oilsAuthCheckLoginPerm()). No need to extract the
544 // workstation org unit here.
546 if (!response) { // Still OK
547 // Confirm user has permission to login w/ the requested type.
548 response = oilsAuthCheckLoginPerm(
549 ctx, atoi(user_id), org_unit, login_type);
554 // No tests failed. Return SUCCESS.
555 response = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_SUCCESS);
559 jsonObjectFree(userObj); // userObj=NULL OK here.
560 osrfAppRespondComplete(ctx, oilsEventToJSON(response));
561 oilsEventFree(response);