/* -*- 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 <kadm5/admin.h>
#include "kdb5.h"
#include "klmdb-int.h"
#include <lmdb.h>
/* 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
};