1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /* plugins/kdb/lmdb/klmdb.c - KDB module using LMDB */
3 /*
4 * Copyright (C) 2018 by the Massachusetts Institute of Technology.
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 *
11 * * Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 *
14 * * Redistributions in binary form must reproduce the above copyright
15 * notice, this list of conditions and the following disclaimer in
16 * the documentation and/or other materials provided with the
17 * distribution.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
22 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
23 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
24 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
28 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
30 * OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32
33 /*
34 * Thread-safety note: unlike the other two in-tree KDB modules, this module
35 * performs no mutex locking to ensure thread safety. As the KDC and kadmind
36 * are single-threaded, and applications are not allowed to access the same
37 * krb5_context in multiple threads simultaneously, there is no current need
38 * for this code to be thread-safe. If a need arises in the future, mutex
39 * locking should be added around the read_txn and load_txn fields of
40 * lmdb_context to ensure that only one thread at a time accesses those
41 * transactions.
42 */
43
44 /*
45 * This KDB module stores principal and policy data using LMDB (Lightning
46 * Memory-Mapped Database). We use two LMDB environments, the first to hold
47 * the majority of principal and policy data (suffix ".mdb") in the "principal"
48 * and "policy" databases, and the second to hold the three non-replicated
49 * account lockout attributes (suffix ".lockout.mdb") in the "lockout"
50 * database. The KDC only needs to write to the lockout database.
51 *
52 * For iteration we create a read transaction in the main environment for the
53 * cursor. Because the iteration callback might need to create its own
54 * transactions for write operations (e.g. for kdb5_util
55 * update_princ_encryption), we set the MDB_NOTLS flag on the main environment,
56 * so that a thread can hold multiple transactions.
57 *
58 * To mitigate the overhead from MDB_NOTLS, we keep around a read_txn handle
59 * in the database context for get operations, using mdb_txn_reset() and
60 * mdb_txn_renew() between calls.
61 *
62 * For database loads, kdb5_util calls the create() method with the "temporary"
63 * db_arg, and then promotes the finished contents at the end with the
64 * promote_db() method. In this case we create or open the same LMDB
65 * environments as above, open a write_txn handle for the lifetime of the
66 * context, and empty out the principal and policy databases. On promote_db()
67 * we commit the transaction. We do not empty the lockout database and write
68 * to it non-transactionally during the load so that we don't block writes by
69 * the KDC; this isn't ideal if the load is aborted, but it shouldn't cause any
70 * practical issues.
71 *
72 * For iprop loads, kdb5_util also includes the "merge_nra" db_arg, signifying
73 * that the lockout attributes from existing principal entries should be
74 * preserved. This attribute is noted in the LMDB context, and put_principal
75 * operations will not write to the lockout database if an existing lockout
76 * entry is already present for the principal.
77 */
78
79 #include "k5-int.h"
80 #include <kadm5/admin.h>
81 #include "kdb5.h"
82 #include "klmdb-int.h"
83 #include <lmdb.h>
84
85 /* The presence of any of these mask bits indicates a change to one of the
86 * three principal lockout attributes. */
87 #define LOCKOUT_MASK (KADM5_LAST_SUCCESS | KADM5_LAST_FAILED | \
88 KADM5_FAIL_AUTH_COUNT)
89
90 /* The default map size (for both environments) in megabytes. */
91 #define DEFAULT_MAPSIZE 128
92
93 #ifndef O_CLOEXEC
94 #define O_CLOEXEC 0
95 #endif
96
97 typedef struct {
98 char *path;
99 char *lockout_path;
100 krb5_boolean temporary; /* save changes until promote_db */
101 krb5_boolean merge_nra; /* preserve existing lockout attributes */
102 krb5_boolean disable_last_success;
103 krb5_boolean disable_lockout;
104 krb5_boolean nosync;
105 size_t mapsize;
106 unsigned int maxreaders;
107
108 MDB_env *env;
109 MDB_env *lockout_env;
110 MDB_dbi princ_db;
111 MDB_dbi policy_db;
112 MDB_dbi lockout_db;
113
114 /* Used for get operations; each transaction is short-lived but we save the
115 * handle between calls to reduce overhead from MDB_NOTLS. */
116 MDB_txn *read_txn;
117
118 /* Write transaction for load operations (create() with the "temporary"
119 * db_arg). */
120 MDB_txn *load_txn;
121 } klmdb_context;
122
123 static krb5_error_code
klerr(krb5_context context,int err,const char * msg)124 klerr(krb5_context context, int err, const char *msg)
125 {
126 krb5_error_code ret;
127 klmdb_context *dbc = context->dal_handle->db_context;
128
129 /* Pass through system errors; map MDB errors to a com_err code. */
130 ret = (err > 0) ? err : KRB5_KDB_ACCESS_ERROR;
131
132 k5_setmsg(context, ret, _("%s (path: %s): %s"), msg, dbc->path,
133 mdb_strerror(err));
134 return ret;
135 }
136
137 /* Using db_args and the profile, create a DB context inside context and
138 * initialize its configurable parameters. */
139 static krb5_error_code
configure_context(krb5_context context,const char * conf_section,char * const * db_args)140 configure_context(krb5_context context, const char *conf_section,
141 char *const *db_args)
142 {
143 krb5_error_code ret;
144 klmdb_context *dbc;
145 char *pval = NULL;
146 const char *path = NULL;
147 profile_t profile = context->profile;
148 int i, bval, ival;
149
150 dbc = k5alloc(sizeof(*dbc), &ret);
151 if (dbc == NULL)
152 return ret;
153 context->dal_handle->db_context = dbc;
154
155 for (i = 0; db_args != NULL && db_args[i] != NULL; i++) {
156 if (strcmp(db_args[i], "temporary") == 0) {
157 dbc->temporary = TRUE;
158 } else if (strcmp(db_args[i], "merge_nra") == 0) {
159 dbc->merge_nra = TRUE;
160 } else if (strncmp(db_args[i], "dbname=", 7) == 0) {
161 path = db_args[i] + 7;
162 } else {
163 ret = EINVAL;
164 k5_setmsg(context, ret, _("Unsupported argument \"%s\" for LMDB"),
165 db_args[i]);
166 goto cleanup;
167 }
168 }
169
170 if (path == NULL) {
171 /* Check for database_name in the db_module section. */
172 ret = profile_get_string(profile, KDB_MODULE_SECTION, conf_section,
173 KRB5_CONF_DATABASE_NAME, NULL, &pval);
174 if (!ret && pval == NULL) {
175 /* For compatibility, check for database_name in the realm. */
176 ret = profile_get_string(profile, KDB_REALM_SECTION,
177 KRB5_DB_GET_REALM(context),
178 KRB5_CONF_DATABASE_NAME, DEFAULT_KDB_FILE,
179 &pval);
180 }
181 if (ret)
182 goto cleanup;
183 path = pval;
184 }
185
186 if (asprintf(&dbc->path, "%s.mdb", path) < 0) {
187 dbc->path = NULL;
188 ret = ENOMEM;
189 goto cleanup;
190 }
191 if (asprintf(&dbc->lockout_path, "%s.lockout.mdb", path) < 0) {
192 dbc->lockout_path = NULL;
193 ret = ENOMEM;
194 goto cleanup;
195 }
196
197 ret = profile_get_boolean(profile, KDB_MODULE_SECTION, conf_section,
198 KRB5_CONF_DISABLE_LAST_SUCCESS, FALSE, &bval);
199 if (ret)
200 goto cleanup;
201 dbc->disable_last_success = bval;
202
203 ret = profile_get_boolean(profile, KDB_MODULE_SECTION, conf_section,
204 KRB5_CONF_DISABLE_LOCKOUT, FALSE, &bval);
205 if (ret)
206 goto cleanup;
207 dbc->disable_lockout = bval;
208
209 ret = profile_get_integer(profile, KDB_MODULE_SECTION, conf_section,
210 KRB5_CONF_MAPSIZE, DEFAULT_MAPSIZE, &ival);
211 if (ret)
212 goto cleanup;
213 dbc->mapsize = (size_t)ival * 1024 * 1024;
214
215 ret = profile_get_integer(profile, KDB_MODULE_SECTION, conf_section,
216 KRB5_CONF_MAX_READERS, 0, &ival);
217 if (ret)
218 goto cleanup;
219 dbc->maxreaders = ival;
220
221 ret = profile_get_boolean(profile, KDB_MODULE_SECTION, conf_section,
222 KRB5_CONF_NOSYNC, FALSE, &bval);
223 if (ret)
224 goto cleanup;
225 dbc->nosync = bval;
226
227 cleanup:
228 profile_release_string(pval);
229 return ret;
230 }
231
232 static krb5_error_code
open_lmdb_env(krb5_context context,klmdb_context * dbc,krb5_boolean is_lockout,krb5_boolean readonly,MDB_env ** env_out)233 open_lmdb_env(krb5_context context, klmdb_context *dbc,
234 krb5_boolean is_lockout, krb5_boolean readonly,
235 MDB_env **env_out)
236 {
237 krb5_error_code ret;
238 const char *path = is_lockout ? dbc->lockout_path : dbc->path;
239 unsigned int flags;
240 MDB_env *env = NULL;
241 int err;
242
243 *env_out = NULL;
244
245 err = mdb_env_create(&env);
246 if (err)
247 goto lmdb_error;
248
249 /* Use a pair of files instead of a subdirectory. */
250 flags = MDB_NOSUBDIR;
251
252 /*
253 * For the primary database, tie read transaction locktable slots to the
254 * transaction and not the thread, so read transactions for iteration
255 * cursors can coexist with short-lived transactions for operations invoked
256 * by the iteration callback..
257 */
258 if (!is_lockout)
259 flags |= MDB_NOTLS;
260
261 if (readonly)
262 flags |= MDB_RDONLY;
263
264 /* Durability for lockout records is never worth the performance penalty.
265 * For the primary environment it might be, so we make it configurable. */
266 if (is_lockout || dbc->nosync)
267 flags |= MDB_NOSYNC;
268
269 /* We use one database in the lockout env, two in the primary env. */
270 err = mdb_env_set_maxdbs(env, is_lockout ? 1 : 2);
271 if (err)
272 goto lmdb_error;
273
274 if (dbc->mapsize) {
275 err = mdb_env_set_mapsize(env, dbc->mapsize);
276 if (err)
277 goto lmdb_error;
278 }
279
280 if (dbc->maxreaders) {
281 err = mdb_env_set_maxreaders(env, dbc->maxreaders);
282 if (err)
283 goto lmdb_error;
284 }
285
286 err = mdb_env_open(env, path, flags, S_IRUSR | S_IWUSR);
287 if (err)
288 goto lmdb_error;
289
290 *env_out = env;
291 return 0;
292
293 lmdb_error:
294 ret = klerr(context, err, _("LMDB environment open failure"));
295 mdb_env_close(env);
296 return ret;
297 }
298
299 /* Read a key from the primary environment, using a saved read transaction from
300 * the database context. Return KRB5_KDB_NOENTRY if the key is not found. */
301 static krb5_error_code
fetch(krb5_context context,MDB_dbi db,MDB_val * key,MDB_val * val_out)302 fetch(krb5_context context, MDB_dbi db, MDB_val *key, MDB_val *val_out)
303 {
304 krb5_error_code ret = 0;
305 klmdb_context *dbc = context->dal_handle->db_context;
306 int err;
307
308 if (dbc->read_txn == NULL)
309 err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &dbc->read_txn);
310 else
311 err = mdb_txn_renew(dbc->read_txn);
312
313 if (!err)
314 err = mdb_get(dbc->read_txn, db, key, val_out);
315
316 if (err == MDB_NOTFOUND)
317 ret = KRB5_KDB_NOENTRY;
318 else if (err)
319 ret = klerr(context, err, _("LMDB read failure"));
320
321 mdb_txn_reset(dbc->read_txn);
322 return ret;
323 }
324
325 /* If we are using a lockout database, try to fetch the lockout attributes for
326 * key and set them in entry. */
327 static void
fetch_lockout(krb5_context context,MDB_val * key,krb5_db_entry * entry)328 fetch_lockout(krb5_context context, MDB_val *key, krb5_db_entry *entry)
329 {
330 klmdb_context *dbc = context->dal_handle->db_context;
331 MDB_txn *txn = NULL;
332 MDB_val val;
333 int err;
334
335 if (dbc->lockout_env == NULL)
336 return;
337 err = mdb_txn_begin(dbc->lockout_env, NULL, MDB_RDONLY, &txn);
338 if (!err)
339 err = mdb_get(txn, dbc->lockout_db, key, &val);
340 if (!err && val.mv_size >= LOCKOUT_RECORD_LEN)
341 klmdb_decode_princ_lockout(context, entry, val.mv_data);
342 mdb_txn_abort(txn);
343 }
344
345 /*
346 * Store a value for key in the specified database within the primary
347 * environment. Use the saved load transaction if one is present, or a
348 * temporary write transaction if not. If no_overwrite is true and the key
349 * already exists, return KRB5_KDB_INUSE. If must_overwrite is true and the
350 * key does not already exist, return KRB5_KDB_NOENTRY.
351 */
352 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)353 put(krb5_context context, MDB_dbi db, char *keystr, uint8_t *bytes, size_t len,
354 krb5_boolean no_overwrite, krb5_boolean must_overwrite)
355 {
356 klmdb_context *dbc = context->dal_handle->db_context;
357 unsigned int putflags = no_overwrite ? MDB_NOOVERWRITE : 0;
358 MDB_txn *temp_txn = NULL, *txn;
359 MDB_val key = { strlen(keystr), keystr }, val = { len, bytes }, dummy;
360 int err;
361
362 if (dbc->load_txn != NULL) {
363 txn = dbc->load_txn;
364 } else {
365 err = mdb_txn_begin(dbc->env, NULL, 0, &temp_txn);
366 if (err)
367 goto error;
368 txn = temp_txn;
369 }
370
371 if (must_overwrite && mdb_get(txn, db, &key, &dummy) == MDB_NOTFOUND) {
372 mdb_txn_abort(temp_txn);
373 return KRB5_KDB_NOENTRY;
374 }
375
376 err = mdb_put(txn, db, &key, &val, putflags);
377 if (err)
378 goto error;
379
380 if (temp_txn != NULL) {
381 err = mdb_txn_commit(temp_txn);
382 temp_txn = NULL;
383 if (err)
384 goto error;
385 }
386
387 return 0;
388
389 error:
390 mdb_txn_abort(temp_txn);
391 if (err == MDB_KEYEXIST)
392 return KRB5_KDB_INUSE;
393 else
394 return klerr(context, err, _("LMDB write failure"));
395 }
396
397 /* Delete an entry from the specified env and database, using a temporary write
398 * transaction. Return KRB5_KDB_NOENTRY if the key does not exist. */
399 static krb5_error_code
del(krb5_context context,MDB_env * env,MDB_dbi db,char * keystr)400 del(krb5_context context, MDB_env *env, MDB_dbi db, char *keystr)
401 {
402 krb5_error_code ret = 0;
403 MDB_txn *txn = NULL;
404 MDB_val key = { strlen(keystr), keystr };
405 int err;
406
407 err = mdb_txn_begin(env, NULL, 0, &txn);
408 if (!err)
409 err = mdb_del(txn, db, &key, NULL);
410 if (!err) {
411 err = mdb_txn_commit(txn);
412 txn = NULL;
413 }
414
415 if (err == MDB_NOTFOUND)
416 ret = KRB5_KDB_NOENTRY;
417 else if (err)
418 ret = klerr(context, err, _("LMDB delete failure"));
419
420 mdb_txn_abort(txn);
421 return ret;
422 }
423
424 /* Zero out and unlink filename. */
425 static krb5_error_code
destroy_file(const char * filename)426 destroy_file(const char *filename)
427 {
428 krb5_error_code ret;
429 struct stat st;
430 ssize_t len;
431 off_t pos;
432 uint8_t buf[BUFSIZ], zbuf[BUFSIZ] = { 0 };
433 int fd;
434
435 fd = open(filename, O_RDWR | O_CLOEXEC, 0);
436 if (fd < 0)
437 return errno;
438 set_cloexec_fd(fd);
439 if (fstat(fd, &st) == -1)
440 goto error;
441
442 memset(zbuf, 0, BUFSIZ);
443 pos = 0;
444 while (pos < st.st_size) {
445 len = read(fd, buf, BUFSIZ);
446 if (len < 0)
447 goto error;
448 /* Only rewrite the block if it's not already zeroed, in case the file
449 * is sparse. */
450 if (memcmp(buf, zbuf, len) != 0) {
451 (void)lseek(fd, pos, SEEK_SET);
452 len = write(fd, zbuf, len);
453 if (len < 0)
454 goto error;
455 }
456 pos += len;
457 }
458 close(fd);
459
460 if (unlink(filename) != 0)
461 return errno;
462 return 0;
463
464 error:
465 ret = errno;
466 close(fd);
467 return ret;
468 }
469
470 static krb5_error_code
klmdb_lib_init()471 klmdb_lib_init()
472 {
473 return 0;
474 }
475
476 static krb5_error_code
klmdb_lib_cleanup()477 klmdb_lib_cleanup()
478 {
479 return 0;
480 }
481
482 static krb5_error_code
klmdb_fini(krb5_context context)483 klmdb_fini(krb5_context context)
484 {
485 klmdb_context *dbc;
486
487 dbc = context->dal_handle->db_context;
488 if (dbc == NULL)
489 return 0;
490 mdb_txn_abort(dbc->read_txn);
491 mdb_txn_abort(dbc->load_txn);
492 mdb_env_close(dbc->env);
493 mdb_env_close(dbc->lockout_env);
494 free(dbc->path);
495 free(dbc->lockout_path);
496 free(dbc);
497 context->dal_handle->db_context = NULL;
498 return 0;
499 }
500
501 static krb5_error_code
klmdb_open(krb5_context context,char * conf_section,char ** db_args,int mode)502 klmdb_open(krb5_context context, char *conf_section, char **db_args, int mode)
503 {
504 krb5_error_code ret;
505 klmdb_context *dbc;
506 krb5_boolean readonly;
507 MDB_txn *txn = NULL;
508 struct stat st;
509 int err;
510
511 if (context->dal_handle->db_context != NULL)
512 return 0;
513
514 ret = configure_context(context, conf_section, db_args);
515 if (ret)
516 return ret;
517 dbc = context->dal_handle->db_context;
518
519 if (stat(dbc->path, &st) != 0) {
520 ret = ENOENT;
521 k5_setmsg(context, ret, _("LMDB file %s does not exist"), dbc->path);
522 goto error;
523 }
524
525 /* Open the primary environment and databases. The KDC can open this
526 * environment read-only. */
527 readonly = (mode & KRB5_KDB_OPEN_RO) || (mode & KRB5_KDB_SRV_TYPE_KDC);
528 ret = open_lmdb_env(context, dbc, FALSE, readonly, &dbc->env);
529 if (ret)
530 goto error;
531 err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &txn);
532 if (err)
533 goto lmdb_error;
534 err = mdb_dbi_open(txn, "principal", 0, &dbc->princ_db);
535 if (err)
536 goto lmdb_error;
537 err = mdb_dbi_open(txn, "policy", 0, &dbc->policy_db);
538 if (err)
539 goto lmdb_error;
540 err = mdb_txn_commit(txn);
541 txn = NULL;
542 if (err)
543 goto lmdb_error;
544
545 /* Open the lockout environment and database if we will need it. */
546 if (!dbc->disable_last_success || !dbc->disable_lockout) {
547 readonly = !!(mode & KRB5_KDB_OPEN_RO);
548 ret = open_lmdb_env(context, dbc, TRUE, readonly, &dbc->lockout_env);
549 if (ret)
550 goto error;
551 err = mdb_txn_begin(dbc->lockout_env, NULL, MDB_RDONLY, &txn);
552 if (err)
553 goto lmdb_error;
554 err = mdb_dbi_open(txn, "lockout", 0, &dbc->lockout_db);
555 if (err)
556 goto lmdb_error;
557 err = mdb_txn_commit(txn);
558 txn = NULL;
559 if (err)
560 goto lmdb_error;
561 }
562
563 return 0;
564
565 lmdb_error:
566 ret = klerr(context, err, _("LMDB open failure"));
567 error:
568 mdb_txn_abort(txn);
569 klmdb_fini(context);
570 return ret;
571 }
572
573 static krb5_error_code
klmdb_create(krb5_context context,char * conf_section,char ** db_args)574 klmdb_create(krb5_context context, char *conf_section, char **db_args)
575 {
576 krb5_error_code ret;
577 klmdb_context *dbc;
578 MDB_txn *txn = NULL;
579 struct stat st;
580 int err;
581
582 if (context->dal_handle->db_context != NULL)
583 return 0;
584
585 ret = configure_context(context, conf_section, db_args);
586 if (ret)
587 return ret;
588 dbc = context->dal_handle->db_context;
589
590 if (!dbc->temporary) {
591 if (stat(dbc->path, &st) == 0) {
592 ret = ENOENT;
593 k5_setmsg(context, ret, _("LMDB file %s already exists"),
594 dbc->path);
595 goto error;
596 }
597 }
598
599 /* Open (and create if necessary) the LMDB environments. */
600 ret = open_lmdb_env(context, dbc, FALSE, FALSE, &dbc->env);
601 if (ret)
602 goto error;
603 ret = open_lmdb_env(context, dbc, TRUE, FALSE, &dbc->lockout_env);
604 if (ret)
605 goto error;
606
607 /* Open the primary databases, creating them if they don't exist. */
608 err = mdb_txn_begin(dbc->env, NULL, 0, &txn);
609 if (err)
610 goto lmdb_error;
611 err = mdb_dbi_open(txn, "principal", MDB_CREATE, &dbc->princ_db);
612 if (err)
613 goto lmdb_error;
614 err = mdb_dbi_open(txn, "policy", MDB_CREATE, &dbc->policy_db);
615 if (err)
616 goto lmdb_error;
617 err = mdb_txn_commit(txn);
618 txn = NULL;
619 if (err)
620 goto lmdb_error;
621
622 /* Create the lockout database if it doesn't exist. */
623 err = mdb_txn_begin(dbc->lockout_env, NULL, 0, &txn);
624 if (err)
625 goto lmdb_error;
626 err = mdb_dbi_open(txn, "lockout", MDB_CREATE, &dbc->lockout_db);
627 if (err)
628 goto lmdb_error;
629 err = mdb_txn_commit(txn);
630 txn = NULL;
631 if (err)
632 goto lmdb_error;
633
634 if (dbc->temporary) {
635 /* Create a load transaction and empty the primary databases within
636 * it. */
637 err = mdb_txn_begin(dbc->env, NULL, 0, &dbc->load_txn);
638 if (err)
639 goto lmdb_error;
640 err = mdb_drop(dbc->load_txn, dbc->princ_db, 0);
641 if (err)
642 goto lmdb_error;
643 err = mdb_drop(dbc->load_txn, dbc->policy_db, 0);
644 if (err)
645 goto lmdb_error;
646 }
647
648 /* Close the lockout environment if we won't need it. */
649 if (dbc->disable_last_success && dbc->disable_lockout) {
650 mdb_env_close(dbc->lockout_env);
651 dbc->lockout_env = NULL;
652 dbc->lockout_db = 0;
653 }
654
655 return 0;
656
657 lmdb_error:
658 ret = klerr(context, err, _("LMDB create error"));
659 error:
660 mdb_txn_abort(txn);
661 klmdb_fini(context);
662 return ret;
663 }
664
665 /* Unlink the "-lock" extension of path. */
666 static krb5_error_code
unlink_lock_file(krb5_context context,const char * path)667 unlink_lock_file(krb5_context context, const char *path)
668 {
669 char *lock_path;
670 int st;
671
672 if (asprintf(&lock_path, "%s-lock", path) < 0)
673 return ENOMEM;
674 st = unlink(lock_path);
675 if (st)
676 k5_prependmsg(context, st, _("Could not unlink %s"), lock_path);
677 free(lock_path);
678 return st;
679 }
680
681 static krb5_error_code
klmdb_destroy(krb5_context context,char * conf_section,char ** db_args)682 klmdb_destroy(krb5_context context, char *conf_section, char **db_args)
683 {
684 krb5_error_code ret;
685 klmdb_context *dbc;
686
687 if (context->dal_handle->db_context != NULL)
688 klmdb_fini(context);
689 ret = configure_context(context, conf_section, db_args);
690 if (ret)
691 goto cleanup;
692 dbc = context->dal_handle->db_context;
693
694 ret = destroy_file(dbc->path);
695 if (ret)
696 goto cleanup;
697 ret = unlink_lock_file(context, dbc->path);
698 if (ret)
699 goto cleanup;
700
701 ret = destroy_file(dbc->lockout_path);
702 if (ret)
703 goto cleanup;
704 ret = unlink_lock_file(context, dbc->lockout_path);
705
706 cleanup:
707 klmdb_fini(context);
708 return ret;
709 }
710
711 static krb5_error_code
klmdb_get_principal(krb5_context context,krb5_const_principal searchfor,unsigned int flags,krb5_db_entry ** entry_out)712 klmdb_get_principal(krb5_context context, krb5_const_principal searchfor,
713 unsigned int flags, krb5_db_entry **entry_out)
714 {
715 krb5_error_code ret;
716 klmdb_context *dbc = context->dal_handle->db_context;
717 MDB_val key, val;
718 char *name = NULL;
719
720 *entry_out = NULL;
721 if (dbc == NULL)
722 return KRB5_KDB_DBNOTINITED;
723
724 ret = krb5_unparse_name(context, searchfor, &name);
725 if (ret)
726 goto cleanup;
727
728 key.mv_data = name;
729 key.mv_size = strlen(name);
730 ret = fetch(context, dbc->princ_db, &key, &val);
731 if (ret)
732 goto cleanup;
733
734 ret = klmdb_decode_princ(context, name, strlen(name),
735 val.mv_data, val.mv_size, entry_out);
736 if (ret)
737 goto cleanup;
738
739 fetch_lockout(context, &key, *entry_out);
740
741 cleanup:
742 krb5_free_unparsed_name(context, name);
743 return ret;
744 }
745
746 static krb5_error_code
klmdb_put_principal(krb5_context context,krb5_db_entry * entry,char ** db_args)747 klmdb_put_principal(krb5_context context, krb5_db_entry *entry, char **db_args)
748 {
749 krb5_error_code ret;
750 klmdb_context *dbc = context->dal_handle->db_context;
751 MDB_val key, val, dummy;
752 MDB_txn *txn = NULL;
753 uint8_t lockout[LOCKOUT_RECORD_LEN], *enc;
754 size_t len;
755 char *name = NULL;
756 int err;
757
758 if (db_args != NULL) {
759 /* This module does not support DB arguments for put_principal. */
760 k5_setmsg(context, EINVAL, _("Unsupported argument \"%s\" for lmdb"),
761 db_args[0]);
762 return EINVAL;
763 }
764
765 if (dbc == NULL)
766 return KRB5_KDB_DBNOTINITED;
767
768 ret = krb5_unparse_name(context, entry->princ, &name);
769 if (ret)
770 goto cleanup;
771
772 ret = klmdb_encode_princ(context, entry, &enc, &len);
773 if (ret)
774 goto cleanup;
775 ret = put(context, dbc->princ_db, name, enc, len, FALSE, FALSE);
776 free(enc);
777 if (ret)
778 goto cleanup;
779
780 /*
781 * Write the lockout attributes to the lockout database if we are using
782 * one. During a load operation, changes to lockout attributes will become
783 * visible before the load is finished, which is an acceptable compromise
784 * on load atomicity.
785 */
786 if (dbc->lockout_env != NULL &&
787 (entry->mask & (LOCKOUT_MASK | KADM5_PRINCIPAL))) {
788 key.mv_data = name;
789 key.mv_size = strlen(name);
790 klmdb_encode_princ_lockout(context, entry, lockout);
791 val.mv_data = lockout;
792 val.mv_size = sizeof(lockout);
793 err = mdb_txn_begin(dbc->lockout_env, NULL, 0, &txn);
794 if (!err && dbc->merge_nra) {
795 /* During an iprop load, do not change existing lockout entries. */
796 if (mdb_get(txn, dbc->lockout_db, &key, &dummy) == 0)
797 goto cleanup;
798 }
799 if (!err)
800 err = mdb_put(txn, dbc->lockout_db, &key, &val, 0);
801 if (!err) {
802 err = mdb_txn_commit(txn);
803 txn = NULL;
804 }
805 if (err) {
806 ret = klerr(context, err, _("LMDB lockout write failure"));
807 goto cleanup;
808 }
809 }
810
811 cleanup:
812 mdb_txn_abort(txn);
813 krb5_free_unparsed_name(context, name);
814 return ret;
815 }
816
817 static krb5_error_code
klmdb_delete_principal(krb5_context context,krb5_const_principal searchfor)818 klmdb_delete_principal(krb5_context context, krb5_const_principal searchfor)
819 {
820 krb5_error_code ret;
821 klmdb_context *dbc = context->dal_handle->db_context;
822 char *name;
823
824 if (dbc == NULL)
825 return KRB5_KDB_DBNOTINITED;
826
827 ret = krb5_unparse_name(context, searchfor, &name);
828 if (ret)
829 return ret;
830
831 ret = del(context, dbc->env, dbc->princ_db, name);
832 if (!ret && dbc->lockout_env != NULL)
833 (void)del(context, dbc->lockout_env, dbc->lockout_db, name);
834
835 krb5_free_unparsed_name(context, name);
836 return ret;
837 }
838
839 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)840 klmdb_iterate(krb5_context context, char *match_expr,
841 krb5_error_code (*func)(void *, krb5_db_entry *), void *arg,
842 krb5_flags iterflags)
843 {
844 krb5_error_code ret;
845 klmdb_context *dbc = context->dal_handle->db_context;
846 krb5_db_entry *entry;
847 MDB_txn *txn = NULL;
848 MDB_cursor *cursor = NULL;
849 MDB_val key, val;
850 MDB_cursor_op op = (iterflags & KRB5_DB_ITER_REV) ? MDB_PREV : MDB_NEXT;
851 int err;
852
853 if (dbc == NULL)
854 return KRB5_KDB_DBNOTINITED;
855
856 err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &txn);
857 if (err)
858 goto lmdb_error;
859 err = mdb_cursor_open(txn, dbc->princ_db, &cursor);
860 if (err)
861 goto lmdb_error;
862 for (;;) {
863 err = mdb_cursor_get(cursor, &key, &val, op);
864 if (err == MDB_NOTFOUND)
865 break;
866 if (err)
867 goto lmdb_error;
868 ret = klmdb_decode_princ(context, key.mv_data, key.mv_size,
869 val.mv_data, val.mv_size, &entry);
870 if (ret)
871 goto cleanup;
872 fetch_lockout(context, &key, entry);
873 ret = (*func)(arg, entry);
874 krb5_db_free_principal(context, entry);
875 if (ret)
876 goto cleanup;
877 }
878 ret = 0;
879 goto cleanup;
880
881 lmdb_error:
882 ret = klerr(context, err, _("LMDB principal iteration failure"));
883 cleanup:
884 mdb_cursor_close(cursor);
885 mdb_txn_abort(txn);
886 return ret;
887 }
888
889 krb5_error_code
klmdb_get_policy(krb5_context context,char * name,osa_policy_ent_t * policy)890 klmdb_get_policy(krb5_context context, char *name, osa_policy_ent_t *policy)
891 {
892 krb5_error_code ret;
893 klmdb_context *dbc = context->dal_handle->db_context;
894 MDB_val key, val;
895
896 *policy = NULL;
897 if (dbc == NULL)
898 return KRB5_KDB_DBNOTINITED;
899
900 key.mv_data = name;
901 key.mv_size = strlen(name);
902 ret = fetch(context, dbc->policy_db, &key, &val);
903 if (ret)
904 return ret;
905 return klmdb_decode_policy(context, name, strlen(name),
906 val.mv_data, val.mv_size, policy);
907 }
908
909 static krb5_error_code
klmdb_create_policy(krb5_context context,osa_policy_ent_t policy)910 klmdb_create_policy(krb5_context context, osa_policy_ent_t policy)
911 {
912 krb5_error_code ret;
913 klmdb_context *dbc = context->dal_handle->db_context;
914 uint8_t *enc;
915 size_t len;
916
917 if (dbc == NULL)
918 return KRB5_KDB_DBNOTINITED;
919
920 ret = klmdb_encode_policy(context, policy, &enc, &len);
921 if (ret)
922 return ret;
923 ret = put(context, dbc->policy_db, policy->name, enc, len, TRUE, FALSE);
924 free(enc);
925 return ret;
926 }
927
928 static krb5_error_code
klmdb_put_policy(krb5_context context,osa_policy_ent_t policy)929 klmdb_put_policy(krb5_context context, osa_policy_ent_t policy)
930 {
931 krb5_error_code ret;
932 klmdb_context *dbc = context->dal_handle->db_context;
933 uint8_t *enc;
934 size_t len;
935
936 if (dbc == NULL)
937 return KRB5_KDB_DBNOTINITED;
938
939 ret = klmdb_encode_policy(context, policy, &enc, &len);
940 if (ret)
941 return ret;
942 ret = put(context, dbc->policy_db, policy->name, enc, len, FALSE, TRUE);
943 free(enc);
944 return ret;
945 }
946
947 static krb5_error_code
klmdb_iter_policy(krb5_context context,char * match_entry,osa_adb_iter_policy_func func,void * arg)948 klmdb_iter_policy(krb5_context context, char *match_entry,
949 osa_adb_iter_policy_func func, void *arg)
950 {
951 krb5_error_code ret;
952 klmdb_context *dbc = context->dal_handle->db_context;
953 osa_policy_ent_t pol;
954 MDB_txn *txn = NULL;
955 MDB_cursor *cursor = NULL;
956 MDB_val key, val;
957 int err;
958
959 if (dbc == NULL)
960 return KRB5_KDB_DBNOTINITED;
961
962 err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &txn);
963 if (err)
964 goto lmdb_error;
965 err = mdb_cursor_open(txn, dbc->policy_db, &cursor);
966 if (err)
967 goto lmdb_error;
968 for (;;) {
969 err = mdb_cursor_get(cursor, &key, &val, MDB_NEXT);
970 if (err == MDB_NOTFOUND)
971 break;
972 if (err)
973 goto lmdb_error;
974 ret = klmdb_decode_policy(context, key.mv_data, key.mv_size,
975 val.mv_data, val.mv_size, &pol);
976 if (ret)
977 goto cleanup;
978 (*func)(arg, pol);
979 krb5_db_free_policy(context, pol);
980 }
981 ret = 0;
982 goto cleanup;
983
984 lmdb_error:
985 ret = klerr(context, err, _("LMDB policy iteration failure"));
986 cleanup:
987 mdb_cursor_close(cursor);
988 mdb_txn_abort(txn);
989 return ret;
990 }
991
992 static krb5_error_code
klmdb_delete_policy(krb5_context context,char * policy)993 klmdb_delete_policy(krb5_context context, char *policy)
994 {
995 klmdb_context *dbc = context->dal_handle->db_context;
996
997 if (dbc == NULL)
998 return KRB5_KDB_DBNOTINITED;
999 return del(context, dbc->env, dbc->policy_db, policy);
1000 }
1001
1002 static krb5_error_code
klmdb_promote_db(krb5_context context,char * conf_section,char ** db_args)1003 klmdb_promote_db(krb5_context context, char *conf_section, char **db_args)
1004 {
1005 krb5_error_code ret = 0;
1006 klmdb_context *dbc = context->dal_handle->db_context;
1007 int err;
1008
1009 if (dbc == NULL)
1010 return KRB5_KDB_DBNOTINITED;
1011 if (dbc->load_txn == NULL)
1012 return EINVAL;
1013 err = mdb_txn_commit(dbc->load_txn);
1014 dbc->load_txn = NULL;
1015 if (err)
1016 ret = klerr(context, err, _("LMDB transaction commit failure"));
1017 klmdb_fini(context);
1018 return ret;
1019 }
1020
1021 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)1022 klmdb_check_policy_as(krb5_context context, krb5_kdc_req *request,
1023 krb5_db_entry *client, krb5_db_entry *server,
1024 krb5_timestamp kdc_time, const char **status,
1025 krb5_pa_data ***e_data)
1026 {
1027 krb5_error_code ret;
1028 klmdb_context *dbc = context->dal_handle->db_context;
1029
1030 if (dbc->disable_lockout)
1031 return 0;
1032
1033 ret = klmdb_lockout_check_policy(context, client, kdc_time);
1034 if (ret == KRB5KDC_ERR_CLIENT_REVOKED)
1035 *status = "LOCKED_OUT";
1036 return ret;
1037 }
1038
1039 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)1040 klmdb_audit_as_req(krb5_context context, krb5_kdc_req *request,
1041 const krb5_address *local_addr,
1042 const krb5_address *remote_addr, krb5_db_entry *client,
1043 krb5_db_entry *server, krb5_timestamp authtime,
1044 krb5_error_code status)
1045 {
1046 klmdb_context *dbc = context->dal_handle->db_context;
1047
1048 (void)klmdb_lockout_audit(context, client, authtime, status,
1049 dbc->disable_last_success, dbc->disable_lockout);
1050 }
1051
1052 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)1053 klmdb_update_lockout(krb5_context context, krb5_db_entry *entry,
1054 krb5_timestamp stamp, krb5_boolean zero_fail_count,
1055 krb5_boolean set_last_success,
1056 krb5_boolean set_last_failure)
1057 {
1058 krb5_error_code ret;
1059 klmdb_context *dbc = context->dal_handle->db_context;
1060 krb5_db_entry dummy = { 0 };
1061 uint8_t lockout[LOCKOUT_RECORD_LEN];
1062 MDB_txn *txn = NULL;
1063 MDB_val key, val;
1064 char *name = NULL;
1065 int err;
1066
1067 if (dbc == NULL)
1068 return KRB5_KDB_DBNOTINITED;
1069 if (dbc->lockout_env == NULL)
1070 return 0;
1071 if (!zero_fail_count && !set_last_success && !set_last_failure)
1072 return 0;
1073
1074 ret = krb5_unparse_name(context, entry->princ, &name);
1075 if (ret)
1076 goto cleanup;
1077 key.mv_data = name;
1078 key.mv_size = strlen(name);
1079
1080 err = mdb_txn_begin(dbc->lockout_env, NULL, 0, &txn);
1081 if (err)
1082 goto lmdb_error;
1083 /* Fetch base lockout info within txn so we update transactionally. */
1084 err = mdb_get(txn, dbc->lockout_db, &key, &val);
1085 if (!err && val.mv_size >= LOCKOUT_RECORD_LEN) {
1086 klmdb_decode_princ_lockout(context, &dummy, val.mv_data);
1087 } else {
1088 dummy.last_success = entry->last_success;
1089 dummy.last_failed = entry->last_failed;
1090 dummy.fail_auth_count = entry->fail_auth_count;
1091 }
1092
1093 if (zero_fail_count)
1094 dummy.fail_auth_count = 0;
1095 if (set_last_success)
1096 dummy.last_success = stamp;
1097 if (set_last_failure) {
1098 dummy.last_failed = stamp;
1099 dummy.fail_auth_count++;
1100 }
1101
1102 klmdb_encode_princ_lockout(context, &dummy, lockout);
1103 val.mv_data = lockout;
1104 val.mv_size = sizeof(lockout);
1105 err = mdb_put(txn, dbc->lockout_db, &key, &val, 0);
1106 if (err)
1107 goto lmdb_error;
1108 err = mdb_txn_commit(txn);
1109 txn = NULL;
1110 if (err)
1111 goto lmdb_error;
1112 goto cleanup;
1113
1114 lmdb_error:
1115 ret = klerr(context, err, _("LMDB lockout update failure"));
1116 cleanup:
1117 krb5_free_unparsed_name(context, name);
1118 mdb_txn_abort(txn);
1119 return 0;
1120 }
1121
1122 kdb_vftabl PLUGIN_SYMBOL_NAME(krb5_lmdb, kdb_function_table) = {
1123 .maj_ver = KRB5_KDB_DAL_MAJOR_VERSION,
1124 .min_ver = 0,
1125 .init_library = klmdb_lib_init,
1126 .fini_library = klmdb_lib_cleanup,
1127 .init_module = klmdb_open,
1128 .fini_module = klmdb_fini,
1129 .create = klmdb_create,
1130 .destroy = klmdb_destroy,
1131 .get_principal = klmdb_get_principal,
1132 .put_principal = klmdb_put_principal,
1133 .delete_principal = klmdb_delete_principal,
1134 .iterate = klmdb_iterate,
1135 .create_policy = klmdb_create_policy,
1136 .get_policy = klmdb_get_policy,
1137 .put_policy = klmdb_put_policy,
1138 .iter_policy = klmdb_iter_policy,
1139 .delete_policy = klmdb_delete_policy,
1140 .promote_db = klmdb_promote_db,
1141 .check_policy_as = klmdb_check_policy_as,
1142 .audit_as_req = klmdb_audit_as_req
1143 };
1144