sssd/0037-KCM-Queue-requests-by-...

910 lines
28 KiB
Diff

From 2b5518eeaacc6245cfa77ee4a7086f16208060fc Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhrozek@redhat.com>
Date: Tue, 21 Mar 2017 13:25:11 +0100
Subject: [PATCH 37/97] KCM: Queue requests by the same UID
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
In order to avoid race conditions, we queue requests towards the KCM
responder coming from the same client UID.
Reviewed-by: Michal Židek <mzidek@redhat.com>
Reviewed-by: Simo Sorce <simo@redhat.com>
Reviewed-by: Lukáš Slebodník <lslebodn@redhat.com>
---
Makefile.am | 21 ++-
src/responder/kcm/kcm.c | 7 +
src/responder/kcm/kcmsrv_cmd.c | 10 +-
src/responder/kcm/kcmsrv_op_queue.c | 264 ++++++++++++++++++++++++++
src/responder/kcm/kcmsrv_ops.c | 44 ++++-
src/responder/kcm/kcmsrv_ops.h | 1 +
src/responder/kcm/kcmsrv_pvt.h | 20 ++
src/tests/cmocka/test_kcm_queue.c | 365 ++++++++++++++++++++++++++++++++++++
8 files changed, 721 insertions(+), 11 deletions(-)
create mode 100644 src/responder/kcm/kcmsrv_op_queue.c
create mode 100644 src/tests/cmocka/test_kcm_queue.c
diff --git a/Makefile.am b/Makefile.am
index e9eaa312c91e3aee40bcf13c90a0ad8c683045d5..91afdd669aa11a3cc316588d3b51d7e8e9c91cb8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -304,7 +304,10 @@ non_interactive_cmocka_based_tests += test_inotify
endif # HAVE_INOTIFY
if BUILD_KCM
-non_interactive_cmocka_based_tests += test_kcm_json
+non_interactive_cmocka_based_tests += \
+ test_kcm_json \
+ test_kcm_queue \
+ $(NULL)
endif # BUILD_KCM
if BUILD_SAMBA
@@ -1501,6 +1504,7 @@ sssd_kcm_SOURCES = \
src/responder/kcm/kcmsrv_ccache_json.c \
src/responder/kcm/kcmsrv_ccache_secrets.c \
src/responder/kcm/kcmsrv_ops.c \
+ src/responder/kcm/kcmsrv_op_queue.c \
src/util/sss_sockets.c \
src/util/sss_krb5.c \
src/util/sss_iobuf.c \
@@ -3402,6 +3406,21 @@ test_kcm_json_LDADD = \
$(SSSD_INTERNAL_LTLIBS) \
libsss_test_common.la \
$(NULL)
+
+test_kcm_queue_SOURCES = \
+ src/tests/cmocka/test_kcm_queue.c \
+ src/responder/kcm/kcmsrv_op_queue.c \
+ $(NULL)
+test_kcm_queue_CFLAGS = \
+ $(AM_CFLAGS) \
+ $(NULL)
+test_kcm_queue_LDADD = \
+ $(CMOCKA_LIBS) \
+ $(SSSD_LIBS) \
+ $(SSSD_INTERNAL_LTLIBS) \
+ libsss_test_common.la \
+ $(NULL)
+
endif # BUILD_KCM
endif # HAVE_CMOCKA
diff --git a/src/responder/kcm/kcm.c b/src/responder/kcm/kcm.c
index 063c27b915b4b92f6259496feee891aa94a498b6..3ee978066c589a5cc38b0ae358f741d389d00e7a 100644
--- a/src/responder/kcm/kcm.c
+++ b/src/responder/kcm/kcm.c
@@ -133,6 +133,13 @@ static int kcm_get_config(struct kcm_ctx *kctx)
goto done;
}
+ kctx->qctx = kcm_ops_queue_create(kctx);
+ if (ret != EOK) {
+ DEBUG(SSSDBG_OP_FAILURE,
+ "Cannot create KCM request queue [%d]: %s\n",
+ ret, strerror(ret));
+ goto done;
+ }
ret = EOK;
done:
return ret;
diff --git a/src/responder/kcm/kcmsrv_cmd.c b/src/responder/kcm/kcmsrv_cmd.c
index 537e88953fd1a190a9a73bcdd430d8e0db8f9291..81015de4a91617de3dca444cde95b636c8d5c0d1 100644
--- a/src/responder/kcm/kcmsrv_cmd.c
+++ b/src/responder/kcm/kcmsrv_cmd.c
@@ -353,14 +353,18 @@ struct kcm_req_ctx {
static void kcm_cmd_request_done(struct tevent_req *req);
-static errno_t kcm_cmd_dispatch(struct kcm_req_ctx *req_ctx)
+static errno_t kcm_cmd_dispatch(struct kcm_ctx *kctx,
+ struct kcm_req_ctx *req_ctx)
{
struct tevent_req *req;
struct cli_ctx *cctx;
cctx = req_ctx->cctx;
- req = kcm_cmd_send(req_ctx, cctx->ev, req_ctx->kctx->kcm_data,
+ req = kcm_cmd_send(req_ctx,
+ cctx->ev,
+ kctx->qctx,
+ req_ctx->kctx->kcm_data,
req_ctx->cctx->creds,
&req_ctx->op_io.request,
req_ctx->op_io.op);
@@ -505,7 +509,7 @@ static void kcm_recv(struct cli_ctx *cctx)
/* do not read anymore, client is done sending */
TEVENT_FD_NOT_READABLE(cctx->cfde);
- ret = kcm_cmd_dispatch(req);
+ ret = kcm_cmd_dispatch(kctx, req);
if (ret != EOK) {
DEBUG(SSSDBG_FATAL_FAILURE,
"Failed to dispatch KCM operation [%d]: %s\n",
diff --git a/src/responder/kcm/kcmsrv_op_queue.c b/src/responder/kcm/kcmsrv_op_queue.c
new file mode 100644
index 0000000000000000000000000000000000000000..f6c425dd5b64877c8b7401e488dd6565157fc9b5
--- /dev/null
+++ b/src/responder/kcm/kcmsrv_op_queue.c
@@ -0,0 +1,264 @@
+/*
+ SSSD
+
+ KCM Server - the KCM operations wait queue
+
+ Copyright (C) Red Hat, 2017
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ 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 "util/util.h"
+#include "util/util_creds.h"
+#include "responder/kcm/kcmsrv_pvt.h"
+
+#define QUEUE_HASH_SIZE 32
+
+struct kcm_ops_queue_entry {
+ struct tevent_req *req;
+ uid_t uid;
+
+ hash_table_t *wait_queue_hash;
+
+ struct kcm_ops_queue_entry *head;
+ struct kcm_ops_queue_entry *next;
+ struct kcm_ops_queue_entry *prev;
+};
+
+struct kcm_ops_queue_ctx {
+ /* UID: dlist of kcm_ops_queue_entry */
+ hash_table_t *wait_queue_hash;
+};
+
+/*
+ * Per-UID wait queue
+ *
+ * They key in the hash table is the UID of the peer. The value of each
+ * hash table entry is a linked list of kcm_ops_queue_entry structures
+ * which primarily hold the tevent request being queued.
+ */
+struct kcm_ops_queue_ctx *kcm_ops_queue_create(TALLOC_CTX *mem_ctx)
+{
+ errno_t ret;
+ struct kcm_ops_queue_ctx *queue_ctx;
+
+ queue_ctx = talloc_zero(mem_ctx, struct kcm_ops_queue_ctx);
+ if (queue_ctx == NULL) {
+ return NULL;
+ }
+
+ ret = sss_hash_create_ex(mem_ctx, QUEUE_HASH_SIZE,
+ &queue_ctx->wait_queue_hash, 0, 0, 0, 0,
+ NULL, NULL);
+ if (ret != EOK) {
+ DEBUG(SSSDBG_CRIT_FAILURE,
+ "sss_hash_create failed [%d]: %s\n", ret, sss_strerror(ret));
+ talloc_free(queue_ctx);
+ return NULL;
+ }
+
+ return queue_ctx;
+}
+
+static int kcm_op_queue_entry_destructor(struct kcm_ops_queue_entry *entry)
+{
+ int ret;
+ struct kcm_ops_queue_entry *next_entry;
+ hash_key_t key;
+
+ if (entry == NULL) {
+ return 1;
+ }
+
+ /* Take the next entry from the queue */
+ next_entry = entry->next;
+
+ /* Remove the current entry from the queue */
+ DLIST_REMOVE(entry->head, entry);
+
+ if (next_entry == NULL) {
+ key.type = HASH_KEY_ULONG;
+ key.ul = entry->uid;
+
+ /* If this was the last entry, remove the key (the UID) from the
+ * hash table to signal the queue is empty
+ */
+ ret = hash_delete(entry->wait_queue_hash, &key);
+ if (ret != HASH_SUCCESS) {
+ DEBUG(SSSDBG_CRIT_FAILURE,
+ "Failed to remove wait queue for user %"SPRIuid"\n",
+ entry->uid);
+ return 1;
+ }
+ return 0;
+ }
+
+ /* Otherwise, mark the current head as done to run the next request */
+ tevent_req_done(next_entry->req);
+ return 0;
+}
+
+static errno_t kcm_op_queue_add(hash_table_t *wait_queue_hash,
+ struct kcm_ops_queue_entry *entry,
+ uid_t uid)
+{
+ errno_t ret;
+ hash_key_t key;
+ hash_value_t value;
+ struct kcm_ops_queue_entry *head = NULL;
+
+ key.type = HASH_KEY_ULONG;
+ key.ul = uid;
+
+ ret = hash_lookup(wait_queue_hash, &key, &value);
+ switch (ret) {
+ case HASH_SUCCESS:
+ /* The key with this UID already exists. Its value is request queue
+ * for the UID, so let's just add the current request to the end
+ * of the queue and wait for the previous requests to finish
+ */
+ if (value.type != HASH_VALUE_PTR) {
+ DEBUG(SSSDBG_CRIT_FAILURE, "Unexpected hash value type.\n");
+ return EINVAL;
+ }
+
+ head = talloc_get_type(value.ptr, struct kcm_ops_queue_entry);
+ if (head == NULL) {
+ DEBUG(SSSDBG_CRIT_FAILURE, "Invalid queue pointer\n");
+ return EINVAL;
+ }
+
+ entry->head = head;
+ DLIST_ADD_END(head, entry, struct kcm_ops_queue_entry *);
+
+ DEBUG(SSSDBG_TRACE_LIBS, "Waiting in queue\n");
+ ret = EAGAIN;
+ break;
+
+ case HASH_ERROR_KEY_NOT_FOUND:
+ /* No request for this UID yet. Enqueue this request in case
+ * another one comes in and return EOK to run the current request
+ * immediatelly
+ */
+ entry->head = entry;
+
+ value.type = HASH_VALUE_PTR;
+ value.ptr = entry;
+
+ ret = hash_enter(wait_queue_hash, &key, &value);
+ if (ret != HASH_SUCCESS) {
+ DEBUG(SSSDBG_CRIT_FAILURE, "hash_enter failed.\n");
+ return EIO;
+ }
+
+ DEBUG(SSSDBG_TRACE_LIBS,
+ "Added a first request to the queue, running immediately\n");
+ ret = EOK;
+ break;
+
+ default:
+ DEBUG(SSSDBG_CRIT_FAILURE, "hash_lookup failed.\n");
+ return EIO;
+ }
+
+ talloc_steal(wait_queue_hash, entry);
+ talloc_set_destructor(entry, kcm_op_queue_entry_destructor);
+ return ret;
+}
+
+struct kcm_op_queue_state {
+ struct kcm_ops_queue_entry *entry;
+};
+
+/*
+ * Enqueue a request.
+ *
+ * If the request queue /for the given ID/ is empty, that is, if this
+ * request is the first one in the queue, run the request immediatelly.
+ *
+ * Otherwise just add it to the queue and wait until the previous request
+ * finishes and only at that point mark the current request as done, which
+ * will trigger calling the recv function and allow the request to continue.
+ */
+struct tevent_req *kcm_op_queue_send(TALLOC_CTX *mem_ctx,
+ struct tevent_context *ev,
+ struct kcm_ops_queue_ctx *qctx,
+ struct cli_creds *client)
+{
+ errno_t ret;
+ struct tevent_req *req;
+ struct kcm_op_queue_state *state;
+ uid_t uid;
+
+ uid = cli_creds_get_uid(client);
+
+ req = tevent_req_create(mem_ctx, &state, struct kcm_op_queue_state);
+ if (req == NULL) {
+ return NULL;
+ }
+
+ state->entry = talloc_zero(state, struct kcm_ops_queue_entry);
+ if (state->entry == NULL) {
+ ret = ENOMEM;
+ goto immediate;
+ }
+ state->entry->req = req;
+ state->entry->uid = uid;
+ state->entry->wait_queue_hash = qctx->wait_queue_hash;
+
+ DEBUG(SSSDBG_FUNC_DATA,
+ "Adding request by %"SPRIuid" to the wait queue\n", uid);
+
+ ret = kcm_op_queue_add(qctx->wait_queue_hash, state->entry, uid);
+ if (ret == EOK) {
+ DEBUG(SSSDBG_TRACE_LIBS,
+ "Wait queue was empty, running immediately\n");
+ goto immediate;
+ } else if (ret != EAGAIN) {
+ DEBUG(SSSDBG_OP_FAILURE,
+ "Cannot enqueue request [%d]: %s\n", ret, sss_strerror(ret));
+ goto immediate;
+ }
+
+ DEBUG(SSSDBG_TRACE_LIBS, "Waiting our turn in the queue\n");
+ return req;
+
+immediate:
+ if (ret == EOK) {
+ tevent_req_done(req);
+ } else {
+ tevent_req_error(req, ret);
+ }
+ tevent_req_post(req, ev);
+ return req;
+}
+
+/*
+ * The queue recv function is called when this request is 'activated'. The queue
+ * entry should be allocated on the same memory context as the enqueued request
+ * to trigger freeing the kcm_ops_queue_entry structure destructor when the
+ * parent request is done and its tevent_req freed. This would in turn unblock
+ * the next request in the queue
+ */
+errno_t kcm_op_queue_recv(struct tevent_req *req,
+ TALLOC_CTX *mem_ctx,
+ struct kcm_ops_queue_entry **_entry)
+{
+ struct kcm_op_queue_state *state = tevent_req_data(req,
+ struct kcm_op_queue_state);
+
+ TEVENT_REQ_RETURN_ON_ERROR(req);
+ *_entry = talloc_steal(mem_ctx, state->entry);
+ return EOK;
+}
diff --git a/src/responder/kcm/kcmsrv_ops.c b/src/responder/kcm/kcmsrv_ops.c
index 50e8cc635424e15d53e3c8d122c5525044f59c8a..2feaf51f227ce9d90f706229ce7ac201b282dc6f 100644
--- a/src/responder/kcm/kcmsrv_ops.c
+++ b/src/responder/kcm/kcmsrv_ops.c
@@ -67,17 +67,21 @@ struct kcm_op {
struct kcm_cmd_state {
struct kcm_op *op;
+ struct tevent_context *ev;
+ struct kcm_ops_queue_entry *queue_entry;
struct kcm_op_ctx *op_ctx;
struct sss_iobuf *reply;
uint32_t op_ret;
};
+static void kcm_cmd_queue_done(struct tevent_req *subreq);
static void kcm_cmd_done(struct tevent_req *subreq);
struct tevent_req *kcm_cmd_send(TALLOC_CTX *mem_ctx,
struct tevent_context *ev,
+ struct kcm_ops_queue_ctx *qctx,
struct kcm_resp_ctx *kcm_data,
struct cli_creds *client,
struct kcm_data *input,
@@ -93,6 +97,7 @@ struct tevent_req *kcm_cmd_send(TALLOC_CTX *mem_ctx,
return NULL;
}
state->op = op;
+ state->ev = ev;
if (op == NULL) {
ret = EINVAL;
@@ -154,18 +159,43 @@ struct tevent_req *kcm_cmd_send(TALLOC_CTX *mem_ctx,
goto immediate;
}
- subreq = op->fn_send(state, ev, state->op_ctx);
+ subreq = kcm_op_queue_send(state, ev, qctx, client);
if (subreq == NULL) {
ret = ENOMEM;
goto immediate;
}
+ tevent_req_set_callback(subreq, kcm_cmd_queue_done, req);
+ return req;
+
+immediate:
+ tevent_req_error(req, ret);
+ tevent_req_post(req, ev);
+ return req;
+}
+
+static void kcm_cmd_queue_done(struct tevent_req *subreq)
+{
+ struct tevent_req *req = tevent_req_callback_data(subreq, struct tevent_req);
+ struct kcm_cmd_state *state = tevent_req_data(req, struct kcm_cmd_state);
+ errno_t ret;
+
+ /* When this request finishes, it frees the queue_entry which unblocks
+ * other requests by the same UID
+ */
+ ret = kcm_op_queue_recv(subreq, state, &state->queue_entry);
+ talloc_zfree(subreq);
+ if (ret != EOK) {
+ DEBUG(SSSDBG_CRIT_FAILURE, "Cannot acquire queue slot\n");
+ tevent_req_error(req, ret);
+ return;
+ }
+
+ subreq = state->op->fn_send(state, state->ev, state->op_ctx);
+ if (subreq == NULL) {
+ tevent_req_error(req, ENOMEM);
+ return;
+ }
tevent_req_set_callback(subreq, kcm_cmd_done, req);
- return req;
-
-immediate:
- tevent_req_error(req, ret);
- tevent_req_post(req, ev);
- return req;
}
static void kcm_cmd_done(struct tevent_req *subreq)
diff --git a/src/responder/kcm/kcmsrv_ops.h b/src/responder/kcm/kcmsrv_ops.h
index 8e6feaf56a10b73c8b6375aea9ef26c392b5b492..67d9f86026bf949548471f2280c130ebefd2f865 100644
--- a/src/responder/kcm/kcmsrv_ops.h
+++ b/src/responder/kcm/kcmsrv_ops.h
@@ -34,6 +34,7 @@ const char *kcm_opt_name(struct kcm_op *op);
struct tevent_req *kcm_cmd_send(TALLOC_CTX *mem_ctx,
struct tevent_context *ev,
+ struct kcm_ops_queue_ctx *qctx,
struct kcm_resp_ctx *kcm_data,
struct cli_creds *client,
struct kcm_data *input,
diff --git a/src/responder/kcm/kcmsrv_pvt.h b/src/responder/kcm/kcmsrv_pvt.h
index 74f30c00014105ed533744779b02c5d42523722d..f081a6bf0c6e40d2f8a83b07f9bbc2abacff359d 100644
--- a/src/responder/kcm/kcmsrv_pvt.h
+++ b/src/responder/kcm/kcmsrv_pvt.h
@@ -25,6 +25,7 @@
#include "config.h"
#include <sys/types.h>
+#include <krb5/krb5.h>
#include "responder/common/responder.h"
/*
@@ -65,6 +66,7 @@ struct kcm_ctx {
int fd_limit;
char *socket_path;
enum kcm_ccdb_be cc_be;
+ struct kcm_ops_queue_ctx *qctx;
struct kcm_resp_ctx *kcm_data;
};
@@ -78,4 +80,22 @@ int kcm_connection_setup(struct cli_ctx *cctx);
*/
krb5_error_code sss2krb5_error(errno_t err);
+/* We enqueue all requests by the same UID to avoid concurrency issues
+ * especially when performing multiple round-trips to sssd-secrets. In
+ * future, we should relax the queue to allow multiple read-only operations
+ * if no write operations are in progress.
+ */
+struct kcm_ops_queue_entry;
+
+struct kcm_ops_queue_ctx *kcm_ops_queue_create(TALLOC_CTX *mem_ctx);
+
+struct tevent_req *kcm_op_queue_send(TALLOC_CTX *mem_ctx,
+ struct tevent_context *ev,
+ struct kcm_ops_queue_ctx *qctx,
+ struct cli_creds *client);
+
+errno_t kcm_op_queue_recv(struct tevent_req *req,
+ TALLOC_CTX *mem_ctx,
+ struct kcm_ops_queue_entry **_entry);
+
#endif /* __KCMSRV_PVT_H__ */
diff --git a/src/tests/cmocka/test_kcm_queue.c b/src/tests/cmocka/test_kcm_queue.c
new file mode 100644
index 0000000000000000000000000000000000000000..ba0d2405629960df5c623848f3207b7c80fa948d
--- /dev/null
+++ b/src/tests/cmocka/test_kcm_queue.c
@@ -0,0 +1,365 @@
+/*
+ Copyright (C) 2017 Red Hat
+
+ SSSD tests: Test KCM wait queue
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ 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 <stdio.h>
+#include <popt.h>
+
+#include "util/util.h"
+#include "util/util_creds.h"
+#include "tests/cmocka/common_mock.h"
+#include "responder/kcm/kcmsrv_pvt.h"
+
+#define INVALID_ID -1
+#define FAST_REQ_ID 0
+#define SLOW_REQ_ID 1
+
+#define FAST_REQ_DELAY 1
+#define SLOW_REQ_DELAY 2
+
+struct timed_request_state {
+ struct tevent_context *ev;
+ struct kcm_ops_queue_ctx *qctx;
+ struct cli_creds *client;
+ int delay;
+ int req_id;
+
+ struct kcm_ops_queue_entry *queue_entry;
+};
+
+static void timed_request_start(struct tevent_req *subreq);
+static void timed_request_done(struct tevent_context *ev,
+ struct tevent_timer *te,
+ struct timeval current_time,
+ void *pvt);
+
+static struct tevent_req *timed_request_send(TALLOC_CTX *mem_ctx,
+ struct tevent_context *ev,
+ struct kcm_ops_queue_ctx *qctx,
+ struct cli_creds *client,
+ int delay,
+ int req_id)
+{
+ struct tevent_req *req;
+ struct tevent_req *subreq;
+ struct timed_request_state *state;
+
+ req = tevent_req_create(mem_ctx, &state, struct timed_request_state);
+ if (req == NULL) {
+ return NULL;
+ }
+ state->ev = ev;
+ state->qctx = qctx;
+ state->client = client;
+ state->delay = delay;
+ state->req_id = req_id;
+
+ DEBUG(SSSDBG_TRACE_ALL, "Request %p with delay %d\n", req, delay);
+
+ subreq = kcm_op_queue_send(state, ev, qctx, client);
+ if (subreq == NULL) {
+ return NULL;
+ }
+ tevent_req_set_callback(subreq, timed_request_start, req);
+
+ return req;
+}
+
+static void timed_request_start(struct tevent_req *subreq)
+{
+ struct timeval tv;
+ struct tevent_timer *timeout = NULL;
+ struct tevent_req *req = tevent_req_callback_data(subreq,
+ struct tevent_req);
+ struct timed_request_state *state = tevent_req_data(req,
+ struct timed_request_state);
+ errno_t ret;
+
+ ret = kcm_op_queue_recv(subreq, state, &state->queue_entry);
+ talloc_zfree(subreq);
+ if (ret != EOK) {
+ tevent_req_error(req, ret);
+ return;
+ }
+
+ tv = tevent_timeval_current_ofs(state->delay, 0);
+ timeout = tevent_add_timer(state->ev, state, tv, timed_request_done, req);
+ if (timeout == NULL) {
+ tevent_req_error(req, ENOMEM);
+ return;
+ }
+
+ return;
+}
+
+static void timed_request_done(struct tevent_context *ev,
+ struct tevent_timer *te,
+ struct timeval current_time,
+ void *pvt)
+{
+ struct tevent_req *req = talloc_get_type(pvt, struct tevent_req);
+ DEBUG(SSSDBG_TRACE_ALL, "Request %p done\n", req);
+ tevent_req_done(req);
+}
+
+static errno_t timed_request_recv(struct tevent_req *req,
+ int *req_id)
+{
+ struct timed_request_state *state = tevent_req_data(req,
+ struct timed_request_state);
+
+ TEVENT_REQ_RETURN_ON_ERROR(req);
+ *req_id = state->req_id;
+ return EOK;
+}
+
+struct test_ctx {
+ struct kcm_ops_queue_ctx *qctx;
+ struct tevent_context *ev;
+
+ int *req_ids;
+
+ int num_requests;
+ int finished_requests;
+ bool done;
+ errno_t error;
+};
+
+static int setup_kcm_queue(void **state)
+{
+ struct test_ctx *tctx;
+
+ tctx = talloc_zero(NULL, struct test_ctx);
+ assert_non_null(tctx);
+
+ tctx->ev = tevent_context_init(tctx);
+ assert_non_null(tctx->ev);
+
+ tctx->qctx = kcm_ops_queue_create(tctx);
+ assert_non_null(tctx->qctx);
+
+ *state = tctx;
+ return 0;
+}
+
+static int teardown_kcm_queue(void **state)
+{
+ struct test_ctx *tctx = talloc_get_type(*state, struct test_ctx);
+ talloc_free(tctx);
+ return 0;
+}
+
+static void test_kcm_queue_done(struct tevent_req *req)
+{
+ struct test_ctx *test_ctx = tevent_req_callback_data(req,
+ struct test_ctx);
+ int req_id = INVALID_ID;
+
+ test_ctx->error = timed_request_recv(req, &req_id);
+ talloc_zfree(req);
+ if (test_ctx->error != EOK) {
+ test_ctx->done = true;
+ return;
+ }
+
+ if (test_ctx->req_ids[test_ctx->finished_requests] != req_id) {
+ DEBUG(SSSDBG_CRIT_FAILURE,
+ "Request %d finished, expected %d\n",
+ req_id, test_ctx->req_ids[test_ctx->finished_requests]);
+ test_ctx->error = EIO;
+ test_ctx->done = true;
+ return;
+ }
+
+ test_ctx->finished_requests++;
+ if (test_ctx->finished_requests == test_ctx->num_requests) {
+ test_ctx->done = true;
+ return;
+ }
+}
+
+/*
+ * Just make sure that a single pass through the queue works
+ */
+static void test_kcm_queue_single(void **state)
+{
+ struct test_ctx *test_ctx = talloc_get_type(*state, struct test_ctx);
+ struct tevent_req *req;
+ struct cli_creds client;
+ static int req_ids[] = { 0 };
+
+ client.ucred.uid = getuid();
+ client.ucred.gid = getgid();
+
+ req = timed_request_send(test_ctx,
+ test_ctx->ev,
+ test_ctx->qctx,
+ &client, 1, 0);
+ assert_non_null(req);
+ tevent_req_set_callback(req, test_kcm_queue_done, test_ctx);
+
+ test_ctx->num_requests = 1;
+ test_ctx->req_ids = req_ids;
+
+ while (test_ctx->done == false) {
+ tevent_loop_once(test_ctx->ev);
+ }
+ assert_int_equal(test_ctx->error, EOK);
+}
+
+/*
+ * Test that multiple requests from the same ID wait for one another
+ */
+static void test_kcm_queue_multi_same_id(void **state)
+{
+ struct test_ctx *test_ctx = talloc_get_type(*state, struct test_ctx);
+ struct tevent_req *req;
+ struct cli_creds client;
+ /* The slow request will finish first because request from
+ * the same ID are serialized
+ */
+ static int req_ids[] = { SLOW_REQ_ID, FAST_REQ_ID };
+
+ client.ucred.uid = getuid();
+ client.ucred.gid = getgid();
+
+ req = timed_request_send(test_ctx,
+ test_ctx->ev,
+ test_ctx->qctx,
+ &client,
+ SLOW_REQ_DELAY,
+ SLOW_REQ_ID);
+ assert_non_null(req);
+ tevent_req_set_callback(req, test_kcm_queue_done, test_ctx);
+
+ req = timed_request_send(test_ctx,
+ test_ctx->ev,
+ test_ctx->qctx,
+ &client,
+ FAST_REQ_DELAY,
+ FAST_REQ_ID);
+ assert_non_null(req);
+ tevent_req_set_callback(req, test_kcm_queue_done, test_ctx);
+
+ test_ctx->num_requests = 2;
+ test_ctx->req_ids = req_ids;
+
+ while (test_ctx->done == false) {
+ tevent_loop_once(test_ctx->ev);
+ }
+ assert_int_equal(test_ctx->error, EOK);
+}
+
+/*
+ * Test that multiple requests from different IDs don't wait for one
+ * another and can run concurrently
+ */
+static void test_kcm_queue_multi_different_id(void **state)
+{
+ struct test_ctx *test_ctx = talloc_get_type(*state, struct test_ctx);
+ struct tevent_req *req;
+ struct cli_creds client;
+ /* In this test, the fast request will finish sooner because
+ * both requests are from different IDs, allowing them to run
+ * concurrently
+ */
+ static int req_ids[] = { FAST_REQ_ID, SLOW_REQ_ID };
+
+ client.ucred.uid = getuid();
+ client.ucred.gid = getgid();
+
+ req = timed_request_send(test_ctx,
+ test_ctx->ev,
+ test_ctx->qctx,
+ &client,
+ SLOW_REQ_DELAY,
+ SLOW_REQ_ID);
+ assert_non_null(req);
+ tevent_req_set_callback(req, test_kcm_queue_done, test_ctx);
+
+ client.ucred.uid = getuid() + 1;
+ client.ucred.gid = getgid() + 1;
+
+ req = timed_request_send(test_ctx,
+ test_ctx->ev,
+ test_ctx->qctx,
+ &client,
+ FAST_REQ_DELAY,
+ FAST_REQ_ID);
+ assert_non_null(req);
+ tevent_req_set_callback(req, test_kcm_queue_done, test_ctx);
+
+ test_ctx->num_requests = 2;
+ test_ctx->req_ids = req_ids;
+
+ while (test_ctx->done == false) {
+ tevent_loop_once(test_ctx->ev);
+ }
+ assert_int_equal(test_ctx->error, EOK);
+}
+
+int main(int argc, const char *argv[])
+{
+ poptContext pc;
+ int opt;
+ int rv;
+ struct poptOption long_options[] = {
+ POPT_AUTOHELP
+ SSSD_DEBUG_OPTS
+ POPT_TABLEEND
+ };
+
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test_setup_teardown(test_kcm_queue_single,
+ setup_kcm_queue,
+ teardown_kcm_queue),
+ cmocka_unit_test_setup_teardown(test_kcm_queue_multi_same_id,
+ setup_kcm_queue,
+ teardown_kcm_queue),
+ cmocka_unit_test_setup_teardown(test_kcm_queue_multi_different_id,
+ setup_kcm_queue,
+ teardown_kcm_queue),
+ };
+
+ /* Set debug level to invalid value so we can deside if -d 0 was used. */
+ debug_level = SSSDBG_INVALID;
+
+ pc = poptGetContext(argv[0], argc, argv, long_options, 0);
+ while((opt = poptGetNextOpt(pc)) != -1) {
+ switch(opt) {
+ default:
+ fprintf(stderr, "\nInvalid option %s: %s\n\n",
+ poptBadOption(pc, 0), poptStrerror(opt));
+ poptPrintUsage(pc, stderr, 0);
+ return 1;
+ }
+ }
+ poptFreeContext(pc);
+
+ DEBUG_CLI_INIT(debug_level);
+
+ /* Even though normally the tests should clean up after themselves
+ * they might not after a failed run. Remove the old db to be sure */
+ tests_set_cwd();
+
+ rv = cmocka_run_group_tests(tests, NULL, NULL);
+
+ return rv;
+}
--
2.12.2