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