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
This commit is contained in:
Firstyear 2024-10-02 12:12:13 +10:00 committed by GitHub
parent 90afc8207c
commit cf63c6b98b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1372 additions and 475 deletions

15
Cargo.lock generated
View file

@ -3346,6 +3346,7 @@ dependencies = [
"kanidm_build_profiles", "kanidm_build_profiles",
"serde", "serde",
"serde_json", "serde_json",
"serde_with",
"tokio", "tokio",
"tokio-util", "tokio-util",
"toml", "toml",
@ -3383,7 +3384,9 @@ dependencies = [
"selinux", "selinux",
"serde", "serde",
"serde_json", "serde_json",
"sha-crypt",
"sketching", "sketching",
"time",
"tokio", "tokio",
"tokio-util", "tokio-util",
"toml", "toml",
@ -5585,6 +5588,18 @@ dependencies = [
"syn 2.0.77", "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]] [[package]]
name = "sha1_smol" name = "sha1_smol"
version = "1.0.1" version = "1.0.1"

View file

@ -258,6 +258,7 @@ serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" }
serde_json = "^1.0.128" serde_json = "^1.0.128"
serde-wasm-bindgen = "0.5" serde-wasm-bindgen = "0.5"
serde_with = "3.9.0" serde_with = "3.9.0"
sha-crypt = "0.5.0"
sha2 = "0.10.8" sha2 = "0.10.8"
shellexpand = "^2.1.2" shellexpand = "^2.1.2"
smartstring = "^1.0.1" smartstring = "^1.0.1"

View file

@ -6,15 +6,16 @@ Kanidm into accounts that can be used on the machine for various interactive tas
## The UNIX Daemon ## The UNIX Daemon
Kanidm provides a UNIX daemon that runs on any client that wants to use PAM and nsswitch Kanidm provides a UNIX daemon that runs on any client that wants to support PAM and nsswitch. This
integration. The daemon can cache the accounts for users who have unreliable networks, or who leave service has many features which are useful even without Kanidm as a network authentication service.
the site where Kanidm is hosted. The daemon is also able to cache missing-entry responses to reduce
network traffic and Kanidm server load.
Additionally, running the daemon means that the PAM and nsswitch integration libraries can be small, The Kanidm UNIX Daemon:
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 * Caches Kanidm users and groups for users with unreliable networks, or for roaming users.
these home directories. * 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: 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, > 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 > 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: You can also configure unixd with the file /etc/kanidm/unixd:
```toml
{{#rustdoc_include ../../../examples/unixd}}
```
> [!NOTE] > [!NOTE]
> >
@ -62,9 +57,17 @@ You can also configure some unixd-specific options with the file /etc/kanidm/uni
> Ubuntu users please see: > 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) > [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 ```bash
systemctl enable --now kanidm-unixd
kanidm-unix status 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 When the daemon is running you can add the nsswitch libraries to /etc/nsswitch.conf
```text ```text
passwd: compat kanidm passwd: kanidm compat
group: compat kanidm 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). [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 ```bash
getent passwd <account name> getent passwd <account name>

View file

@ -23,22 +23,15 @@ Edit the content.
# /etc/pam.d/password-auth # /etc/pam.d/password-auth
auth required pam_env.so auth required pam_env.so
auth required pam_faildelay.so delay=2000000 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 sufficient pam_kanidm.so ignore_unknown_user
auth required pam_deny.so auth required pam_deny.so
account sufficient pam_unix.so
account sufficient pam_localuser.so
account sufficient pam_usertype.so issystem account sufficient pam_usertype.so issystem
account sufficient pam_kanidm.so ignore_unknown_user 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 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_unix.so sha512 shadow nullok try_first_pass use_authtok
password sufficient pam_kanidm.so
password required pam_deny.so password required pam_deny.so
session optional pam_keyinit.so revoke session optional pam_keyinit.so revoke
@ -54,22 +47,15 @@ session optional pam_kanidm.so
auth required pam_env.so auth required pam_env.so
auth required pam_faildelay.so delay=2000000 auth required pam_faildelay.so delay=2000000
auth sufficient pam_fprintd.so 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 sufficient pam_kanidm.so ignore_unknown_user
auth required pam_deny.so auth required pam_deny.so
account sufficient pam_unix.so
account sufficient pam_localuser.so
account sufficient pam_usertype.so issystem account sufficient pam_usertype.so issystem
account sufficient pam_kanidm.so ignore_unknown_user 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 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_unix.so sha512 shadow nullok try_first_pass use_authtok
password sufficient pam_kanidm.so
password required pam_deny.so password required pam_deny.so
session optional pam_keyinit.so revoke 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: example looks like this:
```text ```text
passwd: compat kanidm sss files systemd passwd: kanidm compat systemd
group: compat kanidm sss files systemd group: kanidm compat systemd
shadow: files shadow: files
hosts: files dns myhostname hosts: files dns myhostname
services: sss files services: files
netgroup: sss files netgroup: files
automount: sss files automount: files
aliases: files aliases: files
ethers: files ethers: files

View file

@ -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 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: The content should look like:
```text ```text
# /etc/pam.d/common-account # /etc/pam.d/common-account
# Controls authorisation to this system (who may login) # 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_kanidm.so ignore_unknown_user
account sufficient pam_unix.so
account required pam_deny.so account required pam_deny.so
# /etc/pam.d/common-auth # /etc/pam.d/common-auth
# Controls authentication to this system (verification of credentials) # Controls authentication to this system (verification of credentials)
auth required pam_env.so 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_kanidm.so ignore_unknown_user
auth sufficient pam_unix.so try_first_pass
auth required pam_deny.so auth required pam_deny.so
# /etc/pam.d/common-password # /etc/pam.d/common-password
# Controls flow of what happens when a user invokes the passwd command. Currently does NOT # Controls flow of what happens when a user invokes the passwd command. Currently does NOT
# push password changes back to kanidm # 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 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 # /etc/pam.d/common-session
# Controls setup of the user session once a successful authentication and authorisation has # Controls setup of the user session once a successful authentication and authorisation has
# occurred. # occurred.
session optional pam_systemd.so session optional pam_systemd.so
session required pam_limits.so session required pam_limits.so
session optional pam_unix.so try_first_pass
session optional pam_umask.so 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_kanidm.so
session optional pam_unix.so try_first_pass
session optional pam_env.so session optional pam_env.so
``` ```

View file

@ -1,13 +1,7 @@
## Kanidm Unixd Service Configuration - /etc/kanidm/unixd ## Kanidm Unixd Service Configuration - /etc/kanidm/unixd
# Defines a set of POSIX groups where membership of any of these groups # The configuration file version.
# will be allowed to login via PAM. All POSIX users and groups can be version = '2'
# 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"]
# Kanidm unix will bind all cached credentials to a local Hardware Security # Kanidm unix will bind all cached credentials to a local Hardware Security
# Module (HSM) to prevent exfiltration and attacks against these. In addition, # 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) # Default: Empty set (no overrides)
# allow_local_account_override = ["admin"] # 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"]

View file

@ -12,7 +12,7 @@ Conflicts=nscd.service
[Service] [Service]
DynamicUser=yes DynamicUser=yes
SupplementaryGroups=tss SupplementaryGroups=tss shadow
UMask=0027 UMask=0027
CacheDirectory=kanidm-unixd CacheDirectory=kanidm-unixd
RuntimeDirectory=kanidm-unixd RuntimeDirectory=kanidm-unixd

View file

@ -3240,7 +3240,7 @@ mod tests {
let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble"); let (_code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
let pkce_request = Some(PkceRequest { let pkce_request = Some(PkceRequest {
code_challenge: code_challenge, code_challenge,
code_challenge_method: CodeChallengeMethod::S256, code_challenge_method: CodeChallengeMethod::S256,
}); });
@ -3749,7 +3749,7 @@ mod tests {
client_id: "test_resource_server".to_string(), client_id: "test_resource_server".to_string(),
state: "123".to_string(), state: "123".to_string(),
pkce_request: Some(PkceRequest { pkce_request: Some(PkceRequest {
code_challenge: code_challenge, code_challenge,
code_challenge_method: CodeChallengeMethod::S256, code_challenge_method: CodeChallengeMethod::S256,
}), }),
redirect_uri: Url::parse("app://cheese").unwrap(), redirect_uri: Url::parse("app://cheese").unwrap(),
@ -5135,7 +5135,7 @@ mod tests {
client_id: "test_resource_server".to_string(), client_id: "test_resource_server".to_string(),
state: "123".to_string(), state: "123".to_string(),
pkce_request: Some(PkceRequest { pkce_request: Some(PkceRequest {
code_challenge: code_challenge, code_challenge,
code_challenge_method: CodeChallengeMethod::S256, code_challenge_method: CodeChallengeMethod::S256,
}), }),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(), redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
@ -5192,7 +5192,7 @@ mod tests {
client_id: "test_resource_server".to_string(), client_id: "test_resource_server".to_string(),
state: "123".to_string(), state: "123".to_string(),
pkce_request: Some(PkceRequest { pkce_request: Some(PkceRequest {
code_challenge: code_challenge, code_challenge,
code_challenge_method: CodeChallengeMethod::S256, code_challenge_method: CodeChallengeMethod::S256,
}), }),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(), redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
@ -6363,7 +6363,7 @@ mod tests {
client_id: "test_resource_server".to_string(), client_id: "test_resource_server".to_string(),
state: "123".to_string(), state: "123".to_string(),
pkce_request: Some(PkceRequest { pkce_request: Some(PkceRequest {
code_challenge: code_challenge, code_challenge,
code_challenge_method: CodeChallengeMethod::S256, code_challenge_method: CodeChallengeMethod::S256,
}), }),
redirect_uri: Url::parse("http://localhost:8765/oauth2/result").unwrap(), redirect_uri: Url::parse("http://localhost:8765/oauth2/result").unwrap(),

View file

@ -27,6 +27,7 @@ csv = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_with = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
tokio = { workspace = true, features = ["time","net","macros"] } tokio = { workspace = true, features = ["time","net","macros"] }
tokio-util = { workspace = true, features = ["codec"] } tokio-util = { workspace = true, features = ["codec"] }

View file

@ -3,7 +3,8 @@ use crate::unix_config::{HomeAttr, UidAttr};
pub const DEFAULT_CONFIG_PATH: &str = "/etc/kanidm/unixd"; pub const DEFAULT_CONFIG_PATH: &str = "/etc/kanidm/unixd";
pub const DEFAULT_SOCK_PATH: &str = "/var/run/kanidm-unixd/sock"; 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_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_CONN_TIMEOUT: u64 = 2;
pub const DEFAULT_CACHE_TIMEOUT: u64 = 15; pub const DEFAULT_CACHE_TIMEOUT: u64 = 15;
pub const DEFAULT_SHELL: &str = env!("KANIDM_DEFAULT_UNIX_SHELL_PATH"); pub const DEFAULT_SHELL: &str = env!("KANIDM_DEFAULT_UNIX_SHELL_PATH");

View file

@ -1,11 +1,9 @@
use serde::{ use serde::{Deserialize, Serialize};
de::{self, Visitor},
Deserialize, Deserializer, 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 struct EtcUser {
pub name: String, pub name: String,
pub password: String, pub password: String,
@ -27,36 +25,51 @@ pub fn parse_etc_passwd(bytes: &[u8]) -> Result<Vec<EtcUser>, UnixIntegrationErr
.collect::<Result<Vec<EtcUser>, UnixIntegrationError>>() .collect::<Result<Vec<EtcUser>, UnixIntegrationError>>()
} }
fn members<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> #[serde_as]
where #[derive(Serialize, Deserialize, Debug, PartialEq)]
D: Deserializer<'de>, pub struct EtcShadow {
{ pub name: String,
struct InnerCsv; pub password: String,
// 0 means must change next login.
impl<'de> Visitor<'de> for InnerCsv { // None means all other aging features are disabled
type Value = Vec<String>; pub epoch_change_days: Option<i64>,
// 0 means no age
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { #[serde_as(deserialize_as = "DefaultOnNull")]
formatter.write_str("string") pub days_min_password_age: i64,
} pub days_max_password_age: Option<i64>,
// 0 means no warning
fn visit_str<E>(self, value: &str) -> Result<Vec<String>, E> #[serde_as(deserialize_as = "DefaultOnNull")]
where pub days_warning_period: i64,
E: de::Error, // Number of days after max_password_age passes where the password can
{ // still be accepted such that the user can update their password
Ok(value.split(',').map(|s| s.to_string()).collect()) pub days_inactivity_period: Option<i64>,
} pub epoch_expire_date: Option<i64>,
} pub flag_reserved: Option<u32>,
deserializer.deserialize_str(InnerCsv)
} }
#[derive(Serialize, Deserialize, Debug)] pub fn parse_etc_shadow(bytes: &[u8]) -> Result<Vec<EtcShadow>, 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::<Result<Vec<EtcShadow>, UnixIntegrationError>>()
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct EtcGroup { pub struct EtcGroup {
pub name: String, pub name: String,
pub password: String, pub password: String,
pub gid: u32, pub gid: u32,
#[serde(deserialize_with = "members")] #[serde_as(as = "StringWithSeparator::<CommaSeparator, String>")]
pub members: Vec<String>, pub members: Vec<String>,
} }
@ -80,36 +93,134 @@ mod tests {
const EXAMPLE_PASSWD: &str = r#"root:x:0:0:root:/root:/bin/bash const EXAMPLE_PASSWD: &str = r#"root:x:0:0:root:/root:/bin/bash
systemd-timesync:x:498:498:systemd Time Synchronization:/:/usr/sbin/nologin 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 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] #[test]
fn test_parse_passwd() { fn test_parse_passwd() {
for record in let users =
parse_etc_passwd(EXAMPLE_PASSWD.as_bytes()).expect("Failed to parse passwd data") parse_etc_passwd(EXAMPLE_PASSWD.as_bytes()).expect("Failed to parse passwd data");
{
println!("{:?}", record); 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] #[test]
fn test_parse_group() { fn test_parse_group() {
for record in parse_etc_group(EXAMPLE_GROUP.as_bytes()).expect("Failed to parse group") { let groups = parse_etc_group(EXAMPLE_GROUP.as_bytes()).expect("Failed to parse groups");
println!("{:?}", record);
} 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(),]
}
);
} }
} }

View file

@ -102,6 +102,13 @@ pub enum PamAuthRequest {
Pin { cred: String }, Pin { cred: String },
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct PamServiceInfo {
pub service: String,
pub tty: String,
pub rhost: String,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub enum ClientRequest { pub enum ClientRequest {
SshKey(String), SshKey(String),
@ -111,7 +118,10 @@ pub enum ClientRequest {
NssGroups, NssGroups,
NssGroupByGid(u32), NssGroupByGid(u32),
NssGroupByName(String), NssGroupByName(String),
PamAuthenticateInit(String), PamAuthenticateInit {
account_id: String,
info: PamServiceInfo,
},
PamAuthenticateStep(PamAuthRequest), PamAuthenticateStep(PamAuthRequest),
PamAccountAllowed(String), PamAccountAllowed(String),
PamAccountBeginSession(String), PamAccountBeginSession(String),
@ -131,7 +141,10 @@ impl ClientRequest {
ClientRequest::NssGroups => "NssGroups".to_string(), ClientRequest::NssGroups => "NssGroups".to_string(),
ClientRequest::NssGroupByGid(id) => format!("NssGroupByGid({})", id), ClientRequest::NssGroupByGid(id) => format!("NssGroupByGid({})", id),
ClientRequest::NssGroupByName(id) => format!("NssGroupByName({})", 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::PamAuthenticateStep(_) => "PamAuthenticateStep".to_string(),
ClientRequest::PamAccountAllowed(id) => { ClientRequest::PamAccountAllowed(id) => {
format!("PamAccountAllowed({})", id) format!("PamAccountAllowed({})", id)
@ -173,8 +186,9 @@ impl From<PamAuthResponse> for ClientResponse {
} }
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct HomeDirectoryInfo { pub struct HomeDirectoryInfo {
pub uid: u32,
pub gid: u32, pub gid: u32,
pub name: String, pub name: String,
pub aliases: Vec<String>, pub aliases: Vec<String>,

View file

@ -218,11 +218,15 @@ impl PamHooks for PamKanidm {
install_subscriber(opts.debug); install_subscriber(opts.debug);
// This will == "Ok(Some("ssh"))" on remote auth. let info = match pamh.get_pam_info() {
let tty = pamh.get_tty(); Ok(info) => info,
let rhost = pamh.get_rhost(); 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) { let account_id = match pamh.get_user(None) {
Ok(aid) => aid, 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 { loop {
match_sm_auth_client_response!(daemon_client.call_and_wait(&req, timeout), opts, match_sm_auth_client_response!(daemon_client.call_and_wait(&req, timeout), opts,

View file

@ -5,7 +5,10 @@ use std::{mem, ptr};
use libc::c_char; 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. /// Opaque type, used as a pointer when making pam API calls.
/// ///
@ -143,6 +146,25 @@ impl PamHandle {
} }
} }
pub fn get_item_string<T: PamItem>(&self) -> PamResult<Option<String>> {
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 /// Sets a value in the pam context. The value can be retrieved using
/// `get_item`. /// `get_item`.
/// ///
@ -198,59 +220,35 @@ impl PamHandle {
} }
pub fn get_authtok(&self) -> PamResult<Option<String>> { pub fn get_authtok(&self) -> PamResult<Option<String>> {
let mut ptr: *const PamItemT = ptr::null(); self.get_item_string::<PamAuthTok>()
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)
}
} }
pub fn get_tty(&self) -> PamResult<Option<String>> { pub fn get_tty(&self) -> PamResult<Option<String>> {
let mut ptr: *const PamItemT = ptr::null(); self.get_item_string::<PamTty>()
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)
}
} }
pub fn get_rhost(&self) -> PamResult<Option<String>> { pub fn get_rhost(&self) -> PamResult<Option<String>> {
let mut ptr: *const PamItemT = ptr::null(); self.get_item_string::<PamRHost>()
let (res, item) = unsafe { }
let r = pam_get_item(self, PAM_RHOST, &mut ptr);
let t = if PamResultCode::PAM_SUCCESS == r && !ptr.is_null() { pub fn get_service(&self) -> PamResult<Option<String>> {
let typed_ptr: *const c_char = ptr as *const c_char; self.get_item_string::<PamService>()
Some(CStr::from_ptr(typed_ptr).to_string_lossy().into_owned()) }
} else {
None pub fn get_pam_info(&self) -> PamResult<PamServiceInfo> {
}; let maybe_tty = self.get_tty()?;
(r, t) let maybe_rhost = self.get_rhost()?;
}; let maybe_service = self.get_service()?;
if PamResultCode::PAM_SUCCESS == res {
Ok(item) tracing::debug!(?maybe_tty, ?maybe_rhost, ?maybe_service);
} else {
Err(res) match (maybe_tty, maybe_rhost, maybe_service) {
(Some(tty), Some(rhost), Some(service)) => Ok(PamServiceInfo {
service,
tty,
rhost,
}),
_ => Err(PamResultCode::PAM_CONV_ERR),
} }
} }
} }

View file

@ -75,6 +75,8 @@ selinux = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
sketching = { workspace = true } sketching = { workspace = true }
sha-crypt = { workspace = true }
time = { workspace = true, features = ["std"] }
toml = { workspace = true } toml = { workspace = true }
tokio = { workspace = true, features = [ tokio = { workspace = true, features = [
"rt", "rt",

View file

@ -20,7 +20,7 @@ use kanidm_unix_common::client::call_daemon;
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
use kanidm_unix_common::unix_config::KanidmUnixdConfig; use kanidm_unix_common::unix_config::KanidmUnixdConfig;
use kanidm_unix_common::unix_proto::{ use kanidm_unix_common::unix_proto::{
ClientRequest, ClientResponse, PamAuthRequest, PamAuthResponse, ClientRequest, ClientResponse, PamAuthRequest, PamAuthResponse, PamServiceInfo,
}; };
// use std::io; // use std::io;
use std::path::PathBuf; use std::path::PathBuf;
@ -63,7 +63,14 @@ async fn main() -> ExitCode {
info!("Sending request for user {}", &account_id); 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 { loop {
match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await { match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await {
Ok(r) => match r { Ok(r) => match r {

View file

@ -27,13 +27,14 @@ use futures::{SinkExt, StreamExt};
use kanidm_client::KanidmClientBuilder; use kanidm_client::KanidmClientBuilder;
use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH; use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH;
use kanidm_unix_common::constants::DEFAULT_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_common::unix_proto::{ClientRequest, ClientResponse, TaskRequest, TaskResponse};
use kanidm_unix_resolver::db::{Cache, Db}; 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::kanidm::KanidmProvider;
use kanidm_unix_resolver::idprovider::system::SystemProvider; use kanidm_unix_resolver::idprovider::system::SystemProvider;
use kanidm_unix_resolver::resolver::Resolver; 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 kanidm_utils_users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
use libc::umask; use libc::umask;
@ -41,13 +42,13 @@ use sketching::tracing::span;
use sketching::tracing_forest::traits::*; use sketching::tracing_forest::traits::*;
use sketching::tracing_forest::util::*; use sketching::tracing_forest::util::*;
use sketching::tracing_forest::{self}; use sketching::tracing_forest::{self};
use time::OffsetDateTime;
use tokio::fs::File; use tokio::fs::File;
use tokio::io::AsyncReadExt; // for read_to_end() use tokio::io::AsyncReadExt; // for read_to_end()
use tokio::net::{UnixListener, UnixStream}; use tokio::net::{UnixListener, UnixStream};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::mpsc::{channel, Receiver, Sender};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tokio::time;
use tokio_util::codec::{Decoder, Encoder, Framed}; use tokio_util::codec::{Decoder, Encoder, Framed};
use kanidm_hsm_crypto::{soft::SoftTpm, AuthValue, BoxedDynTpm, Tpm}; use kanidm_hsm_crypto::{soft::SoftTpm, AuthValue, BoxedDynTpm, Tpm};
@ -213,90 +214,67 @@ async fn handle_client(
trace!("Waiting for requests ..."); trace!("Waiting for requests ...");
while let Some(Ok(req)) = reqs.next().await { 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 _enter = span.enter();
let resp = match req { let resp = match req {
ClientRequest::SshKey(account_id) => { ClientRequest::SshKey(account_id) => cachelayer
debug!("sshkey req"); .get_sshkeys(account_id.as_str())
cachelayer .await
.get_sshkeys(account_id.as_str()) .map(ClientResponse::SshKeys)
.await .unwrap_or_else(|_| {
.map(ClientResponse::SshKeys) error!("unable to load keys, returning empty set.");
.unwrap_or_else(|_| { ClientResponse::SshKeys(vec![])
error!("unable to load keys, returning empty set."); }),
ClientResponse::SshKeys(vec![]) ClientRequest::NssAccounts => cachelayer
}) .get_nssaccounts()
} .await
ClientRequest::NssAccounts => { .map(ClientResponse::NssAccounts)
debug!("nssaccounts req"); .unwrap_or_else(|_| {
cachelayer error!("unable to enum accounts");
.get_nssaccounts() ClientResponse::NssAccounts(Vec::new())
.await }),
.map(ClientResponse::NssAccounts) ClientRequest::NssAccountByUid(gid) => cachelayer
.unwrap_or_else(|_| { .get_nssaccount_gid(gid)
error!("unable to enum accounts"); .await
ClientResponse::NssAccounts(Vec::new()) .map(ClientResponse::NssAccount)
}) .unwrap_or_else(|_| {
} error!("unable to load account, returning empty.");
ClientRequest::NssAccountByUid(gid) => { ClientResponse::NssAccount(None)
debug!("nssaccountbyuid req"); }),
cachelayer ClientRequest::NssAccountByName(account_id) => cachelayer
.get_nssaccount_gid(gid) .get_nssaccount_name(account_id.as_str())
.await .await
.map(ClientResponse::NssAccount) .map(ClientResponse::NssAccount)
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
error!("unable to load account, returning empty."); error!("unable to load account, returning empty.");
ClientResponse::NssAccount(None) ClientResponse::NssAccount(None)
}) }),
} ClientRequest::NssGroups => cachelayer
ClientRequest::NssAccountByName(account_id) => { .get_nssgroups()
debug!("nssaccountbyname req"); .await
cachelayer .map(ClientResponse::NssGroups)
.get_nssaccount_name(account_id.as_str()) .unwrap_or_else(|_| {
.await error!("unable to enum groups");
.map(ClientResponse::NssAccount) ClientResponse::NssGroups(Vec::new())
.unwrap_or_else(|_| { }),
error!("unable to load account, returning empty."); ClientRequest::NssGroupByGid(gid) => cachelayer
ClientResponse::NssAccount(None) .get_nssgroup_gid(gid)
}) .await
} .map(ClientResponse::NssGroup)
ClientRequest::NssGroups => { .unwrap_or_else(|_| {
debug!("nssgroups req"); error!("unable to load group, returning empty.");
cachelayer ClientResponse::NssGroup(None)
.get_nssgroups() }),
.await ClientRequest::NssGroupByName(grp_id) => cachelayer
.map(ClientResponse::NssGroups) .get_nssgroup_name(grp_id.as_str())
.unwrap_or_else(|_| { .await
error!("unable to enum groups"); .map(ClientResponse::NssGroup)
ClientResponse::NssGroups(Vec::new()) .unwrap_or_else(|_| {
}) error!("unable to load group, returning empty.");
} ClientResponse::NssGroup(None)
ClientRequest::NssGroupByGid(gid) => { }),
debug!("nssgroupbygid req"); ClientRequest::PamAuthenticateInit { account_id, info } => {
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");
match &pam_auth_session_state { match &pam_auth_session_state {
Some(_auth_session) => { Some(_auth_session) => {
// Invalid to init a request twice. // Invalid to init a request twice.
@ -306,9 +284,13 @@ async fn handle_client(
ClientResponse::Error ClientResponse::Error
} }
None => { None => {
let current_time = OffsetDateTime::now_utc();
match cachelayer match cachelayer
.pam_account_authenticate_init( .pam_account_authenticate_init(
account_id.as_str(), account_id.as_str(),
&info,
current_time,
shutdown_tx.subscribe(), shutdown_tx.subscribe(),
) )
.await .await
@ -322,30 +304,23 @@ async fn handle_client(
} }
} }
} }
ClientRequest::PamAuthenticateStep(pam_next_req) => { ClientRequest::PamAuthenticateStep(pam_next_req) => match &mut pam_auth_session_state {
debug!("pam authenticate step"); Some(auth_session) => cachelayer
match &mut pam_auth_session_state { .pam_account_authenticate_step(auth_session, pam_next_req)
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())
.await .await
.map(ClientResponse::PamStatus) .map(|pam_auth_response| pam_auth_response.into())
.unwrap_or(ClientResponse::Error) .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) => { ClientRequest::PamAccountBeginSession(account_id) => {
debug!("pam account begin session");
match cachelayer match cachelayer
.pam_account_beginsession(account_id.as_str()) .pam_account_beginsession(account_id.as_str())
.await .await
@ -362,8 +337,8 @@ async fn handle_client(
{ {
Ok(()) => { Ok(()) => {
// Now wait for the other end OR timeout. // Now wait for the other end OR timeout.
match time::timeout_at( match tokio::time::timeout_at(
time::Instant::now() + Duration::from_millis(1000), tokio::time::Instant::now() + Duration::from_millis(1000),
rx, rx,
) )
.await .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, _ => ClientResponse::Error,
} }
} }
ClientRequest::InvalidateCache => { ClientRequest::InvalidateCache => cachelayer
debug!("invalidate cache"); .invalidate()
cachelayer .await
.invalidate() .map(|_| ClientResponse::Ok)
.await .unwrap_or(ClientResponse::Error),
.map(|_| ClientResponse::Ok)
.unwrap_or(ClientResponse::Error)
}
ClientRequest::ClearCache => { ClientRequest::ClearCache => {
debug!("clear cache");
if ucred.uid() == 0 { if ucred.uid() == 0 {
cachelayer cachelayer
.clear_cache() .clear_cache()
@ -409,14 +384,13 @@ async fn handle_client(
} }
} }
ClientRequest::Status => { ClientRequest::Status => {
debug!("status check");
let status = cachelayer.provider_status().await; let status = cachelayer.provider_status().await;
ClientResponse::ProviderStatus(status) ClientResponse::ProviderStatus(status)
} }
}; };
reqs.send(resp).await?; reqs.send(resp).await?;
reqs.flush().await?; reqs.flush().await?;
debug!("flushed response!"); trace!("flushed response!");
} }
// Signal any tasks that they need to stop. // Signal any tasks that they need to stop.
@ -439,13 +413,33 @@ async fn process_etc_passwd_group(cachelayer: &Resolver) -> Result<(), Box<dyn E
let users = parse_etc_passwd(contents.as_slice()).map_err(|_| "Invalid passwd content")?; let users = parse_etc_passwd(contents.as_slice()).map_err(|_| "Invalid passwd content")?;
let maybe_shadow = match File::open("/etc/shadow").await {
Ok(mut file) => {
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 file = File::open("/etc/group").await?;
let mut contents = vec![]; let mut contents = vec![];
file.read_to_end(&mut contents).await?; file.read_to_end(&mut contents).await?;
let groups = parse_etc_group(contents.as_slice()).map_err(|_| "Invalid group content")?; 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(()) Ok(())
} }
@ -484,7 +478,10 @@ async fn write_hsm_pin(hsm_pin_path: &str) -> Result<(), Box<dyn Error>> {
fn open_tpm(tcti_name: &str) -> Option<BoxedDynTpm> { fn open_tpm(tcti_name: &str) -> Option<BoxedDynTpm> {
use kanidm_hsm_crypto::tpm::TpmTss; use kanidm_hsm_crypto::tpm::TpmTss;
match TpmTss::new(tcti_name) { match TpmTss::new(tcti_name) {
Ok(tpm) => Some(BoxedDynTpm::new(tpm)), Ok(tpm) => {
debug!("opened hw tpm");
Some(BoxedDynTpm::new(tpm))
}
Err(tpm_err) => { Err(tpm_err) => {
error!(?tpm_err, "Unable to open requested tpm device"); error!(?tpm_err, "Unable to open requested tpm device");
None None
@ -502,7 +499,10 @@ fn open_tpm(_tcti_name: &str) -> Option<BoxedDynTpm> {
fn open_tpm_if_possible(tcti_name: &str) -> BoxedDynTpm { fn open_tpm_if_possible(tcti_name: &str) -> BoxedDynTpm {
use kanidm_hsm_crypto::tpm::TpmTss; use kanidm_hsm_crypto::tpm::TpmTss;
match TpmTss::new(tcti_name) { match TpmTss::new(tcti_name) {
Ok(tpm) => BoxedDynTpm::new(tpm), Ok(tpm) => {
debug!("opened hw tpm");
BoxedDynTpm::new(tpm)
}
Err(tpm_err) => { Err(tpm_err) => {
warn!( warn!(
?tpm_err, ?tpm_err,
@ -515,6 +515,7 @@ fn open_tpm_if_possible(tcti_name: &str) -> BoxedDynTpm {
#[cfg(not(feature = "tpm"))] #[cfg(not(feature = "tpm"))]
fn open_tpm_if_possible(_tcti_name: &str) -> BoxedDynTpm { fn open_tpm_if_possible(_tcti_name: &str) -> BoxedDynTpm {
debug!("opened soft tpm");
BoxedDynTpm::new(SoftTpm::new()) BoxedDynTpm::new(SoftTpm::new())
} }
@ -681,16 +682,7 @@ async fn main() -> ExitCode {
} }
} }
// setup let cfg = match UnixdConfig::new().read_options_from_optional_config(&unixd_path) {
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) {
Ok(v) => v, Ok(v) => v,
Err(_) => { Err(_) => {
error!("Failed to parse {}", unixd_path_str); 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") { if clap_args.get_flag("configtest") {
eprintln!("###################################"); eprintln!("###################################");
eprintln!("Dumping configs:\n###################################"); eprintln!("Dumping configs:\n###################################");
eprintln!("kanidm_unixd config (from {:#?})", &unixd_path); eprintln!("kanidm_unixd config (from {:#?})", &unixd_path);
eprintln!("{}", cfg); eprintln!("{}", cfg);
eprintln!("###################################"); eprintln!("###################################");
eprintln!("Client config (from {:#?})", &cfg_path); if let Some((cb, _)) = client_builder.as_ref() {
eprintln!("{}", cb); eprintln!("kanidm client config (from {:#?})", &cfg_path);
eprintln!("{}", cb);
} else {
eprintln!("kanidm client: disabled");
}
return ExitCode::SUCCESS; return ExitCode::SUCCESS;
} }
@ -714,10 +723,10 @@ async fn main() -> ExitCode {
rm_if_exist(cfg.task_sock_path.as_str()); rm_if_exist(cfg.task_sock_path.as_str());
// Check the db path will be okay. // Check the db path will be okay.
if !cfg.db_path.is_empty() { if !cfg.cache_db_path.is_empty() {
let db_path = PathBuf::from(cfg.db_path.as_str()); 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. // 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() { if !db_parent_path.exists() {
error!( error!(
"Refusing to run, DB folder {} does not exist", "Refusing to run, DB folder {} does not exist",
@ -725,7 +734,7 @@ async fn main() -> ExitCode {
.to_str() .to_str()
.unwrap_or("<db_parent_path invalid>") .unwrap_or("<db_parent_path invalid>")
); );
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); info!(%diag);
return ExitCode::FAILURE return ExitCode::FAILURE
} }
@ -769,26 +778,26 @@ async fn main() -> ExitCode {
} }
// check to see if the db's already there // check to see if the db's already there
if db_path.exists() { if cache_db_path.exists() {
if !db_path.is_file() { if !cache_db_path.is_file() {
error!( error!(
"Refusing to run - DB path {} already exists and is not a file.", "Refusing to run - DB path {} already exists and is not a file.",
db_path.to_str().unwrap_or("<db_path invalid>") cache_db_path.to_str().unwrap_or("<cache_db_path invalid>")
); );
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); info!(%diag);
return ExitCode::FAILURE return ExitCode::FAILURE
}; };
match metadata(&db_path) { match metadata(&cache_db_path) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
error!( error!(
"Unable to read metadata for {} - {:?}", "Unable to read metadata for {} - {:?}",
db_path.to_str().unwrap_or("<db_path invalid>"), cache_db_path.to_str().unwrap_or("<cache_db_path invalid>"),
e 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); info!(%diag);
return ExitCode::FAILURE return ExitCode::FAILURE
} }
@ -797,18 +806,7 @@ async fn main() -> ExitCode {
}; };
} }
let cb = cb.connect_timeout(cfg.conn_timeout); let db = match Db::new(cfg.cache_db_path.as_str()) {
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()) {
Ok(db) => db, Ok(db) => db,
Err(_e) => { Err(_e) => {
error!("Failed to create database"); error!("Failed to create database");
@ -900,7 +898,7 @@ async fn main() -> ExitCode {
Ok(mk) => mk, Ok(mk) => mk,
Err(err) => { Err(err) => {
error!(?err, "Unable to load machine root key - This can occur if you have changed your HSM pin"); 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 return ExitCode::FAILURE
} }
}; };
@ -911,16 +909,39 @@ async fn main() -> ExitCode {
return ExitCode::FAILURE return ExitCode::FAILURE
}; };
let Ok(idprovider) = KanidmProvider::new( info!("Started system provider");
rsclient,
SystemTime::now(), let mut clients: Vec<Arc<dyn IdProvider + Send + Sync>> = Vec::with_capacity(1);
&mut (&mut db_txn).into(),
&mut hsm, // Setup Kanidm provider if the configuration requests it.
&machine_key if let Some((cb, kconfig)) = client_builder {
) else { let cb = cb.connect_timeout(kconfig.conn_timeout);
error!("Failed to configure Kanidm Provider"); let cb = cb.request_timeout(kconfig.request_timeout);
return ExitCode::FAILURE
}; 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); drop(machine_key);
@ -937,14 +958,12 @@ async fn main() -> ExitCode {
} }
// Okay, the hsm is now loaded and ready to go. // Okay, the hsm is now loaded and ready to go.
let cl_inner = match Resolver::new( let cl_inner = match Resolver::new(
db, db,
Arc::new(system_provider), Arc::new(system_provider),
Arc::new(idprovider), clients,
hsm, hsm,
cfg.cache_timeout, cfg.cache_timeout,
cfg.pam_allowed_login_groups.clone(),
cfg.default_shell.clone(), cfg.default_shell.clone(),
cfg.home_prefix.clone(), cfg.home_prefix.clone(),
cfg.home_attr, cfg.home_attr,
@ -1054,6 +1073,9 @@ async fn main() -> ExitCode {
}) })
.and_then(|mut debouncer| debouncer.watcher().watch(Path::new("/etc/group"), RecursiveMode::NonRecursive) .and_then(|mut debouncer| debouncer.watcher().watch(Path::new("/etc/group"), RecursiveMode::NonRecursive)
.map(|()| debouncer) .map(|()| debouncer)
)
.and_then(|mut debouncer| debouncer.watcher().watch(Path::new("/etc/shadow"), RecursiveMode::NonRecursive)
.map(|()| debouncer)
); );
let watcher = let watcher =
match watcher { match watcher {

View file

@ -22,7 +22,7 @@ use bytes::{BufMut, BytesMut};
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH; use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
use kanidm_unix_common::unix_proto::{HomeDirectoryInfo, TaskRequest, TaskResponse}; 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 kanidm_utils_users::{get_effective_gid, get_effective_uid};
use libc::{lchown, umask}; use libc::{lchown, umask};
use sketching::tracing_forest::traits::*; use sketching::tracing_forest::traits::*;
@ -272,7 +272,7 @@ fn create_home_directory(
Ok(()) 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()); let mut reqs = Framed::new(stream, TaskCodec::new());
loop { 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, Ok(v) => v,
Err(_) => { Err(_) => {
error!("Failed to parse {}", unixd_path_str); error!("Failed to parse {}", unixd_path_str);

View file

@ -292,6 +292,8 @@ pub trait IdProvider {
_tpm: &mut tpm::BoxedDynTpm, _tpm: &mut tpm::BoxedDynTpm,
) -> Result<AuthResult, IdpError>; ) -> Result<AuthResult, IdpError>;
async fn unix_user_authorise(&self, _token: &UserToken) -> Result<Option<bool>, IdpError>;
async fn unix_group_get( async fn unix_group_get(
&self, &self,
id: &Id, id: &Id,

View file

@ -1,8 +1,10 @@
use crate::db::KeyStoreTxn; use crate::db::KeyStoreTxn;
use crate::unix_config::KanidmConfig;
use async_trait::async_trait; use async_trait::async_trait;
use kanidm_client::{ClientError, KanidmClient, StatusCode}; use kanidm_client::{ClientError, KanidmClient, StatusCode};
use kanidm_proto::internal::OperationError; use kanidm_proto::internal::OperationError;
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
use std::collections::BTreeSet;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use tokio::sync::{broadcast, Mutex}; use tokio::sync::{broadcast, Mutex};
@ -34,6 +36,7 @@ struct KanidmProviderInternal {
client: KanidmClient, client: KanidmClient,
hmac_key: HmacKey, hmac_key: HmacKey,
crypto_policy: CryptoPolicy, crypto_policy: CryptoPolicy,
pam_allow_groups: BTreeSet<String>,
} }
pub struct KanidmProvider { pub struct KanidmProvider {
@ -43,6 +46,7 @@ pub struct KanidmProvider {
impl KanidmProvider { impl KanidmProvider {
pub fn new( pub fn new(
client: KanidmClient, client: KanidmClient,
config: &KanidmConfig,
now: SystemTime, now: SystemTime,
keystore: &mut KeyStoreTxn, keystore: &mut KeyStoreTxn,
tpm: &mut tpm::BoxedDynTpm, tpm: &mut tpm::BoxedDynTpm,
@ -85,12 +89,15 @@ impl KanidmProvider {
let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(250)); let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(250));
let pam_allow_groups = config.pam_allowed_login_groups.iter().cloned().collect();
Ok(KanidmProvider { Ok(KanidmProvider {
inner: Mutex::new(KanidmProviderInternal { inner: Mutex::new(KanidmProviderInternal {
state: CacheState::OfflineNextCheck(now), state: CacheState::OfflineNextCheck(now),
client, client,
hmac_key, hmac_key,
crypto_policy, crypto_policy,
pam_allow_groups,
}), }),
}) })
} }
@ -602,4 +609,30 @@ impl IdProvider for KanidmProvider {
} }
} }
} }
async fn unix_user_authorise(&self, token: &UserToken) -> Result<Option<bool>, 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))
}
}
} }

View file

@ -1,9 +1,11 @@
use hashbrown::HashMap; use hashbrown::HashMap;
use std::sync::Arc; use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use super::interface::{Id, IdpError}; use super::interface::{AuthCredHandler, AuthRequest, Id, IdpError};
use kanidm_unix_common::unix_passwd::{EtcGroup, EtcUser}; use kanidm_unix_common::unix_passwd::{EtcGroup, EtcShadow, EtcUser};
use kanidm_unix_common::unix_proto::PamAuthRequest;
use kanidm_unix_common::unix_proto::{NssGroup, NssUser}; use kanidm_unix_common::unix_proto::{NssGroup, NssUser};
pub struct SystemProviderInternal { pub struct SystemProviderInternal {
@ -11,6 +13,141 @@ pub struct SystemProviderInternal {
user_list: Vec<Arc<EtcUser>>, user_list: Vec<Arc<EtcUser>>,
groups: HashMap<Id, Arc<EtcGroup>>, groups: HashMap<Id, Arc<EtcGroup>>,
group_list: Vec<Arc<EtcGroup>>, group_list: Vec<Arc<EtcGroup>>,
shadow_enabled: bool,
shadow: HashMap<String, Arc<Shadow>>,
}
pub enum SystemProviderAuthInit {
Begin {
next_request: AuthRequest,
cred_handler: AuthCredHandler,
shadow: Arc<Shadow>,
},
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<String> for CryptPw {
type Error = ();
fn try_from(value: String) -> Result<Self, Self::Error> {
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<time::OffsetDateTime>,
warning_period_start: Option<time::OffsetDateTime>,
inactivity_period_deadline: Option<time::OffsetDateTime>,
}
impl AgingPolicy {
fn new(
change_days: i64,
days_min_password_age: i64,
days_max_password_age: Option<i64>,
days_warning_period: i64,
days_inactivity_period: Option<i64>,
) -> 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<AgingPolicy>,
expiration_date: Option<time::OffsetDateTime>,
}
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 { pub struct SystemProvider {
@ -25,16 +162,73 @@ impl SystemProvider {
user_list: Default::default(), user_list: Default::default(),
groups: Default::default(), groups: Default::default(),
group_list: Default::default(), group_list: Default::default(),
shadow_enabled: Default::default(),
shadow: Default::default(),
}), }),
}) })
} }
pub async fn reload(&self, users: Vec<EtcUser>, groups: Vec<EtcGroup>) { pub async fn reload(
&self,
users: Vec<EtcUser>,
shadow: Option<Vec<EtcShadow>>,
groups: Vec<EtcGroup>,
) {
let mut system_ids_txn = self.inner.lock().await; let mut system_ids_txn = self.inner.lock().await;
system_ids_txn.users.clear(); system_ids_txn.users.clear();
system_ids_txn.user_list.clear(); system_ids_txn.user_list.clear();
system_ids_txn.groups.clear(); system_ids_txn.groups.clear();
system_ids_txn.group_list.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 { for group in groups {
let name = Id::Name(group.name.clone()); let name = Id::Name(group.name.clone());
@ -67,7 +261,7 @@ impl SystemProvider {
if !(group.members.is_empty() if !(group.members.is_empty()
|| (group.members.len() == 1 && group.members.first() == Some(&user.name))) || (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 { } else {
info!(name = %user.name, uid = %user.uid, gid = %user.gid, "user private group is not present on system, synthesising it"); 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; 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<bool> {
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 { pub async fn contains_group(&self, account_id: &Id) -> bool {

View file

@ -1,6 +1,5 @@
// use async_trait::async_trait; // use async_trait::async_trait;
use hashbrown::HashMap; use hashbrown::HashMap;
use std::collections::BTreeSet;
use std::fmt::Display; use std::fmt::Display;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::ops::DerefMut; use std::ops::DerefMut;
@ -10,6 +9,7 @@ use std::sync::Arc;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use lru::LruCache; use lru::LruCache;
use time::OffsetDateTime;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use uuid::Uuid; use uuid::Uuid;
@ -27,11 +27,14 @@ use crate::idprovider::interface::{
UserToken, UserToken,
UserTokenState, UserTokenState,
}; };
use crate::idprovider::system::SystemProvider; use crate::idprovider::system::{
Shadow, SystemAuthResult, SystemProvider, SystemProviderAuthInit, SystemProviderSession,
};
use crate::unix_config::{HomeAttr, UidAttr}; 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::{ use kanidm_unix_common::unix_proto::{
HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, PamAuthResponse, ProviderStatus, HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, PamAuthResponse, PamServiceInfo,
ProviderStatus,
}; };
use kanidm_hsm_crypto::BoxedDynTpm; use kanidm_hsm_crypto::BoxedDynTpm;
@ -58,6 +61,10 @@ pub enum AuthSession {
token: Box<UserToken>, token: Box<UserToken>,
cred_handler: AuthCredHandler, cred_handler: AuthCredHandler,
}, },
System {
cred_handler: AuthCredHandler,
shadow: Arc<Shadow>,
},
Success, Success,
Denied, Denied,
} }
@ -76,7 +83,9 @@ pub struct Resolver {
// A set of remote resolvers, ordered by priority. // A set of remote resolvers, ordered by priority.
clients: Vec<Arc<dyn IdProvider + Sync + Send>>, clients: Vec<Arc<dyn IdProvider + Sync + Send>>,
pam_allow_groups: BTreeSet<String>, // The id of the primary-provider which may use name over spn.
primary_origin: ProviderOrigin,
timeout_seconds: u64, timeout_seconds: u64,
default_shell: String, default_shell: String,
home_prefix: PathBuf, home_prefix: PathBuf,
@ -101,10 +110,9 @@ impl Resolver {
pub async fn new( pub async fn new(
db: Db, db: Db,
system_provider: Arc<SystemProvider>, system_provider: Arc<SystemProvider>,
client: Arc<dyn IdProvider + Sync + Send>, clients: Vec<Arc<dyn IdProvider + Sync + Send>>,
hsm: BoxedDynTpm, hsm: BoxedDynTpm,
timeout_seconds: u64, timeout_seconds: u64,
pam_allow_groups: Vec<String>,
default_shell: String, default_shell: String,
home_prefix: PathBuf, home_prefix: PathBuf,
home_attr: HomeAttr, home_attr: HomeAttr,
@ -114,11 +122,7 @@ impl Resolver {
) -> Result<Self, ()> { ) -> Result<Self, ()> {
let hsm = Mutex::new(hsm); let hsm = Mutex::new(hsm);
if pam_allow_groups.is_empty() { let primary_origin = clients.first().map(|c| c.origin()).unwrap_or_default();
warn!("Will not be able to authorise user logins, pam_allow_groups config is not configured.");
}
let clients: Vec<Arc<dyn IdProvider + Sync + Send>> = vec![client];
let client_ids: HashMap<_, _> = clients let client_ids: HashMap<_, _> = clients
.iter() .iter()
@ -132,9 +136,9 @@ impl Resolver {
hsm, hsm,
system_provider, system_provider,
clients, clients,
primary_origin,
client_ids, client_ids,
timeout_seconds, timeout_seconds,
pam_allow_groups: pam_allow_groups.into_iter().collect(),
default_shell, default_shell,
home_prefix, home_prefix,
home_attr, home_attr,
@ -142,7 +146,6 @@ impl Resolver {
uid_attr_map, uid_attr_map,
gid_attr_map, gid_attr_map,
nxcache: Mutex::new(LruCache::new(NXCACHE_SIZE)), nxcache: Mutex::new(LruCache::new(NXCACHE_SIZE)),
// system_identities,
}) })
} }
@ -200,8 +203,13 @@ impl Resolver {
nxcache_txn.get(id).copied() nxcache_txn.get(id).copied()
} }
pub async fn reload_system_identities(&self, users: Vec<EtcUser>, groups: Vec<EtcGroup>) { pub async fn reload_system_identities(
self.system_provider.reload(users, groups).await &self,
users: Vec<EtcUser>,
shadow: Option<Vec<EtcShadow>>,
groups: Vec<EtcGroup>,
) {
self.system_provider.reload(users, shadow, groups).await
} }
async fn get_cached_usertoken(&self, account_id: &Id) -> Result<(bool, Option<UserToken>), ()> { async fn get_cached_usertoken(&self, account_id: &Id) -> Result<(bool, Option<UserToken>), ()> {
@ -582,32 +590,30 @@ impl Resolver {
.unwrap_or_else(|| Vec::with_capacity(0))) .unwrap_or_else(|| Vec::with_capacity(0)))
} }
#[inline(always)]
fn token_homedirectory_alias(&self, token: &UserToken) -> Option<String> { fn token_homedirectory_alias(&self, token: &UserToken) -> Option<String> {
let is_primary_origin = token.provider == self.primary_origin;
self.home_alias.map(|t| match t { self.home_alias.map(|t| match t {
// If we have an alias. use it. // 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::Uuid => token.uuid.hyphenated().to_string(),
HomeAttr::Spn => token.spn.as_str().to_string(), HomeAttr::Spn | HomeAttr::Name => token.spn.as_str().to_string(),
HomeAttr::Name => token.name.as_str().to_string(),
}) })
} }
#[inline(always)]
fn token_homedirectory_attr(&self, token: &UserToken) -> String { fn token_homedirectory_attr(&self, token: &UserToken) -> String {
let is_primary_origin = token.provider == self.primary_origin;
match self.home_attr { match self.home_attr {
HomeAttr::Name if is_primary_origin => token.name.as_str().to_string(),
HomeAttr::Uuid => token.uuid.hyphenated().to_string(), HomeAttr::Uuid => token.uuid.hyphenated().to_string(),
HomeAttr::Spn => token.spn.as_str().to_string(), HomeAttr::Spn | HomeAttr::Name => token.spn.as_str().to_string(),
HomeAttr::Name => token.name.as_str().to_string(),
} }
} }
#[inline(always)]
fn token_homedirectory(&self, token: &UserToken) -> String { fn token_homedirectory(&self, token: &UserToken) -> String {
self.token_homedirectory_alias(token) self.token_homedirectory_alias(token)
.unwrap_or_else(|| self.token_homedirectory_attr(token)) .unwrap_or_else(|| self.token_homedirectory_attr(token))
} }
#[inline(always)]
fn token_abs_homedirectory(&self, token: &UserToken) -> String { fn token_abs_homedirectory(&self, token: &UserToken) -> String {
self.home_prefix self.home_prefix
.join(self.token_homedirectory(token)) .join(self.token_homedirectory(token))
@ -615,11 +621,11 @@ impl Resolver {
.to_string() .to_string()
} }
#[inline(always)]
fn token_uidattr(&self, token: &UserToken) -> String { fn token_uidattr(&self, token: &UserToken) -> String {
let is_primary_origin = token.provider == self.primary_origin;
match self.uid_attr_map { match self.uid_attr_map {
UidAttr::Spn => token.spn.as_str(), UidAttr::Name if is_primary_origin => token.name.as_str(),
UidAttr::Name => token.name.as_str(), UidAttr::Spn | UidAttr::Name => token.spn.as_str(),
} }
.to_string() .to_string()
} }
@ -673,7 +679,6 @@ impl Resolver {
self.get_nssaccount(Id::Gid(gid)).await self.get_nssaccount(Id::Gid(gid)).await
} }
#[inline(always)]
fn token_gidattr(&self, token: &GroupToken) -> String { fn token_gidattr(&self, token: &GroupToken) -> String {
match self.gid_attr_map { match self.gid_attr_map {
UidAttr::Spn => token.spn.as_str(), UidAttr::Spn => token.spn.as_str(),
@ -686,6 +691,12 @@ impl Resolver {
pub async fn get_nssgroups(&self) -> Result<Vec<NssGroup>, ()> { pub async fn get_nssgroups(&self) -> Result<Vec<NssGroup>, ()> {
let mut r = self.system_provider.get_nssgroups().await; 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?; let l = self.get_cached_grouptokens().await?;
r.reserve(l.len()); r.reserve(l.len());
for tok in l.into_iter() { for tok in l.into_iter() {
@ -732,32 +743,29 @@ impl Resolver {
#[instrument(level = "debug", skip(self))] #[instrument(level = "debug", skip(self))]
pub async fn pam_account_allowed(&self, account_id: &str) -> Result<Option<bool>, ()> { pub async fn pam_account_allowed(&self, account_id: &str) -> Result<Option<bool>, ()> {
let token = self let id = Id::Name(account_id.to_string());
.get_usertoken(&Id::Name(account_id.to_string()))
.await?;
if self.pam_allow_groups.is_empty() { if let Some(answer) = self.system_provider.authorise(&id).await {
// can't allow anything if the group list is zero... return Ok(Some(answer));
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();
debug!( // Not a system account, handle with the provider.
"Checking if user is in allowed groups ({:?}) -> {:?}", let token = self.get_usertoken(&id).await?;
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);
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( pub async fn pam_account_authenticate_init(
&self, &self,
account_id: &str, account_id: &str,
pam_info: &PamServiceInfo,
current_time: OffsetDateTime,
shutdown_rx: broadcast::Receiver<()>, shutdown_rx: broadcast::Receiver<()>,
) -> Result<(AuthSession, PamAuthResponse), ()> { ) -> Result<(AuthSession, PamAuthResponse), ()> {
// Setup an auth session. If possible bring the resolver online. // Setup an auth session. If possible bring the resolver online.
@ -776,9 +786,50 @@ impl Resolver {
let id = Id::Name(account_id.to_string()); let id = Id::Name(account_id.to_string());
if self.system_provider.contains_account(&id).await { match self.system_provider.auth_init(&id, current_time).await {
debug!("Ignoring auth request for system user"); // The system provider will not take part in this authentication.
return Ok((AuthSession::Denied, PamAuthResponse::Unknown)); 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?; let token = self.get_usertoken(&id).await?;
@ -945,6 +996,32 @@ impl Resolver {
) )
.await .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), &mut AuthSession::Success | &mut AuthSession::Denied => Err(IdpError::BadRequest),
}; };
@ -973,12 +1050,19 @@ impl Resolver {
pub async fn pam_account_authenticate( pub async fn pam_account_authenticate(
&self, &self,
account_id: &str, account_id: &str,
current_time: OffsetDateTime,
password: &str, password: &str,
) -> Result<Option<bool>, ()> { ) -> Result<Option<bool>, ()> {
let (_shutdown_tx, shutdown_rx) = broadcast::channel(1); 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 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? .await?
{ {
(auth_session, PamAuthResponse::Password) => { (auth_session, PamAuthResponse::Password) => {
@ -1042,10 +1126,26 @@ impl Resolver {
&self, &self,
account_id: &str, account_id: &str,
) -> Result<Option<HomeDirectoryInfo>, ()> { ) -> Result<Option<HomeDirectoryInfo>, ()> {
let token = self let id = Id::Name(account_id.to_string());
.get_usertoken(&Id::Name(account_id.to_string()))
.await?; 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 { Ok(token.as_ref().map(|tok| HomeDirectoryInfo {
uid: tok.gidnumber,
gid: tok.gidnumber, gid: tok.gidnumber,
name: self.token_homedirectory_attr(tok), name: self.token_homedirectory_attr(tok),
aliases: self aliases: self
@ -1059,7 +1159,12 @@ impl Resolver {
let now = SystemTime::now(); let now = SystemTime::now();
let mut hsm_lock = self.hsm.lock().await; 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() { for client in self.clients.iter() {
let online = client.attempt_online(hsm_lock.deref_mut(), now).await; let online = client.attempt_online(hsm_lock.deref_mut(), now).await;

View file

@ -14,6 +14,64 @@ use serde::Deserialize;
use kanidm_unix_common::constants::*; 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<String>,
sock_path: Option<String>,
task_sock_path: Option<String>,
cache_timeout: Option<u64>,
default_shell: Option<String>,
home_prefix: Option<String>,
home_mount_prefix: Option<String>,
home_attr: Option<String>,
home_alias: Option<String>,
use_etc_skel: Option<bool>,
uid_attr_map: Option<String>,
gid_attr_map: Option<String>,
selinux: Option<bool>,
hsm_pin_path: Option<String>,
hsm_type: Option<String>,
tpm_tcti_name: Option<String>,
kanidm: Option<KanidmConfigV2>,
}
#[derive(Debug, Deserialize)]
pub struct GroupMap {
pub local: String,
pub with: String,
}
#[derive(Debug, Deserialize)]
struct KanidmConfigV2 {
conn_timeout: Option<u64>,
request_timeout: Option<u64>,
pam_allowed_login_groups: Option<Vec<String>>,
extend: Vec<GroupMap>,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ConfigInt { struct ConfigInt {
db_path: Option<String>, db_path: Option<String>,
@ -60,15 +118,12 @@ impl Display for HsmType {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct KanidmUnixdConfig { pub struct UnixdConfig {
pub db_path: String, pub cache_db_path: String,
pub sock_path: String, pub sock_path: String,
pub task_sock_path: String, pub task_sock_path: String,
pub conn_timeout: u64,
pub request_timeout: u64,
pub cache_timeout: u64, pub cache_timeout: u64,
pub unix_sock_timeout: u64, pub unix_sock_timeout: u64,
pub pam_allowed_login_groups: Vec<String>,
pub default_shell: String, pub default_shell: String,
pub home_prefix: PathBuf, pub home_prefix: PathBuf,
pub home_mount_prefix: Option<PathBuf>, pub home_mount_prefix: Option<PathBuf>,
@ -81,29 +136,31 @@ pub struct KanidmUnixdConfig {
pub hsm_type: HsmType, pub hsm_type: HsmType,
pub hsm_pin_path: String, pub hsm_pin_path: String,
pub tpm_tcti_name: String, pub tpm_tcti_name: String,
pub allow_local_account_override: Vec<String>,
pub kanidm_config: Option<KanidmConfig>,
} }
impl Default for KanidmUnixdConfig { #[derive(Debug)]
pub struct KanidmConfig {
pub conn_timeout: u64,
pub request_timeout: u64,
pub pam_allowed_login_groups: Vec<String>,
pub extend: Vec<GroupMap>,
}
impl Default for UnixdConfig {
fn default() -> Self { fn default() -> Self {
KanidmUnixdConfig::new() UnixdConfig::new()
} }
} }
impl Display for KanidmUnixdConfig { impl Display for UnixdConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 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, "sock_path: {}", self.sock_path)?;
writeln!(f, "task_sock_path: {}", self.task_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, "unix_sock_timeout: {}", self.unix_sock_timeout)?;
writeln!(f, "cache_timeout: {}", self.cache_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, "default_shell: {}", self.default_shell)?;
writeln!(f, "home_prefix: {:?}", self.home_prefix)?; writeln!(f, "home_prefix: {:?}", self.home_prefix)?;
match self.home_mount_prefix.as_deref() { 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, "tpm_tcti_name: {}", self.tpm_tcti_name)?;
writeln!(f, "selinux: {}", self.selinux)?; writeln!(f, "selinux: {}", self.selinux)?;
writeln!(
f, if let Some(kconfig) = &self.kanidm_config {
"allow_local_account_override: {:#?}", writeln!(f, "kanidm: enabled")?;
self.allow_local_account_override 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 { 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, 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") { let hsm_pin_path = match env::var("KANIDM_HSM_PIN_PATH") {
Ok(val) => val, Ok(val) => val,
Err(_) => DEFAULT_HSM_PIN_PATH.into(), Err(_) => DEFAULT_HSM_PIN_PATH.into(),
}; };
KanidmUnixdConfig { UnixdConfig {
db_path, cache_db_path,
sock_path: DEFAULT_SOCK_PATH.to_string(), sock_path: DEFAULT_SOCK_PATH.to_string(),
task_sock_path: DEFAULT_TASK_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, unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
cache_timeout: DEFAULT_CACHE_TIMEOUT, cache_timeout: DEFAULT_CACHE_TIMEOUT,
pam_allowed_login_groups: Vec::new(),
default_shell: DEFAULT_SHELL.to_string(), default_shell: DEFAULT_SHELL.to_string(),
home_prefix: DEFAULT_HOME_PREFIX.into(), home_prefix: DEFAULT_HOME_PREFIX.into(),
home_mount_prefix: None, home_mount_prefix: None,
@ -163,7 +227,8 @@ impl KanidmUnixdConfig {
hsm_pin_path, hsm_pin_path,
hsm_type: HsmType::default(), hsm_type: HsmType::default(),
tpm_tcti_name: DEFAULT_TPM_TCTI_NAME.to_string(), 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 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); error!("{:?}", e);
UnixIntegrationError 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<Self, UnixIntegrationError> {
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. // Now map the values into our config.
Ok(KanidmUnixdConfig { Ok(UnixdConfig {
db_path: config.db_path.unwrap_or(self.db_path), cache_db_path: config.db_path.unwrap_or(self.cache_db_path),
sock_path: config.sock_path.unwrap_or(self.sock_path), sock_path: config.sock_path.unwrap_or(self.sock_path),
task_sock_path: config.task_sock_path.unwrap_or(self.task_sock_path), task_sock_path: config.task_sock_path.unwrap_or(self.task_sock_path),
conn_timeout, unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
request_timeout: config.request_timeout.unwrap_or(conn_timeout * 2),
unix_sock_timeout: conn_timeout * 2,
cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout), 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), default_shell: config.default_shell.unwrap_or(self.default_shell),
home_prefix: config home_prefix: config
.home_prefix .home_prefix
@ -302,7 +385,105 @@ impl KanidmUnixdConfig {
tpm_tcti_name: config tpm_tcti_name: config
.tpm_tcti_name .tpm_tcti_name
.unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()), .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<Self, UnixIntegrationError> {
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,
}) })
} }
} }

View file

@ -4,6 +4,7 @@ use std::pin::Pin;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use time::OffsetDateTime;
use kanidm_client::{KanidmClient, KanidmClientBuilder}; use kanidm_client::{KanidmClient, KanidmClientBuilder};
use kanidm_proto::constants::ATTR_ACCOUNT_EXPIRE; 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_GID_ATTR_MAP, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX,
DEFAULT_SHELL, DEFAULT_UID_ATTR_MAP, 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::db::{Cache, Db};
use kanidm_unix_resolver::idprovider::interface::Id; use kanidm_unix_resolver::idprovider::interface::Id;
use kanidm_unix_resolver::idprovider::kanidm::KanidmProvider; use kanidm_unix_resolver::idprovider::kanidm::KanidmProvider;
use kanidm_unix_resolver::idprovider::system::SystemProvider; use kanidm_unix_resolver::idprovider::system::SystemProvider;
use kanidm_unix_resolver::resolver::Resolver; use kanidm_unix_resolver::resolver::Resolver;
use kanidm_unix_resolver::unix_config::{GroupMap, KanidmConfig};
use kanidmd_core::config::{Configuration, IntegrationTestConfig, ServerRole}; use kanidmd_core::config::{Configuration, IntegrationTestConfig, ServerRole};
use kanidmd_core::create_server_core; use kanidmd_core::create_server_core;
use kanidmd_testkit::{is_free_port, PORT_ALLOC}; 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( let idprovider = KanidmProvider::new(
rsclient, 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(), SystemTime::now(),
&mut (&mut dbtxn).into(), &mut (&mut dbtxn).into(),
&mut hsm, &mut hsm,
@ -139,10 +150,9 @@ async fn setup_test(fix_fn: Fixture) -> (Resolver, KanidmClient) {
let cachelayer = Resolver::new( let cachelayer = Resolver::new(
db, db,
Arc::new(system_provider), Arc::new(system_provider),
Arc::new(idprovider), vec![Arc::new(idprovider)],
hsm, hsm,
300, 300,
vec!["allowed_group".to_string()],
DEFAULT_SHELL.to_string(), DEFAULT_SHELL.to_string(),
DEFAULT_HOME_PREFIX.into(), DEFAULT_HOME_PREFIX.into(),
DEFAULT_HOME_ATTR, DEFAULT_HOME_ATTR,
@ -446,11 +456,12 @@ async fn test_cache_account_delete() {
#[tokio::test] #[tokio::test]
async fn test_cache_account_password() { async fn test_cache_account_password() {
let current_time = OffsetDateTime::now_utc();
let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await; let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await;
cachelayer.mark_next_check_now(SystemTime::now()).await; cachelayer.mark_next_check_now(SystemTime::now()).await;
// Test authentication failure. // Test authentication failure.
let a1 = cachelayer let a1 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_INC) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_INC)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert_eq!(a1, Some(false)); assert_eq!(a1, Some(false));
@ -460,7 +471,7 @@ async fn test_cache_account_password() {
// Test authentication success. // Test authentication success.
let a2 = cachelayer let a2 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert_eq!(a2, Some(true)); assert_eq!(a2, Some(true));
@ -477,7 +488,7 @@ async fn test_cache_account_password() {
// test auth (old pw) fail // test auth (old pw) fail
let a3 = cachelayer let a3 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert_eq!(a3, Some(false)); assert_eq!(a3, Some(false));
@ -487,7 +498,7 @@ async fn test_cache_account_password() {
// test auth (new pw) success // test auth (new pw) success
let a4 = cachelayer let a4 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert_eq!(a4, Some(true)); assert_eq!(a4, Some(true));
@ -497,7 +508,7 @@ async fn test_cache_account_password() {
// Test auth success // Test auth success
let a5 = cachelayer let a5 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert_eq!(a5, Some(true)); assert_eq!(a5, Some(true));
@ -506,7 +517,7 @@ async fn test_cache_account_password() {
// Test auth failure. // Test auth failure.
let a6 = cachelayer let a6 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_INC) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_INC)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert_eq!(a6, Some(false)); assert_eq!(a6, Some(false));
@ -519,7 +530,7 @@ async fn test_cache_account_password() {
// test auth good (fail) // test auth good (fail)
let a7 = cachelayer let a7 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert!(a7.is_none()); assert!(a7.is_none());
@ -530,7 +541,7 @@ async fn test_cache_account_password() {
// test auth success // test auth success
let a8 = cachelayer let a8 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert_eq!(a8, Some(true)); assert_eq!(a8, Some(true));
@ -570,6 +581,7 @@ async fn test_cache_account_pam_allowed() {
#[tokio::test] #[tokio::test]
async fn test_cache_account_pam_nonexist() { async fn test_cache_account_pam_nonexist() {
let current_time = OffsetDateTime::now_utc();
let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await; let (cachelayer, _adminclient) = setup_test(fixture(test_fixture)).await;
cachelayer.mark_next_check_now(SystemTime::now()).await; cachelayer.mark_next_check_now(SystemTime::now()).await;
@ -580,7 +592,7 @@ async fn test_cache_account_pam_nonexist() {
assert!(a1.is_none()); assert!(a1.is_none());
let a2 = cachelayer let a2 = cachelayer
.pam_account_authenticate("NO_SUCH_ACCOUNT", TESTACCOUNT1_PASSWORD_B) .pam_account_authenticate("NO_SUCH_ACCOUNT", current_time, TESTACCOUNT1_PASSWORD_B)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert!(a2.is_none()); assert!(a2.is_none());
@ -594,7 +606,7 @@ async fn test_cache_account_pam_nonexist() {
assert!(a1.is_none()); assert!(a1.is_none());
let a2 = cachelayer let a2 = cachelayer
.pam_account_authenticate("NO_SUCH_ACCOUNT", TESTACCOUNT1_PASSWORD_B) .pam_account_authenticate("NO_SUCH_ACCOUNT", current_time, TESTACCOUNT1_PASSWORD_B)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert!(a2.is_none()); assert!(a2.is_none());
@ -602,13 +614,14 @@ async fn test_cache_account_pam_nonexist() {
#[tokio::test] #[tokio::test]
async fn test_cache_account_expiry() { async fn test_cache_account_expiry() {
let current_time = OffsetDateTime::now_utc();
let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await; let (cachelayer, adminclient) = setup_test(fixture(test_fixture)).await;
cachelayer.mark_next_check_now(SystemTime::now()).await; cachelayer.mark_next_check_now(SystemTime::now()).await;
assert!(cachelayer.test_connection().await); assert!(cachelayer.test_connection().await);
// We need one good auth first to prime the cache with a hash. // We need one good auth first to prime the cache with a hash.
let a1 = cachelayer let a1 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert_eq!(a1, Some(true)); assert_eq!(a1, Some(true));
@ -626,7 +639,7 @@ async fn test_cache_account_expiry() {
.unwrap(); .unwrap();
// auth will fail // auth will fail
let a2 = cachelayer let a2 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert_eq!(a2, Some(false)); 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 // Now, check again. Since this uses the cached pw and we are offline, this
// will now succeed. // will now succeed.
let a4 = cachelayer let a4 = cachelayer
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A) .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
.await .await
.expect("failed to authenticate"); .expect("failed to authenticate");
assert_eq!(a4, Some(true)); assert_eq!(a4, Some(true));
@ -760,6 +773,7 @@ async fn test_cache_nxset_account() {
homedir: Default::default(), homedir: Default::default(),
shell: Default::default(), shell: Default::default(),
}], }],
None,
vec![], vec![],
) )
.await; .await;
@ -815,6 +829,7 @@ async fn test_cache_nxset_group() {
cachelayer cachelayer
.reload_system_identities( .reload_system_identities(
vec![], vec![],
None,
vec![EtcGroup { vec![EtcGroup {
name: "testgroup1".to_string(), name: "testgroup1".to_string(),
// Important! We set the GID to differ from what kanidm stores so we can // 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); 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 /// 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 /// 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 /// sqlite "INSERT OR REPLACE INTO" triggers a delete cascade of the foreign key elements