diff --git a/Cargo.lock b/Cargo.lock index 66b13564e..2071e7929 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2937,7 +2937,7 @@ dependencies = [ [[package]] name = "kanidm_lib_file_permissions" -version = "0.1.0" +version = "1.1.0-rc.14-dev" dependencies = [ "kanidm_utils_users", "whoami", diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index f3ae56f4a..8e678832a 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -16,6 +16,7 @@ - [Administration](administrivia.md) - [Accounts and Groups](accounts_and_groups.md) + - [Account Policy](account_policy.md) - [Authentication and Credentials](authentication.md) - [POSIX Accounts and Groups](posix_accounts.md) - [Backup and Restore](backup_restore.md) diff --git a/book/src/account_policy.md b/book/src/account_policy.md new file mode 100644 index 000000000..15aee3085 --- /dev/null +++ b/book/src/account_policy.md @@ -0,0 +1,86 @@ +# Account Policy + +Account Policy defines the security requirements that accounts must meet and influences users +sessions. + +Policy is defined on groups so that membership of a group influences the security of its members. +This allows you to express that if you can access a system or resource, then the account must also +meet the policy requirements. + +## Default Account Policy + +A default Account Policy is applied to `idm_all_accounts`. This provides the defaults that +influence all accounts in Kanidm. This policy can be modified the same as any other group's policy. + +## Policy Resolution + +When an account is affected by multiple policies, the strictest component from each policy is +applied. This can mean that two policies interact and make their combination stricter than their +parts. + +| value | ordering | +| ---------------- | -------------- | +| auth-session | smallest value | +| privilege-expiry | smallest value | + +### Example Resolution + +If we had two policies where the first defined: + +``` +auth-session: 86400 +privilege-expiry: 600 +``` + +And the second + +``` +auth-session: 3600 +privilege-expiry: 3600 +``` + +As the value of auth-session from the second is smaller we would take that. We would take the +smallest value of privilege-expiry from the first. This leaves: + +``` +auth-session: 3600 +privilege-expiry: 600 +``` + +## Enabling Account Policy + +Account Policy is enabled on a group with the command: + +``` +kanidm group account-policy enable +kanidm group account-policy enable my_admin_group +``` + +## Setting Maximum Session Time + +The auth-session value influences the maximum time in seconds that an authenticated session can +exist. After this time, the user must reauthenticate. + +This value provides a difficult balance - forcing frequent re-authentications can frustrate and +annoy users. However extremely long sessions allow a stolen or disclosed session token/device to +read data for an extended period. Due to Kanidm's read/write separation this mitigates the risk of +disclosed sessions as they can only _read_ data, not write it. + +To set the maximum authentication session time + +``` +kanidm group account-policy auth-expiry +kanidm group account-policy auth-expiry my_admin_group 86400 +``` + +## Setting Maximum Privilege Time + +The privilege-expiry time defines how long a session retains its write privileges after a +reauthentication. After this time, the session returns to read-only mode. + +To set the maximum privilege time + +``` +kanidm group account-policy privilege-expiry +kanidm group account-policy privilege-expiry my_admin_group 900 +``` diff --git a/book/src/sync/ldap.md b/book/src/sync/ldap.md index 6be10cf50..d0bc3671c 100644 --- a/book/src/sync/ldap.md +++ b/book/src/sync/ldap.md @@ -72,8 +72,8 @@ You must modify the retro changelog plugin to include the full scope of the data the sync tool can view the changes to the database. Currently dsconf can not modify the include-suffix so you must do this manually. -You need to change the `nsslapd-include-suffix` to match your LDAP baseDN here. You can access -the basedn with: +You need to change the `nsslapd-include-suffix` to match your LDAP baseDN here. You can access the +basedn with: ```bash ldapsearch -H ldaps:// -x -b '' -s base namingContexts diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index 9eafb91f6..a61e33f75 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -11,6 +11,10 @@ license = { workspace = true } homepage = { workspace = true } repository = { workspace = true } +[lib] +test = true +doctest = false + [dependencies] tracing = { workspace = true } reqwest = { workspace = true, default-features = false, features = [ diff --git a/libs/client/src/group.rs b/libs/client/src/group.rs new file mode 100644 index 000000000..f1b949611 --- /dev/null +++ b/libs/client/src/group.rs @@ -0,0 +1,35 @@ +use crate::{ClientError, KanidmClient}; + +impl KanidmClient { + pub async fn group_account_policy_enable(&self, id: &str) -> Result<(), ClientError> { + self.perform_post_request( + &format!("/v1/group/{}/_attr/class", id), + vec!["account_policy".to_string()], + ) + .await + } + + pub async fn group_account_policy_authsession_expiry_set( + &self, + id: &str, + expiry: u32, + ) -> Result<(), ClientError> { + self.perform_put_request( + &format!("/v1/group/{}/_attr/authsession_expiry", id), + vec![expiry.to_string()], + ) + .await + } + + pub async fn group_account_policy_privilege_expiry_set( + &self, + id: &str, + expiry: u32, + ) -> Result<(), ClientError> { + self.perform_put_request( + &format!("/v1/group/{}/_attr/privilege_expiry", id), + vec![expiry.to_string()], + ) + .await + } +} diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index cd1bf98a8..239953def 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -40,6 +40,7 @@ use webauthn_rs_proto::{ PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse, }; +mod group; mod oauth; mod person; mod scim; diff --git a/libs/client/src/system.rs b/libs/client/src/system.rs index 53c4a5e8e..523d1906d 100644 --- a/libs/client/src/system.rs +++ b/libs/client/src/system.rs @@ -40,40 +40,4 @@ impl KanidmClient { self.perform_delete_request_with_body("/v1/system/_attr/denied_name", list) .await } - - pub async fn system_authsession_expiry_get(&self) -> Result { - let list: Option<[String; 1]> = self - .perform_get_request("/v1/system/_attr/authsession_expiry") - .await?; - list.ok_or(ClientError::EmptyResponse).and_then(|s| { - s[0].parse::() - .map_err(|err| ClientError::InvalidResponseFormat(err.to_string())) - }) - } - - pub async fn system_authsession_expiry_set(&self, expiry: u32) -> Result<(), ClientError> { - self.perform_put_request( - "/v1/system/_attr/authsession_expiry", - vec![expiry.to_string()], - ) - .await - } - - pub async fn system_auth_privilege_expiry_get(&self) -> Result { - let list: Option<[String; 1]> = self - .perform_get_request("/v1/system/_attr/privilege_expiry") - .await?; - list.ok_or(ClientError::EmptyResponse).and_then(|s| { - s[0].parse::() - .map_err(|err| ClientError::InvalidResponseFormat(err.to_string())) - }) - } - - pub async fn system_auth_privilege_expiry_set(&self, expiry: u32) -> Result<(), ClientError> { - self.perform_put_request( - "/v1/system/_attr/privilege_expiry", - vec![expiry.to_string()], - ) - .await - } } diff --git a/libs/crypto/Cargo.toml b/libs/crypto/Cargo.toml index df3df00fb..079dd1b54 100644 --- a/libs/crypto/Cargo.toml +++ b/libs/crypto/Cargo.toml @@ -6,6 +6,10 @@ edition = "2021" [features] tpm = ["dep:tss-esapi"] +[lib] +test = true +doctest = false + [dependencies] argon2 = { workspace = true } base64 = { workspace = true } diff --git a/libs/crypto/src/lib.rs b/libs/crypto/src/lib.rs index e816298aa..9ca2605e0 100644 --- a/libs/crypto/src/lib.rs +++ b/libs/crypto/src/lib.rs @@ -84,11 +84,11 @@ impl From for CryptoError { fn from(ossl_err: OpenSSLErrorStack) -> Self { error!(?ossl_err); let code = ossl_err.errors().get(0).map(|e| e.code()).unwrap_or(0); - #[cfg(not(target_family="windows"))] + #[cfg(not(target_family = "windows"))] let result = CryptoError::OpenSSL(code); // this is an .into() because on windows it's a u32 not a u64 - #[cfg(target_family="windows")] + #[cfg(target_family = "windows")] let result = CryptoError::OpenSSL(code.into()); result diff --git a/libs/file_permissions/Cargo.toml b/libs/file_permissions/Cargo.toml index e88efbdde..97d73e817 100644 --- a/libs/file_permissions/Cargo.toml +++ b/libs/file_permissions/Cargo.toml @@ -1,9 +1,18 @@ [package] name = "kanidm_lib_file_permissions" -version = "0.1.0" -edition = "2021" +description = "Kanidm File Permissions Library" +# documentation = "https://docs.rs/kanidm_proto/latest/kanidm_proto/" +version = { workspace = true } +authors = { workspace = true } +rust-version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +test = true +doctest = false [dependencies] diff --git a/libs/file_permissions/src/windows.rs b/libs/file_permissions/src/windows.rs index 6abf74552..a8c546611 100644 --- a/libs/file_permissions/src/windows.rs +++ b/libs/file_permissions/src/windows.rs @@ -1,5 +1,5 @@ use core::fmt; -use std::{path::Path, fs::Metadata}; +use std::{fs::Metadata, path::Path}; /// Check a given file's metadata is read-only for the current user (true = read-only) Stub function if you're building for windows! pub fn readonly(meta: &Metadata) -> bool { eprintln!( diff --git a/libs/profiles/Cargo.toml b/libs/profiles/Cargo.toml index 1c9005477..99555b59a 100644 --- a/libs/profiles/Cargo.toml +++ b/libs/profiles/Cargo.toml @@ -16,6 +16,8 @@ repository = { workspace = true } [lib] name = "profiles" path = "src/lib.rs" +test = false +doctest = false [dependencies] serde = { workspace = true, features = ["derive"] } diff --git a/libs/sketching/Cargo.toml b/libs/sketching/Cargo.toml index 876ebca96..d28630e3a 100644 --- a/libs/sketching/Cargo.toml +++ b/libs/sketching/Cargo.toml @@ -11,6 +11,10 @@ license = { workspace = true } homepage = { workspace = true } repository = { workspace = true } +[lib] +test = false +doctest = false + [dependencies] num_enum = { workspace = true } tracing = { workspace = true, features = ["attributes"] } diff --git a/libs/users/Cargo.toml b/libs/users/Cargo.toml index b29903977..c9ff754cf 100644 --- a/libs/users/Cargo.toml +++ b/libs/users/Cargo.toml @@ -8,5 +8,9 @@ license.workspace = true homepage.workspace = true repository.workspace = true +[lib] +test = true +doctest = false + [dependencies] libc = { workspace = true } diff --git a/libs/users/src/lib.rs b/libs/users/src/lib.rs index dee8cc522..c938f1889 100644 --- a/libs/users/src/lib.rs +++ b/libs/users/src/lib.rs @@ -1,4 +1,4 @@ #[cfg(target_family = "unix")] pub mod unix; #[cfg(target_family = "unix")] -pub use unix::*; \ No newline at end of file +pub use unix::*; diff --git a/libs/users/src/unix.rs b/libs/users/src/unix.rs index f473397a6..b2cea646f 100644 --- a/libs/users/src/unix.rs +++ b/libs/users/src/unix.rs @@ -58,7 +58,6 @@ pub fn get_user_name_by_uid(uid: uid_t) -> Option { Some(name) } - #[test] /// just testing these literally don't panic fn test_get_effective_uid() { @@ -69,4 +68,4 @@ fn test_get_effective_uid() { let username = get_user_name_by_uid(get_current_uid()); assert!(username.is_some()); -} \ No newline at end of file +} diff --git a/proto/Cargo.toml b/proto/Cargo.toml index c3322e99b..13993b46c 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -11,6 +11,10 @@ license = { workspace = true } homepage = { workspace = true } repository = { workspace = true } +[lib] +test = true +doctest = true + [features] wasm = ["webauthn-rs-proto/wasm"] diff --git a/server/core/Cargo.toml b/server/core/Cargo.toml index 34a6168f0..a110c3f98 100644 --- a/server/core/Cargo.toml +++ b/server/core/Cargo.toml @@ -11,6 +11,10 @@ license = { workspace = true } homepage = { workspace = true } repository = { workspace = true } +[lib] +test = true +doctest = false + [dependencies] async-trait = { workspace = true } axum = { workspace = true } diff --git a/server/daemon/Cargo.toml b/server/daemon/Cargo.toml index fb73eff34..feaa88e99 100644 --- a/server/daemon/Cargo.toml +++ b/server/daemon/Cargo.toml @@ -16,6 +16,8 @@ repository = { workspace = true } [[bin]] name = "kanidmd" path = "src/main.rs" +test = true +doctest = false [dependencies] kanidm_proto = { workspace = true } diff --git a/server/lib-macros/Cargo.toml b/server/lib-macros/Cargo.toml index 517149937..b76a5c1ca 100644 --- a/server/lib-macros/Cargo.toml +++ b/server/lib-macros/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" [lib] proc-macro = true +test = true +doctest = false [dependencies] proc-macro2 = { workspace = true } diff --git a/server/lib/Cargo.toml b/server/lib/Cargo.toml index 0e65db9a4..f4fbee2d7 100644 --- a/server/lib/Cargo.toml +++ b/server/lib/Cargo.toml @@ -14,6 +14,8 @@ repository = { workspace = true } [lib] name = "kanidmd_lib" path = "src/lib.rs" +test = true +doctest = false [[bench]] name = "scaling_10k" diff --git a/server/lib/src/constants/acp.rs b/server/lib/src/constants/acp.rs index 645a6011e..6a9ea957c 100644 --- a/server/lib/src/constants/acp.rs +++ b/server/lib/src/constants/acp.rs @@ -1212,7 +1212,6 @@ lazy_static! { ..Default::default() }; - pub static ref IDM_ACP_GROUP_MANAGE_PRIV_V1: BuiltinAcp = BuiltinAcp{ classes: vec![ EntryClass::Object, @@ -1246,6 +1245,56 @@ lazy_static! { ], ..Default::default() }; + + pub static ref IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_PRIV_V1: BuiltinAcp = BuiltinAcp{ + classes: vec![ + EntryClass::Object, + EntryClass::AccessControlProfile, + EntryClass::AccessControlCreate, + EntryClass::AccessControlModify, + EntryClass::AccessControlSearch + ], + name: "idm_acp_group_account_policy_manage", + uuid: UUID_IDM_GROUP_ACCOUNT_POLICY_MANAGE_PRIV, + description: "Builtin IDM Control for management of account policy on groups", + // For now just target SA because we are going to rework this soon and I think + // there isn't a great reason to make more small priv groups that we plan to + // erase. + receiver_group: UUID_SYSTEM_ADMINS, + // group which is not in HP, Recycled, Tombstone + target_scope: ProtoFilter::And(vec![ + match_class_filter!(EntryClass::Group), + ProtoFilter::AndNot(Box::new(FILTER_HP_OR_RECYCLED_OR_TOMBSTONE.clone())), + + ]), + search_attrs: vec![ + Attribute::Class, + Attribute::Name, + Attribute::Uuid, + Attribute::AuthSessionExpiry, + Attribute::PrivilegeExpiry, + ], + modify_removed_attrs: vec![ + Attribute::Class, + Attribute::AuthSessionExpiry, + Attribute::PrivilegeExpiry, + ], + modify_present_attrs: vec![ + Attribute::Class, + Attribute::AuthSessionExpiry, + Attribute::PrivilegeExpiry, + ], + modify_classes: vec![ + EntryClass::AccountPolicy, + ], + create_attrs: vec![ + Attribute::Class, + ], + create_classes: vec![ + EntryClass::AccountPolicy, + ], + ..Default::default() + }; } lazy_static! { diff --git a/server/lib/src/constants/entries.rs b/server/lib/src/constants/entries.rs index 295b025f5..742a3a2f4 100644 --- a/server/lib/src/constants/entries.rs +++ b/server/lib/src/constants/entries.rs @@ -551,6 +551,7 @@ pub enum EntryClass { AccessControlProfile, AccessControlSearch, Account, + AccountPolicy, AttributeType, Class, ClassType, @@ -591,6 +592,7 @@ impl From for &'static str { EntryClass::AccessControlProfile => "access_control_profile", EntryClass::AccessControlSearch => "access_control_search", EntryClass::Account => "account", + EntryClass::AccountPolicy => "account_policy", EntryClass::AttributeType => "attributetype", EntryClass::Class => ATTR_CLASS, EntryClass::ClassType => "classtype", @@ -704,7 +706,7 @@ lazy_static! { Attribute::Description, Value::new_utf8s("System (local) info and metadata object.") ), - (Attribute::Version, Value::Uint32(14)) + (Attribute::Version, Value::Uint32(16)) ); pub static ref E_DOMAIN_INFO_V1: EntryInitNew = entry_init!( diff --git a/server/lib/src/constants/groups.rs b/server/lib/src/constants/groups.rs index 3360aeffa..07dd4bd0b 100644 --- a/server/lib/src/constants/groups.rs +++ b/server/lib/src/constants/groups.rs @@ -410,7 +410,6 @@ lazy_static! { }; - /// Builtin IDM Group for allowing migrations of service accounts into persons pub static ref IDM_HP_SYNC_ACCOUNT_MANAGE_PRIV: BuiltinGroup = BuiltinGroup { name: "idm_hp_sync_account_manage_priv", description: "Builtin IDM Group for managing synchronisation from external identity sources", @@ -421,7 +420,6 @@ lazy_static! { ..Default::default() }; - /// Builtin IDM Group for extending high privilege accounts to be people. pub static ref IDM_ALL_PERSONS: BuiltinGroup = BuiltinGroup { name: "idm_all_persons", description: "Builtin IDM Group for extending high privilege accounts to be people.", @@ -434,10 +432,15 @@ lazy_static! { Filter::Eq(Attribute::Class.to_string(), EntryClass::Account.to_string()), ]) ), + extra_attributes: vec![ + // Enable account policy by default + (Attribute::Class, EntryClass::AccountPolicy.to_value()), + // Enforce this is a system protected object + (Attribute::Class, EntryClass::System.to_value()), + ], ..Default::default() }; - /// Builtin IDM Group for extending high privilege accounts to be people. pub static ref IDM_ALL_ACCOUNTS: BuiltinGroup = BuiltinGroup { name: "idm_all_accounts", description: "Builtin IDM dynamic group containing all entries that can authenticate.", @@ -447,6 +450,12 @@ lazy_static! { dyngroup_filter: Some( Filter::Eq(Attribute::Class.to_string(), EntryClass::Account.to_string()), ), + extra_attributes: vec![ + // Enable account policy by default + (Attribute::Class, EntryClass::AccountPolicy.to_value()), + // Enforce this is a system protected object + (Attribute::Class, EntryClass::System.to_value()), + ], ..Default::default() }; diff --git a/server/lib/src/constants/mod.rs b/server/lib/src/constants/mod.rs index d5cba292f..70c61aaba 100644 --- a/server/lib/src/constants/mod.rs +++ b/server/lib/src/constants/mod.rs @@ -78,9 +78,13 @@ pub const AUTH_SESSION_TIMEOUT: u64 = 300; pub const MFAREG_SESSION_TIMEOUT: u64 = 300; pub const PW_MIN_LENGTH: usize = 10; -// Default - sessions last for 1 hour. +// Maximum - Sessions have no upper bound. +pub const MAXIMUM_AUTH_SESSION_EXPIRY: u32 = u32::MAX; +// Default - sessions last for 1 day pub const DEFAULT_AUTH_SESSION_EXPIRY: u32 = 86400; pub const DEFAULT_AUTH_SESSION_LIMITED_EXPIRY: u32 = 3600; +// Maximum - privileges last for 1 hour. +pub const MAXIMUM_AUTH_PRIVILEGE_EXPIRY: u32 = 3600; // Default - privileges last for 10 minutes. pub const DEFAULT_AUTH_PRIVILEGE_EXPIRY: u32 = 600; // Default - oauth refresh tokens last for 16 hours. diff --git a/server/lib/src/constants/schema.rs b/server/lib/src/constants/schema.rs index 3e855b510..c0a2b4104 100644 --- a/server/lib/src/constants/schema.rs +++ b/server/lib/src/constants/schema.rs @@ -621,6 +621,18 @@ pub static ref SCHEMA_CLASS_DYNGROUP: SchemaClass = SchemaClass { ..Default::default() }; +pub static ref SCHEMA_CLASS_ACCOUNT_POLICY: SchemaClass = SchemaClass { + uuid: UUID_SCHEMA_CLASS_ACCOUNT_POLICY, + name: EntryClass::AccountPolicy.into(), + description: "Policies applied to accounts that are members of a group".to_string(), + systemmay: vec![ + Attribute::AuthSessionExpiry.into(), + Attribute::PrivilegeExpiry.into() + ], + systemsupplements: vec![Attribute::Group.into()], + ..Default::default() +}; + pub static ref SCHEMA_CLASS_ACCOUNT: SchemaClass = SchemaClass { uuid: UUID_SCHEMA_CLASS_ACCOUNT, name: EntryClass::Account.into(), diff --git a/server/lib/src/constants/system_config.rs b/server/lib/src/constants/system_config.rs index 1b6fec38f..253d772e0 100644 --- a/server/lib/src/constants/system_config.rs +++ b/server/lib/src/constants/system_config.rs @@ -2,7 +2,6 @@ use crate::constants::uuids::*; use crate::entry::{Entry, EntryInit, EntryInitNew, EntryNew}; use crate::prelude::{Attribute, EntryClass}; -use crate::prelude::{DEFAULT_AUTH_PRIVILEGE_EXPIRY, DEFAULT_AUTH_SESSION_EXPIRY}; use crate::value::Value; // Default entries for system_config @@ -28,14 +27,6 @@ lazy_static! { "demo_badlist_shohfie3aeci2oobur0aru9uushah6EiPi2woh4hohngoighaiRuepieN3ongoo1" ) ), - ( - Attribute::AuthSessionExpiry, - Value::Uint32(DEFAULT_AUTH_SESSION_EXPIRY) - ), - ( - Attribute::PrivilegeExpiry, - Value::Uint32(DEFAULT_AUTH_PRIVILEGE_EXPIRY) - ), ( Attribute::BadlistPassword, Value::new_iutf8("100preteamare") diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs index 6714c4622..8e3271eaa 100644 --- a/server/lib/src/constants/uuids.rs +++ b/server/lib/src/constants/uuids.rs @@ -57,6 +57,8 @@ pub const UUID_IDM_HP_SYNC_ACCOUNT_MANAGE_PRIV: Uuid = pub const UUID_IDM_UI_ENABLE_EXPERIMENTAL_FEATURES: Uuid = uuid!("00000000-0000-0000-0000-000000000038"); pub const UUID_IDM_ACCOUNT_MAIL_READ_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000039"); +pub const UUID_IDM_GROUP_ACCOUNT_POLICY_MANAGE_PRIV: Uuid = + uuid!("00000000-0000-0000-0000-000000000040"); // pub const UUID_IDM_HIGH_PRIVILEGE: Uuid = uuid!("00000000-0000-0000-0000-000000001000"); @@ -240,6 +242,9 @@ pub const UUID_SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY: Uuid = pub const UUID_SCHEMA_ATTR_IMAGE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000143"); pub const UUID_SCHEMA_ATTR_DENIED_NAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000144"); +// Leave 145 for ldap unix pw bind +pub const UUID_SCHEMA_CLASS_ACCOUNT_POLICY: Uuid = uuid!("00000000-0000-0000-0000-ffff00000146"); + // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. pub const UUID_SYSTEM_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000001"); diff --git a/server/lib/src/idm/account.rs b/server/lib/src/idm/account.rs index 51376dcce..9b2cf62bd 100644 --- a/server/lib/src/idm/account.rs +++ b/server/lib/src/idm/account.rs @@ -11,6 +11,7 @@ use webauthn_rs::prelude::{ AttestedPasskey as DeviceKeyV4, AuthenticationResult, CredentialID, Passkey as PasskeyV4, }; +use super::accountpolicy::ResolvedAccountPolicy; use crate::constants::UUID_ANONYMOUS; use crate::credential::softlock::CredSoftLockPolicy; use crate::credential::Credential; @@ -229,16 +230,28 @@ impl Account { value: &Entry, qs: &mut QueryServerReadTransaction, ) -> Result { - let groups = Group::try_from_account_entry_ro(value, qs)?; + let groups = Group::try_from_account_entry(value, qs)?; try_from_entry!(value, groups) } + #[instrument(level = "trace", skip_all)] + pub(crate) fn try_from_entry_with_policy<'a, TXN>( + value: &Entry, + qs: &mut TXN, + ) -> Result<(Self, ResolvedAccountPolicy), OperationError> + where + TXN: QueryServerTransaction<'a>, + { + let (groups, rap) = Group::try_from_account_entry_with_policy(value, qs)?; + try_from_entry!(value, groups).map(|acct| (acct, rap)) + } + #[instrument(level = "trace", skip_all)] pub(crate) fn try_from_entry_rw( value: &Entry, qs: &mut QueryServerWriteTransaction, ) -> Result { - let groups = Group::try_from_account_entry_rw(value, qs)?; + let groups = Group::try_from_account_entry(value, qs)?; try_from_entry!(value, groups) } @@ -247,16 +260,10 @@ impl Account { value: &Entry, qs: &mut QueryServerReadTransaction, ) -> Result { - let groups = Group::try_from_account_entry_red_ro(value, qs)?; + let groups = Group::try_from_account_entry_reduced(value, qs)?; try_from_entry!(value, groups) } - pub(crate) fn try_from_entry_no_groups( - value: &Entry, - ) -> Result { - try_from_entry!(value, vec![]) - } - /// Given the session_id and other metadata, create a user authentication token /// that represents a users session. Since this metadata can vary from session /// to session, this userauthtoken may contain some data (claims) that may yield diff --git a/server/lib/src/idm/accountpolicy.rs b/server/lib/src/idm/accountpolicy.rs new file mode 100644 index 000000000..010c58b5e --- /dev/null +++ b/server/lib/src/idm/accountpolicy.rs @@ -0,0 +1,153 @@ +use crate::prelude::*; +// use crate::idm::server::IdmServerProxyWriteTransaction; + +#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Default)] +#[repr(u32)] +pub(crate) enum CredentialPolicy { + #[default] + NoPolicy = 0, + MfaRequired = 10, + PasskeyRequired = 20, + AttestedPasskeyRequired = 30, + AttestedResidentKeyRequired = 40, +} + +impl From for CredentialPolicy { + fn from(value: u32) -> Self { + if value >= CredentialPolicy::AttestedResidentKeyRequired as u32 { + CredentialPolicy::AttestedResidentKeyRequired + } else if value >= CredentialPolicy::AttestedPasskeyRequired as u32 { + CredentialPolicy::AttestedPasskeyRequired + } else if value >= CredentialPolicy::PasskeyRequired as u32 { + CredentialPolicy::PasskeyRequired + } else if value >= CredentialPolicy::MfaRequired as u32 { + CredentialPolicy::MfaRequired + } else { + CredentialPolicy::NoPolicy + } + } +} + +#[derive(Clone)] +pub(crate) struct AccountPolicy { + privilege_expiry: u32, + authsession_expiry: u32, + credential_policy: CredentialPolicy, +} + +impl Into> for &EntrySealedCommitted { + fn into(self) -> Option { + if !self.attribute_equality( + Attribute::Class, + &EntryClass::AccountPolicy.to_partialvalue(), + ) { + return None; + } + + let authsession_expiry = self + .get_ava_single_uint32(Attribute::AuthSessionExpiry) + .unwrap_or(MAXIMUM_AUTH_SESSION_EXPIRY); + let privilege_expiry = self + .get_ava_single_uint32(Attribute::PrivilegeExpiry) + .unwrap_or(MAXIMUM_AUTH_PRIVILEGE_EXPIRY); + let credential_policy = CredentialPolicy::default(); + + Some(AccountPolicy { + privilege_expiry, + authsession_expiry, + credential_policy, + }) + } +} + +#[derive(Clone)] +#[cfg_attr(test, derive(Default))] +pub(crate) struct ResolvedAccountPolicy { + privilege_expiry: u32, + authsession_expiry: u32, + credential_policy: CredentialPolicy, +} + +impl ResolvedAccountPolicy { + pub(crate) fn fold_from(iter: I) -> Self + where + I: Iterator, + { + // Start with our maximums + let mut accumulate = ResolvedAccountPolicy { + privilege_expiry: MAXIMUM_AUTH_PRIVILEGE_EXPIRY, + authsession_expiry: MAXIMUM_AUTH_SESSION_EXPIRY, + credential_policy: CredentialPolicy::default(), + }; + + iter.for_each(|acc_pol| { + // Take the smaller expiry + if acc_pol.privilege_expiry < accumulate.privilege_expiry { + accumulate.privilege_expiry = acc_pol.privilege_expiry + } + + // Take the smaller expiry + if acc_pol.authsession_expiry < accumulate.authsession_expiry { + accumulate.authsession_expiry = acc_pol.authsession_expiry + } + + // Take the greater credential type policy + if acc_pol.credential_policy > accumulate.credential_policy { + accumulate.credential_policy = acc_pol.credential_policy + } + }); + + accumulate + } + + pub(crate) fn privilege_expiry(&self) -> u32 { + self.privilege_expiry + } + + pub(crate) fn authsession_expiry(&self) -> u32 { + self.authsession_expiry + } + + /* + pub(crate) fn credential_policy(&self) -> CredentialPolicy { + self.credential_policy + } + */ +} + +#[cfg(test)] +mod tests { + use super::{AccountPolicy, CredentialPolicy, ResolvedAccountPolicy}; + // use crate::prelude::*; + + #[test] + fn test_idm_account_policy_resolve() { + let policy_a = AccountPolicy { + privilege_expiry: 100, + authsession_expiry: 100, + credential_policy: CredentialPolicy::MfaRequired, + }; + + let policy_b = AccountPolicy { + privilege_expiry: 150, + authsession_expiry: 50, + credential_policy: CredentialPolicy::PasskeyRequired, + }; + + let rap = ResolvedAccountPolicy::fold_from([policy_a, policy_b].into_iter()); + + assert_eq!(rap.privilege_expiry(), 100); + assert_eq!(rap.authsession_expiry(), 50); + assert_eq!(rap.credential_policy, CredentialPolicy::PasskeyRequired); + } + + /* + #[idm_test] + async fn test_idm_account_policy_load( + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed, + ) { + todo!(); + } + */ +} diff --git a/server/lib/src/idm/authsession.rs b/server/lib/src/idm/authsession.rs index 56c5ce58a..d700dd1c4 100644 --- a/server/lib/src/idm/authsession.rs +++ b/server/lib/src/idm/authsession.rs @@ -36,7 +36,7 @@ use crate::prelude::*; use crate::value::{Session, SessionState}; use time::OffsetDateTime; -use super::server::AccountPolicy; +use super::accountpolicy::ResolvedAccountPolicy; // Each CredHandler takes one or more credentials and determines if the // handlers requirements can be 100% fulfilled. This is where MFA or other @@ -721,7 +721,7 @@ pub(crate) struct AuthSession { // How do we know what claims to add? account: Account, // This policies that apply to this account - account_policy: AccountPolicy, + account_policy: ResolvedAccountPolicy, // Store how we plan to handle this sessions authentication: this is generally // made apparent by the presentation of an application id or not. If none is presented @@ -747,7 +747,7 @@ impl AuthSession { /// or interleved write operations do not cause inconsistency in this process. pub fn new( account: Account, - account_policy: AccountPolicy, + account_policy: ResolvedAccountPolicy, issue: AuthIssueSession, privileged: bool, webauthn: &Webauthn, @@ -828,7 +828,7 @@ impl AuthSession { /// initial authentication. pub(crate) fn new_reauth( account: Account, - account_policy: AccountPolicy, + account_policy: ResolvedAccountPolicy, session_id: Uuid, session: &Session, cred_id: Uuid, @@ -1257,13 +1257,13 @@ mod tests { use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP}; use crate::credential::{BackupCodes, Credential}; use crate::idm::account::Account; + use crate::idm::accountpolicy::ResolvedAccountPolicy; use crate::idm::audit::AuditEvent; use crate::idm::authsession::{ AuthSession, BAD_AUTH_TYPE_MSG, BAD_BACKUPCODE_MSG, BAD_PASSWORD_MSG, BAD_TOTP_MSG, BAD_WEBAUTHN_MSG, PW_BADLIST_MSG, }; use crate::idm::delayed::DelayedAction; - use crate::idm::server::AccountPolicy; use crate::idm::AuthState; use crate::prelude::*; use crate::utils::readable_password_from_random; @@ -1298,7 +1298,7 @@ mod tests { let (session, state) = AuthSession::new( anon_account, - AccountPolicy::default(), + ResolvedAccountPolicy::default(), AuthIssueSession::Token, false, &webauthn, @@ -1335,7 +1335,7 @@ mod tests { ) => {{ let (session, state) = AuthSession::new( $account.clone(), - AccountPolicy::default(), + ResolvedAccountPolicy::default(), AuthIssueSession::Token, $privileged, $webauthn, @@ -1516,7 +1516,7 @@ mod tests { ) => {{ let (session, state) = AuthSession::new( $account.clone(), - AccountPolicy::default(), + ResolvedAccountPolicy::default(), AuthIssueSession::Token, false, $webauthn, @@ -1844,7 +1844,7 @@ mod tests { ) => {{ let (session, state) = AuthSession::new( $account.clone(), - AccountPolicy::default(), + ResolvedAccountPolicy::default(), AuthIssueSession::Token, false, $webauthn, diff --git a/server/lib/src/idm/group.rs b/server/lib/src/idm/group.rs index ae0165937..e3403ed7f 100644 --- a/server/lib/src/idm/group.rs +++ b/server/lib/src/idm/group.rs @@ -4,6 +4,7 @@ use kanidm_proto::v1::UiHint; use kanidm_proto::v1::{Group as ProtoGroup, OperationError}; use uuid::Uuid; +use super::accountpolicy::{AccountPolicy, ResolvedAccountPolicy}; use crate::entry::{Entry, EntryCommitted, EntryReduced, EntrySealed}; use crate::prelude::*; use crate::value::PartialValue; @@ -16,17 +17,32 @@ pub struct Group { pub ui_hints: BTreeSet, } -macro_rules! try_from_account_e { +macro_rules! entry_groups { ($value:expr, $qs:expr) => {{ - /* - let name = $value - .get_ava_single_iname(Attribute::Name) - .map(str::to_string) - .ok_or_else(|| { - OperationError::InvalidAccountState("Missing attribute: name".to_string()) - })?; - */ + match $value.get_ava_as_refuuid(Attribute::MemberOf) { + Some(riter) => { + // given a list of uuid, make a filter: even if this is empty, the be will + // just give and empty result set. + let f = filter!(f_or( + riter + .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u))) + .collect() + )); + $qs.internal_search(f).map_err(|e| { + admin_error!(?e, "internal search failed"); + e + })? + } + None => { + // No memberof, no groups! + vec![] + } + } + }}; +} +macro_rules! upg_from_account_e { + ($value:expr, $groups:expr) => {{ // Setup the user private group let spn = $value.get_ava_single_proto_string(Attribute::Spn).ok_or( OperationError::InvalidAccountState(format!("Missing attribute: {}", Attribute::Spn)), @@ -43,60 +59,61 @@ macro_rules! try_from_account_e { ui_hints, }; - let mut groups: Vec = match $value.get_ava_as_refuuid(Attribute::MemberOf) { - Some(riter) => { - // given a list of uuid, make a filter: even if this is empty, the be will - // just give and empty result set. - let f = filter!(f_or( - riter - .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u))) - .collect() - )); - let group_entries: Vec<_> = $qs.internal_search(f).map_err(|e| { - admin_error!(?e, "internal search failed"); - e - })?; - // Now convert the group entries to groups. - let groups: Result, _> = group_entries - .iter() - .map(|e| Group::try_from_entry(e.as_ref())) - .collect(); + // Now convert the group entries to groups. + let groups: Result, _> = $groups + .iter() + .map(|e| Group::try_from_entry(e.as_ref())) + .chain(std::iter::once(Ok(upg))) + .collect(); - groups.map_err(|e| { - admin_error!(?e, "failed to transform group entries to groups"); - e - })? - } - None => { - // No memberof, no groups! - vec![] - } - }; - groups.push(upg); - Ok(groups) + groups.map_err(|e| { + error!(?e, "failed to transform group entries to groups"); + e + }) }}; } impl Group { - pub fn try_from_account_entry_red_ro( + pub fn try_from_account_entry_reduced<'a, TXN>( value: &Entry, - qs: &mut QueryServerReadTransaction, - ) -> Result, OperationError> { - try_from_account_e!(value, qs) + qs: &mut TXN, + ) -> Result, OperationError> + where + TXN: QueryServerTransaction<'a>, + { + let groups = entry_groups!(value, qs); + upg_from_account_e!(value, groups) } - pub fn try_from_account_entry_ro( + pub fn try_from_account_entry<'a, TXN>( value: &Entry, - qs: &mut QueryServerReadTransaction, - ) -> Result, OperationError> { - try_from_account_e!(value, qs) + qs: &mut TXN, + ) -> Result, OperationError> + where + TXN: QueryServerTransaction<'a>, + { + let groups = entry_groups!(value, qs); + upg_from_account_e!(value, groups) } - pub fn try_from_account_entry_rw( - value: &Entry, - qs: &mut QueryServerWriteTransaction, - ) -> Result, OperationError> { - try_from_account_e!(value, qs) + pub(crate) fn try_from_account_entry_with_policy<'b, 'a, TXN>( + value: &'b Entry, + qs: &mut TXN, + ) -> Result<(Vec, ResolvedAccountPolicy), OperationError> + where + TXN: QueryServerTransaction<'a>, + { + let groups = entry_groups!(value, qs); + // Get the account policy here. + + let rap = ResolvedAccountPolicy::fold_from(groups.iter().filter_map(|entry| { + let acc_pol: Option = entry.as_ref().into(); + acc_pol + })); + + let r_groups = upg_from_account_e!(value, groups)?; + + Ok((r_groups, rap)) } pub fn try_from_entry( diff --git a/server/lib/src/idm/mod.rs b/server/lib/src/idm/mod.rs index 35c10d219..058e1effe 100644 --- a/server/lib/src/idm/mod.rs +++ b/server/lib/src/idm/mod.rs @@ -4,6 +4,7 @@ //! is implemented. pub mod account; +pub(crate) mod accountpolicy; pub(crate) mod applinks; pub mod audit; pub(crate) mod authsession; diff --git a/server/lib/src/idm/oauth2.rs b/server/lib/src/idm/oauth2.rs index 2aa1c6e5d..4a2a1bd2f 100644 --- a/server/lib/src/idm/oauth2.rs +++ b/server/lib/src/idm/oauth2.rs @@ -1641,7 +1641,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> { return Ok(AccessTokenIntrospectResponse::inactive()); }; - let account = match Account::try_from_entry_no_groups(&entry) { + let account = match Account::try_from_entry_ro(&entry, &mut self.qs_read) { Ok(account) => account, Err(err) => return Err(Oauth2Error::ServerError(err)), }; diff --git a/server/lib/src/idm/radius.rs b/server/lib/src/idm/radius.rs index b5f1060f0..edf983d8e 100644 --- a/server/lib/src/idm/radius.rs +++ b/server/lib/src/idm/radius.rs @@ -62,7 +62,7 @@ impl RadiusAccount { )) })?; - let groups = Group::try_from_account_entry_red_ro(value, qs)?; + let groups = Group::try_from_account_entry_reduced(value, qs)?; let valid_from = value.get_ava_single_datetime(Attribute::AccountValidFrom); diff --git a/server/lib/src/idm/reauth.rs b/server/lib/src/idm/reauth.rs index 88741bd34..47200e128 100644 --- a/server/lib/src/idm/reauth.rs +++ b/server/lib/src/idm/reauth.rs @@ -33,9 +33,8 @@ impl<'a> IdmServerAuthTransaction<'a> { }; // Setup the account record. - let account = Account::try_from_entry_ro(entry.as_ref(), &mut self.qs_read)?; - - let account_policy = (*self.account_policy).clone(); + let (account, account_policy) = + Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_read)?; security_info!( username = %account.name, diff --git a/server/lib/src/idm/server.rs b/server/lib/src/idm/server.rs index 22b4a0245..7cd763d1d 100644 --- a/server/lib/src/idm/server.rs +++ b/server/lib/src/idm/server.rs @@ -68,38 +68,6 @@ pub struct DomainKeys { pub(crate) cookie_key: [u8; 64], } -#[derive(Clone)] -pub(crate) struct AccountPolicy { - privilege_expiry: u32, - authsession_expiry: u32, -} - -impl AccountPolicy { - pub(crate) fn new(privilege_expiry: u32, authsession_expiry: u32) -> Self { - Self { - privilege_expiry, - authsession_expiry, - } - } - - pub(crate) fn privilege_expiry(&self) -> u32 { - self.privilege_expiry - } - - pub(crate) fn authsession_expiry(&self) -> u32 { - self.authsession_expiry - } -} - -impl Default for AccountPolicy { - fn default() -> Self { - Self { - privilege_expiry: DEFAULT_AUTH_PRIVILEGE_EXPIRY, - authsession_expiry: DEFAULT_AUTH_SESSION_EXPIRY, - } - } -} - pub struct IdmServer { // There is a good reason to keep this single thread - it // means that limits to sessions can be easily applied and checked to @@ -120,7 +88,6 @@ pub struct IdmServer { webauthn: Webauthn, oauth2rs: Arc, domain_keys: Arc>, - account_policy: Arc>, } /// Contains methods that require writes, but in the context of writing to the idm in memory structures (maybe the query server too). This is things like authentication. @@ -136,7 +103,6 @@ pub struct IdmServerAuthTransaction<'a> { pub(crate) async_tx: Sender, pub(crate) audit_tx: Sender, pub(crate) webauthn: &'a Webauthn, - pub(crate) account_policy: CowCellReadTxn, pub(crate) domain_keys: CowCellReadTxn, } @@ -144,7 +110,6 @@ pub struct IdmServerCredUpdateTransaction<'a> { pub(crate) qs_read: QueryServerReadTransaction<'a>, // sid: Sid, pub(crate) webauthn: &'a Webauthn, - pub(crate) _account_policy: CowCellReadTxn, pub(crate) cred_update_sessions: BptreeMapReadTxn<'a, Uuid, CredentialUpdateSessionMutex>, pub(crate) domain_keys: CowCellReadTxn, pub(crate) crypto_policy: &'a CryptoPolicy, @@ -166,7 +131,6 @@ pub struct IdmServerProxyWriteTransaction<'a> { pub(crate) sid: Sid, crypto_policy: &'a CryptoPolicy, webauthn: &'a Webauthn, - account_policy: CowCellWriteTxn<'a, AccountPolicy>, pub(crate) domain_keys: CowCellWriteTxn<'a, DomainKeys>, pub(crate) oauth2rs: Oauth2ResourceServersWriteTransaction<'a>, } @@ -191,16 +155,7 @@ impl IdmServer { let (audit_tx, audit_rx) = unbounded(); // Get the domain name, as the relying party id. - let ( - rp_id, - rp_name, - fernet_private_key, - es256_private_key, - cookie_key, - oauth2rs_set, - privilege_expiry, - authsession_expiry, - ) = { + let (rp_id, rp_name, fernet_private_key, es256_private_key, cookie_key, oauth2rs_set) = { let mut qs_read = qs.read().await; ( qs_read.get_domain_name().to_string(), @@ -210,8 +165,6 @@ impl IdmServer { qs_read.get_domain_cookie_key()?, // Add a read/reload of all oauth2 configurations. qs_read.get_oauth2rs_set()?, - qs_read.get_privilege_expiry()?, - qs_read.get_authsession_expiry()?, ) }; @@ -286,10 +239,6 @@ impl IdmServer { async_tx, audit_tx, webauthn, - account_policy: Arc::new(CowCell::new(AccountPolicy::new( - privilege_expiry, - authsession_expiry, - ))), domain_keys, oauth2rs: Arc::new(oauth2rs), }, @@ -319,7 +268,6 @@ impl IdmServer { async_tx: self.async_tx.clone(), audit_tx: self.audit_tx.clone(), webauthn: &self.webauthn, - account_policy: self.account_policy.read(), domain_keys: self.domain_keys.read(), } } @@ -349,7 +297,6 @@ impl IdmServer { sid, crypto_policy: &self.crypto_policy, webauthn: &self.webauthn, - account_policy: self.account_policy.write(), domain_keys: self.domain_keys.write(), oauth2rs: self.oauth2rs.write(), } @@ -360,7 +307,6 @@ impl IdmServer { qs_read: self.qs.read().await, // sid: Sid, webauthn: &self.webauthn, - _account_policy: self.account_policy.read(), cred_update_sessions: self.cred_update_sessions.read(), domain_keys: self.domain_keys.read(), crypto_policy: &self.crypto_policy, @@ -1041,9 +987,8 @@ impl<'a> IdmServerAuthTransaction<'a> { // typing and functionality so we can assess what auth types can // continue, and helps to keep non-needed entry specific data // out of the session tree. - let account = Account::try_from_entry_ro(entry.as_ref(), &mut self.qs_read)?; - - let account_policy = (*self.account_policy).clone(); + let (account, account_policy) = + Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_read)?; trace!(?account.primary); @@ -2073,10 +2018,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { #[instrument(level = "debug", skip_all)] pub fn commit(mut self) -> Result<(), OperationError> { - if self.qs_write.get_changed_system_config() { - self.reload_system_account_policy()?; - }; - if self.qs_write.get_changed_ouath2() { self.qs_write .get_oauth2rs_set() @@ -2132,17 +2073,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { // Commit everything. self.oauth2rs.commit(); self.domain_keys.commit(); - self.account_policy.commit(); self.cred_update_sessions.commit(); trace!("cred_update_session.commit"); self.qs_write.commit() } - - fn reload_system_account_policy(&mut self) -> Result<(), OperationError> { - self.account_policy.authsession_expiry = self.qs_write.get_authsession_expiry()?; - self.account_policy.privilege_expiry = self.qs_write.get_privilege_expiry()?; - Ok(()) - } } // Need tests of the sessions and the auth ... @@ -3717,8 +3651,17 @@ mod tests { //we first set the expiry to a custom value let mut idms_prox_write = idms.proxy_write(ct).await; - let new_authsession_expiry = 1000_u32; - idms_prox_write.account_policy.authsession_expiry = new_authsession_expiry; + let new_authsession_expiry = 1000; + + let modlist = ModifyList::new_purge_and_set( + Attribute::AuthSessionExpiry, + Value::Uint32(new_authsession_expiry), + ); + idms_prox_write + .qs_write + .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist) + .expect("Unable to change default session exp"); + assert!(idms_prox_write.commit().is_ok()); // Start anonymous auth. diff --git a/server/lib/src/idm/unix.rs b/server/lib/src/idm/unix.rs index 890fa0291..a6ed9e4f0 100644 --- a/server/lib/src/idm/unix.rs +++ b/server/lib/src/idm/unix.rs @@ -472,14 +472,14 @@ macro_rules! try_from_account_group_e { } impl UnixGroup { - pub fn try_from_account_entry_rw( + pub(crate) fn try_from_account_entry_rw( value: &Entry, qs: &mut QueryServerWriteTransaction, ) -> Result, OperationError> { try_from_account_group_e!(value, qs) } - pub fn try_from_account_entry_ro( + pub(crate) fn try_from_account_entry_ro( value: &Entry, qs: &mut QueryServerReadTransaction, ) -> Result, OperationError> { @@ -495,13 +495,13 @@ impl UnixGroup { } */ - pub fn try_from_entry_reduced( + pub(crate) fn try_from_entry_reduced( value: &Entry, ) -> Result { try_from_group_e!(value) } - pub fn try_from_entry( + pub(crate) fn try_from_entry( value: &Entry, ) -> Result { try_from_group_e!(value) diff --git a/server/lib/src/plugins/default_values.rs b/server/lib/src/plugins/default_values.rs new file mode 100644 index 000000000..9f4f451e2 --- /dev/null +++ b/server/lib/src/plugins/default_values.rs @@ -0,0 +1,143 @@ +/// Set and maintain default values on entries that require them. This is separate to +/// migrations that enforce entry existence and state on startup, this enforces +/// default values for specific entry uuids over every transaction. +use std::iter::once; +use std::sync::Arc; + +// use crate::event::{CreateEvent, ModifyEvent}; +use crate::plugins::Plugin; +use crate::prelude::*; + +pub struct DefaultValues {} + +impl Plugin for DefaultValues { + fn id() -> &'static str { + "plugin_default_values" + } + + #[instrument( + level = "debug", + name = "default_values::pre_create_transform", + skip_all + )] + fn pre_create_transform( + qs: &mut QueryServerWriteTransaction, + cand: &mut Vec>, + _ce: &CreateEvent, + ) -> Result<(), OperationError> { + Self::modify_inner(qs, cand) + } + + #[instrument(level = "debug", name = "default_values::pre_modify", skip_all)] + fn pre_modify( + qs: &mut QueryServerWriteTransaction, + _pre_cand: &[Arc], + cand: &mut Vec>, + _me: &ModifyEvent, + ) -> Result<(), OperationError> { + Self::modify_inner(qs, cand) + } + + #[instrument(level = "debug", name = "default_values::pre_batch_modify", skip_all)] + fn pre_batch_modify( + qs: &mut QueryServerWriteTransaction, + _pre_cand: &[Arc], + cand: &mut Vec>, + _me: &BatchModifyEvent, + ) -> Result<(), OperationError> { + Self::modify_inner(qs, cand) + } +} + +impl DefaultValues { + fn modify_inner( + _qs: &mut QueryServerWriteTransaction, + cand: &mut [Entry], + ) -> Result<(), OperationError> { + cand.iter_mut().try_for_each(|e| { + // We have to do this rather than get_uuid here because at this stage we haven't + // scheme validated the entry so it's uuid could be missing in theory. + + let e_uuid = match e.get_ava_single_uuid(Attribute::Uuid) { + Some(e_uuid) => e_uuid, + None => { + trace!("entry does not contain a uuid"); + return Ok(()); + } + }; + + if e_uuid == UUID_IDM_ALL_ACCOUNTS { + // Set default account policy values if none exist. + e.add_ava(Attribute::Class, EntryClass::AccountPolicy.to_value()); + + if !e.attribute_pres(Attribute::AuthSessionExpiry) { + e.set_ava(Attribute::AuthSessionExpiry, once( + Value::Uint32(DEFAULT_AUTH_SESSION_EXPIRY), + )); + debug!("default_values: idm_all_accounts - restore default auth_session_expiry"); + } + + // Setup the minimum functional level if one is not set already. + if !e.attribute_pres(Attribute::PrivilegeExpiry) { + e.set_ava(Attribute::PrivilegeExpiry, once( + Value::Uint32(DEFAULT_AUTH_PRIVILEGE_EXPIRY), + )); + debug!("default_values: idm_all_accounts - restore default privilege_session_expiry"); + } + + trace!(?e); + Ok(()) + } else { + Ok(()) + } + }) + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + // test we can create and generate the id + #[qs_test] + async fn test_default_values_idm_all_accounts(server: &QueryServer) { + let mut server_txn = server.write(duration_from_epoch_now()).await; + let e_all_accounts = server_txn + .internal_search_uuid(UUID_IDM_ALL_ACCOUNTS) + .expect("must not fail"); + + assert!(e_all_accounts.attribute_equality( + Attribute::AuthSessionExpiry, + &PartialValue::Uint32(DEFAULT_AUTH_SESSION_EXPIRY) + )); + assert!(e_all_accounts.attribute_equality( + Attribute::PrivilegeExpiry, + &PartialValue::Uint32(DEFAULT_AUTH_PRIVILEGE_EXPIRY) + )); + + // delete the values. + server_txn + .internal_modify_uuid( + UUID_IDM_ALL_ACCOUNTS, + &ModifyList::new_list(vec![ + Modify::Purged(Attribute::AuthSessionExpiry.into()), + Modify::Purged(Attribute::PrivilegeExpiry.into()), + ]), + ) + .expect("failed to modify account"); + + // They are re-populated. + let e_all_accounts = server_txn + .internal_search_uuid(UUID_IDM_ALL_ACCOUNTS) + .expect("must not fail"); + + assert!(e_all_accounts.attribute_equality( + Attribute::AuthSessionExpiry, + &PartialValue::Uint32(DEFAULT_AUTH_SESSION_EXPIRY) + )); + assert!(e_all_accounts.attribute_equality( + Attribute::PrivilegeExpiry, + &PartialValue::Uint32(DEFAULT_AUTH_PRIVILEGE_EXPIRY) + )); + } +} diff --git a/server/lib/src/plugins/dyngroup.rs b/server/lib/src/plugins/dyngroup.rs index d12b902be..8c9c19101 100644 --- a/server/lib/src/plugins/dyngroup.rs +++ b/server/lib/src/plugins/dyngroup.rs @@ -17,7 +17,6 @@ impl DynGroup { #[allow(clippy::too_many_arguments)] fn apply_dyngroup_change( qs: &mut QueryServerWriteTransaction, - ident: &Identity, candidate_tuples: &mut Vec<(Arc, EntryInvalidCommitted)>, affected_uuids: &mut Vec, expect: bool, @@ -25,11 +24,16 @@ impl DynGroup { dyn_groups: &mut DynGroupCache, n_dyn_groups: &[&Entry], ) -> Result<(), OperationError> { + /* + * This triggers even if we are modifying the dyngroups account policy attributes, which + * is allowed now. So we relax this, because systemprotection still blocks the creation + * of dyngroups. if !ident.is_internal() { // It should be impossible to trigger this right now due to protected plugin. error!("It is currently an error to create a dynamic group"); return Err(OperationError::SystemProtectedObject); } + */ // Search all the new groups first. let filt = filter!(FC::Or( @@ -95,7 +99,7 @@ impl DynGroup { Ok(()) } - #[instrument(level = "debug", name = "dyngroup_reload", skip_all)] + #[instrument(level = "debug", name = "dyngroup::reload", skip_all)] pub fn reload(qs: &mut QueryServerWriteTransaction) -> Result<(), OperationError> { let ident_internal = Identity::from_internal(); // Internal search all our definitions. @@ -135,11 +139,11 @@ impl DynGroup { Ok(()) } - #[instrument(level = "debug", name = "dyngroup_post_create", skip_all)] + #[instrument(level = "debug", name = "dyngroup::post_create", skip_all)] pub fn post_create( qs: &mut QueryServerWriteTransaction, cand: &[Entry], - ident: &Identity, + _ident: &Identity, ) -> Result, OperationError> { let mut affected_uuids = Vec::with_capacity(cand.len()); @@ -213,7 +217,6 @@ impl DynGroup { trace!("considering new dyngroups"); Self::apply_dyngroup_change( qs, - ident, &mut candidate_tuples, &mut affected_uuids, false, @@ -235,12 +238,12 @@ impl DynGroup { Ok(affected_uuids) } - #[instrument(level = "debug", name = "memberof_post_modify", skip_all)] + #[instrument(level = "debug", name = "dyngroup::post_modify", skip_all)] pub fn post_modify( qs: &mut QueryServerWriteTransaction, pre_cand: &[Arc>], cand: &[Entry], - ident: &Identity, + _ident: &Identity, ) -> Result, OperationError> { let mut affected_uuids = Vec::with_capacity(cand.len()); @@ -276,7 +279,6 @@ impl DynGroup { trace!("considering modified dyngroups"); Self::apply_dyngroup_change( qs, - ident, &mut candidate_tuples, &mut affected_uuids, true, diff --git a/server/lib/src/plugins/mod.rs b/server/lib/src/plugins/mod.rs index 21f539b3c..cb17dd276 100644 --- a/server/lib/src/plugins/mod.rs +++ b/server/lib/src/plugins/mod.rs @@ -15,6 +15,7 @@ use crate::prelude::*; mod attrunique; mod base; mod cred_import; +mod default_values; mod domain; pub(crate) mod dyngroup; mod eckeygen; @@ -235,6 +236,7 @@ impl Plugins { gidnumber::GidNumber::pre_create_transform(qs, cand, ce)?; domain::Domain::pre_create_transform(qs, cand, ce)?; spn::Spn::pre_create_transform(qs, cand, ce)?; + default_values::DefaultValues::pre_create_transform(qs, cand, ce)?; namehistory::NameHistory::pre_create_transform(qs, cand, ce)?; eckeygen::EcdhKeyGen::pre_create_transform(qs, cand, ce)?; // Should always be last @@ -276,6 +278,7 @@ impl Plugins { domain::Domain::pre_modify(qs, pre_cand, cand, me)?; spn::Spn::pre_modify(qs, pre_cand, cand, me)?; session::SessionConsistency::pre_modify(qs, pre_cand, cand, me)?; + default_values::DefaultValues::pre_modify(qs, pre_cand, cand, me)?; namehistory::NameHistory::pre_modify(qs, pre_cand, cand, me)?; eckeygen::EcdhKeyGen::pre_modify(qs, pre_cand, cand, me)?; // attr unique should always be last @@ -310,6 +313,7 @@ impl Plugins { domain::Domain::pre_batch_modify(qs, pre_cand, cand, me)?; spn::Spn::pre_batch_modify(qs, pre_cand, cand, me)?; session::SessionConsistency::pre_batch_modify(qs, pre_cand, cand, me)?; + default_values::DefaultValues::pre_batch_modify(qs, pre_cand, cand, me)?; namehistory::NameHistory::pre_batch_modify(qs, pre_cand, cand, me)?; eckeygen::EcdhKeyGen::pre_batch_modify(qs, pre_cand, cand, me)?; // attr unique should always be last diff --git a/server/lib/src/plugins/protected.rs b/server/lib/src/plugins/protected.rs index d03de2703..0992ba060 100644 --- a/server/lib/src/plugins/protected.rs +++ b/server/lib/src/plugins/protected.rs @@ -32,6 +32,7 @@ lazy_static! { m.insert(Attribute::BadlistPassword); m.insert(Attribute::DeniedName); m.insert(Attribute::DomainDisplayName); + // Allow modification of account policy values for dyngroups m.insert(Attribute::AuthSessionExpiry); m.insert(Attribute::PrivilegeExpiry); m @@ -108,7 +109,6 @@ impl Plugin for Protected { cand.iter().try_fold((), |(), cand| { if cand.attribute_equality(Attribute::Class, &EntryClass::Tombstone.into()) || cand.attribute_equality(Attribute::Class, &EntryClass::Recycled.into()) - || cand.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into()) { Err(OperationError::SystemProtectedObject) } else { @@ -189,7 +189,6 @@ impl Plugin for Protected { cand.iter().try_fold((), |(), cand| { if cand.attribute_equality(Attribute::Class, &EntryClass::Tombstone.into()) || cand.attribute_equality(Attribute::Class, &EntryClass::Recycled.into()) - || cand.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into()) { Err(OperationError::SystemProtectedObject) } else { diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index 355ea34c6..0da9641f5 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -111,6 +111,10 @@ impl QueryServer { if system_info_version < 15 { write_txn.migrate_14_to_15()?; } + + if system_info_version < 16 { + write_txn.migrate_15_to_16()?; + } } // Reload if anything in migrations requires it. @@ -120,7 +124,7 @@ impl QueryServer { // Now force everything to reload. write_txn.force_all_reload(); - // We are read to run + // We are ready to run write_txn.set_phase(ServerPhase::Running); // Commit all changes, this also triggers the reload. @@ -454,6 +458,57 @@ impl<'a> QueryServerWriteTransaction<'a> { // Complete } + #[instrument(level = "debug", skip_all)] + pub fn migrate_15_to_16(&mut self) -> Result<(), OperationError> { + admin_warn!("starting 15 to 16 migration."); + + let sysconfig_entry = match self.internal_search_uuid(UUID_SYSTEM_CONFIG) { + Ok(entry) => entry, + Err(OperationError::NoMatchingEntries) => return Ok(()), + Err(e) => return Err(e), + }; + + let mut all_account_modlist = Vec::with_capacity(3); + + all_account_modlist.push(Modify::Present( + Attribute::Class.into(), + EntryClass::AccountPolicy.to_value(), + )); + + if let Some(auth_exp) = sysconfig_entry.get_ava_single_uint32(Attribute::AuthSessionExpiry) + { + all_account_modlist.push(Modify::Present( + Attribute::AuthSessionExpiry.into(), + Value::Uint32(auth_exp), + )); + } + + if let Some(priv_exp) = sysconfig_entry.get_ava_single_uint32(Attribute::PrivilegeExpiry) { + all_account_modlist.push(Modify::Present( + Attribute::PrivilegeExpiry.into(), + Value::Uint32(priv_exp), + )); + } + + self.internal_batch_modify( + [ + ( + UUID_SYSTEM_CONFIG, + ModifyList::new_list(vec![ + Modify::Purged(Attribute::AuthSessionExpiry.into()), + Modify::Purged(Attribute::PrivilegeExpiry.into()), + ]), + ), + ( + UUID_IDM_ALL_ACCOUNTS, + ModifyList::new_list(all_account_modlist), + ), + ] + .into_iter(), + ) + // Complete + } + #[instrument(level = "debug", skip_all)] pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> { admin_debug!("initialise_schema_core -> start ..."); @@ -565,6 +620,7 @@ impl<'a> QueryServerWriteTransaction<'a> { let idm_schema_classes: Vec = vec![ SCHEMA_CLASS_ACCOUNT.clone().into(), + SCHEMA_CLASS_ACCOUNT_POLICY.clone().into(), SCHEMA_CLASS_DOMAIN_INFO.clone().into(), SCHEMA_CLASS_DYNGROUP.clone().into(), SCHEMA_CLASS_GROUP.clone().into(), @@ -683,6 +739,7 @@ impl<'a> QueryServerWriteTransaction<'a> { E_IDM_HP_ACP_SYNC_ACCOUNT_MANAGE_PRIV_V1.clone(), IDM_ACP_ACCOUNT_MAIL_READ_PRIV_V1.clone(), IDM_ACCOUNT_SELF_ACP_WRITE_V1.clone(), + IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_PRIV_V1.clone(), ]; let res: Result<(), _> = idm_entries @@ -710,6 +767,9 @@ impl<'a> QueryServerWriteTransaction<'a> { debug_assert!(res.is_ok()); res?; + // Some attributes we don't want to stomp if they already exist. So we conditionally + // modify them. + self.changed_schema = true; self.changed_acp = true; diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs index 37fceceb6..251b8d387 100644 --- a/server/lib/src/server/mod.rs +++ b/server/lib/src/server/mod.rs @@ -869,42 +869,6 @@ pub trait QueryServerTransaction<'a> { }) } - fn get_authsession_expiry(&mut self) -> Result { - self.internal_search_uuid(UUID_SYSTEM_CONFIG) - .and_then(|e| { - if let Some(expiry_time) = e.get_ava_single_uint32(Attribute::AuthSessionExpiry) { - Ok(expiry_time) - } else { - Err(OperationError::NoMatchingAttributes) - } - }) - .map_err(|e| { - admin_error!( - ?e, - "Failed to retrieve authsession_expiry from system configuration" - ); - e - }) - } - - fn get_privilege_expiry(&mut self) -> Result { - self.internal_search_uuid(UUID_SYSTEM_CONFIG) - .and_then(|e| { - if let Some(expiry_time) = e.get_ava_single_uint32(Attribute::PrivilegeExpiry) { - Ok(expiry_time) - } else { - Err(OperationError::NoMatchingAttributes) - } - }) - .map_err(|e| { - admin_error!( - ?e, - "Failed to retrieve privilege_expiry from system configuration" - ); - e - }) - } - fn get_oauth2rs_set(&mut self) -> Result>, OperationError> { self.internal_search(filter!(f_eq( Attribute::Class, @@ -1681,10 +1645,6 @@ impl<'a> QueryServerWriteTransaction<'a> { self.changed_domain } - pub(crate) fn get_changed_system_config(&self) -> bool { - self.changed_system_config - } - fn set_phase(&mut self, phase: ServerPhase) { *self.phase = phase } diff --git a/server/testkit-macros/Cargo.toml b/server/testkit-macros/Cargo.toml index aa94785ac..d1e3e9a1d 100644 --- a/server/testkit-macros/Cargo.toml +++ b/server/testkit-macros/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" [lib] proc-macro = true +test = true +doctest = false [dependencies] proc-macro2 = { workspace = true } diff --git a/server/testkit/Cargo.toml b/server/testkit/Cargo.toml index faf4cbd67..1fd2322c5 100644 --- a/server/testkit/Cargo.toml +++ b/server/testkit/Cargo.toml @@ -14,6 +14,8 @@ repository = { workspace = true } [lib] name = "kanidmd_testkit" path = "src/lib.rs" +test = true +doctest = false [features] default = [] diff --git a/server/testkit/tests/proto_v1_test.rs b/server/testkit/tests/proto_v1_test.rs index 740371086..107e07fca 100644 --- a/server/testkit/tests/proto_v1_test.rs +++ b/server/testkit/tests/proto_v1_test.rs @@ -1767,37 +1767,6 @@ async fn test_server_user_auth_reauthentication(rsclient: KanidmClient) { assert!(uat.purpose_readwrite_active(now)); } -#[kanidmd_testkit::test] -async fn test_authsession_expiry(rsclient: KanidmClient) { - let res = rsclient - .auth_simple_password("admin", ADMIN_TEST_PASSWORD) - .await; - assert!(res.is_ok()); - let authsession_expiry = 2878_u32; - rsclient - .system_authsession_expiry_set(authsession_expiry) - .await - .unwrap(); - let result = rsclient.system_authsession_expiry_get().await.unwrap(); - assert_eq!(authsession_expiry, result); -} - -#[kanidmd_testkit::test] -async fn test_privilege_expiry(rsclient: KanidmClient) { - let res = rsclient - .auth_simple_password("admin", ADMIN_TEST_PASSWORD) - .await; - assert!(res.is_ok()); - let authsession_expiry = 2878_u32; - - rsclient - .system_auth_privilege_expiry_set(authsession_expiry) - .await - .unwrap(); - let result = rsclient.system_auth_privilege_expiry_get().await.unwrap(); - assert_eq!(authsession_expiry, result); -} - async fn start_password_session( rsclient: &KanidmClient, username: &str, diff --git a/server/web_ui/Cargo.toml b/server/web_ui/Cargo.toml index e1b7c93c4..96cab747b 100644 --- a/server/web_ui/Cargo.toml +++ b/server/web_ui/Cargo.toml @@ -16,6 +16,8 @@ repository = "https://github.com/kanidm/kanidm/" [lib] crate-type = ["cdylib", "rlib"] +test = true +doctest = false [dependencies] gloo = { workspace = true } diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index e221c03b8..f34e153a2 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -19,15 +19,21 @@ unix = [] [lib] name = "kanidm_cli" path = "src/cli/lib.rs" +test = true +doctest = false [[bin]] name = "kanidm" path = "src/cli/main.rs" doc = false +test = true +doctest = false [[bin]] name = "kanidm_ssh_authorizedkeys_direct" path = "src/ssh_authorizedkeys.rs" +test = true +doctest = false [dependencies] async-recursion = { workspace = true } diff --git a/tools/cli/src/cli/common.rs b/tools/cli/src/cli/common.rs index 3fd5c359e..eee52bd26 100644 --- a/tools/cli/src/cli/common.rs +++ b/tools/cli/src/cli/common.rs @@ -361,6 +361,7 @@ pub fn prompt_for_username_get_username() -> Result { } } +/* /// This parses the token store and prompts the user to select their username, returns the token as a String /// /// Powered by [prompt_for_username_get_values] @@ -373,3 +374,4 @@ pub fn prompt_for_username_get_token() -> Result { Err(err) => Err(err), } } +*/ diff --git a/tools/cli/src/cli/group/account_policy.rs b/tools/cli/src/cli/group/account_policy.rs new file mode 100644 index 000000000..acc5deed1 --- /dev/null +++ b/tools/cli/src/cli/group/account_policy.rs @@ -0,0 +1,47 @@ +use crate::common::OpType; +use crate::{handle_client_error, GroupAccountPolicyOpt}; + +impl GroupAccountPolicyOpt { + pub fn debug(&self) -> bool { + match self { + GroupAccountPolicyOpt::Enable { copt, .. } + | GroupAccountPolicyOpt::AuthSessionExpiry { copt, .. } + | GroupAccountPolicyOpt::PrivilegedSessionExpiry { copt, .. } => copt.debug, + } + } + + pub async fn exec(&self) { + match self { + GroupAccountPolicyOpt::Enable { name, copt } => { + let client = copt.to_client(OpType::Write).await; + if let Err(e) = client.group_account_policy_enable(&name).await { + handle_client_error(e, &copt.output_mode); + } else { + println!("Group enabled for account policy."); + } + } + GroupAccountPolicyOpt::AuthSessionExpiry { name, expiry, copt } => { + let client = copt.to_client(OpType::Write).await; + if let Err(e) = client + .group_account_policy_authsession_expiry_set(&name, *expiry) + .await + { + handle_client_error(e, &copt.output_mode); + } else { + println!("Updated authsession expiry."); + } + } + GroupAccountPolicyOpt::PrivilegedSessionExpiry { name, expiry, copt } => { + let client = copt.to_client(OpType::Write).await; + if let Err(e) = client + .group_account_policy_privilege_expiry_set(&name, *expiry) + .await + { + handle_client_error(e, &copt.output_mode); + } else { + println!("Updated authsession expiry."); + } + } + } + } +} diff --git a/tools/cli/src/cli/group.rs b/tools/cli/src/cli/group/mod.rs similarity index 97% rename from tools/cli/src/cli/group.rs rename to tools/cli/src/cli/group/mod.rs index 48f40508b..26e7c60f0 100644 --- a/tools/cli/src/cli/group.rs +++ b/tools/cli/src/cli/group/mod.rs @@ -1,6 +1,8 @@ use crate::common::OpType; use crate::{handle_client_error, GroupOpt, GroupPosix, OutputMode}; +mod account_policy; + impl GroupOpt { pub fn debug(&self) -> bool { match self { @@ -17,6 +19,7 @@ impl GroupOpt { GroupPosix::Show(gcopt) => gcopt.copt.debug, GroupPosix::Set(gcopt) => gcopt.copt.debug, }, + GroupOpt::AccountPolicy { commands } => commands.debug(), } } @@ -156,6 +159,7 @@ impl GroupOpt { } } }, + GroupOpt::AccountPolicy { commands } => commands.exec().await, } // end match } } diff --git a/tools/cli/src/cli/lib.rs b/tools/cli/src/cli/lib.rs index 4494667e2..b0e6a2cf9 100644 --- a/tools/cli/src/cli/lib.rs +++ b/tools/cli/src/cli/lib.rs @@ -27,19 +27,19 @@ use uuid::Uuid; include!("../opt/kanidm.rs"); -pub mod common; -pub mod domain; -pub mod group; +mod common; +mod domain; +mod group; #[cfg(feature = "idv-tui")] mod identify_user_tui; -pub mod oauth2; -pub mod person; -pub mod raw; -pub mod recycle; -pub mod serviceaccount; -pub mod session; -pub mod synch; -pub mod system_config; +mod oauth2; +mod person; +mod raw; +mod recycle; +mod serviceaccount; +mod session; +mod synch; +mod system_config; mod webauthn; /// Throws an error and exits the program when we get an error @@ -148,8 +148,6 @@ impl SystemOpt { SystemOpt::Oauth2 { commands } => commands.debug(), SystemOpt::Domain { commands } => commands.debug(), SystemOpt::Synch { commands } => commands.debug(), - SystemOpt::AuthSessionExpiry { commands } => commands.debug(), - SystemOpt::PrivilegedSessionExpiry { commands } => commands.debug(), } } @@ -160,8 +158,6 @@ impl SystemOpt { SystemOpt::Oauth2 { commands } => commands.exec().await, SystemOpt::Domain { commands } => commands.exec().await, SystemOpt::Synch { commands } => commands.exec().await, - SystemOpt::AuthSessionExpiry { commands } => commands.exec().await, - SystemOpt::PrivilegedSessionExpiry { commands } => commands.exec().await, } } } diff --git a/tools/cli/src/cli/system_config/mod.rs b/tools/cli/src/cli/system_config/mod.rs index ee49dfca9..9ebc7cded 100644 --- a/tools/cli/src/cli/system_config/mod.rs +++ b/tools/cli/src/cli/system_config/mod.rs @@ -1,3 +1,2 @@ pub mod badlist; pub mod denied_names; -pub mod session_expiry; diff --git a/tools/cli/src/cli/system_config/session_expiry.rs b/tools/cli/src/cli/system_config/session_expiry.rs deleted file mode 100644 index 2693f981a..000000000 --- a/tools/cli/src/cli/system_config/session_expiry.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::common::OpType; - -use crate::{handle_client_error, AuthSessionExpiryOpt, PrivilegedSessionExpiryOpt}; - -impl AuthSessionExpiryOpt { - pub fn debug(&self) -> bool { - match self { - AuthSessionExpiryOpt::Get(copt) => copt.debug, - AuthSessionExpiryOpt::Set { copt, .. } => copt.debug, - } - } - - pub async fn exec(&self) { - match self { - AuthSessionExpiryOpt::Get(copt) => { - let client = copt.to_client(OpType::Read).await; - match client.system_authsession_expiry_get().await { - Ok(exp_time) => { - println!( - "The current system auth session expiry time is: {exp_time} seconds." - ); - } - Err(e) => handle_client_error(e, &copt.output_mode), - } - } - AuthSessionExpiryOpt::Set { copt, expiry } => { - let client = copt.to_client(OpType::Write).await; - match client.system_authsession_expiry_set(*expiry).await { - Ok(()) => { - println!("The system auth session expiry has been successfully updated.") - } - - Err(e) => handle_client_error(e, &copt.output_mode), - } - } - } - } -} - -impl PrivilegedSessionExpiryOpt { - pub fn debug(&self) -> bool { - match self { - PrivilegedSessionExpiryOpt::Get(copt) => copt.debug, - PrivilegedSessionExpiryOpt::Set { copt, .. } => copt.debug, - } - } - - pub async fn exec(&self) { - match self { - PrivilegedSessionExpiryOpt::Get(copt) => { - let client = copt.to_client(OpType::Read).await; - match client.system_auth_privilege_expiry_get().await { - Ok(exp_time) => { - println!( - "The current system auth privilege expiry time is: {exp_time} seconds." - ); - } - Err(e) => handle_client_error(e, &copt.output_mode), - } - } - PrivilegedSessionExpiryOpt::Set { copt, expiry } => { - let client = copt.to_client(OpType::Write).await; - match client.system_auth_privilege_expiry_set(*expiry).await { - Ok(()) => { - println!("The system auth privilege expiry has been successfully updated.") - } - - Err(e) => handle_client_error(e, &copt.output_mode), - } - } - } - } -} diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index 3029f3e71..5029eebf6 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -86,6 +86,34 @@ pub enum GroupPosix { Set(GroupPosixOpt), } +#[derive(Debug, Subcommand)] +pub enum GroupAccountPolicyOpt { + /// Enable account policy for this group + #[clap(name = "enable")] + Enable { + name: String, + #[clap(flatten)] + copt: CommonOpt, + }, + /// Set the maximum time for session expiry + #[clap(name = "auth-expiry")] + AuthSessionExpiry { + name: String, + expiry: u32, + #[clap(flatten)] + copt: CommonOpt, + }, + /// Configure and display the privilege session expiry + /// Set the maximum time for privilege session expiry + #[clap(name = "privilege-expiry")] + PrivilegedSessionExpiry { + name: String, + expiry: u32, + #[clap(flatten)] + copt: CommonOpt, + }, +} + #[derive(Debug, Subcommand)] pub enum GroupOpt { /// List all groups @@ -122,6 +150,12 @@ pub enum GroupOpt { #[clap(subcommand)] commands: GroupPosix, }, + /// Manage the policies that apply to members of this group. + #[clap(name = "account-policy")] + AccountPolicy { + #[clap(subcommand)] + commands: GroupAccountPolicyOpt + } } #[derive(Debug, Args)] @@ -984,18 +1018,6 @@ pub enum SystemOpt { #[clap(subcommand)] commands: DeniedNamesOpt, }, - /// Configure and display the system auth session expiry - #[clap(name = "auth-expiry")] - AuthSessionExpiry { - #[clap(subcommand)] - commands: AuthSessionExpiryOpt, - }, - /// Configure and display the system auth privilege session expiry - #[clap(name = "privilege-expiry")] - PrivilegedSessionExpiry { - #[clap(subcommand)] - commands: PrivilegedSessionExpiryOpt, - }, #[clap(name = "oauth2")] /// Configure and display oauth2/oidc resource server configuration Oauth2 { diff --git a/tools/orca/Cargo.toml b/tools/orca/Cargo.toml index d692bbe49..19ed3b02a 100644 --- a/tools/orca/Cargo.toml +++ b/tools/orca/Cargo.toml @@ -14,6 +14,8 @@ repository = { workspace = true } [[bin]] name = "orca" path = "src/main.rs" +test = true +doctest = false [dependencies] clap = { workspace = true } diff --git a/unix_integration/Cargo.toml b/unix_integration/Cargo.toml index 3acb355e0..6eb4d6756 100644 --- a/unix_integration/Cargo.toml +++ b/unix_integration/Cargo.toml @@ -21,25 +21,35 @@ tpm = ["dep:tss-esapi", "kanidm_lib_crypto/tpm"] name = "kanidm_unixd" path = "src/daemon.rs" required-features = ["unix"] +test = true +doctest = false [[bin]] name = "kanidm_unixd_tasks" path = "src/tasks_daemon.rs" required-features = ["unix"] +test = true +doctest = false [[bin]] name = "kanidm_ssh_authorizedkeys" path = "src/ssh_authorizedkeys.rs" required-features = ["unix"] +test = true +doctest = false [[bin]] name = "kanidm-unix" path = "src/tool.rs" required-features = ["unix"] +test = true +doctest = false [lib] name = "kanidm_unix_common" path = "src/lib.rs" +test = true +doctest = false [dependencies] async-trait.workspace = true