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 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 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 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 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 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 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 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 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 471 klmdb_lib_init() 472 { 473 return 0; 474 } 475 476 static krb5_error_code 477 klmdb_lib_cleanup() 478 { 479 return 0; 480 } 481 482 static krb5_error_code 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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