From 93ee0159a0f467ced3412d034ec706dd3508901e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20L=C3=B3pez?= <allopez@redhat.com>
Date: Tue, 3 Oct 2023 12:39:49 +0200
Subject: [PATCH] KCM: Remove the oldest expired credential if no more space.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

:feature: When adding a new credential to KCM and the user has
          already reached their limit, the oldest expired credential
          will be removed to free some space.
          If no expired credential is found to be removed, the operation
          will fail as it happened in the previous versions.

Resolves: https://github.com/SSSD/sssd/issues/6667

Reviewed-by: Sumit Bose <sbose@redhat.com>
Reviewed-by: Tomáš Halman <thalman@redhat.com>

Reference: https://github.com/SSSD/sssd/commit/93ee0159a0f467ced3412d034ec706dd3508901e 
Conflict: NA
---
 src/responder/kcm/secrets/secrets.c | 203 +++++++++++++++++++++++++---
 1 file changed, 186 insertions(+), 17 deletions(-)

diff --git a/src/responder/kcm/secrets/secrets.c b/src/responder/kcm/secrets/secrets.c
index 025d1c421..4dc748c3b 100644
--- a/src/responder/kcm/secrets/secrets.c
+++ b/src/responder/kcm/secrets/secrets.c
@@ -18,15 +18,18 @@
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
+#include "config.h"
 
+#include <fcntl.h>
 #include <sys/types.h>
 #include <sys/stat.h>
-#include <fcntl.h>
+#include <time.h>
 #include <uuid/uuid.h>
 
-#include "config.h"
-
+#include "responder/kcm/kcmsrv_ccache.h"
 #include "util/util.h"
+#include "util/util_creds.h"
+#include "util/sss_iobuf.h"
 #include "util/strtonum.h"
 #include "util/crypto/sss_crypto.h"
 #include "sec_pvt.h"
@@ -50,6 +53,10 @@ static struct sss_sec_quota default_kcm_quota = {
     .containers_nest_level = DEFAULT_SEC_CONTAINERS_NEST_LEVEL,
 };
 
+static char *local_dn_to_path(TALLOC_CTX *mem_ctx,
+                              struct ldb_dn *basedn,
+                              struct ldb_dn *dn);
+
 static int local_db_check_containers(TALLOC_CTX *mem_ctx,
                                      struct sss_sec_ctx *sec_ctx,
                                      struct ldb_dn *leaf_dn)
@@ -181,11 +188,166 @@ static struct ldb_dn *per_uid_container(TALLOC_CTX *mem_ctx,
     return uid_base_dn;
 }
 
+static errno_t get_secret_expiration_time(uint8_t *key, size_t key_length,
+                                          uint8_t *sec, size_t sec_length,
+                                          time_t *_expiration)
+{
+    errno_t ret;
+    TALLOC_CTX *tmp_ctx;
+    time_t expiration = 0;
+    struct cli_creds client = {};
+    struct kcm_ccache *cc;
+    struct sss_iobuf *iobuf;
+    krb5_creds **cred_list, **cred;
+    const char *key_str;
+
+    if (_expiration == NULL) {
+        return EINVAL;
+    }
+
+    tmp_ctx = talloc_new(NULL);
+    if (tmp_ctx == NULL) {
+        return ENOMEM;
+    }
+
+    key_str = talloc_strndup(tmp_ctx, (const char *) key, key_length);
+    if (key_str == NULL) {
+        ret = ENOMEM;
+        goto done;
+    }
+
+    iobuf = sss_iobuf_init_readonly(tmp_ctx, sec, sec_length);
+    if (iobuf == NULL) {
+        ret = ENOMEM;
+        goto done;
+    }
+
+    ret = sec_kv_to_ccache_binary(tmp_ctx, key_str, iobuf, &client, &cc);
+    if (ret != EOK) {
+        goto done;
+    }
+
+    cred_list = kcm_cc_unmarshal(tmp_ctx, NULL, cc);
+    if (cred_list == NULL) {
+        ret = ENOMEM;
+        goto done;
+    }
+
+    for (cred = cred_list; *cred != NULL; cred++) {
+        if ((*cred)->times.endtime != 0) {
+            expiration = (time_t) (*cred)->times.endtime;
+            break;
+        }
+    }
+
+    *_expiration = expiration;
+    ret = EOK;
+
+done:
+    talloc_free(tmp_ctx);
+    return ret;
+}
+
+static errno_t local_db_remove_oldest_expired_secret(struct ldb_result *res,
+                                                     struct sss_sec_req *req)
+{
+    struct sss_sec_req *new_req = NULL;
+    const struct ldb_val *val;
+    const struct ldb_val *rdn;
+    struct ldb_message *msg;
+    struct ldb_message_element *elem;
+    struct ldb_dn *basedn;
+    struct ldb_dn *oldest_dn = NULL;
+    time_t oldest_time = time(NULL);
+    time_t expiration;
+    unsigned int i;
+    int ret;
+
+    DEBUG(SSSDBG_TRACE_INTERNAL, "Removing the oldest expired credential\n");
+    /* Between all the messages in result, there is also the key we are
+     * currently treating, but because yet it doesn't have an expiration time,
+     * it will be skipped.
+     */
+    for (i = 0; i < res->count; i++) {
+        msg = res->msgs[i];
+
+        /* Skip cn=default,... or any non cn=... */
+        rdn = ldb_dn_get_rdn_val(msg->dn);
+        if (strcmp(ldb_dn_get_rdn_name(msg->dn), "cn") != 0
+            || strncmp("default", (char *) rdn->data, rdn->length) == 0) {
+            continue;
+        }
+
+        elem = ldb_msg_find_element(msg, SEC_ATTR_SECRET);
+        if (elem != NULL) {
+            if (elem->num_values != 1) {
+                DEBUG(SSSDBG_MINOR_FAILURE,
+                      "Element %s has %u values. Ignoring it.\n",
+                      SEC_ATTR_SECRET, elem->num_values);
+                ret = ERR_MALFORMED_ENTRY;
+                goto done;
+            }
+
+            val = &elem->values[0];
+            ret = get_secret_expiration_time(rdn->data, rdn->length,
+                                             val->data, val->length,
+                                             &expiration);
+            if (ret != EOK) {
+                goto done;
+            }
+            if (expiration > 0 && expiration < oldest_time) {
+                oldest_dn = msg->dn;
+                oldest_time = expiration;
+            }
+        }
+    }
+
+    if (oldest_dn == NULL) {
+        DEBUG(SSSDBG_TRACE_INTERNAL, "Found no expired credential to remove\n");
+        ret = ERR_NO_MATCHING_CREDS;
+        goto done;
+    }
+
+    new_req = talloc_zero(NULL, struct sss_sec_req);
+    if (new_req == NULL) {
+        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to allocate the new request\n");
+        ret = ENOMEM;
+        goto done;
+    }
+
+    basedn = ldb_dn_new(new_req, req->sctx->ldb, req->basedn);
+    if (basedn == NULL) {
+        DEBUG(SSSDBG_OP_FAILURE, "Failed to create a dn: %s\n", req->basedn);
+        ret = EINVAL;
+        goto done;
+    }
+
+    new_req->basedn = req->basedn;
+    new_req->quota = req->quota;
+    new_req->req_dn = oldest_dn;
+    new_req->sctx = req->sctx;
+    new_req->path = local_dn_to_path(new_req, basedn, oldest_dn);
+    if (new_req->path == NULL) {
+        DEBUG(SSSDBG_OP_FAILURE, "Failed to create the path\n");
+        ret = EINVAL;
+        goto done;
+    }
+
+    ret = sss_sec_delete(new_req);
+
+done:
+    if (new_req != NULL)
+        talloc_free(new_req);
+
+    return ret;
+}
+
+
 static int local_db_check_peruid_number_of_secrets(TALLOC_CTX *mem_ctx,
                                                    struct sss_sec_req *req)
 {
     TALLOC_CTX *tmp_ctx;
-    static const char *attrs[] = { NULL };
+    static const char *attrs[] = { SEC_ATTR_SECRET, NULL };
     struct ldb_result *res = NULL;
     struct ldb_dn *cli_basedn = NULL;
     int ret;
@@ -214,13 +376,20 @@ static int local_db_check_peruid_number_of_secrets(TALLOC_CTX *mem_ctx,
     }
 
     if (res->count >= req->quota->max_uid_secrets) {
-        DEBUG(SSSDBG_OP_FAILURE,
-              "Cannot store any more secrets for this client (basedn %s) "
-              "as the maximum allowed limit (%d) has been reached\n",
-              ldb_dn_get_linearized(cli_basedn),
-              req->quota->max_uid_secrets);
-        ret = ERR_SEC_INVALID_TOO_MANY_SECRETS;
-        goto done;
+        /* We reached the limit. Let's try to removed the
+         * oldest expired credential to free some space. */
+        ret = local_db_remove_oldest_expired_secret(res, req);
+        if (ret != EOK) {
+            if (ret == ERR_NO_MATCHING_CREDS) {
+                DEBUG(SSSDBG_OP_FAILURE,
+                      "Cannot store any more secrets for this client (basedn %s) "
+                      "as the maximum allowed limit (%d) has been reached\n",
+                      ldb_dn_get_linearized(cli_basedn),
+                      req->quota->max_uid_secrets);
+                ret = ERR_SEC_INVALID_TOO_MANY_SECRETS;
+            }
+            goto done;
+        }
     }
 
     ret = EOK;
@@ -808,15 +977,15 @@ errno_t sss_sec_put(struct sss_sec_req *req,
         goto done;
     }
 
-    ret = local_db_check_number_of_secrets(msg, req);
+    ret = local_db_check_peruid_number_of_secrets(msg, req);
     if (ret != EOK) {
         DEBUG(SSSDBG_OP_FAILURE,
-              "local_db_check_number_of_secrets failed [%d]: %s\n",
+              "local_db_check_peruid_number_of_secrets failed [%d]: %s\n",
               ret, sss_strerror(ret));
         goto done;
     }
 
-    ret = local_db_check_peruid_number_of_secrets(msg, req);
+    ret = local_db_check_number_of_secrets(msg, req);
     if (ret != EOK) {
         DEBUG(SSSDBG_OP_FAILURE,
               "local_db_check_number_of_secrets failed [%d]: %s\n",
@@ -905,15 +1074,15 @@ errno_t sss_sec_update(struct sss_sec_req *req,
         goto done;
     }
 
-    ret = local_db_check_number_of_secrets(msg, req);
+    ret = local_db_check_peruid_number_of_secrets(msg, req);
     if (ret != EOK) {
         DEBUG(SSSDBG_OP_FAILURE,
-              "local_db_check_number_of_secrets failed [%d]: %s\n",
+              "local_db_check_peruid_number_of_secrets failed [%d]: %s\n",
               ret, sss_strerror(ret));
         goto done;
     }
 
-    ret = local_db_check_peruid_number_of_secrets(msg, req);
+    ret = local_db_check_number_of_secrets(msg, req);
     if (ret != EOK) {
         DEBUG(SSSDBG_OP_FAILURE,
               "local_db_check_number_of_secrets failed [%d]: %s\n",
-- 
2.33.0

