From: Greg Hudson Date: Mon, 2 Apr 2018 17:34:54 +0000 (-0400) Subject: Add LMDB KDB module X-Git-Tag: krb5-1.17-beta1~119 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=03e3115222e7f8ce61b2daec7bcbb2365f3190f9;p=thirdparty%2Fkrb5.git Add LMDB KDB module Add a new KDB module using LMDB. For this module, combine policy and principal databases into one environment with two databases, but split out principal lockout fields into a separate environment so that nothing blocks KDC writes for more than a trivial amount of time. ticket: 8674 (new) --- diff --git a/src/Makefile.in b/src/Makefile.in index db6c0df052..e2c178f70d 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -21,6 +21,7 @@ SUBDIRS=util include lib \ plugins/certauth/test \ plugins/kdb/db2 \ @ldap_plugin_dir@ \ + @lmdb_plugin_dir@ \ plugins/kdb/test \ plugins/kdcpolicy/test \ plugins/preauth/otp \ @@ -517,6 +518,7 @@ pyrunenv.vals: Makefile echo "tls_impl = '$(TLS_IMPL)'" >> $@ echo "have_sasl = '$(HAVE_SASL)'" >> $@ echo "have_spake_openssl = '$(HAVE_SPAKE_OPENSSL)'" >> $@ + echo "have_lmdb = '$(HAVE_LMDB)'" >> $@ echo "sizeof_time_t = $(SIZEOF_TIME_T)" >> $@ runenv.py: pyrunenv.vals diff --git a/src/config/pre.in b/src/config/pre.in index 0306c4544d..b7e4045ef8 100644 --- a/src/config/pre.in +++ b/src/config/pre.in @@ -389,6 +389,7 @@ DL_LIB = @DL_LIB@ CMOCKA_LIBS = @CMOCKA_LIBS@ LDAP_LIBS = @LDAP_LIBS@ +LMDB_LIBS = @LMDB_LIBS@ KRB5_LIB = -lkrb5 K5CRYPTO_LIB = -lk5crypto @@ -449,6 +450,9 @@ HAVE_SASL = @HAVE_SASL@ # Whether we are building support for NIST SPAKE groups using OpenSSL HAVE_SPAKE_OPENSSL = @HAVE_SPAKE_OPENSSL@ +# Whether we are building the LMDB KDB module +HAVE_LMDB = @HAVE_LMDB@ + # Whether we have libresolv 1.1.5 for URI discovery tests HAVE_RESOLV_WRAPPER = @HAVE_RESOLV_WRAPPER@ diff --git a/src/configure.in b/src/configure.in index c2ae7bd21a..9f0c0f2713 100644 --- a/src/configure.in +++ b/src/configure.in @@ -1251,6 +1251,27 @@ AC_CHECK_LIB(aceclnt, SD_Init, [ AC_SUBST(sam2_plugin) CFLAGS=$old_CFLAGS +lmdb_plugin_dir="" +HAVE_LMDB=no +AC_ARG_WITH([lmdb], +AC_HELP_STRING([--with-lmdb], + [compile LMDB database backend module @<:@auto@:>@]),, + [withval=auto]) +if test "$withval" = auto -o "$withval" = yes; then + AC_CHECK_LIB([lmdb],[mdb_env_create],[have_lmdb=true],[have_lmdb=false]) + if test "$have_lmdb" = true; then + LMDB_LIBS=-llmdb + HAVE_LMDB=yes + lmdb_plugin_dir='plugins/kdb/lmdb' + K5_GEN_MAKEFILE(plugins/kdb/lmdb) + elif test "$withval" = yes; then + AC_MSG_ERROR([liblmdb not found]) + fi +fi +AC_SUBST(HAVE_LMDB) +AC_SUBST(LMDB_LIBS) +AC_SUBST(lmdb_plugin_dir) + # Kludge for simple server --- FIXME is this the best way to do this? if test "$ac_cv_lib_socket" = "yes" -a "$ac_cv_lib_nsl" = "yes"; then diff --git a/src/include/k5-int.h b/src/include/k5-int.h index 3eca3aa538..5d84985bf1 100644 --- a/src/include/k5-int.h +++ b/src/include/k5-int.h @@ -264,13 +264,16 @@ typedef unsigned char u_char; #define KRB5_CONF_LDAP_SERVICE_PASSWORD_FILE "ldap_service_password_file" #define KRB5_CONF_LIBDEFAULTS "libdefaults" #define KRB5_CONF_LOGGING "logging" +#define KRB5_CONF_MAPSIZE "mapsize" #define KRB5_CONF_MASTER_KDC "master_kdc" #define KRB5_CONF_MASTER_KEY_NAME "master_key_name" #define KRB5_CONF_MASTER_KEY_TYPE "master_key_type" #define KRB5_CONF_MAX_LIFE "max_life" +#define KRB5_CONF_MAX_READERS "max_readers" #define KRB5_CONF_MAX_RENEWABLE_LIFE "max_renewable_life" #define KRB5_CONF_MODULE "module" #define KRB5_CONF_NOADDRESSES "noaddresses" +#define KRB5_CONF_NOSYNC "nosync" #define KRB5_CONF_NO_HOST_REFERRAL "no_host_referral" #define KRB5_CONF_PERMITTED_ENCTYPES "permitted_enctypes" #define KRB5_CONF_PLUGINS "plugins" diff --git a/src/plugins/kdb/lmdb/Makefile.in b/src/plugins/kdb/lmdb/Makefile.in new file mode 100644 index 0000000000..8e68b17d67 --- /dev/null +++ b/src/plugins/kdb/lmdb/Makefile.in @@ -0,0 +1,27 @@ +mydir=plugins$(S)kdb$(S)lmdb +BUILDTOP=$(REL)..$(S)..$(S).. +MODULE_INSTALL_DIR = $(KRB5_DB_MODULE_DIR) + +LOCALINCLUDES = -I$(srcdir)/../../../lib/kdb + +LIBBASE=klmdb +LIBMAJOR=0 +LIBMINOR=0 +RELDIR=../plugins/kdb/lmdb +# Depends on libk5crypto and libkrb5 +# Also on gssrpc, for xdr stuff. +SHLIB_EXPDEPS = $(KADMSRV_DEPLIBS) $(KDB5_DEPLIBS) $(KRB5_BASE_DEPLIBS) +SHLIB_EXPLIBS = $(KADMSRV_LIBS) $(KRB5_BASE_LIBS) $(LMDB_LIBS) + +DBDIR = liblmdb + +SRCS=$(srcdir)/kdb_lmdb.c $(srcdir)/lockout.c $(srcdir)/marshal.c + +STLIBOBJS=kdb_lmdb.o lockout.o marshal.o + +all-unix: all-liblinks +install-unix: install-libs +clean-unix:: clean-liblinks clean-libs clean-libobjs + +@libnover_frag@ +@libobj_frag@ diff --git a/src/plugins/kdb/lmdb/deps b/src/plugins/kdb/lmdb/deps new file mode 100644 index 0000000000..e4212f7933 --- /dev/null +++ b/src/plugins/kdb/lmdb/deps @@ -0,0 +1,53 @@ +# +# Generated makefile dependencies follow. +# +kdb_lmdb.so kdb_lmdb.po $(OUTPRE)kdb_lmdb.$(OBJEXT): \ + $(BUILDTOP)/include/autoconf.h $(BUILDTOP)/include/gssapi/gssapi.h \ + $(BUILDTOP)/include/gssrpc/types.h $(BUILDTOP)/include/kadm5/admin.h \ + $(BUILDTOP)/include/kadm5/chpass_util_strings.h $(BUILDTOP)/include/kadm5/kadm_err.h \ + $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \ + $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(srcdir)/../../../lib/kdb/kdb5.h \ + $(top_srcdir)/include/gssrpc/auth.h $(top_srcdir)/include/gssrpc/auth_gss.h \ + $(top_srcdir)/include/gssrpc/auth_unix.h $(top_srcdir)/include/gssrpc/clnt.h \ + $(top_srcdir)/include/gssrpc/rename.h $(top_srcdir)/include/gssrpc/rpc.h \ + $(top_srcdir)/include/gssrpc/rpc_msg.h $(top_srcdir)/include/gssrpc/svc.h \ + $(top_srcdir)/include/gssrpc/svc_auth.h $(top_srcdir)/include/gssrpc/xdr.h \ + $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \ + $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \ + $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \ + $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \ + $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/kdb.h \ + $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \ + $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \ + $(top_srcdir)/include/socket-utils.h kdb_lmdb.c klmdb-int.h +lockout.so lockout.po $(OUTPRE)lockout.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \ + $(BUILDTOP)/include/gssapi/gssapi.h $(BUILDTOP)/include/gssrpc/types.h \ + $(BUILDTOP)/include/kadm5/admin.h $(BUILDTOP)/include/kadm5/admin_internal.h \ + $(BUILDTOP)/include/kadm5/chpass_util_strings.h $(BUILDTOP)/include/kadm5/kadm_err.h \ + $(BUILDTOP)/include/kadm5/server_internal.h $(BUILDTOP)/include/krb5/krb5.h \ + $(BUILDTOP)/include/osconf.h $(BUILDTOP)/include/profile.h \ + $(COM_ERR_DEPS) $(srcdir)/../../../lib/kdb/kdb5.h $(top_srcdir)/include/gssrpc/auth.h \ + $(top_srcdir)/include/gssrpc/auth_gss.h $(top_srcdir)/include/gssrpc/auth_unix.h \ + $(top_srcdir)/include/gssrpc/clnt.h $(top_srcdir)/include/gssrpc/rename.h \ + $(top_srcdir)/include/gssrpc/rpc.h $(top_srcdir)/include/gssrpc/rpc_msg.h \ + $(top_srcdir)/include/gssrpc/svc.h $(top_srcdir)/include/gssrpc/svc_auth.h \ + $(top_srcdir)/include/gssrpc/xdr.h $(top_srcdir)/include/k5-buf.h \ + $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \ + $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \ + $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \ + $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \ + $(top_srcdir)/include/kdb.h $(top_srcdir)/include/krb5.h \ + $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \ + $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \ + klmdb-int.h lockout.c +marshal.so marshal.po $(OUTPRE)marshal.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \ + $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \ + $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(top_srcdir)/include/k5-buf.h \ + $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \ + $(top_srcdir)/include/k5-input.h $(top_srcdir)/include/k5-int-pkinit.h \ + $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \ + $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \ + $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/kdb.h \ + $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \ + $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \ + $(top_srcdir)/include/socket-utils.h klmdb-int.h marshal.c diff --git a/src/plugins/kdb/lmdb/kdb_lmdb.c b/src/plugins/kdb/lmdb/kdb_lmdb.c new file mode 100644 index 0000000000..bd288e2236 --- /dev/null +++ b/src/plugins/kdb/lmdb/kdb_lmdb.c @@ -0,0 +1,1143 @@ +/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* plugins/kdb/lmdb/klmdb.c - KDB module using LMDB */ +/* + * Copyright (C) 2018 by the Massachusetts Institute of Technology. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Thread-safety note: unlike the other two in-tree KDB modules, this module + * performs no mutex locking to ensure thread safety. As the KDC and kadmind + * are single-threaded, and applications are not allowed to access the same + * krb5_context in multiple threads simultaneously, there is no current need + * for this code to be thread-safe. If a need arises in the future, mutex + * locking should be added around the read_txn and load_txn fields of + * lmdb_context to ensure that only one thread at a time accesses those + * transactions. + */ + +/* + * This KDB module stores principal and policy data using LMDB (Lightning + * Memory-Mapped Database). We use two LMDB environments, the first to hold + * the majority of principal and policy data (suffix ".mdb") in the "principal" + * and "policy" databases, and the second to hold the three non-replicated + * account lockout attributes (suffix ".lockout.mdb") in the "lockout" + * database. The KDC only needs to write to the lockout database. + * + * For iteration we create a read transaction in the main environment for the + * cursor. Because the iteration callback might need to create its own + * transactions for write operations (e.g. for kdb5_util + * update_princ_encryption), we set the MDB_NOTLS flag on the main environment, + * so that a thread can hold multiple transactions. + * + * To mitigate the overhead from MDB_NOTLS, we keep around a read_txn handle + * in the database context for get operations, using mdb_txn_reset() and + * mdb_txn_renew() between calls. + * + * For database loads, kdb5_util calls the create() method with the "temporary" + * db_arg, and then promotes the finished contents at the end with the + * promote_db() method. In this case we create or open the same LMDB + * environments as above, open a write_txn handle for the lifetime of the + * context, and empty out the principal and policy databases. On promote_db() + * we commit the transaction. We do not empty the lockout database and write + * to it non-transactionally during the load so that we don't block writes by + * the KDC; this isn't ideal if the load is aborted, but it shouldn't cause any + * practical issues. + * + * For iprop loads, kdb5_util also includes the "merge_nra" db_arg, signifying + * that the lockout attributes from existing principal entries should be + * preserved. This attribute is noted in the LMDB context, and put_principal + * operations will not write to the lockout database if an existing lockout + * entry is already present for the principal. + */ + +#include "k5-int.h" +#include +#include "kdb5.h" +#include "klmdb-int.h" +#include + +/* The presence of any of these mask bits indicates a change to one of the + * three principal lockout attributes. */ +#define LOCKOUT_MASK (KADM5_LAST_SUCCESS | KADM5_LAST_FAILED | \ + KADM5_FAIL_AUTH_COUNT) + +/* The default map size (for both environments) in megabytes. */ +#define DEFAULT_MAPSIZE 128 + +#ifndef O_CLOEXEC +#define O_CLOEXEC 0 +#endif + +typedef struct { + char *path; + char *lockout_path; + krb5_boolean temporary; /* save changes until promote_db */ + krb5_boolean merge_nra; /* preserve existing lockout attributes */ + krb5_boolean disable_last_success; + krb5_boolean disable_lockout; + krb5_boolean nosync; + size_t mapsize; + unsigned int maxreaders; + + MDB_env *env; + MDB_env *lockout_env; + MDB_dbi princ_db; + MDB_dbi policy_db; + MDB_dbi lockout_db; + + /* Used for get operations; each transaction is short-lived but we save the + * handle between calls to reduce overhead from MDB_NOTLS. */ + MDB_txn *read_txn; + + /* Write transaction for load operations (create() with the "temporary" + * db_arg). */ + MDB_txn *load_txn; +} klmdb_context; + +static krb5_error_code +klerr(krb5_context context, int err, const char *msg) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + + /* Pass through system errors; map MDB errors to a com_err code. */ + ret = (err > 0) ? err : KRB5_KDB_ACCESS_ERROR; + + k5_setmsg(context, ret, _("%s (path: %s): %s"), msg, dbc->path, + mdb_strerror(err)); + return ret; +} + +/* Using db_args and the profile, create a DB context inside context and + * initialize its configurable parameters. */ +static krb5_error_code +configure_context(krb5_context context, const char *conf_section, + char *const *db_args) +{ + krb5_error_code ret; + klmdb_context *dbc; + char *pval = NULL; + const char *path = NULL; + profile_t profile = context->profile; + int i, bval, ival; + + dbc = k5alloc(sizeof(*dbc), &ret); + if (dbc == NULL) + return ret; + context->dal_handle->db_context = dbc; + + for (i = 0; db_args != NULL && db_args[i] != NULL; i++) { + if (strcmp(db_args[i], "temporary") == 0) { + dbc->temporary = TRUE; + } else if (strcmp(db_args[i], "merge_nra") == 0) { + dbc->merge_nra = TRUE; + } else if (strncmp(db_args[i], "dbname=", 7) == 0) { + path = db_args[i] + 7; + } else { + ret = EINVAL; + k5_setmsg(context, ret, _("Unsupported argument \"%s\" for LMDB"), + db_args[i]); + goto cleanup; + } + } + + if (path == NULL) { + /* Check for database_name in the db_module section. */ + ret = profile_get_string(profile, KDB_MODULE_SECTION, conf_section, + KRB5_CONF_DATABASE_NAME, NULL, &pval); + if (!ret && pval == NULL) { + /* For compatibility, check for database_name in the realm. */ + ret = profile_get_string(profile, KDB_REALM_SECTION, + KRB5_DB_GET_REALM(context), + KRB5_CONF_DATABASE_NAME, DEFAULT_KDB_FILE, + &pval); + } + if (ret) + goto cleanup; + path = pval; + } + + if (asprintf(&dbc->path, "%s.mdb", path) < 0) { + dbc->path = NULL; + ret = ENOMEM; + goto cleanup; + } + if (asprintf(&dbc->lockout_path, "%s.lockout.mdb", path) < 0) { + dbc->lockout_path = NULL; + ret = ENOMEM; + goto cleanup; + } + + ret = profile_get_boolean(profile, KDB_MODULE_SECTION, conf_section, + KRB5_CONF_DISABLE_LAST_SUCCESS, FALSE, &bval); + if (ret) + goto cleanup; + dbc->disable_last_success = bval; + + ret = profile_get_boolean(profile, KDB_MODULE_SECTION, conf_section, + KRB5_CONF_DISABLE_LOCKOUT, FALSE, &bval); + if (ret) + goto cleanup; + dbc->disable_lockout = bval; + + ret = profile_get_integer(profile, KDB_MODULE_SECTION, conf_section, + KRB5_CONF_MAPSIZE, DEFAULT_MAPSIZE, &ival); + if (ret) + goto cleanup; + dbc->mapsize = (size_t)ival * 1024 * 1024; + + ret = profile_get_integer(profile, KDB_MODULE_SECTION, conf_section, + KRB5_CONF_MAX_READERS, 0, &ival); + if (ret) + goto cleanup; + dbc->maxreaders = ival; + + ret = profile_get_boolean(profile, KDB_MODULE_SECTION, conf_section, + KRB5_CONF_NOSYNC, FALSE, &bval); + if (ret) + goto cleanup; + dbc->nosync = bval; + +cleanup: + profile_release_string(pval); + return ret; +} + +static krb5_error_code +open_lmdb_env(krb5_context context, klmdb_context *dbc, + krb5_boolean is_lockout, krb5_boolean readonly, + MDB_env **env_out) +{ + krb5_error_code ret; + const char *path = is_lockout ? dbc->lockout_path : dbc->path; + unsigned int flags; + MDB_env *env = NULL; + int err; + + *env_out = NULL; + + err = mdb_env_create(&env); + if (err) + goto lmdb_error; + + /* Use a pair of files instead of a subdirectory. */ + flags = MDB_NOSUBDIR; + + /* + * For the primary database, tie read transaction locktable slots to the + * transaction and not the thread, so read transactions for iteration + * cursors can coexist with short-lived transactions for operations invoked + * by the iteration callback.. + */ + if (!is_lockout) + flags |= MDB_NOTLS; + + if (readonly) + flags |= MDB_RDONLY; + + /* Durability for lockout records is never worth the performance penalty. + * For the primary environment it might be, so we make it configurable. */ + if (is_lockout || dbc->nosync) + flags |= MDB_NOSYNC; + + /* We use one database in the lockout env, two in the primary env. */ + err = mdb_env_set_maxdbs(env, is_lockout ? 1 : 2); + if (err) + goto lmdb_error; + + if (dbc->mapsize) { + err = mdb_env_set_mapsize(env, dbc->mapsize); + if (err) + goto lmdb_error; + } + + if (dbc->maxreaders) { + err = mdb_env_set_maxreaders(env, dbc->maxreaders); + if (err) + goto lmdb_error; + } + + err = mdb_env_open(env, path, flags, S_IRUSR | S_IWUSR); + if (err) + goto lmdb_error; + + *env_out = env; + return 0; + +lmdb_error: + ret = klerr(context, err, _("LMDB environment open failure")); + mdb_env_close(env); + return ret; +} + +/* Read a key from the primary environment, using a saved read transaction from + * the database context. Return KRB5_KDB_NOENTRY if the key is not found. */ +static krb5_error_code +fetch(krb5_context context, MDB_dbi db, MDB_val *key, MDB_val *val_out) +{ + krb5_error_code ret = 0; + klmdb_context *dbc = context->dal_handle->db_context; + int err; + + if (dbc->read_txn == NULL) + err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &dbc->read_txn); + else + err = mdb_txn_renew(dbc->read_txn); + + if (!err) + err = mdb_get(dbc->read_txn, db, key, val_out); + + if (err == MDB_NOTFOUND) + ret = KRB5_KDB_NOENTRY; + else if (err) + ret = klerr(context, err, _("LMDB read failure")); + + mdb_txn_reset(dbc->read_txn); + return ret; +} + +/* If we are using a lockout database, try to fetch the lockout attributes for + * key and set them in entry. */ +static void +fetch_lockout(krb5_context context, MDB_val *key, krb5_db_entry *entry) +{ + klmdb_context *dbc = context->dal_handle->db_context; + MDB_txn *txn = NULL; + MDB_val val; + int err; + + if (dbc->lockout_env == NULL) + return; + err = mdb_txn_begin(dbc->lockout_env, NULL, MDB_RDONLY, &txn); + if (!err) + err = mdb_get(txn, dbc->lockout_db, key, &val); + if (!err && val.mv_size >= LOCKOUT_RECORD_LEN) + klmdb_decode_princ_lockout(context, entry, val.mv_data); + mdb_txn_abort(txn); +} + +/* + * Store a value for key in the specified database within the primary + * environment. Use the saved load transaction if one is present, or a + * temporary write transaction if not. If no_overwrite is true and the key + * already exists, return KRB5_KDB_INUSE. If must_overwrite is true and the + * key does not already exist, return KRB5_KDB_NOENTRY. + */ +static krb5_error_code +put(krb5_context context, MDB_dbi db, char *keystr, uint8_t *bytes, size_t len, + krb5_boolean no_overwrite, krb5_boolean must_overwrite) +{ + klmdb_context *dbc = context->dal_handle->db_context; + unsigned int putflags = no_overwrite ? MDB_NOOVERWRITE : 0; + MDB_txn *temp_txn = NULL, *txn; + MDB_val key = { strlen(keystr), keystr }, val = { len, bytes }, dummy; + int err; + + if (dbc->load_txn != NULL) { + txn = dbc->load_txn; + } else { + err = mdb_txn_begin(dbc->env, NULL, 0, &temp_txn); + if (err) + goto error; + txn = temp_txn; + } + + if (must_overwrite && mdb_get(txn, db, &key, &dummy) == MDB_NOTFOUND) { + mdb_txn_abort(temp_txn); + return KRB5_KDB_NOENTRY; + } + + err = mdb_put(txn, db, &key, &val, putflags); + if (err) + goto error; + + if (temp_txn != NULL) { + err = mdb_txn_commit(temp_txn); + temp_txn = NULL; + if (err) + goto error; + } + + return 0; + +error: + mdb_txn_abort(temp_txn); + if (err == MDB_KEYEXIST) + return KRB5_KDB_INUSE; + else + return klerr(context, err, _("LMDB write failure")); +} + +/* Delete an entry from the specified env and database, using a temporary write + * transaction. Return KRB5_KDB_NOENTRY if the key does not exist. */ +static krb5_error_code +del(krb5_context context, MDB_env *env, MDB_dbi db, char *keystr) +{ + krb5_error_code ret = 0; + MDB_txn *txn = NULL; + MDB_val key = { strlen(keystr), keystr }; + int err; + + err = mdb_txn_begin(env, NULL, 0, &txn); + if (!err) + err = mdb_del(txn, db, &key, NULL); + if (!err) { + err = mdb_txn_commit(txn); + txn = NULL; + } + + if (err == MDB_NOTFOUND) + ret = KRB5_KDB_NOENTRY; + else if (err) + ret = klerr(context, err, _("LMDB delete failure")); + + mdb_txn_abort(txn); + return ret; +} + +/* Zero out and unlink filename. */ +static krb5_error_code +destroy_file(const char *filename) +{ + krb5_error_code ret; + struct stat st; + ssize_t len; + off_t pos; + uint8_t buf[BUFSIZ], zbuf[BUFSIZ] = { 0 }; + int fd; + + fd = open(filename, O_RDWR | O_CLOEXEC, 0); + if (fd < 0) + return errno; + set_cloexec_fd(fd); + if (fstat(fd, &st) == -1) + goto error; + + memset(zbuf, 0, BUFSIZ); + pos = 0; + while (pos < st.st_size) { + len = read(fd, buf, BUFSIZ); + if (len < 0) + goto error; + /* Only rewrite the block if it's not already zeroed, in case the file + * is sparse. */ + if (memcmp(buf, zbuf, len) != 0) { + (void)lseek(fd, pos, SEEK_SET); + len = write(fd, zbuf, len); + if (len < 0) + goto error; + } + pos += len; + } + close(fd); + + if (unlink(filename) != 0) + return errno; + return 0; + +error: + ret = errno; + close(fd); + return ret; +} + +static krb5_error_code +klmdb_lib_init() +{ + return 0; +} + +static krb5_error_code +klmdb_lib_cleanup() +{ + return 0; +} + +static krb5_error_code +klmdb_fini(krb5_context context) +{ + klmdb_context *dbc; + + dbc = context->dal_handle->db_context; + if (dbc == NULL) + return 0; + mdb_txn_abort(dbc->read_txn); + mdb_txn_abort(dbc->load_txn); + mdb_env_close(dbc->env); + mdb_env_close(dbc->lockout_env); + free(dbc->path); + free(dbc->lockout_path); + free(dbc); + context->dal_handle->db_context = NULL; + return 0; +} + +static krb5_error_code +klmdb_open(krb5_context context, char *conf_section, char **db_args, int mode) +{ + krb5_error_code ret; + klmdb_context *dbc; + krb5_boolean readonly; + MDB_txn *txn = NULL; + struct stat st; + int err; + + if (context->dal_handle->db_context != NULL) + return 0; + + ret = configure_context(context, conf_section, db_args); + if (ret) + return ret; + dbc = context->dal_handle->db_context; + + if (stat(dbc->path, &st) != 0) { + ret = ENOENT; + k5_setmsg(context, ret, _("LMDB file %s does not exist"), dbc->path); + goto error; + } + + /* Open the primary environment and databases. The KDC can open this + * environment read-only. */ + readonly = (mode & KRB5_KDB_OPEN_RO) || (mode & KRB5_KDB_SRV_TYPE_KDC); + ret = open_lmdb_env(context, dbc, FALSE, readonly, &dbc->env); + if (ret) + goto error; + err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &txn); + if (err) + goto lmdb_error; + err = mdb_dbi_open(txn, "principal", 0, &dbc->princ_db); + if (err) + goto lmdb_error; + err = mdb_dbi_open(txn, "policy", 0, &dbc->policy_db); + if (err) + goto lmdb_error; + err = mdb_txn_commit(txn); + txn = NULL; + if (err) + goto lmdb_error; + + /* Open the lockout environment and database if we will need it. */ + if (!dbc->disable_last_success || !dbc->disable_lockout) { + readonly = !!(mode & KRB5_KDB_OPEN_RO); + ret = open_lmdb_env(context, dbc, TRUE, readonly, &dbc->lockout_env); + if (ret) + goto error; + err = mdb_txn_begin(dbc->lockout_env, NULL, MDB_RDONLY, &txn); + if (err) + goto lmdb_error; + err = mdb_dbi_open(txn, "lockout", 0, &dbc->lockout_db); + if (err) + goto lmdb_error; + err = mdb_txn_commit(txn); + txn = NULL; + if (err) + goto lmdb_error; + } + + return 0; + +lmdb_error: + ret = klerr(context, err, _("LMDB open failure")); +error: + mdb_txn_abort(txn); + klmdb_fini(context); + return ret; +} + +static krb5_error_code +klmdb_create(krb5_context context, char *conf_section, char **db_args) +{ + krb5_error_code ret; + klmdb_context *dbc; + MDB_txn *txn = NULL; + struct stat st; + int err; + + if (context->dal_handle->db_context != NULL) + return 0; + + ret = configure_context(context, conf_section, db_args); + if (ret) + return ret; + dbc = context->dal_handle->db_context; + + if (!dbc->temporary) { + if (stat(dbc->path, &st) == 0) { + ret = ENOENT; + k5_setmsg(context, ret, _("LMDB file %s already exists"), + dbc->path); + goto error; + } + } + + /* Open (and create if necessary) the LMDB environments. */ + ret = open_lmdb_env(context, dbc, FALSE, FALSE, &dbc->env); + if (ret) + goto error; + ret = open_lmdb_env(context, dbc, TRUE, FALSE, &dbc->lockout_env); + if (ret) + goto error; + + /* Open the primary databases, creating them if they don't exist. */ + err = mdb_txn_begin(dbc->env, NULL, 0, &txn); + if (err) + goto lmdb_error; + err = mdb_dbi_open(txn, "principal", MDB_CREATE, &dbc->princ_db); + if (err) + goto lmdb_error; + err = mdb_dbi_open(txn, "policy", MDB_CREATE, &dbc->policy_db); + if (err) + goto lmdb_error; + err = mdb_txn_commit(txn); + txn = NULL; + if (err) + goto lmdb_error; + + /* Create the lockout database if it doesn't exist. */ + err = mdb_txn_begin(dbc->lockout_env, NULL, 0, &txn); + if (err) + goto lmdb_error; + err = mdb_dbi_open(txn, "lockout", MDB_CREATE, &dbc->lockout_db); + if (err) + goto lmdb_error; + err = mdb_txn_commit(txn); + txn = NULL; + if (err) + goto lmdb_error; + + if (dbc->temporary) { + /* Create a load transaction and empty the primary databases within + * it. */ + err = mdb_txn_begin(dbc->env, NULL, 0, &dbc->load_txn); + if (err) + goto lmdb_error; + err = mdb_drop(dbc->load_txn, dbc->princ_db, 0); + if (err) + goto lmdb_error; + err = mdb_drop(dbc->load_txn, dbc->policy_db, 0); + if (err) + goto lmdb_error; + } + + /* Close the lockout environment if we won't need it. */ + if (dbc->disable_last_success && dbc->disable_lockout) { + mdb_env_close(dbc->lockout_env); + dbc->lockout_env = NULL; + dbc->lockout_db = 0; + } + + return 0; + +lmdb_error: + ret = klerr(context, err, _("LMDB create error")); +error: + mdb_txn_abort(txn); + klmdb_fini(context); + return ret; +} + +/* Unlink the "-lock" extension of path. */ +static krb5_error_code +unlink_lock_file(krb5_context context, const char *path) +{ + char *lock_path; + int st; + + if (asprintf(&lock_path, "%s-lock", path) < 0) + return ENOMEM; + st = unlink(lock_path); + if (st) + k5_prependmsg(context, st, _("Could not unlink %s"), lock_path); + free(lock_path); + return st; +} + +static krb5_error_code +klmdb_destroy(krb5_context context, char *conf_section, char **db_args) +{ + krb5_error_code ret; + klmdb_context *dbc; + + if (context->dal_handle->db_context != NULL) + klmdb_fini(context); + ret = configure_context(context, conf_section, db_args); + if (ret) + goto cleanup; + dbc = context->dal_handle->db_context; + + ret = destroy_file(dbc->path); + if (ret) + goto cleanup; + ret = unlink_lock_file(context, dbc->path); + if (ret) + goto cleanup; + + ret = destroy_file(dbc->lockout_path); + if (ret) + goto cleanup; + ret = unlink_lock_file(context, dbc->lockout_path); + +cleanup: + klmdb_fini(context); + return ret; +} + +static krb5_error_code +klmdb_get_principal(krb5_context context, krb5_const_principal searchfor, + unsigned int flags, krb5_db_entry **entry_out) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + MDB_val key, val; + char *name = NULL; + + *entry_out = NULL; + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + + ret = krb5_unparse_name(context, searchfor, &name); + if (ret) + goto cleanup; + + key.mv_data = name; + key.mv_size = strlen(name); + ret = fetch(context, dbc->princ_db, &key, &val); + if (ret) + goto cleanup; + + ret = klmdb_decode_princ(context, name, strlen(name), + val.mv_data, val.mv_size, entry_out); + if (ret) + goto cleanup; + + fetch_lockout(context, &key, *entry_out); + +cleanup: + krb5_free_unparsed_name(context, name); + return ret; +} + +static krb5_error_code +klmdb_put_principal(krb5_context context, krb5_db_entry *entry, char **db_args) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + MDB_val key, val, dummy; + MDB_txn *txn = NULL; + uint8_t lockout[LOCKOUT_RECORD_LEN], *enc; + size_t len; + char *name = NULL; + int err; + + if (db_args != NULL) { + /* This module does not support DB arguments for put_principal. */ + k5_setmsg(context, EINVAL, _("Unsupported argument \"%s\" for lmdb"), + db_args[0]); + return EINVAL; + } + + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + + ret = krb5_unparse_name(context, entry->princ, &name); + if (ret) + goto cleanup; + + ret = klmdb_encode_princ(context, entry, &enc, &len); + if (ret) + goto cleanup; + ret = put(context, dbc->princ_db, name, enc, len, FALSE, FALSE); + free(enc); + if (ret) + goto cleanup; + + /* + * Write the lockout attributes to the lockout database if we are using + * one. During a load operation, changes to lockout attributes will become + * visible before the load is finished, which is an acceptable compromise + * on load atomicity. + */ + if (dbc->lockout_env != NULL && + (entry->mask & (LOCKOUT_MASK | KADM5_PRINCIPAL))) { + key.mv_data = name; + key.mv_size = strlen(name); + klmdb_encode_princ_lockout(context, entry, lockout); + val.mv_data = lockout; + val.mv_size = sizeof(lockout); + err = mdb_txn_begin(dbc->lockout_env, NULL, 0, &txn); + if (!err && dbc->merge_nra) { + /* During an iprop load, do not change existing lockout entries. */ + if (mdb_get(txn, dbc->lockout_db, &key, &dummy) == 0) + goto cleanup; + } + if (!err) + err = mdb_put(txn, dbc->lockout_db, &key, &val, 0); + if (!err) { + err = mdb_txn_commit(txn); + txn = NULL; + } + if (err) { + ret = klerr(context, err, _("LMDB lockout write failure")); + goto cleanup; + } + } + +cleanup: + mdb_txn_abort(txn); + krb5_free_unparsed_name(context, name); + return ret; +} + +static krb5_error_code +klmdb_delete_principal(krb5_context context, krb5_const_principal searchfor) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + char *name; + + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + + ret = krb5_unparse_name(context, searchfor, &name); + if (ret) + return ret; + + ret = del(context, dbc->env, dbc->princ_db, name); + if (!ret && dbc->lockout_env != NULL) + (void)del(context, dbc->lockout_env, dbc->lockout_db, name); + + krb5_free_unparsed_name(context, name); + return ret; +} + +static krb5_error_code +klmdb_iterate(krb5_context context, char *match_expr, + krb5_error_code (*func)(void *, krb5_db_entry *), void *arg, + krb5_flags iterflags) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + krb5_db_entry *entry; + MDB_txn *txn = NULL; + MDB_cursor *cursor = NULL; + MDB_val key, val; + MDB_cursor_op op = (iterflags & KRB5_DB_ITER_REV) ? MDB_PREV : MDB_NEXT; + int err; + + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + + err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &txn); + if (err) + goto lmdb_error; + err = mdb_cursor_open(txn, dbc->princ_db, &cursor); + if (err) + goto lmdb_error; + for (;;) { + err = mdb_cursor_get(cursor, &key, &val, op); + if (err == MDB_NOTFOUND) + break; + if (err) + goto lmdb_error; + ret = klmdb_decode_princ(context, key.mv_data, key.mv_size, + val.mv_data, val.mv_size, &entry); + if (ret) + goto cleanup; + fetch_lockout(context, &key, entry); + ret = (*func)(arg, entry); + krb5_db_free_principal(context, entry); + if (ret) + goto cleanup; + } + ret = 0; + goto cleanup; + +lmdb_error: + ret = klerr(context, err, _("LMDB principal iteration failure")); +cleanup: + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + return ret; +} + +krb5_error_code +klmdb_get_policy(krb5_context context, char *name, osa_policy_ent_t *policy) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + MDB_val key, val; + + *policy = NULL; + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + + key.mv_data = name; + key.mv_size = strlen(name); + ret = fetch(context, dbc->policy_db, &key, &val); + if (ret) + return ret; + return klmdb_decode_policy(context, name, strlen(name), + val.mv_data, val.mv_size, policy); +} + +static krb5_error_code +klmdb_create_policy(krb5_context context, osa_policy_ent_t policy) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + uint8_t *enc; + size_t len; + + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + + ret = klmdb_encode_policy(context, policy, &enc, &len); + if (ret) + return ret; + ret = put(context, dbc->policy_db, policy->name, enc, len, TRUE, FALSE); + free(enc); + return ret; +} + +static krb5_error_code +klmdb_put_policy(krb5_context context, osa_policy_ent_t policy) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + uint8_t *enc; + size_t len; + + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + + ret = klmdb_encode_policy(context, policy, &enc, &len); + if (ret) + return ret; + ret = put(context, dbc->policy_db, policy->name, enc, len, FALSE, TRUE); + free(enc); + return ret; +} + +static krb5_error_code +klmdb_iter_policy(krb5_context context, char *match_entry, + osa_adb_iter_policy_func func, void *arg) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + osa_policy_ent_t pol; + MDB_txn *txn = NULL; + MDB_cursor *cursor = NULL; + MDB_val key, val; + int err; + + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + + err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &txn); + if (err) + goto lmdb_error; + err = mdb_cursor_open(txn, dbc->policy_db, &cursor); + if (err) + goto lmdb_error; + for (;;) { + err = mdb_cursor_get(cursor, &key, &val, MDB_NEXT); + if (err == MDB_NOTFOUND) + break; + if (err) + goto lmdb_error; + ret = klmdb_decode_policy(context, key.mv_data, key.mv_size, + val.mv_data, val.mv_size, &pol); + if (ret) + goto cleanup; + (*func)(arg, pol); + krb5_db_free_policy(context, pol); + } + ret = 0; + goto cleanup; + +lmdb_error: + ret = klerr(context, err, _("LMDB policy iteration failure")); +cleanup: + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + return ret; +} + +static krb5_error_code +klmdb_delete_policy(krb5_context context, char *policy) +{ + klmdb_context *dbc = context->dal_handle->db_context; + + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + return del(context, dbc->env, dbc->policy_db, policy); +} + +static krb5_error_code +klmdb_promote_db(krb5_context context, char *conf_section, char **db_args) +{ + krb5_error_code ret = 0; + klmdb_context *dbc = context->dal_handle->db_context; + int err; + + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + if (dbc->load_txn == NULL) + return EINVAL; + err = mdb_txn_commit(dbc->load_txn); + dbc->load_txn = NULL; + if (err) + ret = klerr(context, err, _("LMDB transaction commit failure")); + klmdb_fini(context); + return ret; +} + +static krb5_error_code +klmdb_check_policy_as(krb5_context context, krb5_kdc_req *request, + krb5_db_entry *client, krb5_db_entry *server, + krb5_timestamp kdc_time, const char **status, + krb5_pa_data ***e_data) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + + if (dbc->disable_lockout) + return 0; + + ret = klmdb_lockout_check_policy(context, client, kdc_time); + if (ret == KRB5KDC_ERR_CLIENT_REVOKED) + *status = "LOCKED_OUT"; + return ret; +} + +static void +klmdb_audit_as_req(krb5_context context, krb5_kdc_req *request, + const krb5_address *local_addr, + const krb5_address *remote_addr, krb5_db_entry *client, + krb5_db_entry *server, krb5_timestamp authtime, + krb5_error_code status) +{ + klmdb_context *dbc = context->dal_handle->db_context; + + (void)klmdb_lockout_audit(context, client, authtime, status, + dbc->disable_last_success, dbc->disable_lockout); +} + +krb5_error_code +klmdb_update_lockout(krb5_context context, krb5_db_entry *entry, + krb5_timestamp stamp, krb5_boolean zero_fail_count, + krb5_boolean set_last_success, + krb5_boolean set_last_failure) +{ + krb5_error_code ret; + klmdb_context *dbc = context->dal_handle->db_context; + krb5_db_entry dummy = { 0 }; + uint8_t lockout[LOCKOUT_RECORD_LEN]; + MDB_txn *txn = NULL; + MDB_val key, val; + char *name = NULL; + int err; + + if (dbc == NULL) + return KRB5_KDB_DBNOTINITED; + if (dbc->lockout_env == NULL) + return 0; + if (!zero_fail_count && !set_last_success && !set_last_failure) + return 0; + + ret = krb5_unparse_name(context, entry->princ, &name); + if (ret) + goto cleanup; + key.mv_data = name; + key.mv_size = strlen(name); + + err = mdb_txn_begin(dbc->lockout_env, NULL, 0, &txn); + if (err) + goto lmdb_error; + /* Fetch base lockout info within txn so we update transactionally. */ + err = mdb_get(txn, dbc->lockout_db, &key, &val); + if (!err && val.mv_size >= LOCKOUT_RECORD_LEN) { + klmdb_decode_princ_lockout(context, &dummy, val.mv_data); + } else { + dummy.last_success = entry->last_success; + dummy.last_failed = entry->last_failed; + dummy.fail_auth_count = entry->fail_auth_count; + } + + if (zero_fail_count) + dummy.fail_auth_count = 0; + if (set_last_success) + dummy.last_success = stamp; + if (set_last_failure) { + dummy.last_failed = stamp; + dummy.fail_auth_count++; + } + + klmdb_encode_princ_lockout(context, &dummy, lockout); + val.mv_data = lockout; + val.mv_size = sizeof(lockout); + err = mdb_put(txn, dbc->lockout_db, &key, &val, 0); + if (err) + goto lmdb_error; + err = mdb_txn_commit(txn); + txn = NULL; + if (err) + goto lmdb_error; + goto cleanup; + +lmdb_error: + ret = klerr(context, err, _("LMDB lockout update failure")); +cleanup: + krb5_free_unparsed_name(context, name); + mdb_txn_abort(txn); + return 0; +} + +kdb_vftabl PLUGIN_SYMBOL_NAME(krb5_lmdb, kdb_function_table) = { + .maj_ver = KRB5_KDB_DAL_MAJOR_VERSION, + .min_ver = 0, + .init_library = klmdb_lib_init, + .fini_library = klmdb_lib_cleanup, + .init_module = klmdb_open, + .fini_module = klmdb_fini, + .create = klmdb_create, + .destroy = klmdb_destroy, + .get_principal = klmdb_get_principal, + .put_principal = klmdb_put_principal, + .delete_principal = klmdb_delete_principal, + .iterate = klmdb_iterate, + .create_policy = klmdb_create_policy, + .get_policy = klmdb_get_policy, + .put_policy = klmdb_put_policy, + .iter_policy = klmdb_iter_policy, + .delete_policy = klmdb_delete_policy, + .promote_db = klmdb_promote_db, + .check_policy_as = klmdb_check_policy_as, + .audit_as_req = klmdb_audit_as_req +}; diff --git a/src/plugins/kdb/lmdb/klmdb-int.h b/src/plugins/kdb/lmdb/klmdb-int.h new file mode 100644 index 0000000000..29bceae607 --- /dev/null +++ b/src/plugins/kdb/lmdb/klmdb-int.h @@ -0,0 +1,78 @@ +/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* plugins/kdb/lmdb/klmdb-int.h - internal declarations for LMDB KDB module */ +/* + * Copyright (C) 2018 by the Massachusetts Institute of Technology. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LMDB_INT_H +#define LMDB_INT_H + +/* Length of a principal lockout record (three 32-bit fields) */ +#define LOCKOUT_RECORD_LEN 12 + +krb5_error_code klmdb_encode_princ(krb5_context context, + const krb5_db_entry *entry, + uint8_t **enc_out, size_t *len_out); +void klmdb_encode_princ_lockout(krb5_context context, + const krb5_db_entry *entry, + uint8_t buf[LOCKOUT_RECORD_LEN]); +krb5_error_code klmdb_encode_policy(krb5_context context, + const osa_policy_ent_rec *pol, + uint8_t **enc_out, size_t *len_out); + +krb5_error_code klmdb_decode_princ(krb5_context context, + const void *key, size_t key_len, + const void *enc, size_t enc_len, + krb5_db_entry **entry_out); +void klmdb_decode_princ_lockout(krb5_context context, krb5_db_entry *entry, + const uint8_t buf[LOCKOUT_RECORD_LEN]); +krb5_error_code klmdb_decode_policy(krb5_context context, + const void *key, size_t key_len, + const void *enc, size_t enc_len, + osa_policy_ent_t *pol_out); + +krb5_error_code klmdb_lockout_check_policy(krb5_context context, + krb5_db_entry *entry, + krb5_timestamp stamp); +krb5_error_code klmdb_lockout_audit(krb5_context context, krb5_db_entry *entry, + krb5_timestamp stamp, + krb5_error_code status, + krb5_boolean disable_last_success, + krb5_boolean disable_lockout); +krb5_error_code klmdb_update_lockout(krb5_context context, + krb5_db_entry *entry, + krb5_timestamp stamp, + krb5_boolean zero_fail_count, + krb5_boolean set_last_success, + krb5_boolean set_last_failure); + +krb5_error_code klmdb_get_policy(krb5_context context, char *name, + osa_policy_ent_t *policy); + +#endif /* LMDB_INT_H */ diff --git a/src/plugins/kdb/lmdb/klmdb.exports b/src/plugins/kdb/lmdb/klmdb.exports new file mode 100644 index 0000000000..f2b7c11195 --- /dev/null +++ b/src/plugins/kdb/lmdb/klmdb.exports @@ -0,0 +1 @@ +kdb_function_table diff --git a/src/plugins/kdb/lmdb/lockout.c b/src/plugins/kdb/lmdb/lockout.c new file mode 100644 index 0000000000..380d8b3026 --- /dev/null +++ b/src/plugins/kdb/lmdb/lockout.c @@ -0,0 +1,180 @@ +/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* plugins/kdb/lmdb/lockout.c */ +/* + * Copyright (C) 2009, 2018 by the Massachusetts Institute of Technology. + * All rights reserved. + * + * Export of this software from the United States of America may + * require a specific license from the United States Government. + * It is the responsibility of any person or organization contemplating + * export to obtain such a license before exporting. + * + * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and + * distribute this software and its documentation for any purpose and + * without fee is hereby granted, provided that the above copyright + * notice appear in all copies and that both that copyright notice and + * this permission notice appear in supporting documentation, and that + * the name of M.I.T. not be used in advertising or publicity pertaining + * to distribution of the software without specific, written prior + * permission. Furthermore if you modify this software you must label + * your software as modified software and not distribute it in such a + * fashion that it might be confused with the original M.I.T. software. + * M.I.T. makes no representations about the suitability of + * this software for any purpose. It is provided "as is" without express + * or implied warranty. + */ + +#include "k5-int.h" +#include "kdb.h" +#include +#include "kdb5.h" +#include "klmdb-int.h" + +static krb5_error_code +lookup_lockout_policy(krb5_context context, krb5_db_entry *entry, + krb5_kvno *pw_max_fail, krb5_deltat *pw_failcnt_interval, + krb5_deltat *pw_lockout_duration) +{ + krb5_tl_data tl_data; + krb5_error_code code; + osa_princ_ent_rec adb; + XDR xdrs; + + *pw_max_fail = 0; + *pw_failcnt_interval = 0; + *pw_lockout_duration = 0; + + tl_data.tl_data_type = KRB5_TL_KADM_DATA; + + code = krb5_dbe_lookup_tl_data(context, entry, &tl_data); + if (code != 0 || tl_data.tl_data_length == 0) + return code; + + memset(&adb, 0, sizeof(adb)); + xdrmem_create(&xdrs, (char *)tl_data.tl_data_contents, + tl_data.tl_data_length, XDR_DECODE); + if (!xdr_osa_princ_ent_rec(&xdrs, &adb)) { + xdr_destroy(&xdrs); + return KADM5_XDR_FAILURE; + } + + if (adb.policy != NULL) { + osa_policy_ent_t policy = NULL; + + code = klmdb_get_policy(context, adb.policy, &policy); + if (code == 0) { + *pw_max_fail = policy->pw_max_fail; + *pw_failcnt_interval = policy->pw_failcnt_interval; + *pw_lockout_duration = policy->pw_lockout_duration; + krb5_db_free_policy(context, policy); + } + } + + xdr_destroy(&xdrs); + + xdrmem_create(&xdrs, NULL, 0, XDR_FREE); + xdr_osa_princ_ent_rec(&xdrs, &adb); + xdr_destroy(&xdrs); + + return 0; +} + +/* draft-behera-ldap-password-policy-10.txt 7.1 */ +static krb5_boolean +locked_check_p(krb5_context context, krb5_timestamp stamp, krb5_kvno max_fail, + krb5_timestamp lockout_duration, krb5_db_entry *entry) +{ + krb5_timestamp unlock_time; + + /* If the entry was unlocked since the last failure, it's not locked. */ + if (krb5_dbe_lookup_last_admin_unlock(context, entry, &unlock_time) == 0 && + !ts_after(entry->last_failed, unlock_time)) + return FALSE; + + if (max_fail == 0 || entry->fail_auth_count < max_fail) + return FALSE; + + if (lockout_duration == 0) + return TRUE; /* principal permanently locked */ + + return ts_after(ts_incr(entry->last_failed, lockout_duration), stamp); +} + +krb5_error_code +klmdb_lockout_check_policy(krb5_context context, krb5_db_entry *entry, + krb5_timestamp stamp) +{ + krb5_error_code code; + krb5_kvno max_fail = 0; + krb5_deltat failcnt_interval = 0; + krb5_deltat lockout_duration = 0; + + code = lookup_lockout_policy(context, entry, &max_fail, &failcnt_interval, + &lockout_duration); + if (code != 0) + return code; + + if (locked_check_p(context, stamp, max_fail, lockout_duration, entry)) + return KRB5KDC_ERR_CLIENT_REVOKED; + + return 0; +} + +krb5_error_code +klmdb_lockout_audit(krb5_context context, krb5_db_entry *entry, + krb5_timestamp stamp, krb5_error_code status, + krb5_boolean disable_last_success, + krb5_boolean disable_lockout) +{ + krb5_error_code ret; + krb5_kvno max_fail = 0; + krb5_deltat failcnt_interval = 0, lockout_duration = 0; + krb5_boolean zero_fail_count = FALSE; + krb5_boolean set_last_success = FALSE, set_last_failure = FALSE; + krb5_timestamp unlock_time; + + if (status != 0 && status != KRB5KDC_ERR_PREAUTH_FAILED && + status != KRB5KRB_AP_ERR_BAD_INTEGRITY) + return 0; + + if (!disable_lockout) { + ret = lookup_lockout_policy(context, entry, &max_fail, + &failcnt_interval, &lockout_duration); + if (ret) + return ret; + } + + /* + * Don't continue to modify the DB for an already locked account. + * (In most cases, status will be KRB5KDC_ERR_CLIENT_REVOKED, and + * this check is unneeded, but in rare cases, we can fail with an + * integrity error or preauth failure before a policy check.) + */ + if (locked_check_p(context, stamp, max_fail, lockout_duration, entry)) + return 0; + + /* Only mark the authentication as successful if the entry + * required preauthentication; otherwise we have no idea. */ + if (status == 0 && (entry->attributes & KRB5_KDB_REQUIRES_PRE_AUTH)) { + if (!disable_lockout && entry->fail_auth_count != 0) + zero_fail_count = TRUE; + if (!disable_last_success) + set_last_success = TRUE; + } else if (status != 0 && !disable_lockout) { + /* Reset the failure counter after an administrative unlock. */ + if (krb5_dbe_lookup_last_admin_unlock(context, entry, + &unlock_time) == 0 && + !ts_after(entry->last_failed, unlock_time)) + zero_fail_count = TRUE; + + /* Reset the failure counter after failcnt_interval. */ + if (failcnt_interval != 0 && + ts_after(stamp, ts_incr(entry->last_failed, failcnt_interval))) + zero_fail_count = TRUE; + + set_last_failure = TRUE; + } + + return klmdb_update_lockout(context, entry, stamp, zero_fail_count, + set_last_success, set_last_failure); +} diff --git a/src/plugins/kdb/lmdb/marshal.c b/src/plugins/kdb/lmdb/marshal.c new file mode 100644 index 0000000000..f49a2cbd99 --- /dev/null +++ b/src/plugins/kdb/lmdb/marshal.c @@ -0,0 +1,339 @@ +/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* lib/kdb/kdb_xdr.c */ +/* + * Copyright (C) 2018 by the Massachusetts Institute of Technology. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "k5-int.h" +#include "k5-input.h" +#include +#include "klmdb-int.h" + +static void +put16(struct k5buf *buf, uint16_t num) +{ + uint8_t n[2]; + + store_16_le(num, n); + k5_buf_add_len(buf, n, 2); +} + +static void +put32(struct k5buf *buf, uint32_t num) +{ + uint8_t n[4]; + + store_32_le(num, n); + k5_buf_add_len(buf, n, 4); +} + +static void +put_tl_data(struct k5buf *buf, const krb5_tl_data *tl) +{ + for (; tl != NULL; tl = tl->tl_data_next) { + put16(buf, tl->tl_data_type); + put16(buf, tl->tl_data_length); + k5_buf_add_len(buf, tl->tl_data_contents, tl->tl_data_length); + } +} + +krb5_error_code +klmdb_encode_princ(krb5_context context, const krb5_db_entry *entry, + uint8_t **enc_out, size_t *len_out) +{ + struct k5buf buf; + const krb5_key_data *kd; + int i, j; + + *enc_out = NULL; + *len_out = 0; + + k5_buf_init_dynamic(&buf); + + put32(&buf, entry->attributes); + put32(&buf, entry->max_life); + put32(&buf, entry->max_renewable_life); + put32(&buf, entry->expiration); + put32(&buf, entry->pw_expiration); + put16(&buf, entry->n_tl_data); + put16(&buf, entry->n_key_data); + put_tl_data(&buf, entry->tl_data); + for (i = 0; i < entry->n_key_data; i++) { + kd = &entry->key_data[i]; + put16(&buf, kd->key_data_ver); + put16(&buf, kd->key_data_kvno); + for (j = 0; j < kd->key_data_ver; j++) { + put16(&buf, kd->key_data_type[j]); + put16(&buf, kd->key_data_length[j]); + if (kd->key_data_length[j] > 0) { + k5_buf_add_len(&buf, kd->key_data_contents[j], + kd->key_data_length[j]); + } + } + } + + if (k5_buf_status(&buf) != 0) + return ENOMEM; + + *enc_out = buf.data; + *len_out = buf.len; + return 0; +} + +void +klmdb_encode_princ_lockout(krb5_context context, const krb5_db_entry *entry, + uint8_t buf[LOCKOUT_RECORD_LEN]) +{ + store_32_le(entry->last_success, buf); + store_32_le(entry->last_failed, buf + 4); + store_32_le(entry->fail_auth_count, buf + 8); +} + +krb5_error_code +klmdb_encode_policy(krb5_context context, const osa_policy_ent_rec *pol, + uint8_t **enc_out, size_t *len_out) +{ + struct k5buf buf; + + *enc_out = NULL; + *len_out = 0; + + k5_buf_init_dynamic(&buf); + put32(&buf, pol->pw_min_life); + put32(&buf, pol->pw_max_life); + put32(&buf, pol->pw_min_length); + put32(&buf, pol->pw_min_classes); + put32(&buf, pol->pw_history_num); + put32(&buf, pol->pw_max_fail); + put32(&buf, pol->pw_failcnt_interval); + put32(&buf, pol->pw_lockout_duration); + put32(&buf, pol->attributes); + put32(&buf, pol->max_life); + put32(&buf, pol->max_renewable_life); + + if (pol->allowed_keysalts == NULL) { + put32(&buf, 0); + } else { + put32(&buf, strlen(pol->allowed_keysalts)); + k5_buf_add(&buf, pol->allowed_keysalts); + } + + put16(&buf, pol->n_tl_data); + put_tl_data(&buf, pol->tl_data); + + if (k5_buf_status(&buf) != 0) + return ENOMEM; + + *enc_out = buf.data; + *len_out = buf.len; + return 0; +} + +static krb5_error_code +get_tl_data(struct k5input *in, size_t count, krb5_tl_data **tl) +{ + krb5_error_code ret; + const uint8_t *contents; + size_t i, len; + + for (i = 0; i < count; i++) { + *tl = k5alloc(sizeof(**tl), &ret); + if (*tl == NULL) + return ret; + (*tl)->tl_data_type = k5_input_get_uint16_le(in); + len = (*tl)->tl_data_length = k5_input_get_uint16_le(in); + contents = k5_input_get_bytes(in, len); + if (contents == NULL) + return KRB5_KDB_TRUNCATED_RECORD; + (*tl)->tl_data_contents = k5memdup(contents, len, &ret); + if ((*tl)->tl_data_contents == NULL) + return ret; + tl = &(*tl)->tl_data_next; + } + + return 0; +} + +krb5_error_code +klmdb_decode_princ(krb5_context context, const void *key, size_t key_len, + const void *enc, size_t enc_len, krb5_db_entry **entry_out) +{ + krb5_error_code ret; + struct k5input in; + krb5_db_entry *entry = NULL; + char *princname = NULL; + const uint8_t *contents; + int i, j; + size_t len; + krb5_key_data *kd; + + *entry_out = NULL; + + entry = k5alloc(sizeof(*entry), &ret); + if (entry == NULL) + goto cleanup; + + princname = k5memdup0(key, key_len, &ret); + if (princname == NULL) + goto cleanup; + ret = krb5_parse_name(context, princname, &entry->princ); + if (ret) + goto cleanup; + + k5_input_init(&in, enc, enc_len); + entry->attributes = k5_input_get_uint32_le(&in); + entry->max_life = k5_input_get_uint32_le(&in); + entry->max_renewable_life = k5_input_get_uint32_le(&in); + entry->expiration = k5_input_get_uint32_le(&in); + entry->pw_expiration = k5_input_get_uint32_le(&in); + entry->n_tl_data = k5_input_get_uint16_le(&in); + entry->n_key_data = k5_input_get_uint16_le(&in); + if (entry->n_tl_data < 0 || entry->n_key_data < 0) { + ret = KRB5_KDB_TRUNCATED_RECORD; + goto cleanup; + } + + ret = get_tl_data(&in, entry->n_tl_data, &entry->tl_data); + if (ret) + goto cleanup; + + if (entry->n_key_data > 0) { + entry->key_data = k5calloc(entry->n_key_data, sizeof(*entry->key_data), + &ret); + if (entry->key_data == NULL) + goto cleanup; + } + for (i = 0; i < entry->n_key_data; i++) { + kd = &entry->key_data[i]; + kd->key_data_ver = k5_input_get_uint16_le(&in); + kd->key_data_kvno = k5_input_get_uint16_le(&in); + if (kd->key_data_ver < 0 && + kd->key_data_ver > KRB5_KDB_V1_KEY_DATA_ARRAY) { + ret = KRB5_KDB_BAD_VERSION; + goto cleanup; + } + for (j = 0; j < kd->key_data_ver; j++) { + kd->key_data_type[j] = k5_input_get_uint16_le(&in); + len = kd->key_data_length[j] = k5_input_get_uint16_le(&in); + contents = k5_input_get_bytes(&in, len); + if (contents == NULL) { + ret = KRB5_KDB_TRUNCATED_RECORD; + goto cleanup; + } + if (len > 0) { + kd->key_data_contents[j] = k5memdup(contents, len, &ret); + if (kd->key_data_contents[j] == NULL) + goto cleanup; + } + } + } + + ret = in.status; + if (ret) + goto cleanup; + + entry->len = KRB5_KDB_V1_BASE_LENGTH; + *entry_out = entry; + entry = NULL; + +cleanup: + free(princname); + krb5_db_free_principal(context, entry); + return ret; +} + +void +klmdb_decode_princ_lockout(krb5_context context, krb5_db_entry *entry, + const uint8_t buf[LOCKOUT_RECORD_LEN]) +{ + entry->last_success = load_32_le(buf); + entry->last_failed = load_32_le(buf + 4); + entry->fail_auth_count = load_32_le(buf + 8); +} + +krb5_error_code +klmdb_decode_policy(krb5_context context, const void *key, size_t key_len, + const void *enc, size_t enc_len, osa_policy_ent_t *pol_out) +{ + krb5_error_code ret; + osa_policy_ent_t pol = NULL; + struct k5input in; + const char *str; + size_t len; + + *pol_out = NULL; + pol = k5alloc(sizeof(*pol), &ret); + if (pol == NULL) + goto error; + + pol->name = k5memdup0(key, key_len, &ret); + if (pol->name == NULL) + goto error; + + k5_input_init(&in, enc, enc_len); + pol->pw_min_life = k5_input_get_uint32_le(&in); + pol->pw_max_life = k5_input_get_uint32_le(&in); + pol->pw_min_length = k5_input_get_uint32_le(&in); + pol->pw_min_classes = k5_input_get_uint32_le(&in); + pol->pw_history_num = k5_input_get_uint32_le(&in); + pol->pw_max_fail = k5_input_get_uint32_le(&in); + pol->pw_failcnt_interval = k5_input_get_uint32_le(&in); + pol->pw_lockout_duration = k5_input_get_uint32_le(&in); + pol->attributes = k5_input_get_uint32_le(&in); + pol->max_life = k5_input_get_uint32_le(&in); + pol->max_renewable_life = k5_input_get_uint32_le(&in); + + len = k5_input_get_uint32_le(&in); + if (len > 0) { + str = (char *)k5_input_get_bytes(&in, len); + if (str == NULL) { + ret = KRB5_KDB_TRUNCATED_RECORD; + goto error; + } + pol->allowed_keysalts = k5memdup0(str, len, &ret); + if (pol->allowed_keysalts == NULL) + goto error; + } + + pol->n_tl_data = k5_input_get_uint16_le(&in); + ret = get_tl_data(&in, pol->n_tl_data, &pol->tl_data); + if (ret) + goto error; + + ret = in.status; + if (ret) + goto error; + + *pol_out = pol; + return 0; + +error: + krb5_db_free_policy(context, pol); + return ret; +}