Complete the implementation of the posix account cache (#3041)

Allow caching and checking of shadow entries (passwords)
    Cache and serve system id's
    improve some security warnings
    prepare for multi-resolver
    Allow the kanidm provider to be not configured
    Allow group extension
This commit is contained in:
Firstyear 2024-10-02 12:12:13 +10:00 committed by GitHub
parent 90afc8207c
commit cf63c6b98b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1372 additions and 475 deletions

15
Cargo.lock generated
View file

@ -3346,6 +3346,7 @@ dependencies = [
"kanidm_build_profiles",
"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"

View file

@ -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"

View file

@ -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>

View file

@ -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

View file

@ -29,43 +29,38 @@ cp /etc/pam.d/common-session-pc /etc/pam.d/common-session
cp /etc/pam.d/common-password-pc /etc/pam.d/common-password
```
> 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
```

View file

@ -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"]

View file

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

View file

@ -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(),

View file

@ -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"] }

View file

@ -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");

View file

@ -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(),]
}
);
}
}

View file

@ -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>,

View file

@ -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,

View file

@ -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),
}
}
}

View file

@ -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",

View file

@ -20,7 +20,7 @@ use kanidm_unix_common::client::call_daemon;
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
use kanidm_unix_common::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 {

View file

@ -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 {

View file

@ -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);

View file

@ -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,

View file

@ -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))
}
}
}

View file

@ -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 {

View file

@ -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;

View file

@ -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,
})
}
}

View file

@ -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