LP#1474029: teach Evergreen how to prevent expired staff from logging in
authorGalen Charlton <gmc@equinoxinitiative.org>
Thu, 10 Dec 2020 22:23:47 +0000 (17:23 -0500)
committerBill Erickson <berickxx@gmail.com>
Wed, 10 Feb 2021 20:44:44 +0000 (15:44 -0500)
This patch adds the ability to prevent staff users whose
accounts have expired from logging in. This is controlled
by the new global flag "auth.block_expired_staff_login", which
is not enabled by default. If that flag is turned on, accounts
that have the `STAFF_LOGIN` permission and whose expiration date
is in the past are prevented from logging into any Evergreen
interface, including the staff client, the public catalog, and SIP2.

It should be noted that ordinary patrons are allowed to log into
the public catalog if their circulation privileges have expired. This
feature prevents expired staff users from logging into the public catalog
(and all other Evergreen interfaces and APIs) outright in order to
prevent them from getting into the staff interface anyway by
creative use of Evergreen's authentication APIs.

Evergreen admins are advised to check the expiration status of staff
accounts before turning on the global flag, as otherwise it is
possible to lock staff users out unexpectedly.

Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Signed-off-by: Terran McCanna <tmccanna@georgialibraries.org>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/c-apps/oils_auth_internal.c
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.block_expired_staff_login_flag.sql [new file with mode: 0644]
docs/RELEASE_NOTES_NEXT/Architecture/Block_Login_of_Expired_Staff_Accounts.adoc [new file with mode: 0644]

index d0c46f8..948860f 100644 (file)
@@ -1,3 +1,7 @@
+#define _XOPEN_SOURCE
+#include <time.h>
+#include <string.h>
+#include <strings.h>
 #include "opensrf/osrf_app_session.h"
 #include "opensrf/osrf_application.h"
 #include "opensrf/osrf_settings.h"
@@ -17,6 +21,8 @@
 #define OILS_AUTH_TEMP "temp"
 #define OILS_AUTH_PERSIST "persist"
 
+#define BLOCK_EXPIRED_STAFF_LOGIN_FLAG "auth.block_expired_staff_login"
+
 // Default time for extending a persistent session: ten minutes
 #define DEFAULT_RESET_INTERVAL 10 * 60
 
@@ -374,6 +380,73 @@ int oilsAuthInternalCreateSession(osrfMethodContext* ctx) {
     return 0;
 }
 
+int _checkIfExpiryDatePassed(const char *expire_date) {
+
+    struct tm expire_tm;
+    memset(&expire_tm, 0, sizeof(expire_tm));
+    strptime(expire_date, "%FT%T%z", &expire_tm);
+    time_t now = time(NULL);
+    time_t expire_time_t = mktime(&expire_tm);
+    if (now > expire_time_t) {
+        return 1;
+    } else {
+        return 0;
+    }
+}
+
+int _blockExpiredStaffLogin(osrfMethodContext* ctx, int user_id) {
+    // check global flag whether we're supposed to block or not
+    jsonObject *cgfObj = NULL, *params = NULL;
+    params = jsonNewObject(BLOCK_EXPIRED_STAFF_LOGIN_FLAG);
+    cgfObj = oilsUtilsCStoreReqCtx(
+        ctx, "open-ils.cstore.direct.config.global_flag.retrieve", params);
+    jsonObjectFree(params);
+
+    int may_block_login = 0;
+    char* tmp_str = NULL;
+    if (cgfObj && cgfObj->type != JSON_NULL) {
+        tmp_str = oilsFMGetString(cgfObj, "enabled");
+        if (oilsUtilsIsDBTrue(tmp_str)) {
+            may_block_login = 1;
+        }
+        free(tmp_str);
+    }
+    jsonObjectFree(cgfObj);
+
+    if (!may_block_login) {
+        return 0;
+    }
+
+    // OK, we're supposed to block logins by expired staff accounts,
+    // so let's see if the account is one. We'll do so by seeing
+    // if the account has the STAFF_LOGIN permission anywhere. We
+    // are _not_ checking the login_type, as blocking 'staff' and
+    // 'temp' logins still leaves open the possibility of constructing
+    // an 'opac'-type login that _also_ sets a workstation, which
+    // in turn could be used to set an authtoken cookie that works
+    // in the staff interface. This means, that unlike ordinary patrons,
+    // a staff account that expires will not be able to log into
+    // the public catalog... but then, staff members really ought
+    // to be using a separate account when acting as a library patron
+    // anyway.
+
+    int block_login = 0;
+
+    // using the root org unit as the context org unit.
+    int org_id = -1;
+    char* perms[1];
+    perms[0] = "STAFF_LOGIN";
+    oilsEvent* response = oilsUtilsCheckPerms(user_id, org_id, perms, 1);
+
+    if (!response) {
+        // user has STAFF_LOGIN, so should be blocked
+        block_login = 1;
+    } else {
+        oilsEventFree(response);
+    }
+
+    return block_login;
+}
 
 int oilsAuthInternalValidate(osrfMethodContext* ctx) {
     OSRF_METHOD_VERIFY_CONTEXT(ctx);
@@ -394,7 +467,8 @@ int oilsAuthInternalValidate(osrfMethodContext* ctx) {
     jsonObject *userObj = NULL, *params = NULL;
     char* tmp_str = NULL;
     int user_exists = 0, user_active = 0, 
-        user_barred = 0, user_deleted = 0;
+        user_barred = 0, user_deleted = 0,
+        expired = 0;
 
     // Confirm user exists, active=true, barred=false, deleted=false
     params = jsonNewNumberStringObject(user_id);
@@ -416,12 +490,25 @@ int oilsAuthInternalValidate(osrfMethodContext* ctx) {
         tmp_str = oilsFMGetString(userObj, "deleted");
         user_deleted = oilsUtilsIsDBTrue(tmp_str);
         free(tmp_str);
+
+        tmp_str = oilsFMGetString(userObj, "expire_date");
+        expired = _checkIfExpiryDatePassed(tmp_str);
+        free(tmp_str);
     }
 
     if (!user_exists || user_barred || user_deleted) {
         response = oilsNewEvent(OILS_LOG_MARK_SAFE, OILS_EVENT_AUTH_FAILED);
     }
 
+    if (!response && expired) {
+        if (_blockExpiredStaffLogin(ctx, atoi(user_id))) {
+            tmp_str = oilsFMGetString(userObj, "usrname");
+            osrfLogWarning( OSRF_LOG_MARK, "Blocked login for expired staff user %s", tmp_str );
+            free(tmp_str);
+            response = oilsNewEvent(OILS_LOG_MARK_SAFE, OILS_EVENT_AUTH_FAILED);
+        }
+    }
+
     if (!response && !user_active) {
         // In some cases, it's useful for the caller to know if the
         // patron was unable to login becuase the account is inactive.
index bf3e7ab..4f2948e 100644 (file)
@@ -21249,3 +21249,14 @@ VALUES (
     )
 );
 
+INSERT INTO config.global_flag (name, value, enabled, label)
+VALUES (
+    'auth.block_expired_staff_login',
+    NULL,
+    FALSE,
+    oils_i18n_gettext(
+        'auth.block_expired_staff_login',
+        'Block the ability of expired user with the STAFF_LOGIN permission to log into Evergreen.',
+        'cgf', 'label'
+    )
+);
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.block_expired_staff_login_flag.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.block_expired_staff_login_flag.sql
new file mode 100644 (file)
index 0000000..b7ea9b1
--- /dev/null
@@ -0,0 +1,17 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.global_flag (name, value, enabled, label)
+VALUES (
+    'auth.block_expired_staff_login',
+    NULL,
+    FALSE,
+    oils_i18n_gettext(
+        'auth.block_expired_staff_login',
+        'Block the ability of expired user with the STAFF_LOGIN permission to log into Evergreen.',
+        'cgf', 'label'
+    )
+);
+
+COMMIT;
diff --git a/docs/RELEASE_NOTES_NEXT/Architecture/Block_Login_of_Expired_Staff_Accounts.adoc b/docs/RELEASE_NOTES_NEXT/Architecture/Block_Login_of_Expired_Staff_Accounts.adoc
new file mode 100644 (file)
index 0000000..b2c18c4
--- /dev/null
@@ -0,0 +1,39 @@
+Block Login of Expired Staff Accounts
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Evergreen now has the ability to prevent staff users whose
+accounts have expired from logging in. This is controlled
+by the new global flag "auth.block_expired_staff_login", which
+is not enabled by default. If that flag is turned on, accounts
+that have the `STAFF_LOGIN` permission and whose expiration date
+is in the past are prevented from logging into any Evergreen
+interface, including the staff client, the public catalog, and SIP2.
+
+It should be noted that ordinary patrons are allowed to log into
+the public catalog if their circulation privileges have expired. This
+feature prevents expired staff users from logging into the public catalog
+(and all other Evergreen interfaces and APIs) outright in order to
+prevent them from getting into the staff interface anyway by
+creative use of Evergreen's authentication APIs.
+
+Evergreen admins are advised to check the expiration status of staff
+accounts before turning on the global flag, as otherwise it is
+possible to lock staff users out unexpectedly. The following SQL
+query will identify expired but otherwise un-deleted users that
+would be blocked by turning on the flag:
+
+[source,sql]
+----
+SELECT DISTINCT usrname, expire_date
+FROM actor.usr au, permission.usr_has_perm_at_all(id, 'STAFF_LOGIN')
+WHERE active
+AND NOT deleted
+AND NOT barred
+AND expire_date < NOW()
+----
+
+Note that this query can take a long time to run in large databases
+given the general way that it checks for users that have the
+`STAFF_LOGIN` permission. Replacing the use of
+`permission.usr_has_perm_at_all()` with a query on expired users
+with profiles known to have the `STAFF_LOGIN` permission will
+be much faster.