xref: /freebsd/crypto/krb5/src/plugins/kdb/lmdb/kdb_lmdb.c (revision 7f2fe78b9dd5f51c821d771b63d2e096f6fd49e9)
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