From cf63c6b98b5842fda0e8cd18735c7da46d440bfb Mon Sep 17 00:00:00 2001 From: Firstyear Date: Wed, 2 Oct 2024 12:12:13 +1000 Subject: [PATCH] Complete the implementation of the posix account cache (#3041) Allow caching and checking of shadow entries (passwords) Cache and serve system id's improve some security warnings prepare for multi-resolver Allow the kanidm provider to be not configured Allow group extension --- Cargo.lock | 15 + Cargo.toml | 1 + book/src/integrations/pam_and_nsswitch.md | 48 ++- .../integrations/pam_and_nsswitch/fedora.md | 28 +- .../src/integrations/pam_and_nsswitch/suse.md | 17 +- examples/unixd | 24 +- platform/opensuse/kanidm-unixd.service | 2 +- server/lib/src/idm/oauth2.rs | 10 +- unix_integration/common/Cargo.toml | 1 + unix_integration/common/src/constants.rs | 3 +- unix_integration/common/src/unix_passwd.rs | 215 ++++++++--- unix_integration/common/src/unix_proto.rs | 20 +- unix_integration/pam_kanidm/src/pam/mod.rs | 14 +- unix_integration/pam_kanidm/src/pam/module.rs | 94 +++-- unix_integration/resolver/Cargo.toml | 2 + .../resolver/src/bin/kanidm-unix.rs | 11 +- .../resolver/src/bin/kanidm_unixd.rs | 364 ++++++++++-------- .../resolver/src/bin/kanidm_unixd_tasks.rs | 6 +- .../resolver/src/idprovider/interface.rs | 2 + .../resolver/src/idprovider/kanidm.rs | 33 ++ .../resolver/src/idprovider/system.rs | 262 ++++++++++++- unix_integration/resolver/src/resolver.rs | 223 ++++++++--- unix_integration/resolver/src/unix_config.rs | 265 +++++++++++-- .../resolver/tests/cache_layer_test.rs | 187 ++++++++- 24 files changed, 1372 insertions(+), 475 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9df48c548..3608aa64f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3346,6 +3346,7 @@ dependencies = [ "kanidm_build_profiles", "serde", "serde_json", + "serde_with", "tokio", "tokio-util", "toml", @@ -3383,7 +3384,9 @@ dependencies = [ "selinux", "serde", "serde_json", + "sha-crypt", "sketching", + "time", "tokio", "tokio-util", "toml", @@ -5585,6 +5588,18 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "sha-crypt" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e79009728d8311d42d754f2f319a975f9e38f156fd5e422d2451486c78b286" +dependencies = [ + "base64ct", + "rand", + "sha2", + "subtle", +] + [[package]] name = "sha1_smol" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index a86358bba..8c29f05d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -258,6 +258,7 @@ serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" } serde_json = "^1.0.128" serde-wasm-bindgen = "0.5" serde_with = "3.9.0" +sha-crypt = "0.5.0" sha2 = "0.10.8" shellexpand = "^2.1.2" smartstring = "^1.0.1" diff --git a/book/src/integrations/pam_and_nsswitch.md b/book/src/integrations/pam_and_nsswitch.md index 30af6354e..70c68e66e 100644 --- a/book/src/integrations/pam_and_nsswitch.md +++ b/book/src/integrations/pam_and_nsswitch.md @@ -6,15 +6,16 @@ Kanidm into accounts that can be used on the machine for various interactive tas ## The UNIX Daemon -Kanidm provides a UNIX daemon that runs on any client that wants to use PAM and nsswitch -integration. The daemon can cache the accounts for users who have unreliable networks, or who leave -the site where Kanidm is hosted. The daemon is also able to cache missing-entry responses to reduce -network traffic and Kanidm server load. +Kanidm provides a UNIX daemon that runs on any client that wants to support PAM and nsswitch. This +service has many features which are useful even without Kanidm as a network authentication service. -Additionally, running the daemon means that the PAM and nsswitch integration libraries can be small, -helping to reduce the attack surface of the machine. Similarly, a tasks daemon is available that can -create home directories on first login and supports several features related to aliases and links to -these home directories. +The Kanidm UNIX Daemon: + +* Caches Kanidm users and groups for users with unreliable networks, or for roaming users. +* Securely caches user credentials with optional TPM backed cryptographic operations. +* Automatically creates home directories for users. +* Caches and resolves the content of `/etc/passwd` and `/etc/group` improving system performance. +* Has a small set of hardened libraries to reduce attack surface. We recommend you install the client daemon from your system package manager: @@ -41,16 +42,10 @@ systemctl status kanidm-unixd-tasks > > The `kanidm_unixd_tasks` daemon is not required for PAM and nsswitch functionality. If disabled, > your system will function as usual. It is however strongly recommended due to the features it -> provides supporting Kanidm's capabilities. +> provides. -Both unixd daemons use the connection configuration from /etc/kanidm/config. This is the covered in -[client_tools](../client_tools.md#kanidm-configuration). -You can also configure some unixd-specific options with the file /etc/kanidm/unixd: - -```toml -{{#rustdoc_include ../../../examples/unixd}} -``` +You can also configure unixd with the file /etc/kanidm/unixd: > [!NOTE] > @@ -62,9 +57,17 @@ You can also configure some unixd-specific options with the file /etc/kanidm/uni > Ubuntu users please see: > [Why aren't snaps launching with home_alias set?](../frequently_asked_questions.md#why-arent-snaps-launching-with-home_alias-set) -You can then check the communication status of the daemon: +```toml +{{#rustdoc_include ../../../examples/unixd}} +``` + +If you are using the Kanidm provider features, you also need to configure +`/etc/kanidm/config`. This is the covered in [client_tools](../client_tools.md#kanidm-configuration). + +You can start, and then check the status of the daemon with the following commands: ```bash +systemctl enable --now kanidm-unixd kanidm-unix status ``` @@ -88,14 +91,17 @@ For more information, see the [Troubleshooting](pam_and_nsswitch/troubleshooting When the daemon is running you can add the nsswitch libraries to /etc/nsswitch.conf ```text -passwd: compat kanidm -group: compat kanidm +passwd: kanidm compat +group: kanidm compat ``` -You can [create a user](../accounts/intro.md) then +> NOTE: Unlike other nsswitch modules, Kanidm should be before compat or files. This is because +> Kanidm caches and provides the content from `/etc/passwd` and `/etc/group`. + +Then [create a user](../accounts/intro.md) and [enable POSIX feature on the user](../accounts/posix_accounts_and_groups.md#enabling-posix-attributes-on-accounts). -You can then test that the POSIX extended user is able to be resolved with: +Test that the POSIX extended user is able to be resolved with: ```bash getent passwd diff --git a/book/src/integrations/pam_and_nsswitch/fedora.md b/book/src/integrations/pam_and_nsswitch/fedora.md index dbd725969..b31070a31 100644 --- a/book/src/integrations/pam_and_nsswitch/fedora.md +++ b/book/src/integrations/pam_and_nsswitch/fedora.md @@ -23,22 +23,15 @@ Edit the content. # /etc/pam.d/password-auth auth required pam_env.so auth required pam_faildelay.so delay=2000000 -auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular -auth [default=1 ignore=ignore success=ok] pam_localuser.so -auth sufficient pam_unix.so nullok try_first_pass -auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular auth sufficient pam_kanidm.so ignore_unknown_user auth required pam_deny.so -account sufficient pam_unix.so -account sufficient pam_localuser.so account sufficient pam_usertype.so issystem account sufficient pam_kanidm.so ignore_unknown_user -account required pam_permit.so +account required pam_deny.so password requisite pam_pwquality.so try_first_pass local_users_only password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok -password sufficient pam_kanidm.so password required pam_deny.so session optional pam_keyinit.so revoke @@ -54,22 +47,15 @@ session optional pam_kanidm.so auth required pam_env.so auth required pam_faildelay.so delay=2000000 auth sufficient pam_fprintd.so -auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular -auth [default=1 ignore=ignore success=ok] pam_localuser.so -auth sufficient pam_unix.so nullok try_first_pass -auth [default=1 ignore=ignore success=ok] pam_usertype.so isregular auth sufficient pam_kanidm.so ignore_unknown_user auth required pam_deny.so -account sufficient pam_unix.so -account sufficient pam_localuser.so account sufficient pam_usertype.so issystem account sufficient pam_kanidm.so ignore_unknown_user -account required pam_permit.so +account required pam_deny.so password requisite pam_pwquality.so try_first_pass local_users_only password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok -password sufficient pam_kanidm.so password required pam_deny.so session optional pam_keyinit.so revoke @@ -101,13 +87,13 @@ system-auth should be the same as above. nsswitch should be modified for your us example looks like this: ```text -passwd: compat kanidm sss files systemd -group: compat kanidm sss files systemd +passwd: kanidm compat systemd +group: kanidm compat systemd shadow: files hosts: files dns myhostname -services: sss files -netgroup: sss files -automount: sss files +services: files +netgroup: files +automount: files aliases: files ethers: files diff --git a/book/src/integrations/pam_and_nsswitch/suse.md b/book/src/integrations/pam_and_nsswitch/suse.md index 72d69ff04..e44817129 100644 --- a/book/src/integrations/pam_and_nsswitch/suse.md +++ b/book/src/integrations/pam_and_nsswitch/suse.md @@ -29,43 +29,38 @@ cp /etc/pam.d/common-session-pc /etc/pam.d/common-session cp /etc/pam.d/common-password-pc /etc/pam.d/common-password ``` +> NOTE: Unlike other PAM modules, Kanidm replaces the functionality of `pam_unix` and can authenticate +> local users securely. + The content should look like: ```text # /etc/pam.d/common-account # Controls authorisation to this system (who may login) -account [default=1 ignore=ignore success=ok] pam_localuser.so -account sufficient pam_unix.so -account [default=1 ignore=ignore success=ok] pam_succeed_if.so uid >= 1000 quiet_success quiet_fail account sufficient pam_kanidm.so ignore_unknown_user +account sufficient pam_unix.so account required pam_deny.so # /etc/pam.d/common-auth # Controls authentication to this system (verification of credentials) auth required pam_env.so -auth [default=1 ignore=ignore success=ok] pam_localuser.so -auth sufficient pam_unix.so nullok try_first_pass -auth requisite pam_succeed_if.so uid >= 1000 quiet_success auth sufficient pam_kanidm.so ignore_unknown_user +auth sufficient pam_unix.so try_first_pass auth required pam_deny.so # /etc/pam.d/common-password # Controls flow of what happens when a user invokes the passwd command. Currently does NOT # push password changes back to kanidm -password [default=1 ignore=ignore success=ok] pam_localuser.so password required pam_unix.so nullok shadow try_first_pass -password [default=1 ignore=ignore success=ok] pam_succeed_if.so uid >= 1000 quiet_success quiet_fail -password required pam_kanidm.so # /etc/pam.d/common-session # Controls setup of the user session once a successful authentication and authorisation has # occurred. session optional pam_systemd.so session required pam_limits.so -session optional pam_unix.so try_first_pass session optional pam_umask.so -session [default=1 ignore=ignore success=ok] pam_succeed_if.so uid >= 1000 quiet_success quiet_fail session optional pam_kanidm.so +session optional pam_unix.so try_first_pass session optional pam_env.so ``` diff --git a/examples/unixd b/examples/unixd index aee050b3d..f9f638ce2 100644 --- a/examples/unixd +++ b/examples/unixd @@ -1,13 +1,7 @@ ## Kanidm Unixd Service Configuration - /etc/kanidm/unixd -# Defines a set of POSIX groups where membership of any of these groups -# will be allowed to login via PAM. All POSIX users and groups can be -# resolved by nss regardless of PAM login status. This may be a group -# name, spn, or uuid. -# -# Default: empty set (no access allowed) - -pam_allowed_login_groups = ["posix_group"] +# The configuration file version. +version = '2' # Kanidm unix will bind all cached credentials to a local Hardware Security # Module (HSM) to prevent exfiltration and attacks against these. In addition, @@ -132,3 +126,17 @@ pam_allowed_login_groups = ["posix_group"] # Default: Empty set (no overrides) # allow_local_account_override = ["admin"] + + +# This section enables the Kanidm provider +[kanidm] + +# Defines a set of POSIX groups where membership of any of these groups +# will be allowed to login via PAM. All POSIX users and groups can be +# resolved by NSS regardless of PAM login status. You may specify a +# group's name, SPN or UUID +# +# Default: empty set (no access allowed) + +pam_allowed_login_groups = ["posix_group"] + diff --git a/platform/opensuse/kanidm-unixd.service b/platform/opensuse/kanidm-unixd.service index 3ccb27b97..766d14772 100644 --- a/platform/opensuse/kanidm-unixd.service +++ b/platform/opensuse/kanidm-unixd.service @@ -12,7 +12,7 @@ Conflicts=nscd.service [Service] DynamicUser=yes -SupplementaryGroups=tss +SupplementaryGroups=tss shadow UMask=0027 CacheDirectory=kanidm-unixd RuntimeDirectory=kanidm-unixd diff --git a/server/lib/src/idm/oauth2.rs b/server/lib/src/idm/oauth2.rs index c515338be..1ae395893 100644 --- a/server/lib/src/idm/oauth2.rs +++ b/server/lib/src/idm/oauth2.rs @@ -3240,7 +3240,7 @@ mod tests { let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble"); let pkce_request = Some(PkceRequest { - code_challenge: code_challenge, + code_challenge, code_challenge_method: CodeChallengeMethod::S256, }); @@ -3749,7 +3749,7 @@ mod tests { client_id: "test_resource_server".to_string(), state: "123".to_string(), pkce_request: Some(PkceRequest { - code_challenge: code_challenge, + code_challenge, code_challenge_method: CodeChallengeMethod::S256, }), redirect_uri: Url::parse("app://cheese").unwrap(), @@ -5135,7 +5135,7 @@ mod tests { client_id: "test_resource_server".to_string(), state: "123".to_string(), pkce_request: Some(PkceRequest { - code_challenge: code_challenge, + code_challenge, code_challenge_method: CodeChallengeMethod::S256, }), redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(), @@ -5192,7 +5192,7 @@ mod tests { client_id: "test_resource_server".to_string(), state: "123".to_string(), pkce_request: Some(PkceRequest { - code_challenge: code_challenge, + code_challenge, code_challenge_method: CodeChallengeMethod::S256, }), redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(), @@ -6363,7 +6363,7 @@ mod tests { client_id: "test_resource_server".to_string(), state: "123".to_string(), pkce_request: Some(PkceRequest { - code_challenge: code_challenge, + code_challenge, code_challenge_method: CodeChallengeMethod::S256, }), redirect_uri: Url::parse("http://localhost:8765/oauth2/result").unwrap(), diff --git a/unix_integration/common/Cargo.toml b/unix_integration/common/Cargo.toml index cb408e2af..7bd11d8d5 100644 --- a/unix_integration/common/Cargo.toml +++ b/unix_integration/common/Cargo.toml @@ -27,6 +27,7 @@ csv = { workspace = true } futures = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +serde_with = { workspace = true } toml = { workspace = true } tokio = { workspace = true, features = ["time","net","macros"] } tokio-util = { workspace = true, features = ["codec"] } diff --git a/unix_integration/common/src/constants.rs b/unix_integration/common/src/constants.rs index a29b1b3c0..9ddf23e78 100644 --- a/unix_integration/common/src/constants.rs +++ b/unix_integration/common/src/constants.rs @@ -3,7 +3,8 @@ use crate::unix_config::{HomeAttr, UidAttr}; pub const DEFAULT_CONFIG_PATH: &str = "/etc/kanidm/unixd"; pub const DEFAULT_SOCK_PATH: &str = "/var/run/kanidm-unixd/sock"; pub const DEFAULT_TASK_SOCK_PATH: &str = "/var/run/kanidm-unixd/task_sock"; -pub const DEFAULT_DB_PATH: &str = "/var/cache/kanidm-unixd/kanidm.cache.db"; +pub const DEFAULT_PERSISTENT_DB_PATH: &str = "/var/lib/kanidm-unixd/kanidm.db"; +pub const DEFAULT_CACHE_DB_PATH: &str = "/var/cache/kanidm-unixd/kanidm.cache.db"; pub const DEFAULT_CONN_TIMEOUT: u64 = 2; pub const DEFAULT_CACHE_TIMEOUT: u64 = 15; pub const DEFAULT_SHELL: &str = env!("KANIDM_DEFAULT_UNIX_SHELL_PATH"); diff --git a/unix_integration/common/src/unix_passwd.rs b/unix_integration/common/src/unix_passwd.rs index 1272cbbbe..13f2f3e35 100644 --- a/unix_integration/common/src/unix_passwd.rs +++ b/unix_integration/common/src/unix_passwd.rs @@ -1,11 +1,9 @@ -use serde::{ - de::{self, Visitor}, - Deserialize, Deserializer, Serialize, -}; +use serde::{Deserialize, Serialize}; -use std::fmt; +use serde_with::formats::CommaSeparator; +use serde_with::{serde_as, DefaultOnNull, StringWithSeparator}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct EtcUser { pub name: String, pub password: String, @@ -27,36 +25,51 @@ pub fn parse_etc_passwd(bytes: &[u8]) -> Result, UnixIntegrationErr .collect::, UnixIntegrationError>>() } -fn members<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - struct InnerCsv; - - impl<'de> Visitor<'de> for InnerCsv { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("string") - } - - fn visit_str(self, value: &str) -> Result, E> - where - E: de::Error, - { - Ok(value.split(',').map(|s| s.to_string()).collect()) - } - } - - deserializer.deserialize_str(InnerCsv) +#[serde_as] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct EtcShadow { + pub name: String, + pub password: String, + // 0 means must change next login. + // None means all other aging features are disabled + pub epoch_change_days: Option, + // 0 means no age + #[serde_as(deserialize_as = "DefaultOnNull")] + pub days_min_password_age: i64, + pub days_max_password_age: Option, + // 0 means no warning + #[serde_as(deserialize_as = "DefaultOnNull")] + pub days_warning_period: i64, + // Number of days after max_password_age passes where the password can + // still be accepted such that the user can update their password + pub days_inactivity_period: Option, + pub epoch_expire_date: Option, + pub flag_reserved: Option, } -#[derive(Serialize, Deserialize, Debug)] +pub fn parse_etc_shadow(bytes: &[u8]) -> Result, UnixIntegrationError> { + use csv::ReaderBuilder; + let mut rdr = ReaderBuilder::new() + .has_headers(false) + .delimiter(b':') + .from_reader(bytes); + rdr.deserialize() + .map(|result| { + result.map_err(|err| { + eprintln!("{:?}", err); + UnixIntegrationError + }) + }) + .collect::, UnixIntegrationError>>() +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct EtcGroup { pub name: String, pub password: String, pub gid: u32, - #[serde(deserialize_with = "members")] + #[serde_as(as = "StringWithSeparator::")] pub members: Vec, } @@ -80,36 +93,134 @@ mod tests { const EXAMPLE_PASSWD: &str = r#"root:x:0:0:root:/root:/bin/bash systemd-timesync:x:498:498:systemd Time Synchronization:/:/usr/sbin/nologin -messagebus:x:484:484:User for D-Bus:/run/dbus:/usr/sbin/nologin -tftp:x:483:483:TFTP Account:/srv/tftpboot:/usr/sbin/nologin nobody:x:65534:65534:nobody:/var/lib/nobody:/bin/bash -"#; - - const EXAMPLE_GROUP: &str = r#"root:x:0: -shadow:x:15: -trusted:x:42: -users:x:100: -systemd-journal:x:499: -systemd-timesync:x:498: -kmem:x:497: -lock:x:496: -tty:x:5: -wheel:x:481:admin,testuser "#; #[test] fn test_parse_passwd() { - for record in - parse_etc_passwd(EXAMPLE_PASSWD.as_bytes()).expect("Failed to parse passwd data") - { - println!("{:?}", record); - } + let users = + parse_etc_passwd(EXAMPLE_PASSWD.as_bytes()).expect("Failed to parse passwd data"); + + assert_eq!( + users[0], + EtcUser { + name: "root".to_string(), + password: "x".to_string(), + uid: 0, + gid: 0, + gecos: "root".to_string(), + homedir: "/root".to_string(), + shell: "/bin/bash".to_string(), + } + ); + + assert_eq!( + users[1], + EtcUser { + name: "systemd-timesync".to_string(), + password: "x".to_string(), + uid: 498, + gid: 498, + gecos: "systemd Time Synchronization".to_string(), + homedir: "/".to_string(), + shell: "/usr/sbin/nologin".to_string(), + } + ); + + assert_eq!( + users[2], + EtcUser { + name: "nobody".to_string(), + password: "x".to_string(), + uid: 65534, + gid: 65534, + gecos: "nobody".to_string(), + homedir: "/var/lib/nobody".to_string(), + shell: "/bin/bash".to_string(), + } + ); } + // IMPORTANT this is the password "a". Very secure, totes secret. + const EXAMPLE_SHADOW: &str = r#"sshd:!:19978:::::: +tss:!:19980:::::: +admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//:19980:0:99999:7::: +"#; + + #[test] + fn test_parse_shadow() { + let shadow = + parse_etc_shadow(EXAMPLE_SHADOW.as_bytes()).expect("Failed to parse passwd data"); + + assert_eq!( + shadow[0], + EtcShadow { + name: "sshd".to_string(), + password: "!".to_string(), + epoch_change_days: Some(19978), + days_min_password_age: 0, + days_max_password_age: None, + days_warning_period: 0, + days_inactivity_period: None, + epoch_expire_date: None, + flag_reserved: None + } + ); + + assert_eq!( + shadow[1], + EtcShadow { + name: "tss".to_string(), + password: "!".to_string(), + epoch_change_days: Some(19980), + days_min_password_age: 0, + days_max_password_age: None, + days_warning_period: 0, + days_inactivity_period: None, + epoch_expire_date: None, + flag_reserved: None + } + ); + + assert_eq!(shadow[2], EtcShadow { + name: "admin".to_string(), + password: "$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string(), + epoch_change_days: Some(19980), + days_min_password_age: 0, + days_max_password_age: Some(99999), + days_warning_period: 7, + days_inactivity_period: None, + epoch_expire_date: None, + flag_reserved: None + }); + } + + const EXAMPLE_GROUP: &str = r#"root:x:0: +wheel:x:481:admin,testuser +"#; + #[test] fn test_parse_group() { - for record in parse_etc_group(EXAMPLE_GROUP.as_bytes()).expect("Failed to parse group") { - println!("{:?}", record); - } + let groups = parse_etc_group(EXAMPLE_GROUP.as_bytes()).expect("Failed to parse groups"); + + assert_eq!( + groups[0], + EtcGroup { + name: "root".to_string(), + password: "x".to_string(), + gid: 0, + members: vec![] + } + ); + + assert_eq!( + groups[1], + EtcGroup { + name: "wheel".to_string(), + password: "x".to_string(), + gid: 481, + members: vec!["admin".to_string(), "testuser".to_string(),] + } + ); } } diff --git a/unix_integration/common/src/unix_proto.rs b/unix_integration/common/src/unix_proto.rs index 7801f1437..8ccbcd75c 100644 --- a/unix_integration/common/src/unix_proto.rs +++ b/unix_integration/common/src/unix_proto.rs @@ -102,6 +102,13 @@ pub enum PamAuthRequest { Pin { cred: String }, } +#[derive(Serialize, Deserialize, Debug)] +pub struct PamServiceInfo { + pub service: String, + pub tty: String, + pub rhost: String, +} + #[derive(Serialize, Deserialize, Debug)] pub enum ClientRequest { SshKey(String), @@ -111,7 +118,10 @@ pub enum ClientRequest { NssGroups, NssGroupByGid(u32), NssGroupByName(String), - PamAuthenticateInit(String), + PamAuthenticateInit { + account_id: String, + info: PamServiceInfo, + }, PamAuthenticateStep(PamAuthRequest), PamAccountAllowed(String), PamAccountBeginSession(String), @@ -131,7 +141,10 @@ impl ClientRequest { ClientRequest::NssGroups => "NssGroups".to_string(), ClientRequest::NssGroupByGid(id) => format!("NssGroupByGid({})", id), ClientRequest::NssGroupByName(id) => format!("NssGroupByName({})", id), - ClientRequest::PamAuthenticateInit(id) => format!("PamAuthenticateInit({})", id), + ClientRequest::PamAuthenticateInit { account_id, info } => format!( + "PamAuthenticateInit{{ account_id={} tty={} pam_secvice{} rhost={} }}", + account_id, info.service, info.tty, info.rhost + ), ClientRequest::PamAuthenticateStep(_) => "PamAuthenticateStep".to_string(), ClientRequest::PamAccountAllowed(id) => { format!("PamAccountAllowed({})", id) @@ -173,8 +186,9 @@ impl From for ClientResponse { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct HomeDirectoryInfo { + pub uid: u32, pub gid: u32, pub name: String, pub aliases: Vec, diff --git a/unix_integration/pam_kanidm/src/pam/mod.rs b/unix_integration/pam_kanidm/src/pam/mod.rs index 8b24e425e..6a76c6114 100755 --- a/unix_integration/pam_kanidm/src/pam/mod.rs +++ b/unix_integration/pam_kanidm/src/pam/mod.rs @@ -218,11 +218,15 @@ impl PamHooks for PamKanidm { install_subscriber(opts.debug); - // This will == "Ok(Some("ssh"))" on remote auth. - let tty = pamh.get_tty(); - let rhost = pamh.get_rhost(); + let info = match pamh.get_pam_info() { + Ok(info) => info, + Err(e) => { + error!(err = ?e, "get_pam_info"); + return e; + } + }; - debug!(?args, ?opts, ?tty, ?rhost, "sm_authenticate"); + debug!(?args, ?opts, ?info, "sm_authenticate"); let account_id = match pamh.get_user(None) { Ok(aid) => aid, @@ -273,7 +277,7 @@ impl PamHooks for PamKanidm { } }; - let mut req = ClientRequest::PamAuthenticateInit(account_id); + let mut req = ClientRequest::PamAuthenticateInit { account_id, info }; loop { match_sm_auth_client_response!(daemon_client.call_and_wait(&req, timeout), opts, diff --git a/unix_integration/pam_kanidm/src/pam/module.rs b/unix_integration/pam_kanidm/src/pam/module.rs index a62dcc4d8..8cfcdbb11 100755 --- a/unix_integration/pam_kanidm/src/pam/module.rs +++ b/unix_integration/pam_kanidm/src/pam/module.rs @@ -5,7 +5,10 @@ use std::{mem, ptr}; use libc::c_char; -use crate::pam::constants::{PamFlag, PamItemType, PamResultCode, PAM_AUTHTOK, PAM_RHOST, PAM_TTY}; +use crate::pam::constants::{PamFlag, PamItemType, PamResultCode}; +use crate::pam::items::{PamAuthTok, PamRHost, PamService, PamTty}; + +use kanidm_unix_common::unix_proto::PamServiceInfo; /// Opaque type, used as a pointer when making pam API calls. /// @@ -143,6 +146,25 @@ impl PamHandle { } } + pub fn get_item_string(&self) -> PamResult> { + let mut ptr: *const PamItemT = ptr::null(); + let (res, item) = unsafe { + let r = pam_get_item(self, T::item_type(), &mut ptr); + let t = if PamResultCode::PAM_SUCCESS == r && !ptr.is_null() { + let typed_ptr: *const c_char = ptr as *const c_char; + Some(CStr::from_ptr(typed_ptr).to_string_lossy().into_owned()) + } else { + None + }; + (r, t) + }; + if PamResultCode::PAM_SUCCESS == res { + Ok(item) + } else { + Err(res) + } + } + /// Sets a value in the pam context. The value can be retrieved using /// `get_item`. /// @@ -198,59 +220,35 @@ impl PamHandle { } pub fn get_authtok(&self) -> PamResult> { - let mut ptr: *const PamItemT = ptr::null(); - let (res, item) = unsafe { - let r = pam_get_item(self, PAM_AUTHTOK, &mut ptr); - let t = if PamResultCode::PAM_SUCCESS == r && !ptr.is_null() { - let typed_ptr: *const c_char = ptr as *const c_char; - Some(CStr::from_ptr(typed_ptr).to_string_lossy().into_owned()) - } else { - None - }; - (r, t) - }; - if PamResultCode::PAM_SUCCESS == res { - Ok(item) - } else { - Err(res) - } + self.get_item_string::() } pub fn get_tty(&self) -> PamResult> { - let mut ptr: *const PamItemT = ptr::null(); - let (res, item) = unsafe { - let r = pam_get_item(self, PAM_TTY, &mut ptr); - let t = if PamResultCode::PAM_SUCCESS == r && !ptr.is_null() { - let typed_ptr: *const c_char = ptr as *const c_char; - Some(CStr::from_ptr(typed_ptr).to_string_lossy().into_owned()) - } else { - None - }; - (r, t) - }; - if PamResultCode::PAM_SUCCESS == res { - Ok(item) - } else { - Err(res) - } + self.get_item_string::() } pub fn get_rhost(&self) -> PamResult> { - let mut ptr: *const PamItemT = ptr::null(); - let (res, item) = unsafe { - let r = pam_get_item(self, PAM_RHOST, &mut ptr); - let t = if PamResultCode::PAM_SUCCESS == r && !ptr.is_null() { - let typed_ptr: *const c_char = ptr as *const c_char; - Some(CStr::from_ptr(typed_ptr).to_string_lossy().into_owned()) - } else { - None - }; - (r, t) - }; - if PamResultCode::PAM_SUCCESS == res { - Ok(item) - } else { - Err(res) + self.get_item_string::() + } + + pub fn get_service(&self) -> PamResult> { + self.get_item_string::() + } + + pub fn get_pam_info(&self) -> PamResult { + let maybe_tty = self.get_tty()?; + let maybe_rhost = self.get_rhost()?; + let maybe_service = self.get_service()?; + + tracing::debug!(?maybe_tty, ?maybe_rhost, ?maybe_service); + + match (maybe_tty, maybe_rhost, maybe_service) { + (Some(tty), Some(rhost), Some(service)) => Ok(PamServiceInfo { + service, + tty, + rhost, + }), + _ => Err(PamResultCode::PAM_CONV_ERR), } } } diff --git a/unix_integration/resolver/Cargo.toml b/unix_integration/resolver/Cargo.toml index 3c02db2d5..f4f9c8e2a 100644 --- a/unix_integration/resolver/Cargo.toml +++ b/unix_integration/resolver/Cargo.toml @@ -75,6 +75,8 @@ selinux = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sketching = { workspace = true } +sha-crypt = { workspace = true } +time = { workspace = true, features = ["std"] } toml = { workspace = true } tokio = { workspace = true, features = [ "rt", diff --git a/unix_integration/resolver/src/bin/kanidm-unix.rs b/unix_integration/resolver/src/bin/kanidm-unix.rs index e43ba6ed7..690eb7285 100644 --- a/unix_integration/resolver/src/bin/kanidm-unix.rs +++ b/unix_integration/resolver/src/bin/kanidm-unix.rs @@ -20,7 +20,7 @@ use kanidm_unix_common::client::call_daemon; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; use kanidm_unix_common::unix_config::KanidmUnixdConfig; use kanidm_unix_common::unix_proto::{ - ClientRequest, ClientResponse, PamAuthRequest, PamAuthResponse, + ClientRequest, ClientResponse, PamAuthRequest, PamAuthResponse, PamServiceInfo, }; // use std::io; use std::path::PathBuf; @@ -63,7 +63,14 @@ async fn main() -> ExitCode { info!("Sending request for user {}", &account_id); - let mut req = ClientRequest::PamAuthenticateInit(account_id.clone()); + let mut req = ClientRequest::PamAuthenticateInit { + account_id: account_id.clone(), + info: PamServiceInfo { + service: "kanidm-unix".to_string(), + tty: "/dev/null".to_string(), + rhost: "localhost".to_string(), + }, + }; loop { match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await { Ok(r) => match r { diff --git a/unix_integration/resolver/src/bin/kanidm_unixd.rs b/unix_integration/resolver/src/bin/kanidm_unixd.rs index b0ed140c1..247fbb525 100644 --- a/unix_integration/resolver/src/bin/kanidm_unixd.rs +++ b/unix_integration/resolver/src/bin/kanidm_unixd.rs @@ -27,13 +27,14 @@ use futures::{SinkExt, StreamExt}; use kanidm_client::KanidmClientBuilder; use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; -use kanidm_unix_common::unix_passwd::{parse_etc_group, parse_etc_passwd}; +use kanidm_unix_common::unix_passwd::{parse_etc_group, parse_etc_passwd, parse_etc_shadow}; use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse, TaskRequest, TaskResponse}; use kanidm_unix_resolver::db::{Cache, Db}; +use kanidm_unix_resolver::idprovider::interface::IdProvider; 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}; +use kanidm_unix_resolver::unix_config::{HsmType, UnixdConfig}; use kanidm_utils_users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid}; use libc::umask; @@ -41,13 +42,13 @@ use sketching::tracing::span; use sketching::tracing_forest::traits::*; use sketching::tracing_forest::util::*; use sketching::tracing_forest::{self}; +use time::OffsetDateTime; use tokio::fs::File; use tokio::io::AsyncReadExt; // for read_to_end() use tokio::net::{UnixListener, UnixStream}; use tokio::sync::broadcast; use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::oneshot; -use tokio::time; use tokio_util::codec::{Decoder, Encoder, Framed}; use kanidm_hsm_crypto::{soft::SoftTpm, AuthValue, BoxedDynTpm, Tpm}; @@ -213,90 +214,67 @@ async fn handle_client( trace!("Waiting for requests ..."); while let Some(Ok(req)) = reqs.next().await { - let span = span!(Level::INFO, "client_request"); + let span = span!(Level::DEBUG, "client_request"); let _enter = span.enter(); let resp = match req { - ClientRequest::SshKey(account_id) => { - debug!("sshkey req"); - cachelayer - .get_sshkeys(account_id.as_str()) - .await - .map(ClientResponse::SshKeys) - .unwrap_or_else(|_| { - error!("unable to load keys, returning empty set."); - ClientResponse::SshKeys(vec![]) - }) - } - ClientRequest::NssAccounts => { - debug!("nssaccounts req"); - cachelayer - .get_nssaccounts() - .await - .map(ClientResponse::NssAccounts) - .unwrap_or_else(|_| { - error!("unable to enum accounts"); - ClientResponse::NssAccounts(Vec::new()) - }) - } - ClientRequest::NssAccountByUid(gid) => { - debug!("nssaccountbyuid req"); - cachelayer - .get_nssaccount_gid(gid) - .await - .map(ClientResponse::NssAccount) - .unwrap_or_else(|_| { - error!("unable to load account, returning empty."); - ClientResponse::NssAccount(None) - }) - } - ClientRequest::NssAccountByName(account_id) => { - debug!("nssaccountbyname req"); - cachelayer - .get_nssaccount_name(account_id.as_str()) - .await - .map(ClientResponse::NssAccount) - .unwrap_or_else(|_| { - error!("unable to load account, returning empty."); - ClientResponse::NssAccount(None) - }) - } - ClientRequest::NssGroups => { - debug!("nssgroups req"); - cachelayer - .get_nssgroups() - .await - .map(ClientResponse::NssGroups) - .unwrap_or_else(|_| { - error!("unable to enum groups"); - ClientResponse::NssGroups(Vec::new()) - }) - } - ClientRequest::NssGroupByGid(gid) => { - debug!("nssgroupbygid req"); - cachelayer - .get_nssgroup_gid(gid) - .await - .map(ClientResponse::NssGroup) - .unwrap_or_else(|_| { - error!("unable to load group, returning empty."); - ClientResponse::NssGroup(None) - }) - } - ClientRequest::NssGroupByName(grp_id) => { - debug!("nssgroupbyname req"); - cachelayer - .get_nssgroup_name(grp_id.as_str()) - .await - .map(ClientResponse::NssGroup) - .unwrap_or_else(|_| { - error!("unable to load group, returning empty."); - ClientResponse::NssGroup(None) - }) - } - ClientRequest::PamAuthenticateInit(account_id) => { - debug!("pam authenticate init"); - + ClientRequest::SshKey(account_id) => cachelayer + .get_sshkeys(account_id.as_str()) + .await + .map(ClientResponse::SshKeys) + .unwrap_or_else(|_| { + error!("unable to load keys, returning empty set."); + ClientResponse::SshKeys(vec![]) + }), + ClientRequest::NssAccounts => cachelayer + .get_nssaccounts() + .await + .map(ClientResponse::NssAccounts) + .unwrap_or_else(|_| { + error!("unable to enum accounts"); + ClientResponse::NssAccounts(Vec::new()) + }), + ClientRequest::NssAccountByUid(gid) => cachelayer + .get_nssaccount_gid(gid) + .await + .map(ClientResponse::NssAccount) + .unwrap_or_else(|_| { + error!("unable to load account, returning empty."); + ClientResponse::NssAccount(None) + }), + ClientRequest::NssAccountByName(account_id) => cachelayer + .get_nssaccount_name(account_id.as_str()) + .await + .map(ClientResponse::NssAccount) + .unwrap_or_else(|_| { + error!("unable to load account, returning empty."); + ClientResponse::NssAccount(None) + }), + ClientRequest::NssGroups => cachelayer + .get_nssgroups() + .await + .map(ClientResponse::NssGroups) + .unwrap_or_else(|_| { + error!("unable to enum groups"); + ClientResponse::NssGroups(Vec::new()) + }), + ClientRequest::NssGroupByGid(gid) => cachelayer + .get_nssgroup_gid(gid) + .await + .map(ClientResponse::NssGroup) + .unwrap_or_else(|_| { + error!("unable to load group, returning empty."); + ClientResponse::NssGroup(None) + }), + ClientRequest::NssGroupByName(grp_id) => cachelayer + .get_nssgroup_name(grp_id.as_str()) + .await + .map(ClientResponse::NssGroup) + .unwrap_or_else(|_| { + error!("unable to load group, returning empty."); + ClientResponse::NssGroup(None) + }), + ClientRequest::PamAuthenticateInit { account_id, info } => { match &pam_auth_session_state { Some(_auth_session) => { // Invalid to init a request twice. @@ -306,9 +284,13 @@ async fn handle_client( ClientResponse::Error } None => { + let current_time = OffsetDateTime::now_utc(); + match cachelayer .pam_account_authenticate_init( account_id.as_str(), + &info, + current_time, shutdown_tx.subscribe(), ) .await @@ -322,30 +304,23 @@ async fn handle_client( } } } - ClientRequest::PamAuthenticateStep(pam_next_req) => { - debug!("pam authenticate step"); - match &mut pam_auth_session_state { - Some(auth_session) => cachelayer - .pam_account_authenticate_step(auth_session, pam_next_req) - .await - .map(|pam_auth_response| pam_auth_response.into()) - .unwrap_or(ClientResponse::Error), - None => { - warn!("Attempt to continue auth session while current session is inactive"); - ClientResponse::Error - } - } - } - ClientRequest::PamAccountAllowed(account_id) => { - debug!("pam account allowed"); - cachelayer - .pam_account_allowed(account_id.as_str()) + ClientRequest::PamAuthenticateStep(pam_next_req) => match &mut pam_auth_session_state { + Some(auth_session) => cachelayer + .pam_account_authenticate_step(auth_session, pam_next_req) .await - .map(ClientResponse::PamStatus) - .unwrap_or(ClientResponse::Error) - } + .map(|pam_auth_response| pam_auth_response.into()) + .unwrap_or(ClientResponse::Error), + None => { + warn!("Attempt to continue auth session while current session is inactive"); + ClientResponse::Error + } + }, + ClientRequest::PamAccountAllowed(account_id) => cachelayer + .pam_account_allowed(account_id.as_str()) + .await + .map(ClientResponse::PamStatus) + .unwrap_or(ClientResponse::Error), ClientRequest::PamAccountBeginSession(account_id) => { - debug!("pam account begin session"); match cachelayer .pam_account_beginsession(account_id.as_str()) .await @@ -362,8 +337,8 @@ async fn handle_client( { Ok(()) => { // Now wait for the other end OR timeout. - match time::timeout_at( - time::Instant::now() + Duration::from_millis(1000), + match tokio::time::timeout_at( + tokio::time::Instant::now() + Duration::from_millis(1000), rx, ) .await @@ -384,19 +359,19 @@ async fn handle_client( } } } + Ok(None) => { + // The session can begin, but we do not need to create the home dir. + ClientResponse::Ok + } _ => ClientResponse::Error, } } - ClientRequest::InvalidateCache => { - debug!("invalidate cache"); - cachelayer - .invalidate() - .await - .map(|_| ClientResponse::Ok) - .unwrap_or(ClientResponse::Error) - } + ClientRequest::InvalidateCache => cachelayer + .invalidate() + .await + .map(|_| ClientResponse::Ok) + .unwrap_or(ClientResponse::Error), ClientRequest::ClearCache => { - debug!("clear cache"); if ucred.uid() == 0 { cachelayer .clear_cache() @@ -409,14 +384,13 @@ async fn handle_client( } } ClientRequest::Status => { - debug!("status check"); let status = cachelayer.provider_status().await; ClientResponse::ProviderStatus(status) } }; reqs.send(resp).await?; reqs.flush().await?; - debug!("flushed response!"); + trace!("flushed response!"); } // Signal any tasks that they need to stop. @@ -439,13 +413,33 @@ async fn process_etc_passwd_group(cachelayer: &Resolver) -> Result<(), Box { + let mut contents = vec![]; + file.read_to_end(&mut contents).await?; + + let shadow = + parse_etc_shadow(contents.as_slice()).map_err(|_| "Invalid passwd content")?; + Some(shadow) + } + Err(io_err) => { + warn!( + ?io_err, + "Unable to read /etc/shadow, some features will be disabled." + ); + None + } + }; + let mut file = File::open("/etc/group").await?; let mut contents = vec![]; file.read_to_end(&mut contents).await?; let groups = parse_etc_group(contents.as_slice()).map_err(|_| "Invalid group content")?; - cachelayer.reload_system_identities(users, groups).await; + cachelayer + .reload_system_identities(users, maybe_shadow, groups) + .await; Ok(()) } @@ -484,7 +478,10 @@ async fn write_hsm_pin(hsm_pin_path: &str) -> Result<(), Box> { fn open_tpm(tcti_name: &str) -> Option { use kanidm_hsm_crypto::tpm::TpmTss; match TpmTss::new(tcti_name) { - Ok(tpm) => Some(BoxedDynTpm::new(tpm)), + Ok(tpm) => { + debug!("opened hw tpm"); + Some(BoxedDynTpm::new(tpm)) + } Err(tpm_err) => { error!(?tpm_err, "Unable to open requested tpm device"); None @@ -502,7 +499,10 @@ fn open_tpm(_tcti_name: &str) -> Option { fn open_tpm_if_possible(tcti_name: &str) -> BoxedDynTpm { use kanidm_hsm_crypto::tpm::TpmTss; match TpmTss::new(tcti_name) { - Ok(tpm) => BoxedDynTpm::new(tpm), + Ok(tpm) => { + debug!("opened hw tpm"); + BoxedDynTpm::new(tpm) + } Err(tpm_err) => { warn!( ?tpm_err, @@ -515,6 +515,7 @@ fn open_tpm_if_possible(tcti_name: &str) -> BoxedDynTpm { #[cfg(not(feature = "tpm"))] fn open_tpm_if_possible(_tcti_name: &str) -> BoxedDynTpm { + debug!("opened soft tpm"); BoxedDynTpm::new(SoftTpm::new()) } @@ -681,16 +682,7 @@ async fn main() -> ExitCode { } } - // setup - let cb = match KanidmClientBuilder::new().read_options_from_optional_config(&cfg_path) { - Ok(v) => v, - Err(_) => { - error!("Failed to parse {}", cfg_path_str); - return ExitCode::FAILURE - } - }; - - let cfg = match KanidmUnixdConfig::new().read_options_from_optional_config(&unixd_path) { + let cfg = match UnixdConfig::new().read_options_from_optional_config(&unixd_path) { Ok(v) => v, Err(_) => { error!("Failed to parse {}", unixd_path_str); @@ -698,14 +690,31 @@ async fn main() -> ExitCode { } }; + let client_builder = if let Some(kconfig) = &cfg.kanidm_config { + // setup + let cb = match KanidmClientBuilder::new().read_options_from_optional_config(&cfg_path) { + Ok(v) => v, + Err(_) => { + error!("Failed to parse {}", cfg_path_str); + return ExitCode::FAILURE + } + }; + + Some((cb, kconfig)) + } else { None }; + if clap_args.get_flag("configtest") { eprintln!("###################################"); eprintln!("Dumping configs:\n###################################"); eprintln!("kanidm_unixd config (from {:#?})", &unixd_path); eprintln!("{}", cfg); eprintln!("###################################"); - eprintln!("Client config (from {:#?})", &cfg_path); - eprintln!("{}", cb); + if let Some((cb, _)) = client_builder.as_ref() { + eprintln!("kanidm client config (from {:#?})", &cfg_path); + eprintln!("{}", cb); + } else { + eprintln!("kanidm client: disabled"); + } return ExitCode::SUCCESS; } @@ -714,10 +723,10 @@ async fn main() -> ExitCode { rm_if_exist(cfg.task_sock_path.as_str()); // Check the db path will be okay. - if !cfg.db_path.is_empty() { - let db_path = PathBuf::from(cfg.db_path.as_str()); + if !cfg.cache_db_path.is_empty() { + let cache_db_path = PathBuf::from(cfg.cache_db_path.as_str()); // We only need to check the parent folder path permissions as the db itself may not exist yet. - if let Some(db_parent_path) = db_path.parent() { + if let Some(db_parent_path) = cache_db_path.parent() { if !db_parent_path.exists() { error!( "Refusing to run, DB folder {} does not exist", @@ -725,7 +734,7 @@ async fn main() -> ExitCode { .to_str() .unwrap_or("") ); - let diag = kanidm_lib_file_permissions::diagnose_path(db_path.as_ref()); + let diag = kanidm_lib_file_permissions::diagnose_path(cache_db_path.as_ref()); info!(%diag); return ExitCode::FAILURE } @@ -769,26 +778,26 @@ async fn main() -> ExitCode { } // check to see if the db's already there - if db_path.exists() { - if !db_path.is_file() { + if cache_db_path.exists() { + if !cache_db_path.is_file() { error!( "Refusing to run - DB path {} already exists and is not a file.", - db_path.to_str().unwrap_or("") + cache_db_path.to_str().unwrap_or("") ); - let diag = kanidm_lib_file_permissions::diagnose_path(db_path.as_ref()); + let diag = kanidm_lib_file_permissions::diagnose_path(cache_db_path.as_ref()); info!(%diag); return ExitCode::FAILURE }; - match metadata(&db_path) { + match metadata(&cache_db_path) { Ok(v) => v, Err(e) => { error!( "Unable to read metadata for {} - {:?}", - db_path.to_str().unwrap_or(""), + cache_db_path.to_str().unwrap_or(""), e ); - let diag = kanidm_lib_file_permissions::diagnose_path(db_path.as_ref()); + let diag = kanidm_lib_file_permissions::diagnose_path(cache_db_path.as_ref()); info!(%diag); return ExitCode::FAILURE } @@ -797,18 +806,7 @@ async fn main() -> ExitCode { }; } - let cb = cb.connect_timeout(cfg.conn_timeout); - let cb = cb.request_timeout(cfg.request_timeout); - - let rsclient = match cb.build() { - Ok(rsc) => rsc, - Err(_e) => { - error!("Failed to build async client"); - return ExitCode::FAILURE - } - }; - - let db = match Db::new(cfg.db_path.as_str()) { + let db = match Db::new(cfg.cache_db_path.as_str()) { Ok(db) => db, Err(_e) => { error!("Failed to create database"); @@ -900,7 +898,7 @@ async fn main() -> ExitCode { Ok(mk) => mk, Err(err) => { error!(?err, "Unable to load machine root key - This can occur if you have changed your HSM pin"); - error!("To proceed you must remove the content of the cache db ({}) to reset all keys", cfg.db_path.as_str()); + error!("To proceed you must remove the content of the cache db ({}) to reset all keys", cfg.cache_db_path.as_str()); return ExitCode::FAILURE } }; @@ -911,16 +909,39 @@ async fn main() -> ExitCode { 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 - }; + info!("Started system provider"); + + let mut clients: Vec> = Vec::with_capacity(1); + + // Setup Kanidm provider if the configuration requests it. + if let Some((cb, kconfig)) = client_builder { + let cb = cb.connect_timeout(kconfig.conn_timeout); + let cb = cb.request_timeout(kconfig.request_timeout); + + let rsclient = match cb.build() { + Ok(rsc) => rsc, + Err(_e) => { + error!("Failed to build async client"); + return ExitCode::FAILURE + } + }; + + let Ok(idprovider) = KanidmProvider::new( + rsclient, + kconfig, + SystemTime::now(), + &mut (&mut db_txn).into(), + &mut hsm, + &machine_key + ) else { + error!("Failed to configure Kanidm Provider"); + return ExitCode::FAILURE + }; + + // Now stacked for the resolver. + clients.push(Arc::new(idprovider)); + info!("Started kanidm provider"); + } drop(machine_key); @@ -937,14 +958,12 @@ async fn main() -> ExitCode { } // Okay, the hsm is now loaded and ready to go. - let cl_inner = match Resolver::new( db, Arc::new(system_provider), - Arc::new(idprovider), + clients, hsm, cfg.cache_timeout, - cfg.pam_allowed_login_groups.clone(), cfg.default_shell.clone(), cfg.home_prefix.clone(), cfg.home_attr, @@ -1054,6 +1073,9 @@ async fn main() -> ExitCode { }) .and_then(|mut debouncer| debouncer.watcher().watch(Path::new("/etc/group"), RecursiveMode::NonRecursive) .map(|()| debouncer) + ) + .and_then(|mut debouncer| debouncer.watcher().watch(Path::new("/etc/shadow"), RecursiveMode::NonRecursive) + .map(|()| debouncer) ); let watcher = match watcher { diff --git a/unix_integration/resolver/src/bin/kanidm_unixd_tasks.rs b/unix_integration/resolver/src/bin/kanidm_unixd_tasks.rs index b0f546347..fe1a9510c 100644 --- a/unix_integration/resolver/src/bin/kanidm_unixd_tasks.rs +++ b/unix_integration/resolver/src/bin/kanidm_unixd_tasks.rs @@ -22,7 +22,7 @@ use bytes::{BufMut, BytesMut}; use futures::{SinkExt, StreamExt}; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; use kanidm_unix_common::unix_proto::{HomeDirectoryInfo, TaskRequest, TaskResponse}; -use kanidm_unix_resolver::unix_config::KanidmUnixdConfig; +use kanidm_unix_resolver::unix_config::UnixdConfig; use kanidm_utils_users::{get_effective_gid, get_effective_uid}; use libc::{lchown, umask}; use sketching::tracing_forest::traits::*; @@ -272,7 +272,7 @@ fn create_home_directory( Ok(()) } -async fn handle_tasks(stream: UnixStream, cfg: &KanidmUnixdConfig) { +async fn handle_tasks(stream: UnixStream, cfg: &UnixdConfig) { let mut reqs = Framed::new(stream, TaskCodec::new()); loop { @@ -361,7 +361,7 @@ async fn main() -> ExitCode { } }; - let cfg = match KanidmUnixdConfig::new().read_options_from_optional_config(unixd_path) { + let cfg = match UnixdConfig::new().read_options_from_optional_config(unixd_path) { Ok(v) => v, Err(_) => { error!("Failed to parse {}", unixd_path_str); diff --git a/unix_integration/resolver/src/idprovider/interface.rs b/unix_integration/resolver/src/idprovider/interface.rs index 327ff0037..8b8e6120a 100644 --- a/unix_integration/resolver/src/idprovider/interface.rs +++ b/unix_integration/resolver/src/idprovider/interface.rs @@ -292,6 +292,8 @@ pub trait IdProvider { _tpm: &mut tpm::BoxedDynTpm, ) -> Result; + async fn unix_user_authorise(&self, _token: &UserToken) -> Result, IdpError>; + async fn unix_group_get( &self, id: &Id, diff --git a/unix_integration/resolver/src/idprovider/kanidm.rs b/unix_integration/resolver/src/idprovider/kanidm.rs index 6a691bc5b..ae7bee893 100644 --- a/unix_integration/resolver/src/idprovider/kanidm.rs +++ b/unix_integration/resolver/src/idprovider/kanidm.rs @@ -1,8 +1,10 @@ use crate::db::KeyStoreTxn; +use crate::unix_config::KanidmConfig; use async_trait::async_trait; use kanidm_client::{ClientError, KanidmClient, StatusCode}; use kanidm_proto::internal::OperationError; use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; +use std::collections::BTreeSet; use std::time::{Duration, SystemTime}; use tokio::sync::{broadcast, Mutex}; @@ -34,6 +36,7 @@ struct KanidmProviderInternal { client: KanidmClient, hmac_key: HmacKey, crypto_policy: CryptoPolicy, + pam_allow_groups: BTreeSet, } pub struct KanidmProvider { @@ -43,6 +46,7 @@ pub struct KanidmProvider { impl KanidmProvider { pub fn new( client: KanidmClient, + config: &KanidmConfig, now: SystemTime, keystore: &mut KeyStoreTxn, tpm: &mut tpm::BoxedDynTpm, @@ -85,12 +89,15 @@ impl KanidmProvider { let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(250)); + let pam_allow_groups = config.pam_allowed_login_groups.iter().cloned().collect(); + Ok(KanidmProvider { inner: Mutex::new(KanidmProviderInternal { state: CacheState::OfflineNextCheck(now), client, hmac_key, crypto_policy, + pam_allow_groups, }), }) } @@ -602,4 +609,30 @@ impl IdProvider for KanidmProvider { } } } + + async fn unix_user_authorise(&self, token: &UserToken) -> Result, IdpError> { + let inner = self.inner.lock().await; + + if inner.pam_allow_groups.is_empty() { + // can't allow anything if the group list is zero... + warn!("Cannot authenticate users, no allowed groups in configuration!"); + Ok(Some(false)) + } else { + let user_set: BTreeSet<_> = token + .groups + .iter() + .flat_map(|g| [g.name.clone(), g.uuid.hyphenated().to_string()]) + .collect(); + + debug!( + "Checking if user is in allowed groups ({:?}) -> {:?}", + inner.pam_allow_groups, user_set, + ); + let intersection_count = user_set.intersection(&inner.pam_allow_groups).count(); + debug!("Number of intersecting groups: {}", intersection_count); + debug!("User token is valid: {}", token.valid); + + Ok(Some(intersection_count > 0 && token.valid)) + } + } } diff --git a/unix_integration/resolver/src/idprovider/system.rs b/unix_integration/resolver/src/idprovider/system.rs index bcad2f2eb..1789e0902 100644 --- a/unix_integration/resolver/src/idprovider/system.rs +++ b/unix_integration/resolver/src/idprovider/system.rs @@ -1,9 +1,11 @@ use hashbrown::HashMap; use std::sync::Arc; +use time::OffsetDateTime; use tokio::sync::Mutex; -use super::interface::{Id, IdpError}; -use kanidm_unix_common::unix_passwd::{EtcGroup, EtcUser}; +use super::interface::{AuthCredHandler, AuthRequest, Id, IdpError}; +use kanidm_unix_common::unix_passwd::{EtcGroup, EtcShadow, EtcUser}; +use kanidm_unix_common::unix_proto::PamAuthRequest; use kanidm_unix_common::unix_proto::{NssGroup, NssUser}; pub struct SystemProviderInternal { @@ -11,6 +13,141 @@ pub struct SystemProviderInternal { user_list: Vec>, groups: HashMap>, group_list: Vec>, + + shadow_enabled: bool, + shadow: HashMap>, +} + +pub enum SystemProviderAuthInit { + Begin { + next_request: AuthRequest, + cred_handler: AuthCredHandler, + shadow: Arc, + }, + ShadowMissing, + CredentialsUnavailable, + Expired, + Ignore, +} + +pub enum SystemProviderSession { + Start, + // Not sure that we need this + // StartCreateHome(HomeDirectoryInfo), + Ignore, +} + +pub enum SystemAuthResult { + Denied, + Success, + Next(AuthRequest), +} + +pub enum CryptPw { + Sha256(String), + Sha512(String), +} + +impl TryFrom for CryptPw { + type Error = (); + + fn try_from(value: String) -> Result { + if value.starts_with("$6$") { + Ok(CryptPw::Sha512(value)) + } else if value.starts_with("$5$") { + Ok(CryptPw::Sha256(value)) + } else { + Err(()) + } + } +} + +#[allow(dead_code)] +struct AgingPolicy { + last_change: time::OffsetDateTime, + min_password_change: time::OffsetDateTime, + max_password_change: Option, + warning_period_start: Option, + inactivity_period_deadline: Option, +} + +impl AgingPolicy { + fn new( + change_days: i64, + days_min_password_age: i64, + days_max_password_age: Option, + + days_warning_period: i64, + days_inactivity_period: Option, + ) -> Self { + // Get the changes days to an absolute. + let last_change = OffsetDateTime::UNIX_EPOCH + time::Duration::days(change_days); + + let min_password_change = last_change + time::Duration::days(days_min_password_age); + + let max_password_change = + days_max_password_age.map(|max| last_change + time::Duration::days(max)); + + let (warning_period_start, inactivity_period_deadline) = + if let Some(expiry) = max_password_change.as_ref() { + // Both of these values are relative to the max age, so without a max age + // they are meaningless. + + // If the warning isnt 0 + let warning = if days_warning_period != 0 { + // This is a subtract + Some(*expiry - time::Duration::days(days_warning_period)) + } else { + None + }; + + let inactive = + days_inactivity_period.map(|inactive| *expiry + time::Duration::days(inactive)); + + (warning, inactive) + } else { + (None, None) + }; + + AgingPolicy { + last_change, + min_password_change, + max_password_change, + warning_period_start, + inactivity_period_deadline, + } + } +} + +pub struct Shadow { + crypt_pw: CryptPw, + #[allow(dead_code)] + aging_policy: Option, + expiration_date: Option, +} + +impl Shadow { + pub fn auth_step( + &self, + cred_handler: &mut AuthCredHandler, + pam_next_req: PamAuthRequest, + ) -> SystemAuthResult { + match (cred_handler, pam_next_req) { + (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => { + let is_valid = match &self.crypt_pw { + CryptPw::Sha256(crypt) => sha_crypt::sha256_check(&cred, crypt).is_ok(), + CryptPw::Sha512(crypt) => sha_crypt::sha512_check(&cred, crypt).is_ok(), + }; + + if is_valid { + SystemAuthResult::Success + } else { + SystemAuthResult::Denied + } + } + _ => SystemAuthResult::Denied, + } + } } pub struct SystemProvider { @@ -25,16 +162,73 @@ impl SystemProvider { user_list: Default::default(), groups: Default::default(), group_list: Default::default(), + shadow_enabled: Default::default(), + shadow: Default::default(), }), }) } - pub async fn reload(&self, users: Vec, groups: Vec) { + pub async fn reload( + &self, + users: Vec, + shadow: Option>, + 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(); + system_ids_txn.shadow.clear(); + + system_ids_txn.shadow_enabled = shadow.is_some(); + + if let Some(shadow) = shadow { + let s_iter = shadow.into_iter().filter_map(|shadow_entry| { + let EtcShadow { + name, + password, + epoch_change_days, + days_min_password_age, + days_max_password_age, + days_warning_period, + days_inactivity_period, + epoch_expire_date, + flag_reserved: _, + } = shadow_entry; + + match CryptPw::try_from(password) { + Ok(crypt_pw) => { + let aging_policy = epoch_change_days.map(|change_days| { + AgingPolicy::new( + change_days, + days_min_password_age, + days_max_password_age, + days_warning_period, + days_inactivity_period, + ) + }); + + let expiration_date = epoch_expire_date.map(|expire| { + OffsetDateTime::UNIX_EPOCH + time::Duration::days(expire) + }); + + Some(( + name, + Arc::new(Shadow { + crypt_pw, + aging_policy, + expiration_date, + }), + )) + } + // No valid pw, don't care. + Err(()) => None, + } + }); + + system_ids_txn.shadow.extend(s_iter) + }; for group in groups { let name = Id::Name(group.name.clone()); @@ -67,7 +261,7 @@ impl SystemProvider { 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!"); + error!(name = %user.name, uid = %user.uid, gid = %user.gid, members = ?group.members, "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"); @@ -94,9 +288,65 @@ impl SystemProvider { } } - pub async fn contains_account(&self, account_id: &Id) -> bool { + pub async fn auth_init( + &self, + account_id: &Id, + current_time: OffsetDateTime, + ) -> SystemProviderAuthInit { let inner = self.inner.lock().await; - inner.users.contains_key(account_id) + + let Some(user) = inner.users.get(account_id) else { + // Not for us, not a system user. + return SystemProviderAuthInit::Ignore; + }; + + if !inner.shadow_enabled { + // We were unable to read shadow, so we can't proceed. Return that we don't know + // the user. + return SystemProviderAuthInit::ShadowMissing; + } + + // Does the user have a related shadow entry? + let Some(shadow) = inner.shadow.get(user.name.as_str()) else { + return SystemProviderAuthInit::CredentialsUnavailable; + }; + + // If they do, is there a unix style auth policy attached? + if let Some(expire) = shadow.expiration_date.as_ref() { + if current_time >= *expire { + return SystemProviderAuthInit::Expired; + } + } + + // Good to go, lets try to auth them. + // Today, we only support password, but we can support more in future. + let cred_handler = AuthCredHandler::Password; + + let next_request = AuthRequest::Password; + + SystemProviderAuthInit::Begin { + next_request, + cred_handler, + shadow: shadow.clone(), + } + } + + pub async fn authorise(&self, account_id: &Id) -> Option { + let inner = self.inner.lock().await; + if inner.users.contains_key(account_id) { + Some(true) + } else { + None + } + } + + pub async fn begin_session(&self, account_id: &Id) -> SystemProviderSession { + let inner = self.inner.lock().await; + if inner.users.contains_key(account_id) { + SystemProviderSession::Start + } else { + SystemProviderSession::Ignore + } } pub async fn contains_group(&self, account_id: &Id) -> bool { diff --git a/unix_integration/resolver/src/resolver.rs b/unix_integration/resolver/src/resolver.rs index b485f1248..c6f59550d 100644 --- a/unix_integration/resolver/src/resolver.rs +++ b/unix_integration/resolver/src/resolver.rs @@ -1,6 +1,5 @@ // use async_trait::async_trait; use hashbrown::HashMap; -use std::collections::BTreeSet; use std::fmt::Display; use std::num::NonZeroUsize; use std::ops::DerefMut; @@ -10,6 +9,7 @@ use std::sync::Arc; use std::time::{Duration, SystemTime}; use lru::LruCache; +use time::OffsetDateTime; use tokio::sync::Mutex; use uuid::Uuid; @@ -27,11 +27,14 @@ use crate::idprovider::interface::{ UserToken, UserTokenState, }; -use crate::idprovider::system::SystemProvider; +use crate::idprovider::system::{ + Shadow, SystemAuthResult, SystemProvider, SystemProviderAuthInit, SystemProviderSession, +}; use crate::unix_config::{HomeAttr, UidAttr}; -use kanidm_unix_common::unix_passwd::{EtcGroup, EtcUser}; +use kanidm_unix_common::unix_passwd::{EtcGroup, EtcShadow, EtcUser}; use kanidm_unix_common::unix_proto::{ - HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, PamAuthResponse, ProviderStatus, + HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, PamAuthResponse, PamServiceInfo, + ProviderStatus, }; use kanidm_hsm_crypto::BoxedDynTpm; @@ -58,6 +61,10 @@ pub enum AuthSession { token: Box, cred_handler: AuthCredHandler, }, + System { + cred_handler: AuthCredHandler, + shadow: Arc, + }, Success, Denied, } @@ -76,7 +83,9 @@ pub struct Resolver { // A set of remote resolvers, ordered by priority. clients: Vec>, - pam_allow_groups: BTreeSet, + // The id of the primary-provider which may use name over spn. + primary_origin: ProviderOrigin, + timeout_seconds: u64, default_shell: String, home_prefix: PathBuf, @@ -101,10 +110,9 @@ impl Resolver { pub async fn new( db: Db, system_provider: Arc, - client: Arc, + clients: Vec>, hsm: BoxedDynTpm, timeout_seconds: u64, - pam_allow_groups: Vec, default_shell: String, home_prefix: PathBuf, home_attr: HomeAttr, @@ -114,11 +122,7 @@ impl Resolver { ) -> Result { let hsm = Mutex::new(hsm); - 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 primary_origin = clients.first().map(|c| c.origin()).unwrap_or_default(); let client_ids: HashMap<_, _> = clients .iter() @@ -132,9 +136,9 @@ impl Resolver { hsm, system_provider, clients, + primary_origin, client_ids, timeout_seconds, - pam_allow_groups: pam_allow_groups.into_iter().collect(), default_shell, home_prefix, home_attr, @@ -142,7 +146,6 @@ impl Resolver { uid_attr_map, gid_attr_map, nxcache: Mutex::new(LruCache::new(NXCACHE_SIZE)), - // system_identities, }) } @@ -200,8 +203,13 @@ impl Resolver { nxcache_txn.get(id).copied() } - pub async fn reload_system_identities(&self, users: Vec, groups: Vec) { - self.system_provider.reload(users, groups).await + pub async fn reload_system_identities( + &self, + users: Vec, + shadow: Option>, + groups: Vec, + ) { + self.system_provider.reload(users, shadow, groups).await } async fn get_cached_usertoken(&self, account_id: &Id) -> Result<(bool, Option), ()> { @@ -582,32 +590,30 @@ impl Resolver { .unwrap_or_else(|| Vec::with_capacity(0))) } - #[inline(always)] fn token_homedirectory_alias(&self, token: &UserToken) -> Option { + let is_primary_origin = token.provider == self.primary_origin; self.home_alias.map(|t| match t { // If we have an alias. use it. + HomeAttr::Name if is_primary_origin => token.name.as_str().to_string(), HomeAttr::Uuid => token.uuid.hyphenated().to_string(), - HomeAttr::Spn => token.spn.as_str().to_string(), - HomeAttr::Name => token.name.as_str().to_string(), + HomeAttr::Spn | HomeAttr::Name => token.spn.as_str().to_string(), }) } - #[inline(always)] fn token_homedirectory_attr(&self, token: &UserToken) -> String { + let is_primary_origin = token.provider == self.primary_origin; match self.home_attr { + HomeAttr::Name if is_primary_origin => token.name.as_str().to_string(), HomeAttr::Uuid => token.uuid.hyphenated().to_string(), - HomeAttr::Spn => token.spn.as_str().to_string(), - HomeAttr::Name => token.name.as_str().to_string(), + HomeAttr::Spn | HomeAttr::Name => token.spn.as_str().to_string(), } } - #[inline(always)] fn token_homedirectory(&self, token: &UserToken) -> String { self.token_homedirectory_alias(token) .unwrap_or_else(|| self.token_homedirectory_attr(token)) } - #[inline(always)] fn token_abs_homedirectory(&self, token: &UserToken) -> String { self.home_prefix .join(self.token_homedirectory(token)) @@ -615,11 +621,11 @@ impl Resolver { .to_string() } - #[inline(always)] fn token_uidattr(&self, token: &UserToken) -> String { + let is_primary_origin = token.provider == self.primary_origin; match self.uid_attr_map { - UidAttr::Spn => token.spn.as_str(), - UidAttr::Name => token.name.as_str(), + UidAttr::Name if is_primary_origin => token.name.as_str(), + UidAttr::Spn | UidAttr::Name => token.spn.as_str(), } .to_string() } @@ -673,7 +679,6 @@ impl Resolver { self.get_nssaccount(Id::Gid(gid)).await } - #[inline(always)] fn token_gidattr(&self, token: &GroupToken) -> String { match self.gid_attr_map { UidAttr::Spn => token.spn.as_str(), @@ -686,6 +691,12 @@ impl Resolver { pub async fn get_nssgroups(&self) -> Result, ()> { let mut r = self.system_provider.get_nssgroups().await; + // Get all the system -> extension maps. + + // For each sysgroup. + // if there is an extension. + // locate it, and resolve + extend. + let l = self.get_cached_grouptokens().await?; r.reserve(l.len()); for tok in l.into_iter() { @@ -732,32 +743,29 @@ 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 id = Id::Name(account_id.to_string()); - if self.pam_allow_groups.is_empty() { - // can't allow anything if the group list is zero... - eprintln!("Cannot authenticate users, no allowed groups in configuration!"); - Ok(Some(false)) - } else { - Ok(token.map(|tok| { - let user_set: BTreeSet<_> = tok - .groups - .iter() - .flat_map(|g| [g.name.clone(), g.uuid.hyphenated().to_string()]) - .collect(); + if let Some(answer) = self.system_provider.authorise(&id).await { + return Ok(Some(answer)); + }; - debug!( - "Checking if user is in allowed groups ({:?}) -> {:?}", - self.pam_allow_groups, user_set, - ); - let intersection_count = user_set.intersection(&self.pam_allow_groups).count(); - debug!("Number of intersecting groups: {}", intersection_count); - debug!("User token is valid: {}", tok.valid); + // Not a system account, handle with the provider. + let token = self.get_usertoken(&id).await?; - intersection_count > 0 && tok.valid - })) + // If there is no token, return Ok(None) to trigger unknown-user path in pam. + match token { + Some(token) => { + 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."); + })?; + + client.unix_user_authorise(&token).await.map_err(|err| { + error!(?err, "unable to authorise account"); + }) + } + None => Ok(None), } } @@ -765,6 +773,8 @@ impl Resolver { pub async fn pam_account_authenticate_init( &self, account_id: &str, + pam_info: &PamServiceInfo, + current_time: OffsetDateTime, shutdown_rx: broadcast::Receiver<()>, ) -> Result<(AuthSession, PamAuthResponse), ()> { // Setup an auth session. If possible bring the resolver online. @@ -776,9 +786,50 @@ impl Resolver { let id = Id::Name(account_id.to_string()); - if self.system_provider.contains_account(&id).await { - debug!("Ignoring auth request for system user"); - return Ok((AuthSession::Denied, PamAuthResponse::Unknown)); + match self.system_provider.auth_init(&id, current_time).await { + // The system provider will not take part in this authentication. + SystemProviderAuthInit::Ignore => { + debug!("account unknown to system provider, continue."); + } + // The provider knows the account, and is unable to proceed, + // We return unknown here so that pam_kanidm can be skipped and fall back + // to pam_unix.so. + SystemProviderAuthInit::ShadowMissing => { + warn!( + ?account_id, + "Resolver unable to proceed, /etc/shadow was not accessible." + ); + return Ok((AuthSession::Denied, PamAuthResponse::Unknown)); + } + // There are no credentials for this account + SystemProviderAuthInit::CredentialsUnavailable => { + warn!( + ?account_id, + "Denying auth request for system user with no valid credentials" + ); + return Ok((AuthSession::Denied, PamAuthResponse::Denied)); + } + // The account has expired + SystemProviderAuthInit::Expired => { + warn!( + ?account_id, + "Denying auth request for system user with expired credentials" + ); + return Ok((AuthSession::Denied, PamAuthResponse::Denied)); + } + // The provider knows the account and wants to proceed, + SystemProviderAuthInit::Begin { + next_request, + cred_handler, + shadow, + } => { + let auth_session = AuthSession::System { + shadow, + cred_handler, + }; + + return Ok((auth_session, next_request.into())); + } } let token = self.get_usertoken(&id).await?; @@ -945,6 +996,32 @@ impl Resolver { ) .await } + &mut AuthSession::System { + ref mut cred_handler, + ref shadow, + } => { + // I had a lot of thoughts here, but I think system auth is + // not the same as provider, so I think we special case here and have a separate + // return type. + let system_auth_result = shadow.auth_step(cred_handler, pam_next_req); + + let next = match system_auth_result { + SystemAuthResult::Denied => { + *auth_session = AuthSession::Denied; + + Ok(PamAuthResponse::Denied) + } + SystemAuthResult::Success => { + *auth_session = AuthSession::Success; + + Ok(PamAuthResponse::Success) + } + SystemAuthResult::Next(req) => Ok(req.into()), + }; + + // We shortcut here + return next; + } &mut AuthSession::Success | &mut AuthSession::Denied => Err(IdpError::BadRequest), }; @@ -973,12 +1050,19 @@ impl Resolver { pub async fn pam_account_authenticate( &self, account_id: &str, + current_time: OffsetDateTime, password: &str, ) -> Result, ()> { let (_shutdown_tx, shutdown_rx) = broadcast::channel(1); + let pam_info = PamServiceInfo { + service: "kanidm-unix-test".to_string(), + tty: "/dev/null".to_string(), + rhost: "localhost".to_string(), + }; + let mut auth_session = match self - .pam_account_authenticate_init(account_id, shutdown_rx) + .pam_account_authenticate_init(account_id, &pam_info, current_time, shutdown_rx) .await? { (auth_session, PamAuthResponse::Password) => { @@ -1042,10 +1126,26 @@ impl Resolver { &self, account_id: &str, ) -> Result, ()> { - let token = self - .get_usertoken(&Id::Name(account_id.to_string())) - .await?; + let id = Id::Name(account_id.to_string()); + + match self.system_provider.begin_session(&id).await { + SystemProviderSession::Start => { + return Ok(None); + } + /* + SystemProviderSession::StartCreateHome( + info + ) => { + return Ok(Some(info)); + } + */ + SystemProviderSession::Ignore => {} + }; + + // Not a system account, check based on the token and resolve. + let token = self.get_usertoken(&id).await?; Ok(token.as_ref().map(|tok| HomeDirectoryInfo { + uid: tok.gidnumber, gid: tok.gidnumber, name: self.token_homedirectory_attr(tok), aliases: self @@ -1059,7 +1159,12 @@ impl Resolver { let now = SystemTime::now(); let mut hsm_lock = self.hsm.lock().await; - let mut results = Vec::with_capacity(self.clients.len()); + let mut results = Vec::with_capacity(self.clients.len() + 1); + + results.push(ProviderStatus { + name: "system".to_string(), + online: true, + }); for client in self.clients.iter() { let online = client.attempt_online(hsm_lock.deref_mut(), now).await; diff --git a/unix_integration/resolver/src/unix_config.rs b/unix_integration/resolver/src/unix_config.rs index 9bc2c27d7..3876804eb 100644 --- a/unix_integration/resolver/src/unix_config.rs +++ b/unix_integration/resolver/src/unix_config.rs @@ -14,6 +14,64 @@ use serde::Deserialize; use kanidm_unix_common::constants::*; +// This bit of magic lets us deserialise the old config and the new versions. + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum ConfigUntagged { + Versioned(ConfigVersion), + Legacy(ConfigInt), +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "version")] +enum ConfigVersion { + #[serde(rename = "2")] + V2 { + #[serde(flatten)] + values: ConfigV2, + }, +} + +#[derive(Debug, Deserialize)] +struct ConfigV2 { + cache_db_path: Option, + sock_path: Option, + task_sock_path: Option, + + cache_timeout: Option, + + default_shell: Option, + home_prefix: Option, + home_mount_prefix: Option, + home_attr: Option, + home_alias: Option, + use_etc_skel: Option, + uid_attr_map: Option, + gid_attr_map: Option, + selinux: Option, + + hsm_pin_path: Option, + hsm_type: Option, + tpm_tcti_name: Option, + + kanidm: Option, +} + +#[derive(Debug, Deserialize)] +pub struct GroupMap { + pub local: String, + pub with: String, +} + +#[derive(Debug, Deserialize)] +struct KanidmConfigV2 { + conn_timeout: Option, + request_timeout: Option, + pam_allowed_login_groups: Option>, + extend: Vec, +} + #[derive(Debug, Deserialize)] struct ConfigInt { db_path: Option, @@ -60,15 +118,12 @@ impl Display for HsmType { } #[derive(Debug)] -pub struct KanidmUnixdConfig { - pub db_path: String, +pub struct UnixdConfig { + pub cache_db_path: String, pub sock_path: String, pub task_sock_path: String, - pub conn_timeout: u64, - pub request_timeout: u64, pub cache_timeout: u64, pub unix_sock_timeout: u64, - pub pam_allowed_login_groups: Vec, pub default_shell: String, pub home_prefix: PathBuf, pub home_mount_prefix: Option, @@ -81,29 +136,31 @@ pub struct KanidmUnixdConfig { pub hsm_type: HsmType, pub hsm_pin_path: String, pub tpm_tcti_name: String, - pub allow_local_account_override: Vec, + + pub kanidm_config: Option, } -impl Default for KanidmUnixdConfig { +#[derive(Debug)] +pub struct KanidmConfig { + pub conn_timeout: u64, + pub request_timeout: u64, + pub pam_allowed_login_groups: Vec, + pub extend: Vec, +} + +impl Default for UnixdConfig { fn default() -> Self { - KanidmUnixdConfig::new() + UnixdConfig::new() } } -impl Display for KanidmUnixdConfig { +impl Display for UnixdConfig { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "db_path: {}", &self.db_path)?; + writeln!(f, "cache_db_path: {}", &self.cache_db_path)?; writeln!(f, "sock_path: {}", self.sock_path)?; writeln!(f, "task_sock_path: {}", self.task_sock_path)?; - writeln!(f, "conn_timeout: {}", self.conn_timeout)?; - writeln!(f, "request_timeout: {}", self.request_timeout)?; writeln!(f, "unix_sock_timeout: {}", self.unix_sock_timeout)?; writeln!(f, "cache_timeout: {}", self.cache_timeout)?; - writeln!( - f, - "pam_allowed_login_groups: {:#?}", - self.pam_allowed_login_groups - )?; writeln!(f, "default_shell: {}", self.default_shell)?; writeln!(f, "home_prefix: {:?}", self.home_prefix)?; match self.home_mount_prefix.as_deref() { @@ -123,34 +180,41 @@ impl Display for KanidmUnixdConfig { writeln!(f, "tpm_tcti_name: {}", self.tpm_tcti_name)?; writeln!(f, "selinux: {}", self.selinux)?; - writeln!( - f, - "allow_local_account_override: {:#?}", - self.allow_local_account_override - ) + + if let Some(kconfig) = &self.kanidm_config { + writeln!(f, "kanidm: enabled")?; + writeln!( + f, + "kanidm pam_allowed_login_groups: {:#?}", + kconfig.pam_allowed_login_groups + )?; + writeln!(f, "kanidm conn_timeout: {}", kconfig.conn_timeout)?; + writeln!(f, "kanidm request_timeout: {}", kconfig.request_timeout)?; + } else { + writeln!(f, "kanidm: disabled")?; + }; + + Ok(()) } } -impl KanidmUnixdConfig { +impl UnixdConfig { pub fn new() -> Self { - let db_path = match env::var("KANIDM_DB_PATH") { + let cache_db_path = match env::var("KANIDM_CACHE_DB_PATH") { Ok(val) => val, - Err(_) => DEFAULT_DB_PATH.into(), + Err(_) => DEFAULT_CACHE_DB_PATH.into(), }; let hsm_pin_path = match env::var("KANIDM_HSM_PIN_PATH") { Ok(val) => val, Err(_) => DEFAULT_HSM_PIN_PATH.into(), }; - KanidmUnixdConfig { - db_path, + UnixdConfig { + cache_db_path, sock_path: DEFAULT_SOCK_PATH.to_string(), task_sock_path: DEFAULT_TASK_SOCK_PATH.to_string(), - conn_timeout: DEFAULT_CONN_TIMEOUT, - request_timeout: DEFAULT_CONN_TIMEOUT * 2, unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2, cache_timeout: DEFAULT_CACHE_TIMEOUT, - pam_allowed_login_groups: Vec::new(), default_shell: DEFAULT_SHELL.to_string(), home_prefix: DEFAULT_HOME_PREFIX.into(), home_mount_prefix: None, @@ -163,7 +227,8 @@ impl KanidmUnixdConfig { hsm_pin_path, hsm_type: HsmType::default(), tpm_tcti_name: DEFAULT_TPM_TCTI_NAME.to_string(), - allow_local_account_override: Vec::default(), + + kanidm_config: None, } } @@ -208,25 +273,43 @@ impl KanidmUnixdConfig { UnixIntegrationError })?; - let config: ConfigInt = toml::from_str(contents.as_str()).map_err(|e| { + let config: ConfigUntagged = toml::from_str(contents.as_str()).map_err(|e| { error!("{:?}", e); UnixIntegrationError })?; - let conn_timeout = config.conn_timeout.unwrap_or(self.conn_timeout); + match config { + ConfigUntagged::Legacy(config) => self.apply_from_config_legacy(config), + ConfigUntagged::Versioned(ConfigVersion::V2 { values }) => { + self.apply_from_config_v2(values) + } + } + } + + fn apply_from_config_legacy(self, config: ConfigInt) -> Result { + let extend = config + .allow_local_account_override + .iter() + .map(|name| GroupMap { + local: name.clone(), + with: name.clone(), + }) + .collect(); + + let kanidm_config = Some(KanidmConfig { + conn_timeout: config.conn_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT), + request_timeout: config.request_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT * 2), + pam_allowed_login_groups: config.pam_allowed_login_groups.unwrap_or_default(), + extend, + }); // Now map the values into our config. - Ok(KanidmUnixdConfig { - db_path: config.db_path.unwrap_or(self.db_path), + Ok(UnixdConfig { + cache_db_path: config.db_path.unwrap_or(self.cache_db_path), sock_path: config.sock_path.unwrap_or(self.sock_path), task_sock_path: config.task_sock_path.unwrap_or(self.task_sock_path), - conn_timeout, - request_timeout: config.request_timeout.unwrap_or(conn_timeout * 2), - unix_sock_timeout: conn_timeout * 2, + unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2, cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout), - pam_allowed_login_groups: config - .pam_allowed_login_groups - .unwrap_or(self.pam_allowed_login_groups), default_shell: config.default_shell.unwrap_or(self.default_shell), home_prefix: config .home_prefix @@ -302,7 +385,105 @@ impl KanidmUnixdConfig { tpm_tcti_name: config .tpm_tcti_name .unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()), - allow_local_account_override: config.allow_local_account_override, + kanidm_config, + }) + } + + fn apply_from_config_v2(self, config: ConfigV2) -> Result { + let kanidm_config = if let Some(kconfig) = config.kanidm { + Some(KanidmConfig { + conn_timeout: kconfig.conn_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT), + request_timeout: kconfig.request_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT * 2), + pam_allowed_login_groups: kconfig.pam_allowed_login_groups.unwrap_or_default(), + extend: kconfig.extend, + }) + } else { + None + }; + + // Now map the values into our config. + Ok(UnixdConfig { + cache_db_path: config.cache_db_path.unwrap_or(self.cache_db_path), + sock_path: config.sock_path.unwrap_or(self.sock_path), + task_sock_path: config.task_sock_path.unwrap_or(self.task_sock_path), + unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2, + cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout), + default_shell: config.default_shell.unwrap_or(self.default_shell), + home_prefix: config + .home_prefix + .map(|p| p.into()) + .unwrap_or(self.home_prefix.clone()), + home_mount_prefix: config.home_mount_prefix.map(|p| p.into()), + home_attr: config + .home_attr + .and_then(|v| match v.as_str() { + "uuid" => Some(HomeAttr::Uuid), + "spn" => Some(HomeAttr::Spn), + "name" => Some(HomeAttr::Name), + _ => { + warn!("Invalid home_attr configured, using default ..."); + None + } + }) + .unwrap_or(self.home_attr), + home_alias: config + .home_alias + .and_then(|v| match v.as_str() { + "none" => Some(None), + "uuid" => Some(Some(HomeAttr::Uuid)), + "spn" => Some(Some(HomeAttr::Spn)), + "name" => Some(Some(HomeAttr::Name)), + _ => { + warn!("Invalid home_alias configured, using default ..."); + None + } + }) + .unwrap_or(self.home_alias), + use_etc_skel: config.use_etc_skel.unwrap_or(self.use_etc_skel), + uid_attr_map: config + .uid_attr_map + .and_then(|v| match v.as_str() { + "spn" => Some(UidAttr::Spn), + "name" => Some(UidAttr::Name), + _ => { + warn!("Invalid uid_attr_map configured, using default ..."); + None + } + }) + .unwrap_or(self.uid_attr_map), + gid_attr_map: config + .gid_attr_map + .and_then(|v| match v.as_str() { + "spn" => Some(UidAttr::Spn), + "name" => Some(UidAttr::Name), + _ => { + warn!("Invalid gid_attr_map configured, using default ..."); + None + } + }) + .unwrap_or(self.gid_attr_map), + selinux: match config.selinux.unwrap_or(self.selinux) { + #[cfg(all(target_family = "unix", feature = "selinux"))] + true => selinux_util::supported(), + _ => false, + }, + hsm_pin_path: config.hsm_pin_path.unwrap_or(self.hsm_pin_path), + hsm_type: config + .hsm_type + .and_then(|v| match v.as_str() { + "soft" => Some(HsmType::Soft), + "tpm_if_possible" => Some(HsmType::TpmIfPossible), + "tpm" => Some(HsmType::Tpm), + _ => { + warn!("Invalid hsm_type configured, using default ..."); + None + } + }) + .unwrap_or(self.hsm_type), + tpm_tcti_name: config + .tpm_tcti_name + .unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()), + kanidm_config, }) } } diff --git a/unix_integration/resolver/tests/cache_layer_test.rs b/unix_integration/resolver/tests/cache_layer_test.rs index e4e5df9a1..a8034592f 100644 --- a/unix_integration/resolver/tests/cache_layer_test.rs +++ b/unix_integration/resolver/tests/cache_layer_test.rs @@ -4,6 +4,7 @@ use std::pin::Pin; use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::{Duration, SystemTime}; +use time::OffsetDateTime; use kanidm_client::{KanidmClient, KanidmClientBuilder}; use kanidm_proto::constants::ATTR_ACCOUNT_EXPIRE; @@ -11,12 +12,13 @@ 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_common::unix_passwd::{EtcGroup, EtcShadow, 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 kanidm_unix_resolver::unix_config::{GroupMap, KanidmConfig}; use kanidmd_core::config::{Configuration, IntegrationTestConfig, ServerRole}; use kanidmd_core::create_server_core; use kanidmd_testkit::{is_free_port, PORT_ALLOC}; @@ -125,6 +127,15 @@ async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) { let idprovider = KanidmProvider::new( rsclient, + &KanidmConfig { + conn_timeout: 1, + request_timeout: 1, + pam_allowed_login_groups: vec!["allowed_group".to_string()], + extend: vec![GroupMap { + local: "extensible".to_string(), + with: "testgroup1".to_string(), + }], + }, SystemTime::now(), &mut (&mut dbtxn).into(), &mut hsm, @@ -139,10 +150,9 @@ async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) { let cachelayer = Resolver::new( db, Arc::new(system_provider), - Arc::new(idprovider), + vec![Arc::new(idprovider)], hsm, 300, - vec!["allowed_group".to_string()], DEFAULT_SHELL.to_string(), DEFAULT_HOME_PREFIX.into(), DEFAULT_HOME_ATTR, @@ -446,11 +456,12 @@ async fn test_cache_account_delete() { #[tokio::test] async fn test_cache_account_password() { + let current_time = OffsetDateTime::now_utc(); let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await; cachelayer.mark_next_check_now(SystemTime::now()).await; // Test authentication failure. let a1 = cachelayer - .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_INC) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_INC) .await .expect("failed to authenticate"); assert_eq!(a1, Some(false)); @@ -460,7 +471,7 @@ async fn test_cache_account_password() { // Test authentication success. let a2 = cachelayer - .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A) .await .expect("failed to authenticate"); assert_eq!(a2, Some(true)); @@ -477,7 +488,7 @@ async fn test_cache_account_password() { // test auth (old pw) fail let a3 = cachelayer - .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A) .await .expect("failed to authenticate"); assert_eq!(a3, Some(false)); @@ -487,7 +498,7 @@ async fn test_cache_account_password() { // test auth (new pw) success let a4 = cachelayer - .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B) .await .expect("failed to authenticate"); assert_eq!(a4, Some(true)); @@ -497,7 +508,7 @@ async fn test_cache_account_password() { // Test auth success let a5 = cachelayer - .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B) .await .expect("failed to authenticate"); assert_eq!(a5, Some(true)); @@ -506,7 +517,7 @@ async fn test_cache_account_password() { // Test auth failure. let a6 = cachelayer - .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_INC) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_INC) .await .expect("failed to authenticate"); assert_eq!(a6, Some(false)); @@ -519,7 +530,7 @@ async fn test_cache_account_password() { // test auth good (fail) let a7 = cachelayer - .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B) .await .expect("failed to authenticate"); assert!(a7.is_none()); @@ -530,7 +541,7 @@ async fn test_cache_account_password() { // test auth success let a8 = cachelayer - .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B) .await .expect("failed to authenticate"); assert_eq!(a8, Some(true)); @@ -570,6 +581,7 @@ async fn test_cache_account_pam_allowed() { #[tokio::test] async fn test_cache_account_pam_nonexist() { + let current_time = OffsetDateTime::now_utc(); let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await; cachelayer.mark_next_check_now(SystemTime::now()).await; @@ -580,7 +592,7 @@ async fn test_cache_account_pam_nonexist() { assert!(a1.is_none()); let a2 = cachelayer - .pam_account_authenticate("NO_SUCH_ACCOUNT", TESTACCOUNT1_PASSWORD_B) + .pam_account_authenticate("NO_SUCH_ACCOUNT", current_time, TESTACCOUNT1_PASSWORD_B) .await .expect("failed to authenticate"); assert!(a2.is_none()); @@ -594,7 +606,7 @@ async fn test_cache_account_pam_nonexist() { assert!(a1.is_none()); let a2 = cachelayer - .pam_account_authenticate("NO_SUCH_ACCOUNT", TESTACCOUNT1_PASSWORD_B) + .pam_account_authenticate("NO_SUCH_ACCOUNT", current_time, TESTACCOUNT1_PASSWORD_B) .await .expect("failed to authenticate"); assert!(a2.is_none()); @@ -602,13 +614,14 @@ async fn test_cache_account_pam_nonexist() { #[tokio::test] async fn test_cache_account_expiry() { + let current_time = OffsetDateTime::now_utc(); let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).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. let a1 = cachelayer - .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A) .await .expect("failed to authenticate"); assert_eq!(a1, Some(true)); @@ -626,7 +639,7 @@ async fn test_cache_account_expiry() { .unwrap(); // auth will fail let a2 = cachelayer - .pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A) .await .expect("failed to authenticate"); assert_eq!(a2, Some(false)); @@ -651,7 +664,7 @@ async fn test_cache_account_expiry() { // 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) + .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A) .await .expect("failed to authenticate"); assert_eq!(a4, Some(true)); @@ -760,6 +773,7 @@ async fn test_cache_nxset_account() { homedir: Default::default(), shell: Default::default(), }], + None, vec![], ) .await; @@ -815,6 +829,7 @@ async fn test_cache_nxset_group() { cachelayer .reload_system_identities( vec![], + None, vec![EtcGroup { name: "testgroup1".to_string(), // Important! We set the GID to differ from what kanidm stores so we can @@ -890,6 +905,146 @@ async fn test_cache_nxset_group() { assert_eq!(gs[0].gid, 30001); } +#[tokio::test] +async fn test_cache_authenticate_system_account() { + const SECURE_PASSWORD: &str = "a"; + + let current_time = OffsetDateTime::UNIX_EPOCH + time::Duration::days(365); + let expire_time = OffsetDateTime::UNIX_EPOCH + time::Duration::days(380); + let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await; + + // Important! This is what sets up that testaccount1 won't be resolved + // because it's in the "local" user set. + cachelayer + .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(), + }, + EtcUser { + name: "testaccount2".to_string(), + uid: 30001, + gid: 30001, + password: Default::default(), + gecos: Default::default(), + homedir: Default::default(), + shell: Default::default(), + } + ], + Some(vec![ + EtcShadow { + name: "testaccount1".to_string(), + // The very secure password, "a". + password: "$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string(), + epoch_change_days: None, + days_min_password_age: 0, + days_max_password_age: Some(1), + days_warning_period: 1, + days_inactivity_period: None, + epoch_expire_date: Some(380), + flag_reserved: None + }, + EtcShadow { + name: "testaccount2".to_string(), + // The very secure password, "a". + password: "$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string(), + epoch_change_days: Some(364), + days_min_password_age: 0, + days_max_password_age: Some(2), + days_warning_period: 1, + days_inactivity_period: None, + epoch_expire_date: Some(380), + flag_reserved: None + }, + ]), + vec![], + ) + .await; + + // get the accounts to assert they exist, + let _ = cachelayer + .get_nssaccount_name("testaccount1") + .await + .expect("Failed to get from cache"); + let _ = cachelayer + .get_nssaccount_name("testaccount2") + .await + .expect("Failed to get from cache"); + + // Non exist name + let a1 = cachelayer + .pam_account_authenticate("testaccount69", current_time, SECURE_PASSWORD) + .await + .expect("failed to authenticate"); + assert_eq!(a1, None); + + // Check wrong pw. + let a1 = cachelayer + .pam_account_authenticate("testaccount1", current_time, "wrong password") + .await + .expect("failed to authenticate"); + assert_eq!(a1, Some(false)); + + // Check correct pw (both accounts) + let a1 = cachelayer + .pam_account_authenticate("testaccount1", current_time, SECURE_PASSWORD) + .await + .expect("failed to authenticate"); + assert_eq!(a1, Some(true)); + + let a1 = cachelayer + .pam_account_authenticate("testaccount2", current_time, SECURE_PASSWORD) + .await + .expect("failed to authenticate"); + assert_eq!(a1, Some(true)); + + // Check expired time (both accounts) + let a1 = cachelayer + .pam_account_authenticate("testaccount1", expire_time, SECURE_PASSWORD) + .await + .expect("failed to authenticate"); + assert_eq!(a1, Some(false)); + + let a1 = cachelayer + .pam_account_authenticate("testaccount2", expire_time, SECURE_PASSWORD) + .await + .expect("failed to authenticate"); + assert_eq!(a1, Some(false)); + + // due to how posix auth works, session and authorisation are simpler, and should + // always just return "true". + let a1 = cachelayer + .pam_account_allowed("testaccount1") + .await + .expect("failed to authorise"); + assert_eq!(a1, Some(true)); + + let a1 = cachelayer + .pam_account_allowed("testaccount2") + .await + .expect("failed to authorise"); + assert_eq!(a1, Some(true)); + + // Should we make home dirs? + let a1 = cachelayer + .pam_account_beginsession("testaccount1") + .await + .expect("failed to begin session"); + assert_eq!(a1, None); + + let a1 = cachelayer + .pam_account_beginsession("testaccount2") + .await + .expect("failed to begin session"); + assert_eq!(a1, None); +} + /// Issue 1830. If cache items expire where we have an account and a group, and we /// refresh the group *first*, the group appears to drop it's members. This is because /// sqlite "INSERT OR REPLACE INTO" triggers a delete cascade of the foreign key elements