>From d2b8eeaea62a756e98f50d47372453d79380fab2 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/2] 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                  |  49 +++++++++++++
 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/db/sysdb_ops.c                   |   1 -
 src/man/sssd.conf.5.xml              |  14 ++++
 src/responder/pam/pamsrv.h           |   3 +
 src/responder/pam/pamsrv_cmd.c       | 134 +++++++++++++++++++++++++++++++++--
 9 files changed, 201 insertions(+), 10 deletions(-)

diff --git a/src/confdb/confdb.c b/src/confdb/confdb.c
index 87960c4db6e698ee5c3b2a24ca18f8d53458c7ca..5401594b248f74da2f90f8e8e4e4bc196ccb0bd9 100644
--- a/src/confdb/confdb.c
+++ b/src/confdb/confdb.c
@@ -760,6 +760,46 @@ 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;
+    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;
+    }
+
+    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,
@@ -1264,6 +1304,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 c8c91288cd63df7629a98802b7b5373df92d6ca4..bfd424676d6af896918e836573e6e3a2a46af687 100644
--- a/src/confdb/confdb.h
+++ b/src/confdb/confdb.h
@@ -187,6 +187,7 @@
 #define CONFDB_DOMAIN_PWD_EXPIRATION_WARNING "pwd_expiration_warning"
 #define CONFDB_DOMAIN_REFRESH_EXPIRED_INTERVAL "refresh_expired_interval"
 #define CONFDB_DOMAIN_OFFLINE_TIMEOUT "offline_timeout"
+#define CONFDB_DOMAIN_CACHED_AUTH_TIMEOUT "cached_auth_timeout"
 
 /* Local Provider */
 #define CONFDB_LOCAL_DEFAULT_SHELL   "default_shell"
@@ -247,6 +248,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 f58c52faf7dd3f9199bd0af4286546d4fe804a88..094fffb26206cb25df73fd63d5b2ba3f6a942851 100644
--- a/src/config/SSSDConfig/__init__.py.in
+++ b/src/config/SSSDConfig/__init__.py.in
@@ -148,6 +148,7 @@ option_strings = {
     'dyndns_auth' : _("What kind of authentication should be used to perform the DNS update"),
     'subdomain_enumerate' : _('Control enumeration of trusted domains'),
     'subdomain_refresh_interval' : _('How often should subdomains list be refreshed'),
+    'cached_auth_timeout' : _('How long can be cached credentials used for cached authentication'),
 
     # [provider/ipa]
     'ipa_domain' : _('IPA domain'),
diff --git a/src/config/SSSDConfigTest.py b/src/config/SSSDConfigTest.py
index 60f51ca769503be42ba9c82cdb07cf555407d61b..1582ac53efaaf30b9a827d8bbbe2b6931cfc91ff 100755
--- a/src/config/SSSDConfigTest.py
+++ b/src/config/SSSDConfigTest.py
@@ -546,7 +546,8 @@ class SSSDConfigTestSSSDDomain(unittest.TestCase):
             'hostid_provider',
             'subdomains_provider',
             'realmd_tags',
-            'subdomain_refresh_interval']
+            'subdomain_refresh_interval',
+            'cached_auth_timeout']
 
         self.assertTrue(type(options) == dict,
                         "Options should be a dictionary")
@@ -908,7 +909,8 @@ class SSSDConfigTestSSSDDomain(unittest.TestCase):
             'hostid_provider',
             'subdomains_provider',
             'realmd_tags',
-            'subdomain_refresh_interval']
+            'subdomain_refresh_interval',
+            '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 f1ac6366c73c03fe5c60c79bfe7b15fde9382215..b587a56b8df77834a1ac1499b5b294440c3e0e79 100644
--- a/src/config/etc/sssd.api.conf
+++ b/src/config/etc/sssd.api.conf
@@ -132,6 +132,7 @@ default_shell = str, None, false
 description = str, None, false
 realmd_tags = str, None, false
 subdomain_refresh_interval = int, None, false
+cached_auth_timeout = int, None, false
 
 #Entry cache timeouts
 entry_cache_user_timeout = int, None, false
diff --git a/src/db/sysdb_ops.c b/src/db/sysdb_ops.c
index d90acd615f14091c6597e246569fb59d88a99f6f..ef88f257bedc08d56aadec3f80738e67269705d7 100644
--- a/src/db/sysdb_ops.c
+++ b/src/db/sysdb_ops.c
@@ -3470,7 +3470,6 @@ int sysdb_cache_auth(struct sss_domain_info *domain,
             goto done;
         }
 
-
     } else {
         DEBUG(SSSDBG_CONF_SETTINGS, "Authentication failed.\n");
         authentication_successful = false;
diff --git a/src/man/sssd.conf.5.xml b/src/man/sssd.conf.5.xml
index b5782b7942939fd18fb2e719e0c3e8c31ae25621..f0bc720a6c10e442f7c57b49e1a37947a9adf232 100644
--- a/src/man/sssd.conf.5.xml
+++ b/src/man/sssd.conf.5.xml
@@ -957,6 +957,20 @@ 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>
+                            Default: 0
+                        </para>
+                    </listitem>
+                </varlistentry>
 
             </variablelist>
         </refsect2>
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 ae571fc969bfaf8c9ec90023ca1f4f38d31211e2..833fd096f64a7db1ad580f72cbd5022f77267b33 100644
--- a/src/responder/pam/pamsrv_cmd.c
+++ b/src/responder/pam/pamsrv_cmd.c
@@ -565,7 +565,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)
 {
@@ -603,14 +603,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;
@@ -634,7 +641,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;
@@ -802,8 +810,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;
@@ -852,6 +863,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);
@@ -866,7 +889,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
@@ -1004,7 +1026,6 @@ static int pam_forwarder(struct cli_ctx *cctx, int pam_cmd)
               cctx->client_euid);
     }
 
-
     pd->cmd = pam_cmd;
     pd->priv = cctx->priv;
 
@@ -1420,9 +1441,91 @@ 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 = sysdb_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 errno_t pam_can_user_cache_auth(struct confdb_ctx *cdb,
+                                       struct sss_domain_info *domain,
+                                       int pam_cmd, const char* user,
+                                       bool cached_auth_failed,
+                                       bool *_result)
+{
+    errno_t ret;
+    bool result;
+
+    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) {
+            DEBUG(SSSDBG_MINOR_FAILURE,
+                  "pam_is_last_online_login_fresh failed: %s:[%d]\n",
+                  sss_strerror(ret), ret);
+            goto done;
+        }
+    } else {
+        result = false;
+    }
+
+    ret = EOK;
+
+done:
+    if (ret == EOK) {
+        *_result = result;
+    }
+
+    return ret;
+}
+
 static void pam_dom_forwarder(struct pam_auth_req *preq)
 {
     int ret;
+    bool can_user_cache_auth;
     struct pam_ctx *pctx =
             talloc_get_type(preq->cctx->rctx->pvt_ctx, struct pam_ctx);
 
@@ -1451,7 +1554,24 @@ static void pam_dom_forwarder(struct pam_auth_req *preq)
         return;
     }
 
-    if (!NEED_CHECK_PROVIDER(preq->domain->provider)) {
+    ret = pam_can_user_cache_auth(pctx->rctx->cdb, preq->domain,
+                                  preq->pd->cmd, preq->pd->user,
+                                  preq->cached_auth_failed,
+                                  &can_user_cache_auth);
+    if (ret == EOK) {
+        if (can_user_cache_auth) {
+            preq->use_cached_auth = true;
+            pam_reply(preq);
+            return;
+        }
+    } else {
+        /* non fatal */
+        DEBUG(SSSDBG_MINOR_FAILURE,
+              "pam_can_user_cache_auth failed: %s:[%d]\n",
+              sss_strerror(ret), ret);
+    }
+
+    if (!NEED_CHECK_PROVIDER(preq->domain->provider) ) {
         preq->callback = pam_reply;
         ret = LOCAL_pam_handler(preq);
     }
-- 
2.1.0

