mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
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:
parent
90afc8207c
commit
cf63c6b98b
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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(),]
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue