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