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 size_t i;
149 int bval, ival;
150
151 dbc = k5alloc(sizeof(*dbc), &ret);
152 if (dbc == NULL)
153 return ret;
154 context->dal_handle->db_context = dbc;
155
156 for (i = 0; db_args != NULL && db_args[i] != NULL; i++) {
157 if (strcmp(db_args[i], "temporary") == 0) {
158 dbc->temporary = TRUE;
159 } else if (strcmp(db_args[i], "merge_nra") == 0) {
160 dbc->merge_nra = TRUE;
161 } else if (strncmp(db_args[i], "dbname=", 7) == 0) {
162 path = db_args[i] + 7;
163 } else {
164 ret = EINVAL;
165 k5_setmsg(context, ret, _("Unsupported argument \"%s\" for LMDB"),
166 db_args[i]);
167 goto cleanup;
168 }
169 }
170
171 if (path == NULL) {
172 /* Check for database_name in the db_module section. */
173 ret = profile_get_string(profile, KDB_MODULE_SECTION, conf_section,
174 KRB5_CONF_DATABASE_NAME, NULL, &pval);
175 if (!ret && pval == NULL) {
176 /* For compatibility, check for database_name in the realm. */
177 ret = profile_get_string(profile, KDB_REALM_SECTION,
178 KRB5_DB_GET_REALM(context),
179 KRB5_CONF_DATABASE_NAME, DEFAULT_KDB_FILE,
180 &pval);
181 }
182 if (ret)
183 goto cleanup;
184 path = pval;
185 }
186
187 if (asprintf(&dbc->path, "%s.mdb", path) < 0) {
188 dbc->path = NULL;
189 ret = ENOMEM;
190 goto cleanup;
191 }
192 if (asprintf(&dbc->lockout_path, "%s.lockout.mdb", path) < 0) {
193 dbc->lockout_path = NULL;
194 ret = ENOMEM;
195 goto cleanup;
196 }
197
198 ret = profile_get_boolean(profile, KDB_MODULE_SECTION, conf_section,
199 KRB5_CONF_DISABLE_LAST_SUCCESS, FALSE, &bval);
200 if (ret)
201 goto cleanup;
202 dbc->disable_last_success = bval;
203
204 ret = profile_get_boolean(profile, KDB_MODULE_SECTION, conf_section,
205 KRB5_CONF_DISABLE_LOCKOUT, FALSE, &bval);
206 if (ret)
207 goto cleanup;
208 dbc->disable_lockout = bval;
209
210 ret = profile_get_integer(profile, KDB_MODULE_SECTION, conf_section,
211 KRB5_CONF_MAPSIZE, DEFAULT_MAPSIZE, &ival);
212 if (ret)
213 goto cleanup;
214 dbc->mapsize = (size_t)ival * 1024 * 1024;
215
216 ret = profile_get_integer(profile, KDB_MODULE_SECTION, conf_section,
217 KRB5_CONF_MAX_READERS, 0, &ival);
218 if (ret)
219 goto cleanup;
220 dbc->maxreaders = ival;
221
222 ret = profile_get_boolean(profile, KDB_MODULE_SECTION, conf_section,
223 KRB5_CONF_NOSYNC, FALSE, &bval);
224 if (ret)
225 goto cleanup;
226 dbc->nosync = bval;
227
228 cleanup:
229 profile_release_string(pval);
230 return ret;
231 }
232
233 static krb5_error_code
open_lmdb_env(krb5_context context,klmdb_context * dbc,krb5_boolean is_lockout,krb5_boolean readonly,MDB_env ** env_out)234 open_lmdb_env(krb5_context context, klmdb_context *dbc,
235 krb5_boolean is_lockout, krb5_boolean readonly,
236 MDB_env **env_out)
237 {
238 krb5_error_code ret;
239 const char *path = is_lockout ? dbc->lockout_path : dbc->path;
240 unsigned int flags;
241 MDB_env *env = NULL;
242 int err;
243
244 *env_out = NULL;
245
246 err = mdb_env_create(&env);
247 if (err)
248 goto lmdb_error;
249
250 /* Use a pair of files instead of a subdirectory. */
251 flags = MDB_NOSUBDIR;
252
253 /*
254 * For the primary database, tie read transaction locktable slots to the
255 * transaction and not the thread, so read transactions for iteration
256 * cursors can coexist with short-lived transactions for operations invoked
257 * by the iteration callback..
258 */
259 if (!is_lockout)
260 flags |= MDB_NOTLS;
261
262 if (readonly)
263 flags |= MDB_RDONLY;
264
265 /* Durability for lockout records is never worth the performance penalty.
266 * For the primary environment it might be, so we make it configurable. */
267 if (is_lockout || dbc->nosync)
268 flags |= MDB_NOSYNC;
269
270 /* We use one database in the lockout env, two in the primary env. */
271 err = mdb_env_set_maxdbs(env, is_lockout ? 1 : 2);
272 if (err)
273 goto lmdb_error;
274
275 if (dbc->mapsize) {
276 err = mdb_env_set_mapsize(env, dbc->mapsize);
277 if (err)
278 goto lmdb_error;
279 }
280
281 if (dbc->maxreaders) {
282 err = mdb_env_set_maxreaders(env, dbc->maxreaders);
283 if (err)
284 goto lmdb_error;
285 }
286
287 err = mdb_env_open(env, path, flags, S_IRUSR | S_IWUSR);
288 if (err)
289 goto lmdb_error;
290
291 *env_out = env;
292 return 0;
293
294 lmdb_error:
295 ret = klerr(context, err, _("LMDB environment open failure"));
296 mdb_env_close(env);
297 return ret;
298 }
299
300 /* Read a key from the primary environment, using a saved read transaction from
301 * the database context. Return KRB5_KDB_NOENTRY if the key is not found. */
302 static krb5_error_code
fetch(krb5_context context,MDB_dbi db,MDB_val * key,MDB_val * val_out)303 fetch(krb5_context context, MDB_dbi db, MDB_val *key, MDB_val *val_out)
304 {
305 krb5_error_code ret = 0;
306 klmdb_context *dbc = context->dal_handle->db_context;
307 int err;
308
309 if (dbc->read_txn == NULL)
310 err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &dbc->read_txn);
311 else
312 err = mdb_txn_renew(dbc->read_txn);
313
314 if (!err)
315 err = mdb_get(dbc->read_txn, db, key, val_out);
316
317 if (err == MDB_NOTFOUND)
318 ret = KRB5_KDB_NOENTRY;
319 else if (err)
320 ret = klerr(context, err, _("LMDB read failure"));
321
322 mdb_txn_reset(dbc->read_txn);
323 return ret;
324 }
325
326 /* If we are using a lockout database, try to fetch the lockout attributes for
327 * key and set them in entry. */
328 static void
fetch_lockout(krb5_context context,MDB_val * key,krb5_db_entry * entry)329 fetch_lockout(krb5_context context, MDB_val *key, krb5_db_entry *entry)
330 {
331 klmdb_context *dbc = context->dal_handle->db_context;
332 MDB_txn *txn = NULL;
333 MDB_val val;
334 int err;
335
336 if (dbc->lockout_env == NULL)
337 return;
338 err = mdb_txn_begin(dbc->lockout_env, NULL, MDB_RDONLY, &txn);
339 if (!err)
340 err = mdb_get(txn, dbc->lockout_db, key, &val);
341 if (!err && val.mv_size >= LOCKOUT_RECORD_LEN)
342 klmdb_decode_princ_lockout(context, entry, val.mv_data);
343 mdb_txn_abort(txn);
344 }
345
346 /*
347 * Store a value for key in the specified database within the primary
348 * environment. Use the saved load transaction if one is present, or a
349 * temporary write transaction if not. If no_overwrite is true and the key
350 * already exists, return KRB5_KDB_INUSE. If must_overwrite is true and the
351 * key does not already exist, return KRB5_KDB_NOENTRY.
352 */
353 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)354 put(krb5_context context, MDB_dbi db, char *keystr, uint8_t *bytes, size_t len,
355 krb5_boolean no_overwrite, krb5_boolean must_overwrite)
356 {
357 klmdb_context *dbc = context->dal_handle->db_context;
358 unsigned int putflags = no_overwrite ? MDB_NOOVERWRITE : 0;
359 MDB_txn *temp_txn = NULL, *txn;
360 MDB_val key = { strlen(keystr), keystr }, val = { len, bytes }, dummy;
361 int err;
362
363 if (dbc->load_txn != NULL) {
364 txn = dbc->load_txn;
365 } else {
366 err = mdb_txn_begin(dbc->env, NULL, 0, &temp_txn);
367 if (err)
368 goto error;
369 txn = temp_txn;
370 }
371
372 if (must_overwrite && mdb_get(txn, db, &key, &dummy) == MDB_NOTFOUND) {
373 mdb_txn_abort(temp_txn);
374 return KRB5_KDB_NOENTRY;
375 }
376
377 err = mdb_put(txn, db, &key, &val, putflags);
378 if (err)
379 goto error;
380
381 if (temp_txn != NULL) {
382 err = mdb_txn_commit(temp_txn);
383 temp_txn = NULL;
384 if (err)
385 goto error;
386 }
387
388 return 0;
389
390 error:
391 mdb_txn_abort(temp_txn);
392 if (err == MDB_KEYEXIST)
393 return KRB5_KDB_INUSE;
394 else
395 return klerr(context, err, _("LMDB write failure"));
396 }
397
398 /* Delete an entry from the specified env and database, using a temporary write
399 * transaction. Return KRB5_KDB_NOENTRY if the key does not exist. */
400 static krb5_error_code
del(krb5_context context,MDB_env * env,MDB_dbi db,char * keystr)401 del(krb5_context context, MDB_env *env, MDB_dbi db, char *keystr)
402 {
403 krb5_error_code ret = 0;
404 MDB_txn *txn = NULL;
405 MDB_val key = { strlen(keystr), keystr };
406 int err;
407
408 err = mdb_txn_begin(env, NULL, 0, &txn);
409 if (!err)
410 err = mdb_del(txn, db, &key, NULL);
411 if (!err) {
412 err = mdb_txn_commit(txn);
413 txn = NULL;
414 }
415
416 if (err == MDB_NOTFOUND)
417 ret = KRB5_KDB_NOENTRY;
418 else if (err)
419 ret = klerr(context, err, _("LMDB delete failure"));
420
421 mdb_txn_abort(txn);
422 return ret;
423 }
424
425 /* Zero out and unlink filename. */
426 static krb5_error_code
destroy_file(const char * filename)427 destroy_file(const char *filename)
428 {
429 krb5_error_code ret;
430 struct stat st;
431 ssize_t len;
432 off_t pos;
433 uint8_t buf[BUFSIZ], zbuf[BUFSIZ] = { 0 };
434 int fd;
435
436 fd = open(filename, O_RDWR | O_CLOEXEC, 0);
437 if (fd < 0)
438 return errno;
439 set_cloexec_fd(fd);
440 if (fstat(fd, &st) == -1)
441 goto error;
442
443 memset(zbuf, 0, BUFSIZ);
444 pos = 0;
445 while (pos < st.st_size) {
446 len = read(fd, buf, BUFSIZ);
447 if (len < 0)
448 goto error;
449 /* Only rewrite the block if it's not already zeroed, in case the file
450 * is sparse. */
451 if (memcmp(buf, zbuf, len) != 0) {
452 (void)lseek(fd, pos, SEEK_SET);
453 len = write(fd, zbuf, len);
454 if (len < 0)
455 goto error;
456 }
457 pos += len;
458 }
459 close(fd);
460
461 if (unlink(filename) != 0)
462 return errno;
463 return 0;
464
465 error:
466 ret = errno;
467 close(fd);
468 return ret;
469 }
470
471 static krb5_error_code
klmdb_lib_init(void)472 klmdb_lib_init(void)
473 {
474 return 0;
475 }
476
477 static krb5_error_code
klmdb_lib_cleanup(void)478 klmdb_lib_cleanup(void)
479 {
480 return 0;
481 }
482
483 static krb5_error_code
klmdb_fini(krb5_context context)484 klmdb_fini(krb5_context context)
485 {
486 klmdb_context *dbc;
487
488 dbc = context->dal_handle->db_context;
489 if (dbc == NULL)
490 return 0;
491 mdb_txn_abort(dbc->read_txn);
492 mdb_txn_abort(dbc->load_txn);
493 mdb_env_close(dbc->env);
494 mdb_env_close(dbc->lockout_env);
495 free(dbc->path);
496 free(dbc->lockout_path);
497 free(dbc);
498 context->dal_handle->db_context = NULL;
499 return 0;
500 }
501
502 static krb5_error_code
klmdb_open(krb5_context context,char * conf_section,char ** db_args,int mode)503 klmdb_open(krb5_context context, char *conf_section, char **db_args, int mode)
504 {
505 krb5_error_code ret;
506 klmdb_context *dbc;
507 krb5_boolean readonly;
508 MDB_txn *txn = NULL;
509 struct stat st;
510 int err;
511
512 if (context->dal_handle->db_context != NULL)
513 return 0;
514
515 ret = configure_context(context, conf_section, db_args);
516 if (ret)
517 return ret;
518 dbc = context->dal_handle->db_context;
519
520 if (stat(dbc->path, &st) != 0) {
521 ret = ENOENT;
522 k5_setmsg(context, ret, _("LMDB file %s does not exist"), dbc->path);
523 goto error;
524 }
525
526 /* Open the primary environment and databases. The KDC can open this
527 * environment read-only. */
528 readonly = (mode & KRB5_KDB_OPEN_RO) || (mode & KRB5_KDB_SRV_TYPE_KDC);
529 ret = open_lmdb_env(context, dbc, FALSE, readonly, &dbc->env);
530 if (ret)
531 goto error;
532 err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &txn);
533 if (err)
534 goto lmdb_error;
535 err = mdb_dbi_open(txn, "principal", 0, &dbc->princ_db);
536 if (err)
537 goto lmdb_error;
538 err = mdb_dbi_open(txn, "policy", 0, &dbc->policy_db);
539 if (err)
540 goto lmdb_error;
541 err = mdb_txn_commit(txn);
542 txn = NULL;
543 if (err)
544 goto lmdb_error;
545
546 /* Open the lockout environment and database if we will need it. */
547 if (!dbc->disable_last_success || !dbc->disable_lockout) {
548 readonly = !!(mode & KRB5_KDB_OPEN_RO);
549 ret = open_lmdb_env(context, dbc, TRUE, readonly, &dbc->lockout_env);
550 if (ret)
551 goto error;
552 err = mdb_txn_begin(dbc->lockout_env, NULL, MDB_RDONLY, &txn);
553 if (err)
554 goto lmdb_error;
555 err = mdb_dbi_open(txn, "lockout", 0, &dbc->lockout_db);
556 if (err)
557 goto lmdb_error;
558 err = mdb_txn_commit(txn);
559 txn = NULL;
560 if (err)
561 goto lmdb_error;
562 }
563
564 return 0;
565
566 lmdb_error:
567 ret = klerr(context, err, _("LMDB open failure"));
568 error:
569 mdb_txn_abort(txn);
570 klmdb_fini(context);
571 return ret;
572 }
573
574 static krb5_error_code
klmdb_create(krb5_context context,char * conf_section,char ** db_args)575 klmdb_create(krb5_context context, char *conf_section, char **db_args)
576 {
577 krb5_error_code ret;
578 klmdb_context *dbc;
579 MDB_txn *txn = NULL;
580 struct stat st;
581 int err;
582
583 if (context->dal_handle->db_context != NULL)
584 return 0;
585
586 ret = configure_context(context, conf_section, db_args);
587 if (ret)
588 return ret;
589 dbc = context->dal_handle->db_context;
590
591 if (!dbc->temporary) {
592 if (stat(dbc->path, &st) == 0) {
593 ret = ENOENT;
594 k5_setmsg(context, ret, _("LMDB file %s already exists"),
595 dbc->path);
596 goto error;
597 }
598 }
599
600 /* Open (and create if necessary) the LMDB environments. */
601 ret = open_lmdb_env(context, dbc, FALSE, FALSE, &dbc->env);
602 if (ret)
603 goto error;
604 ret = open_lmdb_env(context, dbc, TRUE, FALSE, &dbc->lockout_env);
605 if (ret)
606 goto error;
607
608 /* Open the primary databases, creating them if they don't exist. */
609 err = mdb_txn_begin(dbc->env, NULL, 0, &txn);
610 if (err)
611 goto lmdb_error;
612 err = mdb_dbi_open(txn, "principal", MDB_CREATE, &dbc->princ_db);
613 if (err)
614 goto lmdb_error;
615 err = mdb_dbi_open(txn, "policy", MDB_CREATE, &dbc->policy_db);
616 if (err)
617 goto lmdb_error;
618 err = mdb_txn_commit(txn);
619 txn = NULL;
620 if (err)
621 goto lmdb_error;
622
623 /* Create the lockout database if it doesn't exist. */
624 err = mdb_txn_begin(dbc->lockout_env, NULL, 0, &txn);
625 if (err)
626 goto lmdb_error;
627 err = mdb_dbi_open(txn, "lockout", MDB_CREATE, &dbc->lockout_db);
628 if (err)
629 goto lmdb_error;
630 err = mdb_txn_commit(txn);
631 txn = NULL;
632 if (err)
633 goto lmdb_error;
634
635 if (dbc->temporary) {
636 /* Create a load transaction and empty the primary databases within
637 * it. */
638 err = mdb_txn_begin(dbc->env, NULL, 0, &dbc->load_txn);
639 if (err)
640 goto lmdb_error;
641 err = mdb_drop(dbc->load_txn, dbc->princ_db, 0);
642 if (err)
643 goto lmdb_error;
644 err = mdb_drop(dbc->load_txn, dbc->policy_db, 0);
645 if (err)
646 goto lmdb_error;
647 }
648
649 /* Close the lockout environment if we won't need it. */
650 if (dbc->disable_last_success && dbc->disable_lockout) {
651 mdb_env_close(dbc->lockout_env);
652 dbc->lockout_env = NULL;
653 dbc->lockout_db = 0;
654 }
655
656 return 0;
657
658 lmdb_error:
659 ret = klerr(context, err, _("LMDB create error"));
660 error:
661 mdb_txn_abort(txn);
662 klmdb_fini(context);
663 return ret;
664 }
665
666 /* Unlink the "-lock" extension of path. */
667 static krb5_error_code
unlink_lock_file(krb5_context context,const char * path)668 unlink_lock_file(krb5_context context, const char *path)
669 {
670 char *lock_path;
671 int st;
672
673 if (asprintf(&lock_path, "%s-lock", path) < 0)
674 return ENOMEM;
675 st = unlink(lock_path);
676 if (st)
677 k5_prependmsg(context, st, _("Could not unlink %s"), lock_path);
678 free(lock_path);
679 return st;
680 }
681
682 static krb5_error_code
klmdb_destroy(krb5_context context,char * conf_section,char ** db_args)683 klmdb_destroy(krb5_context context, char *conf_section, char **db_args)
684 {
685 krb5_error_code ret;
686 klmdb_context *dbc;
687
688 if (context->dal_handle->db_context != NULL)
689 klmdb_fini(context);
690 ret = configure_context(context, conf_section, db_args);
691 if (ret)
692 goto cleanup;
693 dbc = context->dal_handle->db_context;
694
695 ret = destroy_file(dbc->path);
696 if (ret)
697 goto cleanup;
698 ret = unlink_lock_file(context, dbc->path);
699 if (ret)
700 goto cleanup;
701
702 ret = destroy_file(dbc->lockout_path);
703 if (ret)
704 goto cleanup;
705 ret = unlink_lock_file(context, dbc->lockout_path);
706
707 cleanup:
708 klmdb_fini(context);
709 return ret;
710 }
711
712 static krb5_error_code
klmdb_get_principal(krb5_context context,krb5_const_principal searchfor,unsigned int flags,krb5_db_entry ** entry_out)713 klmdb_get_principal(krb5_context context, krb5_const_principal searchfor,
714 unsigned int flags, krb5_db_entry **entry_out)
715 {
716 krb5_error_code ret;
717 klmdb_context *dbc = context->dal_handle->db_context;
718 MDB_val key, val;
719 char *name = NULL;
720
721 *entry_out = NULL;
722 if (dbc == NULL)
723 return KRB5_KDB_DBNOTINITED;
724
725 ret = krb5_unparse_name(context, searchfor, &name);
726 if (ret)
727 goto cleanup;
728
729 key.mv_data = name;
730 key.mv_size = strlen(name);
731 ret = fetch(context, dbc->princ_db, &key, &val);
732 if (ret)
733 goto cleanup;
734
735 ret = klmdb_decode_princ(context, name, strlen(name),
736 val.mv_data, val.mv_size, entry_out);
737 if (ret)
738 goto cleanup;
739
740 fetch_lockout(context, &key, *entry_out);
741
742 cleanup:
743 krb5_free_unparsed_name(context, name);
744 return ret;
745 }
746
747 static krb5_error_code
klmdb_put_principal(krb5_context context,krb5_db_entry * entry,char ** db_args)748 klmdb_put_principal(krb5_context context, krb5_db_entry *entry, char **db_args)
749 {
750 krb5_error_code ret;
751 klmdb_context *dbc = context->dal_handle->db_context;
752 MDB_val key, val, dummy;
753 MDB_txn *txn = NULL;
754 uint8_t lockout[LOCKOUT_RECORD_LEN], *enc;
755 size_t len;
756 char *name = NULL;
757 int err;
758
759 if (db_args != NULL) {
760 /* This module does not support DB arguments for put_principal. */
761 k5_setmsg(context, EINVAL, _("Unsupported argument \"%s\" for lmdb"),
762 db_args[0]);
763 return EINVAL;
764 }
765
766 if (dbc == NULL)
767 return KRB5_KDB_DBNOTINITED;
768
769 ret = krb5_unparse_name(context, entry->princ, &name);
770 if (ret)
771 goto cleanup;
772
773 ret = klmdb_encode_princ(context, entry, &enc, &len);
774 if (ret)
775 goto cleanup;
776 ret = put(context, dbc->princ_db, name, enc, len, FALSE, FALSE);
777 free(enc);
778 if (ret)
779 goto cleanup;
780
781 /*
782 * Write the lockout attributes to the lockout database if we are using
783 * one. During a load operation, changes to lockout attributes will become
784 * visible before the load is finished, which is an acceptable compromise
785 * on load atomicity.
786 */
787 if (dbc->lockout_env != NULL &&
788 (entry->mask & (LOCKOUT_MASK | KADM5_PRINCIPAL))) {
789 key.mv_data = name;
790 key.mv_size = strlen(name);
791 klmdb_encode_princ_lockout(context, entry, lockout);
792 val.mv_data = lockout;
793 val.mv_size = sizeof(lockout);
794 err = mdb_txn_begin(dbc->lockout_env, NULL, 0, &txn);
795 if (!err && dbc->merge_nra) {
796 /* During an iprop load, do not change existing lockout entries. */
797 if (mdb_get(txn, dbc->lockout_db, &key, &dummy) == 0)
798 goto cleanup;
799 }
800 if (!err)
801 err = mdb_put(txn, dbc->lockout_db, &key, &val, 0);
802 if (!err) {
803 err = mdb_txn_commit(txn);
804 txn = NULL;
805 }
806 if (err) {
807 ret = klerr(context, err, _("LMDB lockout write failure"));
808 goto cleanup;
809 }
810 }
811
812 cleanup:
813 mdb_txn_abort(txn);
814 krb5_free_unparsed_name(context, name);
815 return ret;
816 }
817
818 static krb5_error_code
klmdb_delete_principal(krb5_context context,krb5_const_principal searchfor)819 klmdb_delete_principal(krb5_context context, krb5_const_principal searchfor)
820 {
821 krb5_error_code ret;
822 klmdb_context *dbc = context->dal_handle->db_context;
823 char *name;
824
825 if (dbc == NULL)
826 return KRB5_KDB_DBNOTINITED;
827
828 ret = krb5_unparse_name(context, searchfor, &name);
829 if (ret)
830 return ret;
831
832 ret = del(context, dbc->env, dbc->princ_db, name);
833 if (!ret && dbc->lockout_env != NULL)
834 (void)del(context, dbc->lockout_env, dbc->lockout_db, name);
835
836 krb5_free_unparsed_name(context, name);
837 return ret;
838 }
839
840 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)841 klmdb_iterate(krb5_context context, char *match_expr,
842 krb5_error_code (*func)(void *, krb5_db_entry *), void *arg,
843 krb5_flags iterflags)
844 {
845 krb5_error_code ret;
846 klmdb_context *dbc = context->dal_handle->db_context;
847 krb5_db_entry *entry;
848 MDB_txn *txn = NULL;
849 MDB_cursor *cursor = NULL;
850 MDB_val key, val;
851 MDB_cursor_op op = (iterflags & KRB5_DB_ITER_REV) ? MDB_PREV : MDB_NEXT;
852 int err;
853
854 if (dbc == NULL)
855 return KRB5_KDB_DBNOTINITED;
856
857 err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &txn);
858 if (err)
859 goto lmdb_error;
860 err = mdb_cursor_open(txn, dbc->princ_db, &cursor);
861 if (err)
862 goto lmdb_error;
863 for (;;) {
864 err = mdb_cursor_get(cursor, &key, &val, op);
865 if (err == MDB_NOTFOUND)
866 break;
867 if (err)
868 goto lmdb_error;
869 ret = klmdb_decode_princ(context, key.mv_data, key.mv_size,
870 val.mv_data, val.mv_size, &entry);
871 if (ret)
872 goto cleanup;
873 fetch_lockout(context, &key, entry);
874 ret = (*func)(arg, entry);
875 krb5_db_free_principal(context, entry);
876 if (ret)
877 goto cleanup;
878 }
879 ret = 0;
880 goto cleanup;
881
882 lmdb_error:
883 ret = klerr(context, err, _("LMDB principal iteration failure"));
884 cleanup:
885 mdb_cursor_close(cursor);
886 mdb_txn_abort(txn);
887 return ret;
888 }
889
890 krb5_error_code
klmdb_get_policy(krb5_context context,char * name,osa_policy_ent_t * policy)891 klmdb_get_policy(krb5_context context, char *name, osa_policy_ent_t *policy)
892 {
893 krb5_error_code ret;
894 klmdb_context *dbc = context->dal_handle->db_context;
895 MDB_val key, val;
896
897 *policy = NULL;
898 if (dbc == NULL)
899 return KRB5_KDB_DBNOTINITED;
900
901 key.mv_data = name;
902 key.mv_size = strlen(name);
903 ret = fetch(context, dbc->policy_db, &key, &val);
904 if (ret)
905 return ret;
906 return klmdb_decode_policy(context, name, strlen(name),
907 val.mv_data, val.mv_size, policy);
908 }
909
910 static krb5_error_code
klmdb_create_policy(krb5_context context,osa_policy_ent_t policy)911 klmdb_create_policy(krb5_context context, osa_policy_ent_t policy)
912 {
913 krb5_error_code ret;
914 klmdb_context *dbc = context->dal_handle->db_context;
915 uint8_t *enc;
916 size_t len;
917
918 if (dbc == NULL)
919 return KRB5_KDB_DBNOTINITED;
920
921 ret = klmdb_encode_policy(context, policy, &enc, &len);
922 if (ret)
923 return ret;
924 ret = put(context, dbc->policy_db, policy->name, enc, len, TRUE, FALSE);
925 free(enc);
926 return ret;
927 }
928
929 static krb5_error_code
klmdb_put_policy(krb5_context context,osa_policy_ent_t policy)930 klmdb_put_policy(krb5_context context, osa_policy_ent_t policy)
931 {
932 krb5_error_code ret;
933 klmdb_context *dbc = context->dal_handle->db_context;
934 uint8_t *enc;
935 size_t len;
936
937 if (dbc == NULL)
938 return KRB5_KDB_DBNOTINITED;
939
940 ret = klmdb_encode_policy(context, policy, &enc, &len);
941 if (ret)
942 return ret;
943 ret = put(context, dbc->policy_db, policy->name, enc, len, FALSE, TRUE);
944 free(enc);
945 return ret;
946 }
947
948 static krb5_error_code
klmdb_iter_policy(krb5_context context,char * match_entry,osa_adb_iter_policy_func func,void * arg)949 klmdb_iter_policy(krb5_context context, char *match_entry,
950 osa_adb_iter_policy_func func, void *arg)
951 {
952 krb5_error_code ret;
953 klmdb_context *dbc = context->dal_handle->db_context;
954 osa_policy_ent_t pol;
955 MDB_txn *txn = NULL;
956 MDB_cursor *cursor = NULL;
957 MDB_val key, val;
958 int err;
959
960 if (dbc == NULL)
961 return KRB5_KDB_DBNOTINITED;
962
963 err = mdb_txn_begin(dbc->env, NULL, MDB_RDONLY, &txn);
964 if (err)
965 goto lmdb_error;
966 err = mdb_cursor_open(txn, dbc->policy_db, &cursor);
967 if (err)
968 goto lmdb_error;
969 for (;;) {
970 err = mdb_cursor_get(cursor, &key, &val, MDB_NEXT);
971 if (err == MDB_NOTFOUND)
972 break;
973 if (err)
974 goto lmdb_error;
975 ret = klmdb_decode_policy(context, key.mv_data, key.mv_size,
976 val.mv_data, val.mv_size, &pol);
977 if (ret)
978 goto cleanup;
979 (*func)(arg, pol);
980 krb5_db_free_policy(context, pol);
981 }
982 ret = 0;
983 goto cleanup;
984
985 lmdb_error:
986 ret = klerr(context, err, _("LMDB policy iteration failure"));
987 cleanup:
988 mdb_cursor_close(cursor);
989 mdb_txn_abort(txn);
990 return ret;
991 }
992
993 static krb5_error_code
klmdb_delete_policy(krb5_context context,char * policy)994 klmdb_delete_policy(krb5_context context, char *policy)
995 {
996 klmdb_context *dbc = context->dal_handle->db_context;
997
998 if (dbc == NULL)
999 return KRB5_KDB_DBNOTINITED;
1000 return del(context, dbc->env, dbc->policy_db, policy);
1001 }
1002
1003 static krb5_error_code
klmdb_promote_db(krb5_context context,char * conf_section,char ** db_args)1004 klmdb_promote_db(krb5_context context, char *conf_section, char **db_args)
1005 {
1006 krb5_error_code ret = 0;
1007 klmdb_context *dbc = context->dal_handle->db_context;
1008 int err;
1009
1010 if (dbc == NULL)
1011 return KRB5_KDB_DBNOTINITED;
1012 if (dbc->load_txn == NULL)
1013 return EINVAL;
1014 err = mdb_txn_commit(dbc->load_txn);
1015 dbc->load_txn = NULL;
1016 if (err)
1017 ret = klerr(context, err, _("LMDB transaction commit failure"));
1018 klmdb_fini(context);
1019 return ret;
1020 }
1021
1022 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)1023 klmdb_check_policy_as(krb5_context context, krb5_kdc_req *request,
1024 krb5_db_entry *client, krb5_db_entry *server,
1025 krb5_timestamp kdc_time, const char **status,
1026 krb5_pa_data ***e_data)
1027 {
1028 krb5_error_code ret;
1029 klmdb_context *dbc = context->dal_handle->db_context;
1030
1031 if (dbc->disable_lockout)
1032 return 0;
1033
1034 ret = klmdb_lockout_check_policy(context, client, kdc_time);
1035 if (ret == KRB5KDC_ERR_CLIENT_REVOKED)
1036 *status = "LOCKED_OUT";
1037 return ret;
1038 }
1039
1040 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)1041 klmdb_audit_as_req(krb5_context context, krb5_kdc_req *request,
1042 const krb5_address *local_addr,
1043 const krb5_address *remote_addr, krb5_db_entry *client,
1044 krb5_db_entry *server, krb5_timestamp authtime,
1045 krb5_error_code status)
1046 {
1047 klmdb_context *dbc = context->dal_handle->db_context;
1048
1049 (void)klmdb_lockout_audit(context, client, authtime, status,
1050 dbc->disable_last_success, dbc->disable_lockout);
1051 }
1052
1053 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)1054 klmdb_update_lockout(krb5_context context, krb5_db_entry *entry,
1055 krb5_timestamp stamp, krb5_boolean zero_fail_count,
1056 krb5_boolean set_last_success,
1057 krb5_boolean set_last_failure)
1058 {
1059 krb5_error_code ret;
1060 klmdb_context *dbc = context->dal_handle->db_context;
1061 krb5_db_entry dummy = { 0 };
1062 uint8_t lockout[LOCKOUT_RECORD_LEN];
1063 MDB_txn *txn = NULL;
1064 MDB_val key, val;
1065 char *name = NULL;
1066 int err;
1067
1068 if (dbc == NULL)
1069 return KRB5_KDB_DBNOTINITED;
1070 if (dbc->lockout_env == NULL)
1071 return 0;
1072 if (!zero_fail_count && !set_last_success && !set_last_failure)
1073 return 0;
1074
1075 ret = krb5_unparse_name(context, entry->princ, &name);
1076 if (ret)
1077 goto cleanup;
1078 key.mv_data = name;
1079 key.mv_size = strlen(name);
1080
1081 err = mdb_txn_begin(dbc->lockout_env, NULL, 0, &txn);
1082 if (err)
1083 goto lmdb_error;
1084 /* Fetch base lockout info within txn so we update transactionally. */
1085 err = mdb_get(txn, dbc->lockout_db, &key, &val);
1086 if (!err && val.mv_size >= LOCKOUT_RECORD_LEN) {
1087 klmdb_decode_princ_lockout(context, &dummy, val.mv_data);
1088 } else {
1089 dummy.last_success = entry->last_success;
1090 dummy.last_failed = entry->last_failed;
1091 dummy.fail_auth_count = entry->fail_auth_count;
1092 }
1093
1094 if (zero_fail_count)
1095 dummy.fail_auth_count = 0;
1096 if (set_last_success)
1097 dummy.last_success = stamp;
1098 if (set_last_failure) {
1099 dummy.last_failed = stamp;
1100 dummy.fail_auth_count++;
1101 }
1102
1103 klmdb_encode_princ_lockout(context, &dummy, lockout);
1104 val.mv_data = lockout;
1105 val.mv_size = sizeof(lockout);
1106 err = mdb_put(txn, dbc->lockout_db, &key, &val, 0);
1107 if (err)
1108 goto lmdb_error;
1109 err = mdb_txn_commit(txn);
1110 txn = NULL;
1111 if (err)
1112 goto lmdb_error;
1113 goto cleanup;
1114
1115 lmdb_error:
1116 ret = klerr(context, err, _("LMDB lockout update failure"));
1117 cleanup:
1118 krb5_free_unparsed_name(context, name);
1119 mdb_txn_abort(txn);
1120 return 0;
1121 }
1122
1123 kdb_vftabl PLUGIN_SYMBOL_NAME(krb5_lmdb, kdb_function_table) = {
1124 .maj_ver = KRB5_KDB_DAL_MAJOR_VERSION,
1125 .min_ver = 0,
1126 .init_library = klmdb_lib_init,
1127 .fini_library = klmdb_lib_cleanup,
1128 .init_module = klmdb_open,
1129 .fini_module = klmdb_fini,
1130 .create = klmdb_create,
1131 .destroy = klmdb_destroy,
1132 .get_principal = klmdb_get_principal,
1133 .put_principal = klmdb_put_principal,
1134 .delete_principal = klmdb_delete_principal,
1135 .iterate = klmdb_iterate,
1136 .create_policy = klmdb_create_policy,
1137 .get_policy = klmdb_get_policy,
1138 .put_policy = klmdb_put_policy,
1139 .iter_policy = klmdb_iter_policy,
1140 .delete_policy = klmdb_delete_policy,
1141 .promote_db = klmdb_promote_db,
1142 .check_policy_as = klmdb_check_policy_as,
1143 .audit_as_req = klmdb_audit_as_req
1144 };
1145