From b1099dfa3be3ad3dc198ea2e1cc10cab15f57e84 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Fri, 16 Aug 2024 09:54:35 +1000 Subject: [PATCH] Foundations of pam/nss multi resolver This starts the support for multi-resolver operation as well as a system level nss resolver. In future we'll add the remaining support to auth system users with pam too. --- Cargo.lock | 40 +- book/src/SUMMARY.md | 1 + .../designs/unixd_multi_resolver_2024.md | 418 ++++++++ libs/client/src/lib.rs | 7 +- unix_integration/common/src/unix_proto.rs | 41 + .../resolver/src/bin/kanidm-unix.rs | 11 +- .../resolver/src/bin/kanidm_unixd.rs | 49 +- unix_integration/resolver/src/db.rs | 252 +---- .../resolver/src/idprovider/interface.rs | 115 ++- .../resolver/src/idprovider/kanidm.rs | 452 ++++++--- .../resolver/src/idprovider/mod.rs | 1 + .../resolver/src/idprovider/system.rs | 126 +++ unix_integration/resolver/src/resolver.rs | 923 +++++++----------- .../resolver/tests/cache_layer_test.rs | 199 ++-- 14 files changed, 1574 insertions(+), 1061 deletions(-) create mode 100644 book/src/developers/designs/unixd_multi_resolver_2024.md create mode 100644 unix_integration/resolver/src/idprovider/system.rs diff --git a/Cargo.lock b/Cargo.lock index a21be1637..fda90af8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -743,9 +743,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "bytemuck" -version = "1.16.1" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" [[package]] name = "byteorder" @@ -1982,7 +1982,7 @@ dependencies = [ "gix-utils", "itoa", "thiserror", - "winnow 0.6.16", + "winnow 0.6.18", ] [[package]] @@ -2026,7 +2026,7 @@ dependencies = [ "smallvec", "thiserror", "unicode-bom", - "winnow 0.6.16", + "winnow 0.6.18", ] [[package]] @@ -2183,7 +2183,7 @@ dependencies = [ "itoa", "smallvec", "thiserror", - "winnow 0.6.16", + "winnow 0.6.18", ] [[package]] @@ -2266,7 +2266,7 @@ dependencies = [ "gix-validate", "memmap2", "thiserror", - "winnow 0.6.16", + "winnow 0.6.18", ] [[package]] @@ -2604,7 +2604,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.3.0", "slab", "tokio", "tokio-util", @@ -2623,7 +2623,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.2.6", + "indexmap 2.3.0", "slab", "tokio", "tokio-util", @@ -3023,9 +3023,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -4645,7 +4645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_derive", ] @@ -5572,7 +5572,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_derive", "serde_json", @@ -5845,9 +5845,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" @@ -6099,9 +6099,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" @@ -6109,7 +6109,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "toml_datetime", "winnow 0.5.40", ] @@ -6458,7 +6458,7 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_json", "utoipa-gen", @@ -7091,9 +7091,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.16" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index bd3217826..4130f87fe 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -87,6 +87,7 @@ - [Replication Coordinator](developers/designs/replication_coordinator.md) - [Replication Design and Notes](developers/designs/replication_design_and_notes.md) - [REST Interface](developers/designs/rest_interface.md) + - [Unixd Multi Resolver 2024](developers/designs/unixd_multi_resolver_2024.md) - [Python Module](developers/python_module.md) - [RADIUS Module Development](developers/radius.md) - [Release Checklist](developers/release_checklist.md) diff --git a/book/src/developers/designs/unixd_multi_resolver_2024.md b/book/src/developers/designs/unixd_multi_resolver_2024.md new file mode 100644 index 000000000..b23d2b23d --- /dev/null +++ b/book/src/developers/designs/unixd_multi_resolver_2024.md @@ -0,0 +1,418 @@ +## Unixd MultiResolver Support + +Up until July 2024 the purpose and motivation of the Kanidm Unixd component (`unix_integration` in +the source tree) was to allow Unix-like platforms to authenticate and resolve users against a Kanidm +instance. + +However, throughout 2023 and 2024 this project has expanded in scope - from the addition of TPM +support to protect cached credentials (the first pam module to do so!), to use of the framework by +himmelblau to enable Azure AD authentication. + +We also have new features we want to add including LDAP backends (as an alternative to SSSD), the +ability to resolve local system users, as well as support for PIV and CTAP2 for desktop login. + +This has pushed the current design of the resolver to it's limits, and it's increasingly challenging +to improve it as a result. This will necesitate a major rework of the project. + +### Current Architecture + +``` + ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ + + ┌───────┐ ┌───────┐ ┌───────┐ │ ┌───────────────────┐ │ + │ │ │ │ │ │ │ │ + │ NSS │ │ PAM │ │ CLI │ │ │ Tasks Daemon │ │ + │ │ │ │ │ │ │ │ + └───────┘ └───────┘ └───────┘ │ └───────────────────┘ │ + ▲ ▲ ▲ ▲ + ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ┤ + │ ▼ ▼ ▼ │ + ┌─────────────────────────────┐ ┌───────────┐ │ + │ │ │ │ │ + │ ClientCodec │ │ Tasks │ │ + │ │ │ │ │ + └─────────────────────────────┘ └───────────┘ │ +┌ ─ ─ ─ ─ ─ ┘ ▲ ▲ + │ │ │ +│ ▼ │ + ┌───────────────┐ ┌────────────────────────────────┐ │ │ +│ │ │ │ │ │ + │ Kani Client │◀────▶│ Daemon / Event Loop │─────┘ │ +│ │ │ │ │ + └───────────────┘ └────────────────────────────────┘ │ +│ ▲ ▲ + │ │ │ +│ ▼ ▼ + ┌──────────────────┐ ┌────────┐ │ +│ │ │ │ │ + │ DB / Cache │ │ TPM │ │ +│ │ │ │ │ + └──────────────────┘ └────────┘ │ +│ + ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ── ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ +``` + +The current design treated the client as a trivial communication layer. The daemon/event loop +contained all state including if the resolver was online or offline. Additionally the TPM and +password caching operations primarily occured in the daemon layer, which limited the access of these +features to the client backend itself. + +### Future Features + +#### Files Resolver + +The ability to resolve and authenticate local users from `/etc/{passwd,group,shadow}`. The classic +mechanisms to resolve this are considered "slow" since they require a full-file-parse each +operation. + +In addition, these files are limited by their formats and can not be extended with future +authentication mechanisms like CTAP2 or PIV. + +Unixd already needs to parse these files to understand and prevent conflicts between local items and +remote ones. Extending this functionality will allow us to resolve local users from memory. + +Not only this, we need to store information *permanently* that goes beyore what /etc/passwd and +similar can store. It would be damaging to users if their CTAP2 (passkeys) were deleted randomly +on a cache clear! + +#### Local Group Extension + +An often requested feature is the ability to extend a local group with the members from a remote +group. Often people attempt to achieve this by "overloading" a group remotely such as creating a +group called "wheel" in Kanidm and then attempting to resolve it on their systems. This can +introduce issues as different distributions may have the same groups but with different gidNumbers +which can break systems, or it can cause locally configured items to be masked. + +Instead, we should allow group _extension_. A local group can be nominated for extension, and paired +to a remote group. For example this could be configured as: + +``` +[group."wheel"] +extend_from = "opensuse_wheel" +``` + +This allows the local group "wheel" to be resolved and _extended_ with the members from the remote +group `opensuse_wheel`. + +#### Multiple Resolvers + +We would like to support multiple backends simultaneously and in our source tree. This is a major +motivator of this rework as the himmelblau project wishes to contribute their client layer into our +source tree, while maintaining the bulk of their authentication code in a separate libhimmelblau +project. + +We also want to support LDAP and other resolvers too. + +The major challenge here is that this shift the cache state from the daemon to the client. This +requires each client to track it's own online/offline state and to self-handle that state machine +correctly. Since we won't allow dynamic modules this mitigates the risk as we can audit all the +source of interfaces committed to the project for correct behaviour here. + +#### Resolvers That Can't Resolve Without Authentication Attempts + +Some resolvers are unable to resolve accounts without actually attempting an authentication attempt +such as Himmelblau. This isn't a limitation of Himmelblau, but of Azure AD itself. + +This has consequences on how we performance authentication flows generally. + +#### Domain Joining of Resolvers + +Some Clients (and in the future Kanidm) need to be able to persist some state related to Domain +Joining, where the client registers to the authentication provider. This provides extra +functionality beyond the scope of this document, but a domain join work flow needs to be possible +for the providers in some manner. + +#### Encrypted Caches + +To protect caches from offline modification content should be optionally encrypted / signed in the +future. + +#### CTAP2 / TPM-PIN + +We want to allow local authentication with CTAP2 or a TPM with PIN. Both provide stronger assurances +of both who the user is, and that they are in posession of a specific cryptographic device. The +nice part of this is that they both implement hardware bruteforce protections. For soft-tpm we +can emulate this with a strict bruteforce lockout prevention mechanism. + +The weakness is that PIN's which are used on both CTAP2 and TPM, tend to be shorter, ranging from +4 to 8 characters, generally numeric. This makes them unsuitable for remote auth. + +This means for SSH without keys, we *must* use a passphrase or similar instead. We must not allow +SSH auth with PIN to a TPM as this can easily become a remote DOS via the bruteforce prevention +mechanism. + +This introduces it's own challenge - we are now juggling multiple potential credentials and need +to account for their addition and removal, as well as changing. + +Another significant challenge is that linux is heavily embedded in "passwords as the only factor" +meaning that many systems are underdeveloped like gnome keyring - this expects stacked pam modules +to unlock the keyring as it proceeds. + + +*Local Users* + +Local Users will expect on login equivalent functionality that `/etc/passwd` provides today, meaning +that local wallets and keyrings are unlocked at login. This necesitates that any CTAP2 or TPM unlock +need to be able to unlock the keyring. + +This also has implications for how users expect to interact with the feature. A user will expect that +changing their PIN will continue to allow unlock of their system. And a change of the users password +should not invalidate their existing PIN's or CTAP devices. To achieve this we will need some methods +to cryptographically protect credentials and allow these updates. + +To achieve this, we need to make the compromise that the users password must be stored in a reversible +form on the system. Without this, the various wallets/keyrings won't work. This trade is acceptable +since `pam_kanidm` is already a module that handles password material in plaintext, so having a +mechanism to securely retrieve this *while* the user is entering equivalent security material is +reasonable. + +The primary shift is that rather than storing a *kdf/hash* of the users output, we will be storing +an authenticated encrypted object where valid decryption of that object is proof that the password +matches. + +For the soft-tpm, due to PIN's short length, we will need to aggressively increase the KDF rounds +and consider HMAC of the output. + +``` + HMAC-Secret + Password PIN output + │ │ │ + │ │ │ + │ │ │ + ▼ ▼ ▼ +┌──────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ │ │ │ │ │ +│ KDF │ │ PIN Object │ │ CTAP Object │ +│ │ │ │ │ │ +└──────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ ▲ │ ▲ + │ │ │ │ │ + │ Releases │ │ + ├───────KDF value──────┴─────┼───────────────┘ │ + │ + │ │ │ + ▼ +┌──────────────────┐ │ │ +│ │ +│ Sealed Object │ │ │ +│ │─ ─ ─ ─Unlocks─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ +│ │ +└──────────────────┘ + │ + Release + Password + │ + ▼ +┌──────────────────┐ +│ │ +│pam_gnome_keyring │ +│ pam_kwallet │ +│ │ +└──────────────────┘ +``` + +*Remote Users (such as Kanidm)* + +After a lot of thinking, the conclusion we arrived at is that trying to handle password stacking for +later pam modules is out of scope at this time. + +Initially, remote users will be expected to have a password they can use to access the system. In +the future we may derive a way to distribute TPM PIN objects securely to domain joined machines. + +We may allow PINs to be set on a per-machine basis, rather than syncing them via the remote source. + +This would require that a change of the password remotely invalidates set PINs unless we think of +some way around this. + +We also think that in the case of things like password managers such as desktop wallets, these should +have passwords that are the concern of the user, not the remote auth source so that our IDM has no +knowledge of the material to unlock these. + +### Challenges + +- The order of backend resolvers needs to be stable. +- Accounts/Groups should _not_ move/flip-flop between resolvers. +- Resolvers need to uniquely identify entries in some manner. +- The ability to store persistent/permananent accounts in the DB that can _not_ be purged in a cache + clear. +- Simplicity of the interfaces for providers so that they don't need to duplicate effort. +- Ability to clear _single items_ from the cache rather than a full clear. +- Resolvers that can't pre-resolve items + +### New Architecture + +``` + ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ + + ┌───────┐ ┌───────┐ ┌───────┐ │ ┌───────────────────┐ │ + │ │ │ │ │ │ │ │ + │ NSS │ │ PAM │ │ CLI │ │ │ Tasks Daemon │ │ + │ │ │ │ │ │ │ │ + └───────┘ └───────┘ └───────┘ │ └───────────────────┘ │ + ▲ ▲ ▲ ▲ +┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ┤ + ▼ ▼ ▼ │ +│ ┌─────────────────────────────┐ ┌───────────┐ │ + │ │ │ │ +│ │ ClientCodec │ │ Tasks │ │ + ┌──────────┐ │ │ │ │ +│ │ │ └─────────────────────────────┘ └───────────┘ │ + │ Files │◀────┐ ▲ ▲ +│ │ │ │ │ │ │ + └──────────┘ │ ▼ │ +│ ┌───────────────┐│ ┌────────────────────────────────┐ │ │ + │ │└─────┤ │ │ +│ │ Kani Client │◀─┬──▶│ Daemon / Event Loop │─────┘ │ + │ │ │ │ │ +│ └───────────────┘◀─│┐ └────────────────────────────────┘ │ + ┌───────────────┐ │ ▲ +│ │ │ ││ │ │ + │ LDAP Client │◀─┤ ▼ +│ │ │ ││ ┌────────┐ ┌──────────────────┐ │ + └───────────────┘◀ ┼ │ │ │ │ +│ ┌───────────────┐ │└ ─ ─│ TPM │ │ DB / Cache │ │ + │ Himmleblau │ │ │ │ │ │ +│ │ Client │◀─┘ └────────┘ └──────────────────┘ │ + │ │ +└ ┴───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ +``` + +#### Online/Offline State Machine + +The major change that that this diagram may not clearly show is that the online/offline state +machine moves into each of the named clients (excluding files). This also has some future impacts on +things like pre-emptive item reloading and other task scheduling. This will require the backends to +"upcall" into the daemon, as the TPM transaction needs to be passed from the daemon back down to the +provider. Alternately, the provider needs to be able to register scheduled tasks into the daemon +with some generic interface. + +#### Resolution Flow + +The most important change is that with multiple resolvers we need to change how accounts resolve. In +pseudo code the "online" flow (ignoring caches) is: + +``` +if files.contains(item_id): + if item_id.is_extensible: + # This only seeks items from the providers, not files for extensibility. + item += resolver.get(item_id.extended_from) + return item + +# Providers are sorted by priority. +for provider in providers: + if provider.contains(item_id) + return item + +return None +``` + +Key points here: + +- One provider is marked as "default". +- Providers are sorted by priority from highest to lowest. +- Default always sorts as "highest". +- The default provider returns items with Name OR SPN. +- Non-default providers always return by SPN. + +Once at item is located it is then added to the cache. The provider marks the item with a cache +timeout that the cache respects. The item is also marked to which provider is the _origin_ of the +item. + +Once an item-id exists in the cache, it may only be serviced by the corresponding origin provider. +This prevents an earlier stacked provider from "hijacking" an item from another provider. Only if +the provider indicates the item no longer exists OR the cache is cleared of that item (either by +single item or full clear) can the item change provider as the item goes through the general +resolution path. + +If we consider these behaviours now with the cache, the flow becomes: + +``` +def resolve: + if files.contains(item_id): + if item_id.is_extensible: + # This only seeks items from the providers, not files for extensibility. + item += resolver.get(item_id.extended_from) + return item + + resolver.get(item_id) + + +def resolver.get: + # Hot Path + if cache.contains(item): + if item.expired: + provider = item.provider + # refresh if possible + let refreshed_item = provider.refresh(item) + match refreshed_item { + Missing => break; # Bail and let other providers have at it. + Offline => Return the cached item + Updated => item = refreshed_item + }; + + return item + + # Cold Path + # + # Providers are sorted by priority. Default provider is first. + # + # Providers are looped *only* if an item isn't already in + # the cache in some manner. + let item = { + for provider in providers: + if provider.contains(item_id) + if provider.is_default(): + item.id = name || spn + else: + item.id = spn + break item + } + + cache.add(item) + + return None +``` + +#### Cache and Database Persistence + +The existing cache has always been considered ephemeral and able to be deleted at any time. With a +move to Domain Join and other needs for long term persistence our cache must now also include +elements that are permanent. + +The existing cache of items also is highly limited by the fact that we "rolled our own" db schema +and rely on sqlite heavily. + +We should migrate to a primarily in-memory cache, where sqlite is used only for persistence. The +sqlite content should be optionally able to be encrypted by a TPM bound key. + +To obsfucate details, the sqlite db should be a single table of key:value where keys are uuids +associated to the item. The uuid is a local detail, not related to the provider. + +The cache should move to a concread based concurrent tree which will also allow us to multi-thread +the resolver for high performance deployments. Mail servers is an often requested use case for +Kanidm in this space. + +#### Extensible Entries + +Currently UserToken and GroupTokens are limited and are unable to contain provider specific keys. We +should allow a generic BTreeMap of Key:Values. This will allow providers to extend entries as +required + +#### Offline Password/Credential Caching + +The caching of credentials should move to be a provider specific mechanism supported by the presence +of extensible UserToken entries. This also allows other types of credentials to be stored that can +be consumed by the User. + +#### Alternate Credential Caching + +A usecase is that for application passwords a mail server may wish to cache and locally store the +application password. Only domain joined systems should be capable of this, and need to protect the +application password appropriately. + + + + + + + diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index c1b7aa3b5..3e3e6cff2 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -692,8 +692,11 @@ impl KanidmClient { #[cfg(any(test, debug_assertions))] if !matching { - error!("You're in debug/dev mode, so we're going to quit here."); - std::process::exit(1); + if !std::env::var("KANIDM_DEV_YOLO").is_ok() { + eprintln!("⚠️ You're in debug/dev mode, so we're going to quit here."); + eprintln!("If you really must do this, set KANIDM_DEV_YOLO=1"); + std::process::exit(1); + } } // Check is done once, mark as no longer needing to occur diff --git a/unix_integration/common/src/unix_proto.rs b/unix_integration/common/src/unix_proto.rs index 9da232a7a..7801f1437 100644 --- a/unix_integration/common/src/unix_proto.rs +++ b/unix_integration/common/src/unix_proto.rs @@ -1,14 +1,33 @@ +use crate::unix_passwd::{EtcGroup, EtcUser}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] pub struct NssUser { pub name: String, + pub uid: u32, pub gid: u32, pub gecos: String, pub homedir: String, pub shell: String, } +impl From<&T> for NssUser +where + T: AsRef, +{ + fn from(etc_user: &T) -> Self { + let etc_user = etc_user.as_ref(); + NssUser { + name: etc_user.name.clone(), + uid: etc_user.uid, + gid: etc_user.gid, + gecos: etc_user.gecos.clone(), + homedir: etc_user.homedir.clone(), + shell: etc_user.shell.clone(), + } + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct NssGroup { pub name: String, @@ -16,6 +35,20 @@ pub struct NssGroup { pub members: Vec, } +impl From<&T> for NssGroup +where + T: AsRef, +{ + fn from(etc_group: &T) -> Self { + let etc_group = etc_group.as_ref(); + NssGroup { + name: etc_group.name.clone(), + gid: etc_group.gid, + members: etc_group.members.clone(), + } + } +} + /* RFC8628: 3.2. Device Authorization Response */ #[derive(Serialize, Deserialize, Clone, Debug)] pub struct DeviceAuthorizationResponse { @@ -111,6 +144,12 @@ impl ClientRequest { } } +#[derive(Serialize, Deserialize, Debug)] +pub struct ProviderStatus { + pub name: String, + pub online: bool, +} + #[derive(Serialize, Deserialize, Debug)] pub enum ClientResponse { SshKeys(Vec), @@ -122,6 +161,8 @@ pub enum ClientResponse { PamStatus(Option), PamAuthenticateStepResponse(PamAuthResponse), + ProviderStatus(Vec), + Ok, Error, } diff --git a/unix_integration/resolver/src/bin/kanidm-unix.rs b/unix_integration/resolver/src/bin/kanidm-unix.rs index d4905b618..e43ba6ed7 100644 --- a/unix_integration/resolver/src/bin/kanidm-unix.rs +++ b/unix_integration/resolver/src/bin/kanidm-unix.rs @@ -102,6 +102,7 @@ async fn main() -> ExitCode { | ClientResponse::NssAccount(_) | ClientResponse::NssGroup(_) | ClientResponse::NssGroups(_) + | ClientResponse::ProviderStatus(_) | ClientResponse::Ok | ClientResponse::Error | ClientResponse::PamStatus(_) => { @@ -228,7 +229,15 @@ async fn main() -> ExitCode { } else { match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await { Ok(r) => match r { - ClientResponse::Ok => println!("working!"), + ClientResponse::ProviderStatus(results) => { + for provider in results { + println!( + "{}: {}", + provider.name, + if provider.online { "online" } else { "offline" } + ); + } + } _ => { error!("Error: unexpected response -> {:?}", r); } diff --git a/unix_integration/resolver/src/bin/kanidm_unixd.rs b/unix_integration/resolver/src/bin/kanidm_unixd.rs index 14eec06a0..b0ed140c1 100644 --- a/unix_integration/resolver/src/bin/kanidm_unixd.rs +++ b/unix_integration/resolver/src/bin/kanidm_unixd.rs @@ -19,7 +19,7 @@ use std::path::{Path, PathBuf}; use std::process::ExitCode; use std::str::FromStr; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, SystemTime}; use bytes::{BufMut, BytesMut}; use clap::{Arg, ArgAction, Command}; @@ -31,6 +31,7 @@ use kanidm_unix_common::unix_passwd::{parse_etc_group, parse_etc_passwd}; use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse, TaskRequest, TaskResponse}; use kanidm_unix_resolver::db::{Cache, Db}; use kanidm_unix_resolver::idprovider::kanidm::KanidmProvider; +use kanidm_unix_resolver::idprovider::system::SystemProvider; use kanidm_unix_resolver::resolver::Resolver; use kanidm_unix_resolver::unix_config::{HsmType, KanidmUnixdConfig}; @@ -409,11 +410,8 @@ async fn handle_client( } ClientRequest::Status => { debug!("status check"); - if cachelayer.test_connection().await { - ClientResponse::Ok - } else { - ClientResponse::Error - } + let status = cachelayer.provider_status().await; + ClientResponse::ProviderStatus(status) } }; reqs.send(resp).await?; @@ -447,12 +445,7 @@ async fn process_etc_passwd_group(cachelayer: &Resolver) -> Result<(), Box ExitCode { } }; - let idprovider = KanidmProvider::new(rsclient); - let db = match Db::new(cfg.db_path.as_str()) { Ok(db) => db, Err(_e) => { @@ -837,6 +828,8 @@ async fn main() -> ExitCode { // Check for and create the hsm pin if required. if let Err(err) = write_hsm_pin(cfg.hsm_pin_path.as_str()).await { + let diag = kanidm_lib_file_permissions::diagnose_path(cfg.hsm_pin_path.as_ref()); + info!(%diag); error!(?err, "Failed to create HSM PIN into {}", cfg.hsm_pin_path.as_str()); return ExitCode::FAILURE }; @@ -845,6 +838,8 @@ async fn main() -> ExitCode { let hsm_pin = match read_hsm_pin(cfg.hsm_pin_path.as_str()).await { Ok(hp) => hp, Err(err) => { + let diag = kanidm_lib_file_permissions::diagnose_path(cfg.hsm_pin_path.as_ref()); + info!(%diag); error!(?err, "Failed to read HSM PIN from {}", cfg.hsm_pin_path.as_str()); return ExitCode::FAILURE } @@ -910,6 +905,25 @@ async fn main() -> ExitCode { } }; + let Ok(system_provider) = SystemProvider::new( + ) else { + error!("Failed to configure System Provider"); + return ExitCode::FAILURE + }; + + let Ok(idprovider) = KanidmProvider::new( + rsclient, + SystemTime::now(), + &mut (&mut db_txn).into(), + &mut hsm, + &machine_key + ) else { + error!("Failed to configure Kanidm Provider"); + return ExitCode::FAILURE + }; + + drop(machine_key); + if let Err(err) = db_txn.commit() { error!(?err, "Failed to commit database transaction, unable to proceed"); return ExitCode::FAILURE @@ -926,9 +940,9 @@ async fn main() -> ExitCode { let cl_inner = match Resolver::new( db, - Box::new(idprovider), + Arc::new(system_provider), + Arc::new(idprovider), hsm, - machine_key, cfg.cache_timeout, cfg.pam_allowed_login_groups.clone(), cfg.default_shell.clone(), @@ -937,7 +951,6 @@ async fn main() -> ExitCode { cfg.home_alias, cfg.uid_attr_map, cfg.gid_attr_map, - cfg.allow_local_account_override.clone(), ) .await { @@ -955,6 +968,8 @@ async fn main() -> ExitCode { let task_listener = match UnixListener::bind(cfg.task_sock_path.as_str()) { Ok(l) => l, Err(_e) => { + let diag = kanidm_lib_file_permissions::diagnose_path(cfg.task_sock_path.as_ref()); + info!(%diag); error!("Failed to bind UNIX socket {}", cfg.task_sock_path.as_str()); return ExitCode::FAILURE } diff --git a/unix_integration/resolver/src/db.rs b/unix_integration/resolver/src/db.rs index f9c5f9a27..7d4d8e6ab 100644 --- a/unix_integration/resolver/src/db.rs +++ b/unix_integration/resolver/src/db.rs @@ -1,12 +1,8 @@ use std::convert::TryFrom; use std::fmt; -use std::time::Duration; use crate::idprovider::interface::{GroupToken, Id, UserToken}; use async_trait::async_trait; -use kanidm_lib_crypto::CryptoPolicy; -use kanidm_lib_crypto::DbPasswordV1; -use kanidm_lib_crypto::Password; use libc::umask; use rusqlite::{Connection, OptionalExtension}; use tokio::sync::{Mutex, MutexGuard}; @@ -14,7 +10,7 @@ use uuid::Uuid; use serde::{de::DeserializeOwned, Serialize}; -use kanidm_hsm_crypto::{HmacKey, LoadableHmacKey, LoadableMachineKey, Tpm}; +use kanidm_hsm_crypto::{LoadableHmacKey, LoadableMachineKey}; const DBV_MAIN: &str = "main"; @@ -49,13 +45,11 @@ pub enum CacheError { pub struct Db { conn: Mutex, - crypto_policy: CryptoPolicy, } pub struct DbTxn<'a> { conn: MutexGuard<'a, Connection>, committed: bool, - crypto_policy: &'a CryptoPolicy, } pub struct KeyStoreTxn<'a, 'b> { @@ -83,15 +77,9 @@ impl Db { DbError::Sqlite })?; let _ = unsafe { umask(before) }; - // We only build a single thread. If we need more than one, we'll - // need to re-do this to account for path = "" for debug. - let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(250)); - - debug!("Configured {:?}", crypto_policy); Ok(Db { conn: Mutex::new(conn), - crypto_policy, }) } } @@ -103,7 +91,7 @@ impl Cache for Db { #[allow(clippy::expect_used)] async fn write<'db>(&'db self) -> Self::Txn<'db> { let conn = self.conn.lock().await; - DbTxn::new(conn, &self.crypto_policy) + DbTxn::new(conn) } } @@ -114,16 +102,15 @@ impl fmt::Debug for Db { } impl<'a> DbTxn<'a> { - fn new(conn: MutexGuard<'a, Connection>, crypto_policy: &'a CryptoPolicy) -> Self { + fn new(conn: MutexGuard<'a, Connection>) -> Self { // Start the transaction - // debug!("Starting db WR txn ..."); + // trace!("Starting db WR txn ..."); #[allow(clippy::expect_used)] conn.execute("BEGIN TRANSACTION", []) .expect("Unable to begin transaction!"); DbTxn { committed: false, conn, - crypto_policy, } } @@ -324,7 +311,7 @@ impl<'a> DbTxn<'a> { ":value": &data, }) .map(|r| { - debug!("insert -> {:?}", r); + trace!("insert -> {:?}", r); }) .map_err(|e| self.sqlite_error("execute", &e)) } @@ -545,7 +532,7 @@ impl<'a> DbTxn<'a> { ":value": &data, }) .map(|r| { - debug!("insert -> {:?}", r); + trace!("insert -> {:?}", r); }) .map_err(|e| self.sqlite_error("execute", &e)) } @@ -587,7 +574,7 @@ impl<'a> DbTxn<'a> { ":value": &data, }) .map(|r| { - debug!("insert -> {:?}", r); + trace!("insert -> {:?}", r); }) .map_err(|e| self.sqlite_error("execute", &e)) } @@ -715,7 +702,7 @@ impl<'a> DbTxn<'a> { ":expiry": &expire, }) .map(|r| { - debug!("insert -> {:?}", r); + trace!("insert -> {:?}", r); }) .map_err(|error| self.sqlite_transaction_error(&error, &stmt))?; } @@ -730,7 +717,7 @@ impl<'a> DbTxn<'a> { stmt.execute([&account_uuid]) .map(|r| { - debug!("delete memberships -> {:?}", r); + trace!("delete memberships -> {:?}", r); }) .map_err(|error| self.sqlite_transaction_error(&error, &stmt))?; @@ -745,7 +732,7 @@ impl<'a> DbTxn<'a> { ":g_uuid": &g.uuid.as_hyphenated().to_string(), }) .map(|r| { - debug!("insert membership -> {:?}", r); + trace!("insert membership -> {:?}", r); }) .map_err(|error| self.sqlite_transaction_error(&error, &stmt)) }) @@ -771,88 +758,6 @@ impl<'a> DbTxn<'a> { .map_err(|e| self.sqlite_error("account_t delete", &e)) } - pub fn update_account_password( - &mut self, - a_uuid: Uuid, - cred: &str, - hsm: &mut dyn Tpm, - hmac_key: &HmacKey, - ) -> Result<(), CacheError> { - let pw = - Password::new_argon2id_hsm(self.crypto_policy, cred, hsm, hmac_key).map_err(|e| { - error!("password error -> {:?}", e); - CacheError::Cryptography - })?; - - let dbpw = pw.to_dbpasswordv1(); - let data = serde_json::to_vec(&dbpw).map_err(|e| { - error!("json error -> {:?}", e); - CacheError::SerdeJson - })?; - - self.conn - .execute( - "UPDATE account_t SET password = :data WHERE uuid = :a_uuid", - named_params! { - ":a_uuid": &a_uuid.as_hyphenated().to_string(), - ":data": &data, - }, - ) - .map_err(|e| self.sqlite_error("update account_t password", &e)) - .map(|_| ()) - } - - pub fn check_account_password( - &mut self, - a_uuid: Uuid, - cred: &str, - hsm: &mut dyn Tpm, - hmac_key: &HmacKey, - ) -> Result { - let mut stmt = self - .conn - .prepare("SELECT password FROM account_t WHERE uuid = :a_uuid AND password IS NOT NULL") - .map_err(|e| self.sqlite_error("select prepare", &e))?; - - // Makes tuple (token, expiry) - let data_iter = stmt - .query_map([a_uuid.as_hyphenated().to_string()], |row| row.get(0)) - .map_err(|e| self.sqlite_error("query_map", &e))?; - let data: Result>, _> = data_iter - .map(|v| v.map_err(|e| self.sqlite_error("map", &e))) - .collect(); - - let data = data?; - - if data.is_empty() { - info!("No cached password, failing authentication"); - return Ok(false); - } - - if data.len() >= 2 { - error!("invalid db state, multiple entries matched query?"); - return Err(CacheError::TooManyResults); - } - - let pw = data.first().map(|raw| { - // Map the option from data.first. - let dbpw: DbPasswordV1 = serde_json::from_slice(raw.as_slice()).map_err(|e| { - error!("json error -> {:?}", e); - })?; - Password::try_from(dbpw) - }); - - let pw = match pw { - Some(Ok(p)) => p, - _ => return Ok(false), - }; - - pw.verify_ctx(cred, Some((hsm, hmac_key))).map_err(|e| { - error!("password error -> {:?}", e); - CacheError::Cryptography - }) - } - pub fn get_group(&mut self, grp_id: &Id) -> Result, CacheError> { let data = match grp_id { Id::Name(n) => self.get_group_data_name(n.as_str()), @@ -907,7 +812,7 @@ impl<'a> DbTxn<'a> { data.iter() .map(|token| { // token convert with json. - // debug!("{:?}", token); + // trace!("{:?}", token); serde_json::from_slice(token.as_slice()).map_err(|e| { error!("json error -> {:?}", e); CacheError::SerdeJson @@ -935,7 +840,7 @@ impl<'a> DbTxn<'a> { .iter() .filter_map(|token| { // token convert with json. - // debug!("{:?}", token); + // trace!("{:?}", token); serde_json::from_slice(token.as_slice()) .map_err(|e| { error!("json error -> {:?}", e); @@ -971,7 +876,7 @@ impl<'a> DbTxn<'a> { ":expiry": &expire, }) .map(|r| { - debug!("insert -> {:?}", r); + trace!("insert -> {:?}", r); }) .map_err(|e| self.sqlite_error("execute", &e)) } @@ -1002,7 +907,7 @@ impl<'a> Drop for DbTxn<'a> { // Abort fn drop(&mut self) { if !self.committed { - // debug!("Aborting BE WR txn"); + // trace!("Aborting BE WR txn"); #[allow(clippy::expect_used)] self.conn .execute("ROLLBACK TRANSACTION", []) @@ -1013,25 +918,8 @@ impl<'a> Drop for DbTxn<'a> { #[cfg(test)] mod tests { - use super::{Cache, Db}; use crate::idprovider::interface::{GroupToken, Id, ProviderOrigin, UserToken}; - use kanidm_hsm_crypto::{AuthValue, Tpm}; - - const TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test"; - const TESTACCOUNT1_PASSWORD_B: &str = "password b for account1 test"; - - #[cfg(feature = "tpm")] - fn setup_tpm() -> Box { - use kanidm_hsm_crypto::tpm::TpmTss; - Box::new(TpmTss::new("device:/dev/tpmrm0").expect("Unable to build Tpm Context")) - } - - #[cfg(not(feature = "tpm"))] - fn setup_tpm() -> Box { - use kanidm_hsm_crypto::soft::SoftTpm; - Box::new(SoftTpm::new()) - } #[tokio::test] async fn test_cache_db_account_basic() { @@ -1041,7 +929,7 @@ mod tests { assert!(dbtxn.migrate().is_ok()); let mut ut1 = UserToken { - provider: ProviderOrigin::Files, + provider: ProviderOrigin::System, name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), @@ -1051,6 +939,7 @@ mod tests { groups: Vec::new(), sshkeys: vec!["key-a".to_string()], valid: true, + extra_keys: Default::default(), }; let id_name = Id::Name("testuser".to_string()); @@ -1126,11 +1015,12 @@ mod tests { assert!(dbtxn.migrate().is_ok()); let mut gt1 = GroupToken { - provider: ProviderOrigin::Files, + provider: ProviderOrigin::System, name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2000, uuid: uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"), + extra_keys: Default::default(), }; let id_name = Id::Name("testgroup".to_string()); @@ -1202,23 +1092,25 @@ mod tests { assert!(dbtxn.migrate().is_ok()); let gt1 = GroupToken { - provider: ProviderOrigin::Files, + provider: ProviderOrigin::System, name: "testuser".to_string(), spn: "testuser@example.com".to_string(), gidnumber: 2000, uuid: uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"), + extra_keys: Default::default(), }; let gt2 = GroupToken { - provider: ProviderOrigin::Files, + provider: ProviderOrigin::System, name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2001, uuid: uuid::uuid!("b500be97-8552-42a5-aca0-668bc5625705"), + extra_keys: Default::default(), }; let mut ut1 = UserToken { - provider: ProviderOrigin::Files, + provider: ProviderOrigin::System, name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), @@ -1228,6 +1120,7 @@ mod tests { groups: vec![gt1.clone(), gt2], sshkeys: vec!["key-a".to_string()], valid: true, + extra_keys: Default::default(), }; // First, add the groups. @@ -1265,91 +1158,6 @@ mod tests { assert!(dbtxn.commit().is_ok()); } - #[tokio::test] - async fn test_cache_db_account_password() { - sketching::test_init(); - - let db = Db::new("").expect("failed to create."); - - let mut dbtxn = db.write().await; - assert!(dbtxn.migrate().is_ok()); - - let mut hsm = setup_tpm(); - - let auth_value = AuthValue::ephemeral().unwrap(); - - let loadable_machine_key = hsm.machine_key_create(&auth_value).unwrap(); - let machine_key = hsm - .machine_key_load(&auth_value, &loadable_machine_key) - .unwrap(); - - let loadable_hmac_key = hsm.hmac_key_create(&machine_key).unwrap(); - let hmac_key = hsm.hmac_key_load(&machine_key, &loadable_hmac_key).unwrap(); - - let uuid1 = uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"); - let mut ut1 = UserToken { - provider: ProviderOrigin::Files, - name: "testuser".to_string(), - spn: "testuser@example.com".to_string(), - displayname: "Test User".to_string(), - gidnumber: 2000, - uuid: uuid1, - shell: None, - groups: Vec::new(), - sshkeys: vec!["key-a".to_string()], - valid: true, - }; - - // Test that with no account, is false - assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A, &mut *hsm, &hmac_key), - Ok(false) - )); - // test adding an account - dbtxn.update_account(&ut1, 0).unwrap(); - // check with no password is false. - assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A, &mut *hsm, &hmac_key), - Ok(false) - )); - // update the pw - assert!(dbtxn - .update_account_password(uuid1, TESTACCOUNT1_PASSWORD_A, &mut *hsm, &hmac_key) - .is_ok()); - // Check it now works. - assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A, &mut *hsm, &hmac_key), - Ok(true) - )); - assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B, &mut *hsm, &hmac_key), - Ok(false) - )); - // Update the pw - assert!(dbtxn - .update_account_password(uuid1, TESTACCOUNT1_PASSWORD_B, &mut *hsm, &hmac_key) - .is_ok()); - // Check it matches. - assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A, &mut *hsm, &hmac_key), - Ok(false) - )); - assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B, &mut *hsm, &hmac_key), - Ok(true) - )); - - // Check that updating the account does not break the password. - ut1.displayname = "Test User Update".to_string(); - dbtxn.update_account(&ut1, 0).unwrap(); - assert!(matches!( - dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B, &mut *hsm, &hmac_key), - Ok(true) - )); - - assert!(dbtxn.commit().is_ok()); - } - #[tokio::test] async fn test_cache_db_group_rename_duplicate() { sketching::test_init(); @@ -1358,19 +1166,21 @@ mod tests { assert!(dbtxn.migrate().is_ok()); let mut gt1 = GroupToken { - provider: ProviderOrigin::Files, + provider: ProviderOrigin::System, name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2000, uuid: uuid::uuid!("0302b99c-f0f6-41ab-9492-852692b0fd16"), + extra_keys: Default::default(), }; let gt2 = GroupToken { - provider: ProviderOrigin::Files, + provider: ProviderOrigin::System, name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2001, uuid: uuid::uuid!("799123b2-3802-4b19-b0b8-1ffae2aa9a4b"), + extra_keys: Default::default(), }; let id_name = Id::Name("testgroup".to_string()); @@ -1415,7 +1225,7 @@ mod tests { assert!(dbtxn.migrate().is_ok()); let mut ut1 = UserToken { - provider: ProviderOrigin::Files, + provider: ProviderOrigin::System, name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), @@ -1425,10 +1235,11 @@ mod tests { groups: Vec::new(), sshkeys: vec!["key-a".to_string()], valid: true, + extra_keys: Default::default(), }; let ut2 = UserToken { - provider: ProviderOrigin::Files, + provider: ProviderOrigin::System, name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), @@ -1438,6 +1249,7 @@ mod tests { groups: Vec::new(), sshkeys: vec!["key-a".to_string()], valid: true, + extra_keys: Default::default(), }; let id_name = Id::Name("testuser".to_string()); diff --git a/unix_integration/resolver/src/idprovider/interface.rs b/unix_integration/resolver/src/idprovider/interface.rs index ae99127d6..327ff0037 100644 --- a/unix_integration/resolver/src/idprovider/interface.rs +++ b/unix_integration/resolver/src/idprovider/interface.rs @@ -1,12 +1,17 @@ -use crate::db::KeyStoreTxn; use async_trait::async_trait; use kanidm_unix_common::unix_proto::{ DeviceAuthorizationResponse, PamAuthRequest, PamAuthResponse, }; use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fmt; +use std::time::SystemTime; use tokio::sync::broadcast; use uuid::Uuid; +pub type XKeyId = String; + pub use kanidm_hsm_crypto as tpm; /// Errors that the IdProvider may return. These drive the resolver state machine @@ -33,22 +38,59 @@ pub enum IdpError { Tpm, } +pub enum UserTokenState { + /// Indicate to the resolver that the cached UserToken should be used, if present. + UseCached, + /// The requested entity is not found, or has been removed. + NotFound, + + /// Update the cache state with the data found in this UserToken. + Update(UserToken), +} + +pub enum GroupTokenState { + /// Indicate to the resolver that the cached GroupToken should be used, if present. + UseCached, + /// The requested entity is not found, or has been removed. + NotFound, + + /// Update the cache state with the data found in this GroupToken. + Update(GroupToken), +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Id { Name(String), Gid(u32), } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, Eq, PartialEq, Hash)] pub enum ProviderOrigin { // To allow transition, we have an ignored type that effectively // causes these items to be nixed. #[default] Ignore, - Files, + /// Provided by /etc/passwd or /etc/group + System, Kanidm, } +impl fmt::Display for ProviderOrigin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProviderOrigin::Ignore => { + write!(f, "Ignored") + } + ProviderOrigin::System => { + write!(f, "System") + } + ProviderOrigin::Kanidm => { + write!(f, "Kanidm") + } + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GroupToken { #[serde(default)] @@ -57,12 +99,16 @@ pub struct GroupToken { pub spn: String, pub uuid: Uuid, pub gidnumber: u32, + + #[serde(flatten)] + pub extra_keys: BTreeMap, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UserToken { #[serde(default)] pub provider: ProviderOrigin, + pub name: String, pub spn: String, pub uuid: Uuid, @@ -70,10 +116,16 @@ pub struct UserToken { pub displayname: String, pub shell: Option, pub groups: Vec, + // Could there be a better type here? pub sshkeys: Vec, // Defaults to false. pub valid: bool, + + // These are opaque extra keys that the provider can interpret for internal + // functions. + #[serde(flatten)] + pub extra_keys: BTreeMap, } #[derive(Debug)] @@ -147,22 +199,21 @@ pub enum AuthResult { Next(AuthRequest), } -pub enum AuthCacheAction { - None, - PasswordHashUpdate { cred: String }, -} - #[async_trait] #[allow(clippy::too_many_arguments)] pub trait IdProvider { - async fn configure_hsm_keys( - &self, - _keystore: &mut KeyStoreTxn, - _tpm: &mut tpm::BoxedDynTpm, - _machine_key: &tpm::MachineKey, - ) -> Result<(), IdpError> { - Ok(()) - } + /// Retrieve this providers origin + fn origin(&self) -> ProviderOrigin; + + /// Attempt to go online *immediately* + async fn attempt_online(&self, _tpm: &mut tpm::BoxedDynTpm, _now: SystemTime) -> bool; + + /// Mark that this provider should attempt to go online next time it + /// recieves a request + async fn mark_next_check(&self, _now: SystemTime); + + /// Force this provider offline immediately. + async fn mark_offline(&self); /// This is similar to a "domain join" process. What do we actually need to pass here /// for this to work for kanidm or himmelblau? Should we make it take a generic? @@ -177,23 +228,19 @@ pub trait IdProvider { } */ - async fn provider_authenticate(&self, _tpm: &mut tpm::BoxedDynTpm) -> Result<(), IdpError>; - async fn unix_user_get( &self, _id: &Id, _token: Option<&UserToken>, _tpm: &mut tpm::BoxedDynTpm, - _machine_key: &tpm::MachineKey, - ) -> Result; + _now: SystemTime, + ) -> Result; async fn unix_user_online_auth_init( &self, _account_id: &str, - _token: Option<&UserToken>, - _keystore: &mut KeyStoreTxn, + _token: &UserToken, _tpm: &mut tpm::BoxedDynTpm, - _machine_key: &tpm::MachineKey, _shutdown_rx: &broadcast::Receiver<()>, ) -> Result<(AuthRequest, AuthCredHandler), IdpError>; @@ -202,17 +249,20 @@ pub trait IdProvider { _account_id: &str, _cred_handler: &mut AuthCredHandler, _pam_next_req: PamAuthRequest, - _keystore: &mut KeyStoreTxn, _tpm: &mut tpm::BoxedDynTpm, - _machine_key: &tpm::MachineKey, _shutdown_rx: &broadcast::Receiver<()>, - ) -> Result<(AuthResult, AuthCacheAction), IdpError>; + ) -> Result; + + async fn unix_unknown_user_online_auth_init( + &self, + _account_id: &str, + _tpm: &mut tpm::BoxedDynTpm, + _shutdown_rx: &broadcast::Receiver<()>, + ) -> Result, IdpError>; async fn unix_user_offline_auth_init( &self, - _account_id: &str, - _token: Option<&UserToken>, - _keystore: &mut KeyStoreTxn, + _token: &UserToken, ) -> Result<(AuthRequest, AuthCredHandler), IdpError>; // I thought about this part of the interface a lot. we could have the @@ -236,19 +286,16 @@ pub trait IdProvider { // TPM key. async fn unix_user_offline_auth_step( &self, - _account_id: &str, _token: &UserToken, _cred_handler: &mut AuthCredHandler, _pam_next_req: PamAuthRequest, - _keystore: &mut KeyStoreTxn, _tpm: &mut tpm::BoxedDynTpm, - _machine_key: &tpm::MachineKey, - _online_at_init: bool, ) -> Result; async fn unix_group_get( &self, id: &Id, _tpm: &mut tpm::BoxedDynTpm, - ) -> Result; + _now: SystemTime, + ) -> Result; } diff --git a/unix_integration/resolver/src/idprovider/kanidm.rs b/unix_integration/resolver/src/idprovider/kanidm.rs index f5cc9e62d..6a691bc5b 100644 --- a/unix_integration/resolver/src/idprovider/kanidm.rs +++ b/unix_integration/resolver/src/idprovider/kanidm.rs @@ -3,36 +3,96 @@ use async_trait::async_trait; use kanidm_client::{ClientError, KanidmClient, StatusCode}; use kanidm_proto::internal::OperationError; use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; -use tokio::sync::{broadcast, RwLock}; +use std::time::{Duration, SystemTime}; +use tokio::sync::{broadcast, Mutex}; + +use kanidm_lib_crypto::CryptoPolicy; +use kanidm_lib_crypto::DbPasswordV1; +use kanidm_lib_crypto::Password; use super::interface::{ - // KeyStore, - tpm, - tpm::Tpm, - AuthCacheAction, - AuthCredHandler, - AuthRequest, - AuthResult, - GroupToken, - Id, - IdProvider, - IdpError, - ProviderOrigin, - UserToken, + tpm::{self, HmacKey, Tpm}, + AuthCredHandler, AuthRequest, AuthResult, GroupToken, GroupTokenState, Id, IdProvider, + IdpError, ProviderOrigin, UserToken, UserTokenState, }; use kanidm_unix_common::unix_proto::PamAuthRequest; -const TAG_IDKEY: &str = "idkey"; +const KANIDM_HMAC_KEY: &str = "kanidm-hmac-key"; +const KANIDM_PWV1_KEY: &str = "kanidm-pw-v1"; + +const OFFLINE_NEXT_CHECK: Duration = Duration::from_secs(60); + +#[derive(Debug, Clone)] +enum CacheState { + Online, + Offline, + OfflineNextCheck(SystemTime), +} + +struct KanidmProviderInternal { + state: CacheState, + client: KanidmClient, + hmac_key: HmacKey, + crypto_policy: CryptoPolicy, +} pub struct KanidmProvider { - client: RwLock, + inner: Mutex, } impl KanidmProvider { - pub fn new(client: KanidmClient) -> Self { - KanidmProvider { - client: RwLock::new(client), - } + pub fn new( + client: KanidmClient, + now: SystemTime, + keystore: &mut KeyStoreTxn, + tpm: &mut tpm::BoxedDynTpm, + machine_key: &tpm::MachineKey, + ) -> Result { + // FUTURE: Randomised jitter on next check at startup. + + // Initially retrieve our HMAC key. + let loadable_hmac_key: Option = keystore + .get_tagged_hsm_key(KANIDM_HMAC_KEY) + .map_err(|ks_err| { + error!(?ks_err); + IdpError::KeyStore + })?; + + let loadable_hmac_key = if let Some(loadable_hmac_key) = loadable_hmac_key { + loadable_hmac_key + } else { + let loadable_hmac_key = tpm.hmac_key_create(machine_key).map_err(|tpm_err| { + error!(?tpm_err); + IdpError::Tpm + })?; + + keystore + .insert_tagged_hsm_key(KANIDM_HMAC_KEY, &loadable_hmac_key) + .map_err(|ks_err| { + error!(?ks_err); + IdpError::KeyStore + })?; + + loadable_hmac_key + }; + + let hmac_key = tpm + .hmac_key_load(machine_key, &loadable_hmac_key) + .map_err(|tpm_err| { + error!(?tpm_err); + IdpError::Tpm + })?; + + let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(250)); + + Ok(KanidmProvider { + inner: Mutex::new(KanidmProviderInternal { + state: CacheState::OfflineNextCheck(now), + client, + hmac_key, + crypto_policy, + }), + }) } } @@ -63,6 +123,7 @@ impl From for UserToken { groups, sshkeys, valid, + extra_keys: Default::default(), } } } @@ -82,73 +143,174 @@ impl From for GroupToken { spn, uuid, gidnumber, + extra_keys: Default::default(), + } + } +} + +impl UserToken { + pub fn kanidm_update_cached_password( + &mut self, + crypto_policy: &CryptoPolicy, + cred: &str, + tpm: &mut tpm::BoxedDynTpm, + hmac_key: &HmacKey, + ) { + let pw = match Password::new_argon2id_hsm(crypto_policy, cred, tpm, hmac_key) { + Ok(pw) => pw, + Err(reason) => { + // Clear cached pw. + self.extra_keys.remove(KANIDM_PWV1_KEY); + warn!( + ?reason, + "unable to apply kdf to password, clearing cached password." + ); + return; + } + }; + + let pw_value = match serde_json::to_value(pw.to_dbpasswordv1()) { + Ok(pw) => pw, + Err(reason) => { + // Clear cached pw. + self.extra_keys.remove(KANIDM_PWV1_KEY); + warn!( + ?reason, + "unable to serialise credential, clearing cached password." + ); + return; + } + }; + + self.extra_keys.insert(KANIDM_PWV1_KEY.into(), pw_value); + debug!(spn = %self.spn, "Updated cached pw"); + } + + pub fn kanidm_check_cached_password( + &self, + cred: &str, + tpm: &mut tpm::BoxedDynTpm, + hmac_key: &HmacKey, + ) -> bool { + let pw_value = match self.extra_keys.get(KANIDM_PWV1_KEY) { + Some(pw_value) => pw_value, + None => { + debug!(spn = %self.spn, "no cached pw available"); + return false; + } + }; + + let dbpw = match serde_json::from_value::(pw_value.clone()) { + Ok(dbpw) => dbpw, + Err(reason) => { + warn!(spn = %self.spn, ?reason, "unable to deserialise credential"); + return false; + } + }; + + let pw = match Password::try_from(dbpw) { + Ok(pw) => pw, + Err(reason) => { + warn!(spn = %self.spn, ?reason, "unable to process credential"); + return false; + } + }; + + pw.verify_ctx(cred, Some((tpm, hmac_key))) + .unwrap_or_default() + } +} + +impl KanidmProviderInternal { + async fn check_online(&mut self, tpm: &mut tpm::BoxedDynTpm, now: SystemTime) -> bool { + match self.state { + // Proceed + CacheState::Online => true, + CacheState::OfflineNextCheck(at_time) if now >= at_time => { + // Attempt online. If fails, return token. + self.attempt_online(tpm, now).await + } + CacheState::OfflineNextCheck(_) | CacheState::Offline => false, + } + } + + async fn attempt_online(&mut self, _tpm: &mut tpm::BoxedDynTpm, now: SystemTime) -> bool { + match self.client.auth_anonymous().await { + Ok(_uat) => { + self.state = CacheState::Online; + true + } + Err(ClientError::Transport(err)) => { + warn!(?err, "transport failure"); + self.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK); + false + } + Err(err) => { + error!(?err, "Provider authentication failed"); + self.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK); + false + } } } } #[async_trait] impl IdProvider for KanidmProvider { - async fn configure_hsm_keys( - &self, - keystore: &mut KeyStoreTxn, - tpm: &mut tpm::BoxedDynTpm, - machine_key: &tpm::MachineKey, - ) -> Result<(), IdpError> { - let id_key: Option = - keystore.get_tagged_hsm_key(TAG_IDKEY).map_err(|ks_err| { - error!(?ks_err); - IdpError::KeyStore - })?; - - if id_key.is_none() { - let loadable_id_key = tpm - .identity_key_create(machine_key, None, tpm::KeyAlgorithm::Ecdsa256) - .map_err(|tpm_err| { - error!(?tpm_err); - IdpError::Tpm - })?; - - keystore - .insert_tagged_hsm_key(TAG_IDKEY, &loadable_id_key) - .map_err(|ks_err| { - error!(?ks_err); - IdpError::KeyStore - })?; - } - - Ok(()) + fn origin(&self) -> ProviderOrigin { + ProviderOrigin::Kanidm } - // Needs .read on all types except re-auth. - async fn provider_authenticate(&self, _tpm: &mut tpm::BoxedDynTpm) -> Result<(), IdpError> { - match self.client.write().await.auth_anonymous().await { - Ok(_uat) => Ok(()), - Err(err) => { - error!(?err, "Provider authentication failed"); - Err(IdpError::ProviderUnauthorised) - } - } + async fn attempt_online(&self, tpm: &mut tpm::BoxedDynTpm, now: SystemTime) -> bool { + let mut inner = self.inner.lock().await; + inner.check_online(tpm, now).await + } + + async fn mark_next_check(&self, now: SystemTime) { + let mut inner = self.inner.lock().await; + inner.state = CacheState::OfflineNextCheck(now); + } + + async fn mark_offline(&self) { + let mut inner = self.inner.lock().await; + inner.state = CacheState::Offline; } async fn unix_user_get( &self, id: &Id, - _token: Option<&UserToken>, - _tpm: &mut tpm::BoxedDynTpm, - _machine_key: &tpm::MachineKey, - ) -> Result { - match self + token: Option<&UserToken>, + tpm: &mut tpm::BoxedDynTpm, + now: SystemTime, + ) -> Result { + let mut inner = self.inner.lock().await; + + if !inner.check_online(tpm, now).await { + // We are offline, return that we should use a cached token. + return Ok(UserTokenState::UseCached); + } + + // We are ONLINE, do the get. + match inner .client - .read() - .await .idm_account_unix_token_get(id.to_string().as_str()) .await { - Ok(tok) => Ok(UserToken::from(tok)), - Err(ClientError::Transport(err)) => { - error!(?err); - Err(IdpError::Transport) + Ok(tok) => { + let mut ut = UserToken::from(tok); + + if let Some(previous_token) = token { + ut.extra_keys = previous_token.extra_keys.clone(); + } + + Ok(UserTokenState::Update(ut)) } + // Offline? + Err(ClientError::Transport(err)) => { + error!(?err, "transport error"); + inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK); + Ok(UserTokenState::UseCached) + } + // Provider session error, need to re-auth Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => { match reason { Some(OperationError::NotAuthenticated) => warn!( @@ -164,8 +326,10 @@ impl IdProvider for KanidmProvider { e, opid ), }; - Err(IdpError::ProviderUnauthorised) + inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK); + Ok(UserTokenState::UseCached) } + // 404 / Removed. Err(ClientError::Http( StatusCode::BAD_REQUEST, Some(OperationError::NoMatchingEntries), @@ -185,8 +349,9 @@ impl IdProvider for KanidmProvider { ?opid, "entry has been removed or is no longer a valid posix account" ); - Err(IdpError::NotFound) + Ok(UserTokenState::NotFound) } + // Something is really wrong? We did get a response though, so we are still online. Err(err) => { error!(?err, "client error"); Err(IdpError::BadRequest) @@ -197,42 +362,66 @@ impl IdProvider for KanidmProvider { async fn unix_user_online_auth_init( &self, _account_id: &str, - _token: Option<&UserToken>, - _keystore: &mut KeyStoreTxn, + _token: &UserToken, _tpm: &mut tpm::BoxedDynTpm, - _machine_key: &tpm::MachineKey, _shutdown_rx: &broadcast::Receiver<()>, ) -> Result<(AuthRequest, AuthCredHandler), IdpError> { // Not sure that I need to do much here? Ok((AuthRequest::Password, AuthCredHandler::Password)) } + async fn unix_unknown_user_online_auth_init( + &self, + _account_id: &str, + _tpm: &mut tpm::BoxedDynTpm, + _shutdown_rx: &broadcast::Receiver<()>, + ) -> Result, IdpError> { + // We do not support unknown user auth. + Ok(None) + } + async fn unix_user_online_auth_step( &self, account_id: &str, cred_handler: &mut AuthCredHandler, pam_next_req: PamAuthRequest, - _keystore: &mut KeyStoreTxn, - _tpm: &mut tpm::BoxedDynTpm, - _machine_key: &tpm::MachineKey, + tpm: &mut tpm::BoxedDynTpm, _shutdown_rx: &broadcast::Receiver<()>, - ) -> Result<(AuthResult, AuthCacheAction), IdpError> { + ) -> Result { match (cred_handler, pam_next_req) { (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { - match self + let inner = self.inner.lock().await; + + let auth_result = inner .client - .read() - .await .idm_account_unix_cred_verify(account_id, &cred) - .await - { - Ok(Some(n_tok)) => Ok(( - AuthResult::Success { - token: UserToken::from(n_tok), - }, - AuthCacheAction::PasswordHashUpdate { cred }, - )), - Ok(None) => Ok((AuthResult::Denied, AuthCacheAction::None)), + .await; + + trace!(?auth_result); + + match auth_result { + Ok(Some(n_tok)) => { + let mut token = UserToken::from(n_tok); + token.kanidm_update_cached_password( + &inner.crypto_policy, + cred.as_str(), + tpm, + &inner.hmac_key, + ); + + Ok(AuthResult::Success { token }) + } + Ok(None) => { + // TODO: i'm not a huge fan of this rn, but currently the way we handle + // an expired account is we return Ok(None). + // + // We can't tell the difference between expired and incorrect password. + // So in these cases we have to clear the cached password. :( + // + // In future once we have domain join, we should be getting the user token + // at the start of the auth and checking for account validity instead. + Ok(AuthResult::Denied) + } Err(ClientError::Transport(err)) => { error!(?err); Err(IdpError::Transport) @@ -298,46 +487,74 @@ impl IdProvider for KanidmProvider { async fn unix_user_offline_auth_init( &self, - _account_id: &str, - _token: Option<&UserToken>, - _keystore: &mut KeyStoreTxn, + _token: &UserToken, ) -> Result<(AuthRequest, AuthCredHandler), IdpError> { - // Not sure that I need to do much here? Ok((AuthRequest::Password, AuthCredHandler::Password)) } async fn unix_user_offline_auth_step( &self, - _account_id: &str, - _token: &UserToken, - _cred_handler: &mut AuthCredHandler, - _pam_next_req: PamAuthRequest, - _keystore: &mut KeyStoreTxn, - _tpm: &mut tpm::BoxedDynTpm, - _machine_key: &tpm::MachineKey, - _online_at_init: bool, + token: &UserToken, + cred_handler: &mut AuthCredHandler, + pam_next_req: PamAuthRequest, + tpm: &mut tpm::BoxedDynTpm, ) -> Result { - // We need any cached credentials here. - Err(IdpError::BadRequest) + match (cred_handler, pam_next_req) { + (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { + let inner = self.inner.lock().await; + + if token.kanidm_check_cached_password(cred.as_str(), tpm, &inner.hmac_key) { + // TODO: We can update the token here and then do lockouts. + Ok(AuthResult::Success { + token: token.clone(), + }) + } else { + Ok(AuthResult::Denied) + } + } + ( + AuthCredHandler::DeviceAuthorizationGrant, + PamAuthRequest::DeviceAuthorizationGrant { .. }, + ) => { + error!("DeviceAuthorizationGrant not implemented!"); + Err(IdpError::BadRequest) + } + _ => { + error!("invalid authentication request state"); + Err(IdpError::BadRequest) + } + } } async fn unix_group_get( &self, id: &Id, - _tpm: &mut tpm::BoxedDynTpm, - ) -> Result { - match self + tpm: &mut tpm::BoxedDynTpm, + now: SystemTime, + ) -> Result { + let mut inner = self.inner.lock().await; + + if !inner.check_online(tpm, now).await { + // We are offline, return that we should use a cached token. + return Ok(GroupTokenState::UseCached); + } + + match inner .client - .read() - .await .idm_group_unix_token_get(id.to_string().as_str()) .await { - Ok(tok) => Ok(GroupToken::from(tok)), - Err(ClientError::Transport(err)) => { - error!(?err); - Err(IdpError::Transport) + Ok(tok) => { + let gt = GroupToken::from(tok); + Ok(GroupTokenState::Update(gt)) } + // Offline? + Err(ClientError::Transport(err)) => { + error!(?err, "transport error"); + inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK); + Ok(GroupTokenState::UseCached) + } + // Provider session error, need to re-auth Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => { match reason { Some(OperationError::NotAuthenticated) => warn!( @@ -353,8 +570,10 @@ impl IdProvider for KanidmProvider { e, opid ), }; - Err(IdpError::ProviderUnauthorised) + inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK); + Ok(GroupTokenState::UseCached) } + // 404 / Removed. Err(ClientError::Http( StatusCode::BAD_REQUEST, Some(OperationError::NoMatchingEntries), @@ -372,10 +591,11 @@ impl IdProvider for KanidmProvider { )) => { debug!( ?opid, - "entry has been removed or is no longer a valid posix group" + "entry has been removed or is no longer a valid posix account" ); - Err(IdpError::NotFound) + Ok(GroupTokenState::NotFound) } + // Something is really wrong? We did get a response though, so we are still online. Err(err) => { error!(?err, "client error"); Err(IdpError::BadRequest) diff --git a/unix_integration/resolver/src/idprovider/mod.rs b/unix_integration/resolver/src/idprovider/mod.rs index 77559216e..26af86101 100644 --- a/unix_integration/resolver/src/idprovider/mod.rs +++ b/unix_integration/resolver/src/idprovider/mod.rs @@ -1,2 +1,3 @@ pub mod interface; pub mod kanidm; +pub mod system; diff --git a/unix_integration/resolver/src/idprovider/system.rs b/unix_integration/resolver/src/idprovider/system.rs new file mode 100644 index 000000000..bcad2f2eb --- /dev/null +++ b/unix_integration/resolver/src/idprovider/system.rs @@ -0,0 +1,126 @@ +use hashbrown::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +use super::interface::{Id, IdpError}; +use kanidm_unix_common::unix_passwd::{EtcGroup, EtcUser}; +use kanidm_unix_common::unix_proto::{NssGroup, NssUser}; + +pub struct SystemProviderInternal { + users: HashMap>, + user_list: Vec>, + groups: HashMap>, + group_list: Vec>, +} + +pub struct SystemProvider { + inner: Mutex, +} + +impl SystemProvider { + pub fn new() -> Result { + Ok(SystemProvider { + inner: Mutex::new(SystemProviderInternal { + users: Default::default(), + user_list: Default::default(), + groups: Default::default(), + group_list: Default::default(), + }), + }) + } + + pub async fn reload(&self, users: Vec, groups: Vec) { + let mut system_ids_txn = self.inner.lock().await; + system_ids_txn.users.clear(); + system_ids_txn.user_list.clear(); + system_ids_txn.groups.clear(); + system_ids_txn.group_list.clear(); + + for group in groups { + let name = Id::Name(group.name.clone()); + let gid = Id::Gid(group.gid); + let group = Arc::new(group); + + if system_ids_txn.groups.insert(name, group.clone()).is_some() { + error!(name = %group.name, gid = %group.gid, "group name conflict"); + }; + if system_ids_txn.groups.insert(gid, group.clone()).is_some() { + error!(name = %group.name, gid = %group.gid, "group id conflict"); + } + system_ids_txn.group_list.push(group); + } + + for user in users { + let name = Id::Name(user.name.clone()); + let uid = Id::Gid(user.uid); + let gid = Id::Gid(user.gid); + + if user.uid != user.gid { + error!(name = %user.name, uid = %user.uid, gid = %user.gid, "user uid and gid are not the same, this may be a security risk!"); + } + + // Security checks. + if let Some(group) = system_ids_txn.groups.get(&gid) { + if group.name != user.name { + error!(name = %user.name, uid = %user.uid, gid = %user.gid, "user private group does not appear to have the same name as the user, this may be a security risk!"); + } + if !(group.members.is_empty() + || (group.members.len() == 1 && group.members.first() == Some(&user.name))) + { + error!(name = %user.name, uid = %user.uid, gid = %user.gid, "user private group must not have members, THIS IS A SECURITY RISK!"); + } + } else { + info!(name = %user.name, uid = %user.uid, gid = %user.gid, "user private group is not present on system, synthesising it"); + let group = Arc::new(EtcGroup { + name: user.name.clone(), + password: String::new(), + gid: user.gid, + members: vec![user.name.clone()], + }); + + system_ids_txn.groups.insert(name.clone(), group.clone()); + system_ids_txn.groups.insert(gid.clone(), group.clone()); + system_ids_txn.group_list.push(group); + } + + let user = Arc::new(user); + if system_ids_txn.users.insert(name, user.clone()).is_some() { + error!(name = %user.name, uid = %user.uid, "user name conflict"); + } + if system_ids_txn.users.insert(uid, user.clone()).is_some() { + error!(name = %user.name, uid = %user.uid, "user id conflict"); + } + system_ids_txn.user_list.push(user); + } + } + + pub async fn contains_account(&self, account_id: &Id) -> bool { + let inner = self.inner.lock().await; + inner.users.contains_key(account_id) + } + + pub async fn contains_group(&self, account_id: &Id) -> bool { + let inner = self.inner.lock().await; + inner.groups.contains_key(account_id) + } + + pub async fn get_nssaccount(&self, account_id: &Id) -> Option { + let inner = self.inner.lock().await; + inner.users.get(account_id).map(NssUser::from) + } + + pub async fn get_nssaccounts(&self) -> Vec { + let inner = self.inner.lock().await; + inner.user_list.iter().map(NssUser::from).collect() + } + + pub async fn get_nssgroup(&self, grp_id: &Id) -> Option { + let inner = self.inner.lock().await; + inner.groups.get(grp_id).map(NssGroup::from) + } + + pub async fn get_nssgroups(&self) -> Vec { + let inner = self.inner.lock().await; + inner.group_list.iter().map(NssGroup::from).collect() + } +} diff --git a/unix_integration/resolver/src/resolver.rs b/unix_integration/resolver/src/resolver.rs index 315c1558d..b485f1248 100644 --- a/unix_integration/resolver/src/resolver.rs +++ b/unix_integration/resolver/src/resolver.rs @@ -1,11 +1,12 @@ // use async_trait::async_trait; -use hashbrown::HashSet; +use hashbrown::HashMap; use std::collections::BTreeSet; use std::fmt::Display; use std::num::NonZeroUsize; -use std::ops::{Add, DerefMut, Sub}; +use std::ops::DerefMut; use std::path::{Path, PathBuf}; use std::string::ToString; +use std::sync::Arc; use std::time::{Duration, SystemTime}; use lru::LruCache; @@ -14,41 +15,37 @@ use uuid::Uuid; use crate::db::{Cache, Db}; use crate::idprovider::interface::{ - AuthCacheAction, AuthCredHandler, AuthResult, GroupToken, + GroupTokenState, Id, IdProvider, IdpError, + ProviderOrigin, // KeyStore, UserToken, + UserTokenState, }; +use crate::idprovider::system::SystemProvider; use crate::unix_config::{HomeAttr, UidAttr}; +use kanidm_unix_common::unix_passwd::{EtcGroup, EtcUser}; use kanidm_unix_common::unix_proto::{ - HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, PamAuthResponse, + HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, PamAuthResponse, ProviderStatus, }; -use kanidm_hsm_crypto::{BoxedDynTpm, HmacKey, MachineKey, Tpm}; +use kanidm_hsm_crypto::BoxedDynTpm; use tokio::sync::broadcast; const NXCACHE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(128) }; -#[derive(Debug, Clone)] -enum CacheState { - Online, - Offline, - OfflineNextCheck(SystemTime), -} - -#[derive(Debug)] pub enum AuthSession { - InProgress { + Online { + client: Arc, account_id: String, id: Id, token: Option>, - online_at_init: bool, cred_handler: AuthCredHandler, /// Some authentication operations may need to spawn background tasks. These tasks need /// to know when to stop as the caller has disconnected. This reciever allows that, so @@ -56,6 +53,11 @@ pub enum AuthSession { /// when they need to stop. shutdown_rx: broadcast::Receiver<()>, }, + Offline { + client: Arc, + token: Box, + cred_handler: AuthCredHandler, + }, Success, Denied, } @@ -64,17 +66,16 @@ pub struct Resolver { // Generic / modular types. db: Db, hsm: Mutex, - machine_key: MachineKey, - hmac_key: HmacKey, // A local passwd/shadow resolver. - nxset: Mutex>, + system_provider: Arc, - // A set of remote resolvers - client: Box, + // client: Box, + client_ids: HashMap>, + + // A set of remote resolvers, ordered by priority. + clients: Vec>, - // Types to update still. - state: Mutex, pam_allow_groups: BTreeSet, timeout_seconds: u64, default_shell: String, @@ -83,7 +84,6 @@ pub struct Resolver { home_alias: Option, uid_attr_map: UidAttr, gid_attr_map: UidAttr, - allow_id_overrides: HashSet, nxcache: Mutex>, } @@ -100,10 +100,9 @@ impl Resolver { #[allow(clippy::too_many_arguments)] pub async fn new( db: Db, - client: Box, + system_provider: Arc, + client: Arc, hsm: BoxedDynTpm, - machine_key: MachineKey, - // cache timeout timeout_seconds: u64, pam_allow_groups: Vec, default_shell: String, @@ -112,69 +111,28 @@ impl Resolver { home_alias: Option, uid_attr_map: UidAttr, gid_attr_map: UidAttr, - allow_id_overrides: Vec, ) -> Result { let hsm = Mutex::new(hsm); - let mut hsm_lock = hsm.lock().await; - - // Setup our internal keys - let mut dbtxn = db.write().await; - - let loadable_hmac_key = match dbtxn.get_hsm_hmac_key() { - Ok(Some(hmk)) => hmk, - Ok(None) => { - // generate a new key. - let loadable_hmac_key = hsm_lock.hmac_key_create(&machine_key).map_err(|err| { - error!(?err, "Unable to create hmac key"); - })?; - - dbtxn - .insert_hsm_hmac_key(&loadable_hmac_key) - .map_err(|err| { - error!(?err, "Unable to persist hmac key"); - })?; - - loadable_hmac_key - } - Err(err) => { - error!(?err, "Unable to retrieve loadable hmac key from db"); - return Err(()); - } - }; - - let hmac_key = hsm_lock - .hmac_key_load(&machine_key, &loadable_hmac_key) - .map_err(|err| { - error!(?err, "Unable to load hmac key"); - })?; - - // Ask the client what keys it wants the HSM to configure. - - let result = client - .configure_hsm_keys(&mut (&mut dbtxn).into(), hsm_lock.deref_mut(), &machine_key) - .await; - - drop(hsm_lock); - - result.map_err(|err| { - error!(?err, "Client was unable to configure hsm keys"); - })?; - - dbtxn.commit().map_err(|_| ())?; if pam_allow_groups.is_empty() { warn!("Will not be able to authorise user logins, pam_allow_groups config is not configured."); } + let clients: Vec> = vec![client]; + + let client_ids: HashMap<_, _> = clients + .iter() + .map(|provider| (provider.origin(), provider.clone())) + .collect(); + // We assume we are offline at start up, and we mark the next "online check" as // being valid from "now". Ok(Resolver { db, hsm, - machine_key, - hmac_key, - client, - state: Mutex::new(CacheState::OfflineNextCheck(SystemTime::now())), + system_provider, + clients, + client_ids, timeout_seconds, pam_allow_groups: pam_allow_groups.into_iter().collect(), default_shell, @@ -183,30 +141,23 @@ impl Resolver { home_alias, uid_attr_map, gid_attr_map, - allow_id_overrides: allow_id_overrides.into_iter().map(Id::Name).collect(), - nxset: Mutex::new(HashSet::new()), nxcache: Mutex::new(LruCache::new(NXCACHE_SIZE)), + // system_identities, }) } - async fn get_cachestate(&self) -> CacheState { - let g = self.state.lock().await; - (*g).clone() - } - - async fn set_cachestate(&self, state: CacheState) { - let mut g = self.state.lock().await; - *g = state; - } - - // Need a way to mark online/offline. - pub async fn attempt_online(&self) { - self.set_cachestate(CacheState::OfflineNextCheck(SystemTime::now())) - .await; + #[instrument(level = "debug", skip_all)] + pub async fn mark_next_check_now(&self, now: SystemTime) { + for c in self.clients.iter() { + c.mark_next_check(now).await; + } } + #[instrument(level = "debug", skip_all)] pub async fn mark_offline(&self) { - self.set_cachestate(CacheState::Offline).await; + for c in self.clients.iter() { + c.mark_offline().await; + } } #[instrument(level = "debug", skip_all)] @@ -249,26 +200,8 @@ impl Resolver { nxcache_txn.get(id).copied() } - pub async fn reload_nxset(&self, iter: impl Iterator) { - let mut nxset_txn = self.nxset.lock().await; - nxset_txn.clear(); - for (name, gid) in iter { - let name = Id::Name(name); - let gid = Id::Gid(gid); - - // Skip anything that the admin opted in to - if !(self.allow_id_overrides.contains(&gid) || self.allow_id_overrides.contains(&name)) - { - debug!("Adding {:?}:{:?} to resolver exclusion set", name, gid); - nxset_txn.insert(name); - nxset_txn.insert(gid); - } - } - } - - pub async fn check_nxset(&self, name: &str, idnumber: u32) -> bool { - let nxset_txn = self.nxset.lock().await; - nxset_txn.contains(&Id::Gid(idnumber)) || nxset_txn.contains(&Id::Name(name.to_string())) + pub async fn reload_system_identities(&self, users: Vec, groups: Vec) { + self.system_provider.reload(users, groups).await } async fn get_cached_usertoken(&self, account_id: &Id) -> Result<(bool, Option), ()> { @@ -401,15 +334,6 @@ impl Resolver { token.shell = Some(self.default_shell.clone()) } - // Filter out groups that are in the nxset - { - let nxset_txn = self.nxset.lock().await; - token.groups.retain(|g| { - !(nxset_txn.contains(&Id::Gid(g.gidnumber)) - || nxset_txn.contains(&Id::Name(g.name.clone()))) - }); - } - let mut dbtxn = self.db.write().await; token .groups @@ -456,87 +380,70 @@ impl Resolver { .map_err(|_| ()) } - async fn set_cache_userpassword(&self, a_uuid: Uuid, cred: &str) -> Result<(), ()> { - let mut dbtxn = self.db.write().await; - let mut hsm_txn = self.hsm.lock().await; - dbtxn - .update_account_password(a_uuid, cred, hsm_txn.deref_mut(), &self.hmac_key) - .and_then(|x| dbtxn.commit().map(|_| x)) - .map_err(|_| ()) - } - - async fn check_cache_userpassword(&self, a_uuid: Uuid, cred: &str) -> Result { - let mut dbtxn = self.db.write().await; - let mut hsm_txn = self.hsm.lock().await; - dbtxn - .check_account_password(a_uuid, cred, hsm_txn.deref_mut(), &self.hmac_key) - .and_then(|x| dbtxn.commit().map(|_| x)) - .map_err(|_| ()) - } - async fn refresh_usertoken( &self, account_id: &Id, token: Option, ) -> Result, ()> { + // TODO: Move this to the caller. + let now = SystemTime::now(); + let mut hsm_lock = self.hsm.lock().await; - let user_get_result = self - .client - .unix_user_get( - account_id, - token.as_ref(), - hsm_lock.deref_mut(), - &self.machine_key, - ) - .await; + let user_get_result = if let Some(tok) = token.as_ref() { + // Re-use the provider that the token is from. + match self.client_ids.get(&tok.provider) { + Some(client) => { + client + .unix_user_get(account_id, token.as_ref(), hsm_lock.deref_mut(), now) + .await + } + None => { + error!(provider = ?tok.provider, "Token was resolved by a provider that no longer appears to be present."); + // We don't know if this is permanent or transient, so just useCached, unless + // the admin clears tokens from providers that are no longer present. + Ok(UserTokenState::UseCached) + } + } + } else { + // We've never seen it before, so iterate over the providers in priority order. + 'search: { + for client in self.clients.iter() { + match client + .unix_user_get(account_id, token.as_ref(), hsm_lock.deref_mut(), now) + .await + { + // Ignore this one. + Ok(UserTokenState::NotFound) => {} + result => break 'search result, + } + } + break 'search Ok(UserTokenState::NotFound); + } + }; drop(hsm_lock); match user_get_result { - Ok(mut n_tok) => { - if self.check_nxset(&n_tok.name, n_tok.gidnumber).await { - // Refuse to release the token, it's in the denied set. - debug!( - "Account {:?} is in denied set, refusing to release token. It may need to be in the allow_local_account_override configuration list.", - account_id - ); - self.delete_cache_usertoken(n_tok.uuid).await?; - Ok(None) - } else { - // We have the token! - self.set_cache_usertoken(&mut n_tok).await?; - Ok(Some(n_tok)) - } + Ok(UserTokenState::Update(mut n_tok)) => { + // We have the token! + self.set_cache_usertoken(&mut n_tok).await?; + Ok(Some(n_tok)) } - Err(IdpError::Transport) => { - error!("transport error, moving to offline"); - // Something went wrong, mark offline. - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - Ok(token) - } - Err(IdpError::ProviderUnauthorised) => { - // Something went wrong, mark offline to force a re-auth ASAP. - let time = SystemTime::now().sub(Duration::from_secs(1)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - Ok(token) - } - Err(IdpError::NotFound) => { - // We were able to contact the server but the entry has been removed, or - // is not longer a valid posix account. + Ok(UserTokenState::NotFound) => { + // It previously existed, so now purge it. if let Some(tok) = token { self.delete_cache_usertoken(tok.uuid).await?; }; // Cache the NX here. self.set_nxcache(account_id).await; - Ok(None) } - Err(IdpError::KeyStore) | Err(IdpError::BadRequest) | Err(IdpError::Tpm) => { - // Some other transient error, continue with the token. + Ok(UserTokenState::UseCached) => Ok(token), + Err(err) => { + // Something went wrong, we don't know what, but lets return the token + // anyway. + error!(?err); Ok(token) } } @@ -547,43 +454,51 @@ impl Resolver { grp_id: &Id, token: Option, ) -> Result, ()> { + // TODO: Move this to the caller. + let now = SystemTime::now(); + let mut hsm_lock = self.hsm.lock().await; - let group_get_result = self - .client - .unix_group_get(grp_id, hsm_lock.deref_mut()) - .await; + let group_get_result = if let Some(tok) = token.as_ref() { + // Re-use the provider that the token is from. + match self.client_ids.get(&tok.provider) { + Some(client) => { + client + .unix_group_get(grp_id, hsm_lock.deref_mut(), now) + .await + } + None => { + error!(provider = ?tok.provider, "Token was resolved by a provider that no longer appears to be present."); + // We don't know if this is permanent or transient, so just useCached, unless + // the admin clears tokens from providers that are no longer present. + Ok(GroupTokenState::UseCached) + } + } + } else { + // We've never seen it before, so iterate over the providers in priority order. + 'search: { + for client in self.clients.iter() { + match client + .unix_group_get(grp_id, hsm_lock.deref_mut(), now) + .await + { + // Ignore this one. + Ok(GroupTokenState::NotFound) => {} + result => break 'search result, + } + } + break 'search Ok(GroupTokenState::NotFound); + } + }; drop(hsm_lock); match group_get_result { - Ok(n_tok) => { - if self.check_nxset(&n_tok.name, n_tok.gidnumber).await { - // Refuse to release the token, it's in the denied set. - self.delete_cache_grouptoken(n_tok.uuid).await?; - Ok(None) - } else { - // We have the token! - self.set_cache_grouptoken(&n_tok).await?; - Ok(Some(n_tok)) - } + Ok(GroupTokenState::Update(n_tok)) => { + self.set_cache_grouptoken(&n_tok).await?; + Ok(Some(n_tok)) } - Err(IdpError::Transport) => { - error!("transport error, moving to offline"); - // Something went wrong, mark offline. - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - Ok(token) - } - Err(IdpError::ProviderUnauthorised) => { - // Something went wrong, mark offline. - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - Ok(token) - } - Err(IdpError::NotFound) => { + Ok(GroupTokenState::NotFound) => { if let Some(tok) = token { self.delete_cache_grouptoken(tok.uuid).await?; }; @@ -591,58 +506,28 @@ impl Resolver { self.set_nxcache(grp_id).await; Ok(None) } - Err(IdpError::KeyStore) | Err(IdpError::BadRequest) | Err(IdpError::Tpm) => { + Ok(GroupTokenState::UseCached) => Ok(token), + Err(err) => { // Some other transient error, continue with the token. + error!(?err); Ok(token) } } } #[instrument(level = "debug", skip(self))] - async fn get_usertoken(&self, account_id: Id) -> Result, ()> { + async fn get_usertoken(&self, account_id: &Id) -> Result, ()> { // get the item from the cache - let (expired, item) = self.get_cached_usertoken(&account_id).await.map_err(|e| { + let (expired, item) = self.get_cached_usertoken(account_id).await.map_err(|e| { debug!("get_usertoken error -> {:?}", e); })?; - let state = self.get_cachestate().await; - - match (expired, state) { - (_, CacheState::Offline) => { - debug!("offline, returning cached item"); - Ok(item) - } - (false, CacheState::OfflineNextCheck(time)) => { - debug!( - "offline valid, next check {:?}, returning cached item", - time - ); - // Still valid within lifetime, return. - Ok(item) - } - (false, CacheState::Online) => { - debug!("online valid, returning cached item"); - // Still valid within lifetime, return. - Ok(item) - } - (true, CacheState::OfflineNextCheck(time)) => { - debug!("offline expired, next check {:?}, refresh cache", time); - // Attempt to refresh the item - // Return it. - if SystemTime::now() >= time && self.test_connection().await { - // We brought ourselves online, lets go - self.refresh_usertoken(&account_id, item).await - } else { - // Unable to bring up connection, return cache. - Ok(item) - } - } - (true, CacheState::Online) => { - debug!("online expired, refresh cache"); - // Attempt to refresh the item - // Return it. - self.refresh_usertoken(&account_id, item).await - } + // If the token isn't found, get_cached will set expired = true. + if expired { + self.refresh_usertoken(account_id, item).await + } else { + // Still valid, return the cached entry. + Ok(item) } .map(|t| { debug!("token -> {:?}", t); @@ -656,45 +541,16 @@ impl Resolver { debug!("get_grouptoken error -> {:?}", e); })?; - let state = self.get_cachestate().await; - - match (expired, state) { - (_, CacheState::Offline) => { - debug!("offline, returning cached item"); - Ok(item) - } - (false, CacheState::OfflineNextCheck(time)) => { - debug!( - "offline valid, next check {:?}, returning cached item", - time - ); - // Still valid within lifetime, return. - Ok(item) - } - (false, CacheState::Online) => { - debug!("online valid, returning cached item"); - // Still valid within lifetime, return. - Ok(item) - } - (true, CacheState::OfflineNextCheck(time)) => { - debug!("offline expired, next check {:?}, refresh cache", time); - // Attempt to refresh the item - // Return it. - if SystemTime::now() >= time && self.test_connection().await { - // We brought ourselves online, lets go - self.refresh_grouptoken(&grp_id, item).await - } else { - // Unable to bring up connection, return cache. - Ok(item) - } - } - (true, CacheState::Online) => { - debug!("online expired, refresh cache"); - // Attempt to refresh the item - // Return it. - self.refresh_grouptoken(&grp_id, item).await - } + if expired { + self.refresh_grouptoken(&grp_id, item).await + } else { + // Still valid, return the cached entry. + Ok(item) } + .map(|t| { + debug!("token -> {:?}", t); + t + }) } async fn get_groupmembers(&self, g_uuid: Uuid) -> Vec { @@ -711,7 +567,9 @@ impl Resolver { // Get ssh keys for an account id #[instrument(level = "debug", skip(self))] pub async fn get_sshkeys(&self, account_id: &str) -> Result, ()> { - let token = self.get_usertoken(Id::Name(account_id.to_string())).await?; + let token = self + .get_usertoken(&Id::Name(account_id.to_string())) + .await?; Ok(token .map(|t| { // Only return keys if the account is valid @@ -768,24 +626,37 @@ impl Resolver { #[instrument(level = "debug", skip_all)] pub async fn get_nssaccounts(&self) -> Result, ()> { - self.get_cached_usertokens().await.map(|l| { - l.into_iter() - .map(|tok| NssUser { - homedir: self.token_abs_homedirectory(&tok), - name: self.token_uidattr(&tok), - gid: tok.gidnumber, - gecos: tok.displayname, - shell: tok.shell.unwrap_or_else(|| self.default_shell.clone()), - }) - .collect() - }) + // We don't need to filter the cached tokens as the cache shouldn't + // have anything that collides with system. + let system_nss_users = self.system_provider.get_nssaccounts().await; + + let cached = self.get_cached_usertokens().await?; + + Ok(system_nss_users + .into_iter() + .chain(cached.into_iter().map(|tok| NssUser { + homedir: self.token_abs_homedirectory(&tok), + name: self.token_uidattr(&tok), + uid: tok.gidnumber, + gid: tok.gidnumber, + gecos: tok.displayname, + shell: tok.shell.unwrap_or_else(|| self.default_shell.clone()), + })) + .collect()) } + #[instrument(level = "debug", skip_all)] async fn get_nssaccount(&self, account_id: Id) -> Result, ()> { - let token = self.get_usertoken(account_id).await?; + if let Some(nss_user) = self.system_provider.get_nssaccount(&account_id).await { + debug!("system provider satisfied request"); + return Ok(Some(nss_user)); + } + + let token = self.get_usertoken(&account_id).await?; Ok(token.map(|tok| NssUser { homedir: self.token_abs_homedirectory(&tok), name: self.token_uidattr(&tok), + uid: tok.gidnumber, gid: tok.gidnumber, gecos: tok.displayname, shell: tok.shell.unwrap_or_else(|| self.default_shell.clone()), @@ -813,8 +684,10 @@ impl Resolver { #[instrument(level = "debug", skip_all)] pub async fn get_nssgroups(&self) -> Result, ()> { + let mut r = self.system_provider.get_nssgroups().await; + let l = self.get_cached_grouptokens().await?; - let mut r: Vec<_> = Vec::with_capacity(l.len()); + r.reserve(l.len()); for tok in l.into_iter() { let members = self.get_groupmembers(tok.uuid).await; r.push(NssGroup { @@ -827,6 +700,11 @@ impl Resolver { } async fn get_nssgroup(&self, grp_id: Id) -> Result, ()> { + if let Some(nss_group) = self.system_provider.get_nssgroup(&grp_id).await { + debug!("system provider satisfied request"); + return Ok(Some(nss_group)); + } + let token = self.get_grouptoken(grp_id).await?; // Get members set. match token { @@ -854,7 +732,9 @@ impl Resolver { #[instrument(level = "debug", skip(self))] pub async fn pam_account_allowed(&self, account_id: &str) -> Result, ()> { - let token = self.get_usertoken(Id::Name(account_id.to_string())).await?; + let token = self + .get_usertoken(&Id::Name(account_id.to_string())) + .await?; if self.pam_allow_groups.is_empty() { // can't allow anything if the group list is zero... @@ -874,7 +754,7 @@ impl Resolver { ); let intersection_count = user_set.intersection(&self.pam_allow_groups).count(); debug!("Number of intersecting groups: {}", intersection_count); - debug!("User has valid token: {}", tok.valid); + debug!("User token is valid: {}", tok.valid); intersection_count > 0 && tok.valid })) @@ -892,67 +772,133 @@ impl Resolver { // weird interactions - they should assume online/offline only for // the duration of their operation. A failure of connectivity during // an online operation will take the cache offline however. + let now = SystemTime::now(); let id = Id::Name(account_id.to_string()); - let (_expired, token) = self.get_cached_usertoken(&id).await?; - let state = self.get_cachestate().await; - let online_at_init = if !matches!(state, CacheState::Online) { - // Attempt a cache online. - self.test_connection().await - } else { - true - }; + if self.system_provider.contains_account(&id).await { + debug!("Ignoring auth request for system user"); + return Ok((AuthSession::Denied, PamAuthResponse::Unknown)); + } - let maybe_err = if online_at_init { - let mut hsm_lock = self.hsm.lock().await; - let mut dbtxn = self.db.write().await; + let token = self.get_usertoken(&id).await?; - self.client - .unix_user_online_auth_init( - account_id, - token.as_ref(), - &mut (&mut dbtxn).into(), - hsm_lock.deref_mut(), - &self.machine_key, - &shutdown_rx, - ) - .await - } else { - let mut dbtxn = self.db.write().await; + // Get the provider associated to this token. - // Can the auth proceed offline? - self.client - .unix_user_offline_auth_init(account_id, token.as_ref(), &mut (&mut dbtxn).into()) - .await - }; + let mut hsm_lock = self.hsm.lock().await; - match maybe_err { - Ok((next_req, cred_handler)) => { - let auth_session = AuthSession::InProgress { - account_id: account_id.to_string(), - id, - token: token.map(Box::new), - online_at_init, - cred_handler, - shutdown_rx, - }; + // We don't care if we are expired - we will always attempt to go + // online and perform this operation online if possible. - // Now identify what credentials are needed next. The auth session tells - // us this. + if let Some(token) = token { + // We have a token, we know what provider is needed + let client = self.client_ids.get(&token.provider) + .cloned() + .ok_or_else(|| { + error!(provider = ?token.provider, "Token was resolved by a provider that no longer appears to be present."); + })?; - Ok((auth_session, next_req.into())) - } - Err(IdpError::NotFound) => Ok((AuthSession::Denied, PamAuthResponse::Unknown)), - Err(IdpError::ProviderUnauthorised) | Err(IdpError::Transport) => { - error!("transport error, moving to offline"); - // Something went wrong, mark offline. - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) + let online_at_init = client.attempt_online(hsm_lock.deref_mut(), now).await; + // if we are online, we try and start an online auth. + debug!(?online_at_init); + + if online_at_init { + let init_result = client + .unix_user_online_auth_init( + account_id, + &token, + hsm_lock.deref_mut(), + &shutdown_rx, + ) .await; - Err(()) + + match init_result { + Ok((next_req, cred_handler)) => { + let auth_session = AuthSession::Online { + client, + account_id: account_id.to_string(), + id, + token: Some(Box::new(token)), + cred_handler, + shutdown_rx, + }; + Ok((auth_session, next_req.into())) + } + Err(err) => { + error!(?err, "Unable to start authentication"); + Err(()) + } + } + } else { + // Can the auth proceed offline? + let init_result = client.unix_user_offline_auth_init(&token).await; + + match init_result { + Ok((next_req, cred_handler)) => { + let auth_session = AuthSession::Offline { + client, + token: Box::new(token), + cred_handler, + }; + Ok((auth_session, next_req.into())) + } + Err(err) => { + error!(?err, "Unable to start authentication"); + Err(()) + } + } } - Err(IdpError::BadRequest) | Err(IdpError::KeyStore) | Err(IdpError::Tpm) => Err(()), + } else { + // We don't know anything about this user. Can we try to auth them? + + // TODO: If any provider is offline should we fail the auth? I can imagine a possible + // issue where if we had provides A, B, C stacked, and A was offline, then B could + // service an auth that A *should* have serviced. + + for client in self.clients.iter() { + let online_at_init = client.attempt_online(hsm_lock.deref_mut(), now).await; + debug!(?online_at_init); + + if !online_at_init { + warn!(?account_id, "Unable to proceed with authentication, all providers must be online for unknown user authentication."); + return Ok((AuthSession::Denied, PamAuthResponse::Unknown)); + } + } + + for client in self.clients.iter() { + let init_result = client + .unix_unknown_user_online_auth_init( + account_id, + hsm_lock.deref_mut(), + &shutdown_rx, + ) + .await; + + match init_result { + Ok(Some((next_req, cred_handler))) => { + let auth_session = AuthSession::Online { + client: client.clone(), + account_id: account_id.to_string(), + id, + token: None, + cred_handler, + shutdown_rx, + }; + return Ok((auth_session, next_req.into())); + } + Ok(None) => { + // Not for us, check the next provider. + } + Err(err) => { + error!(?err, "Unable to start authentication"); + return Err(()); + } + } + } + + // No module signaled that they want it, bail. + warn!("No provider is willing to service authentication of unknown account."); + Ok((AuthSession::Denied, PamAuthResponse::Unknown)) } } @@ -962,194 +908,63 @@ impl Resolver { auth_session: &mut AuthSession, pam_next_req: PamAuthRequest, ) -> Result { - let state = self.get_cachestate().await; - - let maybe_err = match (&mut *auth_session, state) { - ( - &mut AuthSession::InProgress { - ref account_id, - id: _, - token: _, - online_at_init: true, - ref mut cred_handler, - ref shutdown_rx, - }, - CacheState::Online, - ) => { + let maybe_err = match &mut *auth_session { + &mut AuthSession::Online { + ref client, + ref account_id, + id: _, + token: _, + ref mut cred_handler, + ref shutdown_rx, + } => { let mut hsm_lock = self.hsm.lock().await; - let mut dbtxn = self.db.write().await; - - let maybe_cache_action = self - .client + client .unix_user_online_auth_step( account_id, cred_handler, pam_next_req, - &mut (&mut dbtxn).into(), hsm_lock.deref_mut(), - &self.machine_key, shutdown_rx, ) - .await; - - drop(hsm_lock); - dbtxn.commit().map_err(|_| ())?; - - match maybe_cache_action { - Ok((res, AuthCacheAction::None)) => Ok(res), - Ok(( - AuthResult::Success { token }, - AuthCacheAction::PasswordHashUpdate { cred }, - )) => { - // Might need a rework with the tpm code. - self.set_cache_userpassword(token.uuid, &cred).await?; - Ok(AuthResult::Success { token }) - } - // I think this state is actually invalid? - Ok((_, AuthCacheAction::PasswordHashUpdate { .. })) => { - // Ok(res) - error!("provider gave back illogical password hash update with a nonsuccess condition"); - Err(IdpError::BadRequest) - } - Err(e) => Err(e), - } + .await } - /* - ( - &mut AuthSession::InProgress { - account_id: _, - id: _, - token: _, - online_at_init: true, - cred_handler: _, - }, - _, - ) => { - // Fail, we went offline. - error!("Unable to proceed with authentication, resolver has gone offline"); - Err(IdpError::Transport) - } - */ - ( - &mut AuthSession::InProgress { - ref account_id, - id: _, - token: Some(ref token), - online_at_init, - ref mut cred_handler, - // Only need in online auth. - shutdown_rx: _, - }, - _, - ) => { + &mut AuthSession::Offline { + ref client, + ref token, + ref mut cred_handler, + } => { // We are offline, continue. Remember, authsession should have // *everything you need* to proceed here! - // - // Rather than calling client, should this actually be self - // contained to the resolver so that it has generic offline-paths - // that are possible? - match (&cred_handler, &pam_next_req) { - (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { - match self.check_cache_userpassword(token.uuid, cred).await { - Ok(true) => Ok(AuthResult::Success { - token: *token.clone(), - }), - Ok(false) => Ok(AuthResult::Denied), - Err(()) => { - // We had a genuine backend error of some description. - return Err(()); - } - } - } - (AuthCredHandler::Password, _) => { - // AuthCredHandler::Password is only valid with a cred provided - return Err(()); - } - (AuthCredHandler::DeviceAuthorizationGrant, _) => { - // AuthCredHandler::DeviceAuthorizationGrant is invalid for offline auth - return Err(()); - } - (AuthCredHandler::MFA { .. }, _) => { - // AuthCredHandler::MFA is invalid for offline auth - return Err(()); - } - (AuthCredHandler::SetupPin, _) => { - // AuthCredHandler::SetupPin is invalid for offline auth - return Err(()); - } - (AuthCredHandler::Pin, PamAuthRequest::Pin { .. }) => { - // The Pin acts as a single device password, and can be - // used to unlock the TPM to validate the authentication. - let mut hsm_lock = self.hsm.lock().await; - let mut dbtxn = self.db.write().await; - - let auth_result = self - .client - .unix_user_offline_auth_step( - account_id, - token, - cred_handler, - pam_next_req, - &mut (&mut dbtxn).into(), - hsm_lock.deref_mut(), - &self.machine_key, - online_at_init, - ) - .await; - - drop(hsm_lock); - dbtxn.commit().map_err(|_| ())?; - - auth_result - } - (AuthCredHandler::Pin, _) => { - // AuthCredHandler::Pin is only valid with a cred provided - return Err(()); - } - } - } - (&mut AuthSession::InProgress { token: None, .. }, _) => { - // Can't do much with offline auth when there is no token ... - warn!("Unable to proceed with offline auth, no token available"); - Err(IdpError::NotFound) - } - (&mut AuthSession::Success, _) | (&mut AuthSession::Denied, _) => { - Err(IdpError::BadRequest) + let mut hsm_lock = self.hsm.lock().await; + client + .unix_user_offline_auth_step( + token, + cred_handler, + pam_next_req, + hsm_lock.deref_mut(), + ) + .await } + &mut AuthSession::Success | &mut AuthSession::Denied => Err(IdpError::BadRequest), }; match maybe_err { // What did the provider direct us to do next? Ok(AuthResult::Success { mut token }) => { - if self.check_nxset(&token.name, token.gidnumber).await { - // Refuse to release the token, it's in the denied set. - self.delete_cache_usertoken(token.uuid).await?; - *auth_session = AuthSession::Denied; + debug!("provider authentication success."); + self.set_cache_usertoken(&mut token).await?; + *auth_session = AuthSession::Success; - Ok(PamAuthResponse::Unknown) - } else { - debug!("provider authentication success."); - self.set_cache_usertoken(&mut token).await?; - *auth_session = AuthSession::Success; - - Ok(PamAuthResponse::Success) - } + Ok(PamAuthResponse::Success) } Ok(AuthResult::Denied) => { *auth_session = AuthSession::Denied; + Ok(PamAuthResponse::Denied) } Ok(AuthResult::Next(req)) => Ok(req.into()), Err(IdpError::NotFound) => Ok(PamAuthResponse::Unknown), - Err(IdpError::ProviderUnauthorised) | Err(IdpError::Transport) => { - error!("transport error, moving to offline"); - // Something went wrong, mark offline. - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - Err(()) - } - Err(IdpError::KeyStore) | Err(IdpError::BadRequest) | Err(IdpError::Tpm) => Err(()), + _ => Err(()), } } @@ -1227,7 +1042,9 @@ impl Resolver { &self, account_id: &str, ) -> Result, ()> { - let token = self.get_usertoken(Id::Name(account_id.to_string())).await?; + let token = self + .get_usertoken(&Id::Name(account_id.to_string())) + .await?; Ok(token.as_ref().map(|tok| HomeDirectoryInfo { gid: tok.gidnumber, name: self.token_homedirectory_attr(tok), @@ -1238,43 +1055,37 @@ impl Resolver { })) } + pub async fn provider_status(&self) -> Vec { + let now = SystemTime::now(); + let mut hsm_lock = self.hsm.lock().await; + + let mut results = Vec::with_capacity(self.clients.len()); + + for client in self.clients.iter() { + let online = client.attempt_online(hsm_lock.deref_mut(), now).await; + + let name = client.origin().to_string(); + + results.push(ProviderStatus { name, online }) + } + + results + } + #[instrument(level = "debug", skip_all)] pub async fn test_connection(&self) -> bool { - let state = self.get_cachestate().await; - match state { - CacheState::Offline => { - debug!("Offline -> no change"); - false - } - CacheState::OfflineNextCheck(_time) => { - let mut hsm_lock = self.hsm.lock().await; + let now = SystemTime::now(); + let mut hsm_lock = self.hsm.lock().await; - let prov_auth_result = self - .client - .provider_authenticate(hsm_lock.deref_mut()) - .await; + for client in self.clients.iter() { + let status = client.attempt_online(hsm_lock.deref_mut(), now).await; - drop(hsm_lock); - - match prov_auth_result { - Ok(()) => { - debug!("OfflineNextCheck -> authenticated"); - self.set_cachestate(CacheState::Online).await; - true - } - Err(e) => { - debug!("OfflineNextCheck -> disconnected, staying offline. {:?}", e); - let time = SystemTime::now().add(Duration::from_secs(15)); - self.set_cachestate(CacheState::OfflineNextCheck(time)) - .await; - false - } - } - } - CacheState::Online => { - debug!("Online, no change"); - true + if !status { + return false; } } + + // All online + true } } diff --git a/unix_integration/resolver/tests/cache_layer_test.rs b/unix_integration/resolver/tests/cache_layer_test.rs index 39b1a716a..68595fec2 100644 --- a/unix_integration/resolver/tests/cache_layer_test.rs +++ b/unix_integration/resolver/tests/cache_layer_test.rs @@ -2,7 +2,8 @@ use std::future::Future; use std::pin::Pin; use std::sync::atomic::Ordering; -use std::time::Duration; +use std::sync::Arc; +use std::time::{Duration, SystemTime}; use kanidm_client::{KanidmClient, KanidmClientBuilder}; use kanidm_proto::constants::ATTR_ACCOUNT_EXPIRE; @@ -10,9 +11,11 @@ use kanidm_unix_common::constants::{ DEFAULT_GID_ATTR_MAP, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_SHELL, DEFAULT_UID_ATTR_MAP, }; +use kanidm_unix_common::unix_passwd::{EtcGroup, EtcUser}; use kanidm_unix_resolver::db::{Cache, Db}; use kanidm_unix_resolver::idprovider::interface::Id; use kanidm_unix_resolver::idprovider::kanidm::KanidmProvider; +use kanidm_unix_resolver::idprovider::system::SystemProvider; use kanidm_unix_resolver::resolver::Resolver; use kanidmd_core::config::{Configuration, IntegrationTestConfig, ServerRole}; use kanidmd_core::create_server_core; @@ -101,18 +104,13 @@ async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) { .build() .expect("Failed to build client"); - let idprovider = KanidmProvider::new(rsclient); - let db = Db::new( "", // The sqlite db path, this is in memory. ) .expect("Failed to setup DB"); let mut dbtxn = db.write().await; - dbtxn - .migrate() - .and_then(|_| dbtxn.commit()) - .expect("Unable to migrate cache db"); + dbtxn.migrate().expect("Unable to migrate cache db"); let mut hsm = BoxedDynTpm::new(SoftTpm::new()); @@ -123,11 +121,26 @@ async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) { .machine_key_load(&auth_value, &loadable_machine_key) .unwrap(); + let system_provider = SystemProvider::new().unwrap(); + + let idprovider = KanidmProvider::new( + rsclient, + SystemTime::now(), + &mut (&mut dbtxn).into(), + &mut hsm, + &machine_key, + ) + .unwrap(); + + drop(machine_key); + + dbtxn.commit().expect("Unable to commit dbtxn"); + let cachelayer = Resolver::new( db, - Box::new(idprovider), + Arc::new(system_provider), + Arc::new(idprovider), hsm, - machine_key, 300, vec!["allowed_group".to_string()], DEFAULT_SHELL.to_string(), @@ -136,7 +149,6 @@ async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) { DEFAULT_HOME_ALIAS, DEFAULT_UID_ATTR_MAP, DEFAULT_GID_ATTR_MAP, - vec!["masked_group".to_string()], ) .await .expect("Failed to build cache layer."); @@ -231,7 +243,7 @@ async fn test_cache_sshkey() { assert!(sk.is_empty()); // Bring ourselves online. - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); let sk = cachelayer @@ -262,7 +274,7 @@ async fn test_cache_account() { assert!(ut.is_none()); // go online - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); // get the account @@ -305,7 +317,7 @@ async fn test_cache_group() { assert!(gt.is_none()); // go online. Get the group - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); let gt = cachelayer .get_nssgroup_name("testgroup1") @@ -326,7 +338,7 @@ async fn test_cache_group() { // clear cache, go online assert!(cachelayer.invalidate().await.is_ok()); - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); // get an account with the group @@ -361,7 +373,7 @@ async fn test_cache_group() { async fn test_cache_group_delete() { let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await; // get the group - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); let gt = cachelayer .get_nssgroup_name("testgroup1") @@ -395,7 +407,7 @@ async fn test_cache_group_delete() { async fn test_cache_account_delete() { let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await; // get the account - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); let ut = cachelayer .get_nssaccount_name("testaccount1") @@ -435,7 +447,7 @@ async fn test_cache_account_delete() { #[tokio::test] async fn test_cache_account_password() { let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await; - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; // Test authentication failure. let a1 = cachelayer .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_INC) @@ -513,7 +525,7 @@ async fn test_cache_account_password() { assert!(a7.is_none()); // go online - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); // test auth success @@ -527,7 +539,7 @@ async fn test_cache_account_password() { #[tokio::test] async fn test_cache_account_pam_allowed() { let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await; - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; // Should fail let a1 = cachelayer @@ -559,7 +571,7 @@ async fn test_cache_account_pam_allowed() { #[tokio::test] async fn test_cache_account_pam_nonexist() { let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await; - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; let a1 = cachelayer .pam_account_allowed("NO_SUCH_ACCOUNT") @@ -591,7 +603,7 @@ async fn test_cache_account_pam_nonexist() { #[tokio::test] async fn test_cache_account_expiry() { let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await; - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); // We need one good auth first to prime the cache with a hash. @@ -636,12 +648,13 @@ async fn test_cache_account_expiry() { // go offline cachelayer.mark_offline().await; - // Now, check again ... + // Now, check again. Since this uses the cached pw and we are offline, this + // will now succeed. let a4 = cachelayer .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A) .await .expect("failed to authenticate"); - assert!(a4 == Some(false)); + assert!(a4 == Some(true)); // ssh keys should be empty let sk = cachelayer @@ -661,7 +674,7 @@ async fn test_cache_account_expiry() { #[tokio::test] async fn test_cache_nxcache() { let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await; - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); // Is it in the nxcache? @@ -737,20 +750,22 @@ async fn test_cache_nxset_account() { // Important! This is what sets up that testaccount1 won't be resolved // because it's in the "local" user set. cachelayer - .reload_nxset(vec![("testaccount1".to_string(), 20000)].into_iter()) + .reload_system_identities( + vec![EtcUser { + name: "testaccount1".to_string(), + uid: 30000, + gid: 30000, + password: Default::default(), + gecos: Default::default(), + homedir: Default::default(), + shell: Default::default(), + }], + vec![], + ) .await; - // Force offline. Show we have no account - cachelayer.mark_offline().await; - - let ut = cachelayer - .get_nssaccount_name("testaccount1") - .await - .expect("Failed to get from cache"); - assert!(ut.is_none()); - // go online - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); // get the account @@ -758,7 +773,10 @@ async fn test_cache_nxset_account() { .get_nssaccount_name("testaccount1") .await .expect("Failed to get from cache"); - assert!(ut.is_none()); + + let ut = ut.unwrap(); + // Assert the user is the system version. + assert_eq!(ut.uid, 30000); // go offline cachelayer.mark_offline().await; @@ -768,14 +786,24 @@ async fn test_cache_nxset_account() { .get_nssaccount_name("testaccount1") .await .expect("Failed to get from cache"); - assert!(ut.is_none()); - // Finally, check it's not in all accounts. + let ut = ut.unwrap(); + // Assert the user is the system version. + assert_eq!(ut.uid, 30000); + + // Finally, check it's the system version in all accounts. let us = cachelayer .get_nssaccounts() .await .expect("failed to list all accounts"); - assert!(us.is_empty()); + + let us: Vec<_> = us + .into_iter() + .filter(|nss_user| nss_user.name == "testaccount1") + .collect(); + + assert_eq!(us.len(), 1); + assert_eq!(us[0].gid, 30000); } #[tokio::test] @@ -785,25 +813,30 @@ async fn test_cache_nxset_group() { // Important! This is what sets up that testgroup1 won't be resolved // because it's in the "local" group set. cachelayer - .reload_nxset(vec![("testgroup1".to_string(), 20001)].into_iter()) + .reload_system_identities( + vec![], + vec![EtcGroup { + name: "testgroup1".to_string(), + // Important! We set the GID to differ from what kanidm stores so we can + // tell we got the system version. + gid: 30001, + password: Default::default(), + members: Default::default(), + }], + ) .await; - // Force offline. Show we have no groups. - cachelayer.mark_offline().await; - let gt = cachelayer - .get_nssgroup_name("testgroup1") - .await - .expect("Failed to get from cache"); - assert!(gt.is_none()); - // go online. Get the group - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); let gt = cachelayer .get_nssgroup_name("testgroup1") .await .expect("Failed to get from cache"); - assert!(gt.is_none()); + + // We get the group, it's the system version. Check the gid. + let gt = gt.unwrap(); + assert_eq!(gt.gid, 30001); // go offline. still works cachelayer.mark_offline().await; @@ -811,15 +844,16 @@ async fn test_cache_nxset_group() { .get_nssgroup_name("testgroup1") .await .expect("Failed to get from cache"); - assert!(gt.is_none()); + + let gt = gt.unwrap(); + assert_eq!(gt.gid, 30001); // clear cache, go online assert!(cachelayer.invalidate().await.is_ok()); - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); - // get an account with the group - // DO NOT get the group yet. + // get a kanidm account with the kanidm equivalent group let ut = cachelayer .get_nssaccount_name("testaccount1") .await @@ -829,56 +863,31 @@ async fn test_cache_nxset_group() { // go offline. cachelayer.mark_offline().await; - // show we have the group despite no direct calls + // show that the group we have is still the system version, and lacks our + // member. let gt = cachelayer .get_nssgroup_name("testgroup1") .await .expect("Failed to get from cache"); - assert!(gt.is_none()); - // Finally, check we only have the upg in the list + let gt = gt.unwrap(); + assert_eq!(gt.gid, 30001); + assert!(gt.members.is_empty()); + + // Finally, check we only have the system group version in the list. let gs = cachelayer .get_nssgroups() .await .expect("failed to list all groups"); - assert!(gs.len() == 1); - assert!(gs[0].name == "testaccount1@idm.example.com"); -} -#[tokio::test] -async fn test_cache_nxset_allow_overrides() { - let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await; + let gs: Vec<_> = gs + .into_iter() + .filter(|nss_group| nss_group.name == "testgroup1") + .collect(); - // Important! masked_group is set as an allowed override group even though - // it's been "inserted" to the nxset. This means it will still resolve! - cachelayer - .reload_nxset(vec![("masked_group".to_string(), 20003)].into_iter()) - .await; - - // Force offline. Show we have no groups. - cachelayer.mark_offline().await; - let gt = cachelayer - .get_nssgroup_name("masked_group") - .await - .expect("Failed to get from cache"); - assert!(gt.is_none()); - - // go online. Get the group - cachelayer.attempt_online().await; - assert!(cachelayer.test_connection().await); - let gt = cachelayer - .get_nssgroup_name("masked_group") - .await - .expect("Failed to get from cache"); - assert!(gt.is_some()); - - // go offline. still works - cachelayer.mark_offline().await; - let gt = cachelayer - .get_nssgroup_name("masked_group") - .await - .expect("Failed to get from cache"); - assert!(gt.is_some()); + debug!("{:?}", gs); + assert_eq!(gs.len(), 1); + assert_eq!(gs[0].gid, 30001); } /// Issue 1830. If cache items expire where we have an account and a group, and we @@ -892,7 +901,7 @@ async fn test_cache_nxset_allow_overrides() { async fn test_cache_group_fk_deferred() { let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await; - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); // Get the account then the group. @@ -912,7 +921,7 @@ async fn test_cache_group_fk_deferred() { // Invalidate all items. cachelayer.mark_offline().await; assert!(cachelayer.invalidate().await.is_ok()); - cachelayer.attempt_online().await; + cachelayer.mark_next_check_now(SystemTime::now()).await; assert!(cachelayer.test_connection().await); // Get the *group*. It *should* still have it's members.