>From 653aba050aacbf5629d4112b01736f355d42aebc Mon Sep 17 00:00:00 2001
From: Pavel Reichl <preichl@redhat.com>
Date: Thu, 16 Apr 2015 03:41:58 -0400
Subject: [PATCH 2/3] PAM: authenticate agains cache

Enable authenticating users from cache even when SSSD is in online mode.

Introduce new option `cached_auth_timeout`.

Resolves:
https://fedorahosted.org/sssd/ticket/1807
---
 src/confdb/confdb.c                  |  62 +++++++++++++
 src/confdb/confdb.h                  |   2 +
 src/config/SSSDConfig/__init__.py.in |   1 +
 src/config/SSSDConfigTest.py         |   6 +-
 src/config/etc/sssd.api.conf         |   1 +
 src/man/sssd.conf.5.xml              |  24 +++++
 src/responder/pam/pamsrv.h           |   3 +
 src/responder/pam/pamsrv_cmd.c       | 170 +++++++++++++++++++++++++++++++++--
 8 files changed, 261 insertions(+), 8 deletions(-)

diff --git a/src/confdb/confdb.c b/src/confdb/confdb.c
index 9af754912e248b83a71b253109e408bf004eda31..3a8a1c01b92e62302ac4f787ccd085be9d8f05c3 100644
--- a/src/confdb/confdb.c
+++ b/src/confdb/confdb.c
@@ -760,6 +760,59 @@ static uint32_t confdb_get_min_id(struct sss_domain_info *domain)
     return defval;
 }
 
+static errno_t init_cached_auth_timeout(struct confdb_ctx *cdb,
+                                        struct ldb_message *msg,
+                                        uint32_t *_cached_auth_timeout)
+{
+    int cred_expiration;
+    int id_timeout;
+    errno_t ret;
+    uint32_t cached_auth_timeout;
+
+    ret = get_entry_as_uint32(msg, &cached_auth_timeout,
+                              CONFDB_DOMAIN_CACHED_AUTH_TIMEOUT, 0);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_FATAL_FAILURE,
+              "Invalid value for [%s]\n", CONFDB_DOMAIN_CACHED_AUTH_TIMEOUT);
+        goto done;
+    }
+
+    ret = confdb_get_int(cdb, CONFDB_PAM_CONF_ENTRY,
+                         CONFDB_PAM_CRED_TIMEOUT, 0, &cred_expiration);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_CRIT_FAILURE,
+              "Failed to read expiration time of offline credentials.\n");
+        goto done;
+    }
+
+    /* convert from days to seconds */
+    cred_expiration *= 3600 * 24;
+    if (cred_expiration != 0 &&
+        cred_expiration < cached_auth_timeout) {
+        cached_auth_timeout = cred_expiration;
+    }
+
+    /* Set up the PAM identity timeout */
+    ret = confdb_get_int(cdb, CONFDB_PAM_CONF_ENTRY,
+                         CONFDB_PAM_ID_TIMEOUT, 5,
+                         &id_timeout);
+    if (ret != EOK) goto done;
+
+    if (cached_auth_timeout > id_timeout) {
+        DEBUG(SSSDBG_MINOR_FAILURE,
+              "cached_auth_timeout is greater than pam_id_timeout so be aware "
+              "that back end could be called to handle initgroups.\n");
+    }
+
+    ret = EOK;
+
+done:
+    if (ret == EOK) {
+        *_cached_auth_timeout = cached_auth_timeout;
+    }
+    return ret;
+}
+
 static int confdb_get_domain_internal(struct confdb_ctx *cdb,
                                       TALLOC_CTX *mem_ctx,
                                       const char *name,
@@ -1277,6 +1330,15 @@ static int confdb_get_domain_internal(struct confdb_ctx *cdb,
         goto done;
     }
 
+    ret = init_cached_auth_timeout(cdb, res->msgs[0],
+                                   &domain->cached_auth_timeout);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_CRIT_FAILURE,
+              "init_cached_auth_timeout failed: %s:[%d].\n",
+              sss_strerror(ret), ret);
+        goto done;
+    }
+
     domain->has_views = false;
     domain->view_name = NULL;
 
diff --git a/src/confdb/confdb.h b/src/confdb/confdb.h
index 801a13fc2345e4b940963ad095bf01385c4bcd85..b2ec2e0b98a9be2d50009df524a1072e9b1c15c7 100644
--- a/src/confdb/confdb.h
+++ b/src/confdb/confdb.h
@@ -188,6 +188,7 @@
 #define CONFDB_DOMAIN_REFRESH_EXPIRED_INTERVAL "refresh_expired_interval"
 #define CONFDB_DOMAIN_OFFLINE_TIMEOUT "offline_timeout"
 #define CONFDB_DOMAIN_SUBDOMAIN_INHERIT "subdomain_inherit"
+#define CONFDB_DOMAIN_CACHED_AUTH_TIMEOUT "cached_auth_timeout"
 
 /* Local Provider */
 #define CONFDB_LOCAL_DEFAULT_SHELL   "default_shell"
@@ -248,6 +249,7 @@ struct sss_domain_info {
 
     uint32_t refresh_expired_interval;
     uint32_t subdomain_refresh_interval;
+    uint32_t cached_auth_timeout;
 
     int pwd_expiration_warning;
 
diff --git a/src/config/SSSDConfig/__init__.py.in b/src/config/SSSDConfig/__init__.py.in
index f2d9bf0190745d4a5296c56212e5ff97681db59c..4d45e42af4fa32717caa69cc621834cdffc27431 100644
--- a/src/config/SSSDConfig/__init__.py.in
+++ b/src/config/SSSDConfig/__init__.py.in
@@ -149,6 +149,7 @@ option_strings = {
     'subdomain_enumerate' : _('Control enumeration of trusted domains'),
     'subdomain_refresh_interval' : _('How often should subdomains list be refreshed'),
     'subdomain_inherit' : _('List of options that should be inherited into a subdomain'),
+    'cached_auth_timeout' : _('How long can cached credentials be used for cached authentication'),
 
     # [provider/ipa]
     'ipa_domain' : _('IPA domain'),
diff --git a/src/config/SSSDConfigTest.py b/src/config/SSSDConfigTest.py
index c6ba9f05106990d9e43df106305babb2dafdeea5..1d6107ceac1bde7acbfd2682cc144a4ef0881311 100755
--- a/src/config/SSSDConfigTest.py
+++ b/src/config/SSSDConfigTest.py
@@ -547,7 +547,8 @@ class SSSDConfigTestSSSDDomain(unittest.TestCase):
             'subdomains_provider',
             'realmd_tags',
             'subdomain_refresh_interval',
-            'subdomain_inherit']
+            'subdomain_inherit',
+            'cached_auth_timeout']
 
         self.assertTrue(type(options) == dict,
                         "Options should be a dictionary")
@@ -910,7 +911,8 @@ class SSSDConfigTestSSSDDomain(unittest.TestCase):
             'subdomains_provider',
             'realmd_tags',
             'subdomain_refresh_interval',
-            'subdomain_inherit']
+            'subdomain_inherit',
+            'cached_auth_timeout']
 
         self.assertTrue(type(options) == dict,
                         "Options should be a dictionary")
diff --git a/src/config/etc/sssd.api.conf b/src/config/etc/sssd.api.conf
index 7ad84cd826a648ca61d9d2ede70e7886049d469a..29fd896ccd7a3aa5ff81e15b771746a80ffc01af 100644
--- a/src/config/etc/sssd.api.conf
+++ b/src/config/etc/sssd.api.conf
@@ -133,6 +133,7 @@ description = str, None, false
 realmd_tags = str, None, false
 subdomain_refresh_interval = int, None, false
 subdomain_inherit = str, None, false
+cached_auth_timeout = int, None, false
 
 #Entry cache timeouts
 entry_cache_user_timeout = int, None, false
diff --git a/src/man/sssd.conf.5.xml b/src/man/sssd.conf.5.xml
index 75d13a631e23c0c9518e05f48d66209c8c47d237..7d3a57b0eadec48d64cc9ddcb226b89a1600b432 100644
--- a/src/man/sssd.conf.5.xml
+++ b/src/man/sssd.conf.5.xml
@@ -2176,6 +2176,30 @@ pam_account_expired_message = Account expired, please call help desk.
                         </para>
                     </listitem>
                 </varlistentry>
+                <varlistentry>
+                    <term>cached_auth_timeout (int)</term>
+                    <listitem>
+                        <para>
+                            Specifies time in seconds since last successful
+                            online authentication for which user will be
+                            authenticated using cached credentials while
+                            SSSD is in the online mode.
+                        </para>
+                        <para>
+                            Special value 0 implies that this feature is
+                            disabled.
+                        </para>
+                        <para>
+                            Please note that if <quote>cached_auth_timeout</quote>
+                            is longer than <quote>pam_id_timeout</quote> then the
+                            back end could be called to handle
+                            <quote>initgroups.</quote>
+                        </para>
+                        <para>
+                            Default: 0
+                        </para>
+                    </listitem>
+                </varlistentry>
             </variablelist>
         </para>
 
diff --git a/src/responder/pam/pamsrv.h b/src/responder/pam/pamsrv.h
index 066f35a428a9af81d665309b4ab5a80cf69561ba..0278006467a41b28a1d47a8777074f265c5c478e 100644
--- a/src/responder/pam/pamsrv.h
+++ b/src/responder/pam/pamsrv.h
@@ -60,6 +60,9 @@ struct pam_auth_req {
     bool is_uid_trusted;
     bool check_provider;
     void *data;
+    bool use_cached_auth;
+    /* whether cached authentication was tried and failed */
+    bool cached_auth_failed;
 
     struct pam_auth_dp_req *dpreq_spy;
 };
diff --git a/src/responder/pam/pamsrv_cmd.c b/src/responder/pam/pamsrv_cmd.c
index 3bd67639585e83bb0945e7e4750553370c6566e0..c144406aaf2f1ef926f8b1241b42a360764177f8 100644
--- a/src/responder/pam/pamsrv_cmd.c
+++ b/src/responder/pam/pamsrv_cmd.c
@@ -45,6 +45,10 @@ enum pam_verbosity {
 static errno_t
 pam_null_last_online_auth_with_curr_token(struct sss_domain_info *domain,
                                           const char *username);
+static errno_t
+pam_get_last_online_auth_with_curr_token(struct sss_domain_info *domain,
+                                         const char *name,
+                                         uint64_t *_value);
 static void pam_reply(struct pam_auth_req *preq);
 
 static errno_t pack_user_info_account_expired(TALLOC_CTX *mem_ctx,
@@ -568,7 +572,7 @@ static errno_t get_password_for_cache_auth(struct sss_auth_token *authtok,
 
 static int pam_forwarder(struct cli_ctx *cctx, int pam_cmd);
 static void pam_handle_cached_login(struct pam_auth_req *preq, int ret,
-                                    time_t expire_date, time_t delayed_until);
+                                    time_t expire_date, time_t delayed_until, bool cached_auth);
 
 static void pam_reply(struct pam_auth_req *preq)
 {
@@ -606,14 +610,21 @@ static void pam_reply(struct pam_auth_req *preq)
     DEBUG(SSSDBG_FUNC_DATA,
           "pam_reply called with result [%d]: %s.\n",
           pd->pam_status, pam_strerror(NULL, pd->pam_status));
+    if (pd->pam_status == PAM_AUTHINFO_UNAVAIL || preq->use_cached_auth) {
 
-    if (pd->pam_status == PAM_AUTHINFO_UNAVAIL) {
         switch(pd->cmd) {
         case SSS_PAM_AUTHENTICATE:
             if ((preq->domain != NULL) &&
                 (preq->domain->cache_credentials == true) &&
                 (pd->offline_auth == false)) {
                 const char *password = NULL;
+                bool use_cached_auth;
+
+                /* backup value of preq->use_cached_auth*/
+                use_cached_auth = preq->use_cached_auth;
+                /* set to false to avoid entering this branch when pam_reply()
+                 * is recursively called from pam_handle_cached_login() */
+                preq->use_cached_auth = false;
 
                 /* do auth with offline credentials */
                 pd->offline_auth = true;
@@ -637,7 +648,8 @@ static void pam_reply(struct pam_auth_req *preq)
                                        pctx->rctx->cdb, false,
                                        &exp_date, &delay_until);
 
-                pam_handle_cached_login(preq, ret, exp_date, delay_until);
+                pam_handle_cached_login(preq, ret, exp_date, delay_until,
+                                        use_cached_auth);
                 return;
             }
             break;
@@ -805,8 +817,11 @@ done:
     sss_cmd_done(cctx, preq);
 }
 
+static void pam_dom_forwarder(struct pam_auth_req *preq);
+
 static void pam_handle_cached_login(struct pam_auth_req *preq, int ret,
-                                    time_t expire_date, time_t delayed_until)
+                                    time_t expire_date, time_t delayed_until,
+                                    bool use_cached_auth)
 {
     uint32_t resp_type;
     size_t resp_len;
@@ -855,6 +870,18 @@ static void pam_handle_cached_login(struct pam_auth_req *preq, int ret,
                 }
             }
             break;
+        case PAM_AUTH_ERR:
+            /* Was this attempt to authenticate from cache? */
+            if (use_cached_auth) {
+                /* Don't try cached authentication again, try online check. */
+                DEBUG(SSSDBG_FUNC_DATA,
+                      "Cached authentication failed for: %s\n",
+                      preq->pd->user);
+                preq->cached_auth_failed = true;
+                pam_dom_forwarder(preq);
+                return;
+            }
+            break;
         default:
             DEBUG(SSSDBG_TRACE_LIBS,
                   "cached login returned: %d\n", preq->pd->pam_status);
@@ -869,7 +896,6 @@ static void pam_check_user_dp_callback(uint16_t err_maj, uint32_t err_min,
                                        const char *err_msg, void *ptr);
 static int pam_check_user_search(struct pam_auth_req *preq);
 static int pam_check_user_done(struct pam_auth_req *preq, int ret);
-static void pam_dom_forwarder(struct pam_auth_req *preq);
 
 /* TODO: we should probably return some sort of cookie that is set in the
  * PAM_ENVIRONMENT, so that we can save performing some calls and cache
@@ -1423,6 +1449,76 @@ static void pam_check_user_dp_callback(uint16_t err_maj, uint32_t err_min,
     }
 }
 
+static errno_t pam_is_last_online_login_fresh(struct sss_domain_info *domain,
+                                              const char* user,
+                                              struct confdb_ctx *cdb,
+                                              int cached_auth_timeout,
+                                              bool *_result)
+{
+    errno_t ret;
+    bool result;
+    uint64_t last_login;
+
+    ret = pam_get_last_online_auth_with_curr_token(domain, user, &last_login);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_MINOR_FAILURE,
+              "sysdb_get_last_online_auth_with_curr_token failed: %s:[%d]\n",
+              sss_strerror(ret), ret);
+        goto done;
+    }
+
+    result = time(NULL) < (last_login + cached_auth_timeout);
+    ret = EOK;
+
+done:
+    if (ret == EOK) {
+        *_result = result;
+    }
+    return ret;
+}
+
+static bool pam_is_cmd_cachable(int cmd)
+{
+    bool is_cachable;
+
+    switch(cmd) {
+    case SSS_PAM_AUTHENTICATE:
+        is_cachable = true;
+        break;
+    default:
+        is_cachable = false;
+    }
+
+    return is_cachable;
+}
+
+static bool pam_can_user_cache_auth(struct confdb_ctx *cdb,
+                                    struct sss_domain_info *domain,
+                                    int pam_cmd, const char* user,
+                                    bool cached_auth_failed)
+{
+    errno_t ret;
+    bool result = false;
+
+    if (!cached_auth_failed /* don't try cached auth again */
+            && domain->cache_credentials
+            && domain->cached_auth_timeout > 0
+            && pam_is_cmd_cachable(pam_cmd)) {
+
+        ret = pam_is_last_online_login_fresh(domain, user, cdb,
+                                             domain->cached_auth_timeout,
+                                             &result);
+        if (ret != EOK) {
+            /* non-critical, consider fail as 'non-fresh value' */
+            DEBUG(SSSDBG_MINOR_FAILURE,
+                  "pam_is_last_online_login_fresh failed: %s:[%d]\n",
+                  sss_strerror(ret), ret);
+        }
+    }
+
+    return result;
+}
+
 static void pam_dom_forwarder(struct pam_auth_req *preq)
 {
     int ret;
@@ -1454,7 +1550,17 @@ static void pam_dom_forwarder(struct pam_auth_req *preq)
         return;
     }
 
-    if (!NEED_CHECK_PROVIDER(preq->domain->provider)) {
+    if (pam_can_user_cache_auth(pctx->rctx->cdb,
+                                preq->domain,
+                                preq->pd->cmd,
+                                preq->pd->user,
+                                preq->cached_auth_failed)) {
+        preq->use_cached_auth = true;
+        pam_reply(preq);
+        return;
+    }
+
+    if (!NEED_CHECK_PROVIDER(preq->domain->provider) ) {
         preq->callback = pam_reply;
         ret = LOCAL_pam_handler(preq);
     }
@@ -1585,3 +1691,55 @@ pam_null_last_online_auth_with_curr_token(struct sss_domain_info *domain,
 {
     return pam_set_last_online_auth_with_curr_token(domain, username, 0);
 }
+
+static errno_t
+pam_get_last_online_auth_with_curr_token(struct sss_domain_info *domain,
+                                         const char *name,
+                                         uint64_t *_value)
+{
+    TALLOC_CTX *tmp_ctx = NULL;
+    const char *attrs[] = { SYSDB_LAST_ONLINE_AUTH_WITH_CURR_TOKEN, NULL };
+    struct ldb_message *ldb_msg;
+    uint64_t value;
+    errno_t ret;
+
+    if (name == NULL || *name == '\0') {
+        DEBUG(SSSDBG_CRIT_FAILURE, "Missing user name.\n");
+        ret = EINVAL;
+        goto done;
+    }
+
+    if (domain->sysdb == NULL) {
+        DEBUG(SSSDBG_CRIT_FAILURE, "Missing sysdb db context.\n");
+        ret = EINVAL;
+        goto done;
+    }
+
+    tmp_ctx = talloc_new(NULL);
+    if (tmp_ctx == NULL) {
+        ret = ENOMEM;
+        goto done;
+    }
+
+    ret = sysdb_search_user_by_name(tmp_ctx, domain, name, attrs, &ldb_msg);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_CRIT_FAILURE,
+              "sysdb_search_user_by_name failed [%d][%s].\n",
+              ret, strerror(ret));
+        goto done;
+    }
+
+    /* Check offline_auth_cache_timeout */
+    value = ldb_msg_find_attr_as_uint64(ldb_msg,
+                                        SYSDB_LAST_ONLINE_AUTH_WITH_CURR_TOKEN,
+                                        0);
+    ret = EOK;
+
+done:
+    if (ret == EOK) {
+        *_value = value;
+    }
+
+    talloc_free(tmp_ctx);
+    return ret;
+}
-- 
2.1.0

