diff --git a/Cargo.lock b/Cargo.lock index c04d02a3f..9ef3b252f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,7 +155,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.24", + "time 0.3.25", ] [[package]] @@ -426,11 +426,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.65.1" +version = "0.66.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.3.3", "cexpr", "clang-sys", "lazy_static", @@ -563,11 +563,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -771,7 +772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", - "time 0.3.24", + "time 0.3.25", "version_check", ] @@ -788,7 +789,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "time 0.3.24", + "time 0.3.25", "url", ] @@ -1088,9 +1089,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" dependencies = [ "serde", ] @@ -1343,7 +1344,7 @@ dependencies = [ "mime", "serde", "serde_json", - "time 0.3.24", + "time 0.3.25", "tokio", "url", "webdriver", @@ -2227,7 +2228,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "time 0.3.24", + "time 0.3.25", "tokio", "toml", "tracing", @@ -2273,7 +2274,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "time 0.3.24", + "time 0.3.25", "tracing", "url", "urlencoding", @@ -2300,7 +2301,7 @@ dependencies = [ "serde", "serde_json", "shellexpand", - "time 0.3.24", + "time 0.3.25", "tokio", "tracing", "tracing-subscriber", @@ -2386,7 +2387,7 @@ dependencies = [ "serde_json", "serde_with", "sketching", - "time 0.3.24", + "time 0.3.25", "tokio", "tokio-openssl", "tokio-util", @@ -2439,7 +2440,7 @@ dependencies = [ "smartstring", "smolset", "sshkeys", - "time 0.3.24", + "time 0.3.25", "tokio", "tokio-util", "toml", @@ -2484,7 +2485,7 @@ dependencies = [ "serde_json", "sketching", "testkit-macros", - "time 0.3.24", + "time 0.3.25", "tokio", "tracing", "url", @@ -2505,7 +2506,7 @@ dependencies = [ "serde", "serde-wasm-bindgen 0.5.0", "serde_json", - "time 0.3.24", + "time 0.3.25", "url", "uuid", "wasm-bindgen", @@ -2519,9 +2520,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ "kqueue-sys", "libc", @@ -2529,9 +2530,9 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", @@ -2750,9 +2751,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matchit" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67827e6ea8ee8a7c4a72227ef4fc08957040acffdb5f122733b24fa12daff41b" +checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" [[package]] name = "mathru" @@ -3310,9 +3311,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d2d1d55045829d65aad9d389139882ad623b33b904e7c9f1b10c5b8927298e5" +checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" dependencies = [ "thiserror", "ucd-trie", @@ -3355,18 +3356,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", @@ -3824,9 +3825,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" dependencies = [ "bitflags 2.3.3", "errno", @@ -3875,7 +3876,7 @@ dependencies = [ "peg", "serde", "serde_json", - "time 0.3.24", + "time 0.3.25", "tracing", "tracing-subscriber", "url", @@ -3939,9 +3940,9 @@ dependencies = [ [[package]] name = "selinux-sys" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b8dbf5dd0b21d466538786194081b2c4d61878e1427f7e52f99d5845432483" +checksum = "d56602385930248c57e45f6174a6a48e12b723d0cc2ae8f467fcbe80c0d06f41" dependencies = [ "bindgen", "cc", @@ -4085,7 +4086,7 @@ dependencies = [ "serde", "serde_json", "serde_with_macros", - "time 0.3.24", + "time 0.3.25", ] [[package]] @@ -4331,9 +4332,9 @@ checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tempfile" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", "fastrand", @@ -4415,9 +4416,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" dependencies = [ "deranged", "itoa", @@ -4719,7 +4720,7 @@ dependencies = [ "sharded-slab", "smallvec", "thread_local", - "time 0.3.24", + "time 0.3.25", "tracing", "tracing-core", "tracing-log", @@ -5113,7 +5114,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "time 0.3.24", + "time 0.3.25", "unicode-segmentation", "url", ] @@ -5370,9 +5371,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd122eb777186e60c3fdf765a58ac76e41c582f1f535fbf3314434c6b58f3f7" +checksum = "acaaa1190073b2b101e15083c38ee8ec891b5e05cbee516521e94ec008f61e64" dependencies = [ "memchr", ] @@ -5401,7 +5402,7 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.24", + "time 0.3.25", ] [[package]] @@ -5536,5 +5537,5 @@ dependencies = [ "lazy_static", "quick-error", "regex", - "time 0.3.24", + "time 0.3.25", ] diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index c981133e4..0a005920c 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -61,6 +61,7 @@ pub enum ClientError { TotpVerifyFailed(Uuid, TotpSecret), TotpInvalidSha1(Uuid), JsonDecode(reqwest::Error, String), + InvalidResponseFormat(String), JsonEncode(SerdeJsonError), SystemError, ConfigParseIssue(String), diff --git a/libs/client/src/system.rs b/libs/client/src/system.rs index 514baf99f..b94685d65 100644 --- a/libs/client/src/system.rs +++ b/libs/client/src/system.rs @@ -23,4 +23,40 @@ impl KanidmClient { self.perform_delete_request_with_body("/v1/system/_attr/badlist_password", 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/proto/src/constants.rs b/proto/src/constants.rs index 67e3ff608..1eed72ada 100644 --- a/proto/src/constants.rs +++ b/proto/src/constants.rs @@ -24,6 +24,7 @@ pub const ATTR_ACP_TARGET_SCOPE: &str = "acp_targetscope"; pub const ATTR_API_TOKEN_SESSION: &str = "api_token_session"; pub const ATTR_ATTR: &str = "attr"; pub const ATTR_ATTRIBUTENAME: &str = "attributename"; +pub const ATTR_AUTH_SESSION_EXPIRY: &str = "authsession_expiry"; pub const ATTR_BADLIST_PASSWORD: &str = "badlist_password"; pub const ATTR_CLAIM: &str = "claim"; pub const ATTR_CLASS: &str = "class"; @@ -91,6 +92,7 @@ pub const ATTR_PASSWORD_IMPORT: &str = "password_import"; pub const ATTR_PHANTOM: &str = "phantom"; pub const ATTR_PRIMARY_CREDENTIAL: &str = "primary_credential"; pub const ATTR_PRIVATE_COOKIE_KEY: &str = "private_cookie_key"; +pub const ATTR_PRIVILEGE_EXPIRY: &str = "privilege_expiry"; pub const ATTR_RADIUS_SECRET: &str = "radius_secret"; pub const ATTR_RECYCLED: &str = "recycled"; pub const ATTR_REPLICATED: &str = "replicated"; diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs index c385e09d1..1171cd9c5 100644 --- a/server/core/src/https/v1.rs +++ b/server/core/src/https/v1.rs @@ -1219,6 +1219,24 @@ pub async fn system_delete_attr( .await } +pub async fn system_put_attr( + State(state): State, + Path(attr): Path, + Extension(kopid): Extension, + Json(values): Json>, +) -> impl IntoResponse { + let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SystemConfig.into())); + json_rest_event_put_attr( + state, + STR_UUID_SYSTEM_CONFIG.to_string(), + attr, + filter, + values, + kopid, + ) + .await +} + pub async fn recycle_bin_get( State(state): State, Extension(kopid): Extension, @@ -1661,6 +1679,7 @@ pub fn router(state: ServerState) -> Router { "/v1/system/_attr/:attr", get(system_get_attr) .post(system_post_attr) + .put(system_put_attr) .delete(system_delete_attr), ) .route("/v1/recycle_bin", get(recycle_bin_get)) diff --git a/server/daemon/src/main.rs b/server/daemon/src/main.rs index 1cc80c230..cc1ef9fa7 100644 --- a/server/daemon/src/main.rs +++ b/server/daemon/src/main.rs @@ -341,7 +341,6 @@ async fn main() -> ExitCode { config.update_output_mode(opt.commands.commonopt().output_mode.to_owned().into()); config.update_trust_x_forward_for(sconfig.trust_x_forward_for); config.update_admin_bind_path(&sconfig.adminbindpath); - match &opt.commands { // we aren't going to touch the DB so we can carry on KanidmdOpt::HealthCheck(_) => (), diff --git a/server/lib/src/constants/acp.rs b/server/lib/src/constants/acp.rs index ffa83b7bf..f22700902 100644 --- a/server/lib/src/constants/acp.rs +++ b/server/lib/src/constants/acp.rs @@ -1447,6 +1447,43 @@ lazy_static! { ); } +lazy_static! { + pub static ref E_IDM_ACP_SYSTEM_CONFIG_SESSION_EXP_PRIV_V1: EntryInitNew = entry_init!( + (ATTR_CLASS, EntryClass::Object.to_value()), + (ATTR_CLASS, EntryClass::AccessControlProfile.to_value()), + (ATTR_CLASS, EntryClass::AccessControlModify.to_value()), + (ATTR_CLASS, EntryClass::AccessControlSearch.to_value()), + (ATTR_NAME, Value::new_iname("idm_acp_system_config_session_exp_priv")), + (ATTR_UUID, Value::Uuid(UUID_IDM_ACP_SYSTEM_CONFIG_SESSION_EXP_PRIV_V1)), + ( + Attribute::Description.as_ref(), + Value::new_utf8s("Builtin IDM Control for granting session expiry configuration rights") + ), + ( + ATTR_ACP_RECEIVER_GROUP, + Value::Refer(UUID_SYSTEM_ADMINS) + ), + ( + ATTR_ACP_TARGET_SCOPE, + Value::new_json_filter_s( + "{\"and\": [{\"eq\": [\"uuid\",\"00000000-0000-0000-0000-ffffff000027\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}" + ) + .expect("Invalid JSON filter") + ), + (ATTR_ACP_SEARCH_ATTR, Value::new_iutf8("class")), + (ATTR_ACP_SEARCH_ATTR, Value::new_iutf8("name")), + (ATTR_ACP_SEARCH_ATTR, Value::new_iutf8("uuid")), + (ATTR_ACP_SEARCH_ATTR, Value::new_iutf8("description")), + (ATTR_ACP_SEARCH_ATTR, Attribute::AuthSessionExpiry.to_value()), + (ATTR_ACP_MODIFY_PRESENTATTR, Attribute::AuthSessionExpiry.to_value()), + (ATTR_ACP_MODIFY_REMOVEDATTR, Attribute::AuthSessionExpiry.to_value()), + (ATTR_ACP_SEARCH_ATTR, Attribute::PrivilegeExpiry.to_value()), + (ATTR_ACP_MODIFY_PRESENTATTR, Attribute::PrivilegeExpiry.to_value()), + (ATTR_ACP_MODIFY_REMOVEDATTR, Attribute::PrivilegeExpiry.to_value()) + + ); +} + lazy_static! { pub static ref E_IDM_ACP_ACCOUNT_UNIX_EXTEND_PRIV_V1: EntryInitNew = entry_init!( (ATTR_CLASS, EntryClass::Object.to_value()), diff --git a/server/lib/src/constants/entries.rs b/server/lib/src/constants/entries.rs index 6138ebae9..d9d5565a5 100644 --- a/server/lib/src/constants/entries.rs +++ b/server/lib/src/constants/entries.rs @@ -50,6 +50,7 @@ pub enum Attribute { ApiTokenSession, Attr, AttributeName, + AuthSessionExpiry, BadlistPassword, Claim, Class, @@ -115,6 +116,7 @@ pub enum Attribute { Phantom, PrimaryCredential, PrivateCookieKey, + PrivilegeExpiry, RadiusSecret, Replicated, Rs256PrivateKeyDer, @@ -189,6 +191,7 @@ impl TryFrom for Attribute { ATTR_API_TOKEN_SESSION => Attribute::ApiTokenSession, ATTR_ATTR => Attribute::Attr, ATTR_ATTRIBUTENAME => Attribute::AttributeName, + ATTR_AUTH_SESSION_EXPIRY => Attribute::AuthSessionExpiry, ATTR_BADLIST_PASSWORD => Attribute::BadlistPassword, ATTR_CLAIM => Attribute::Claim, ATTR_CLASS => Attribute::Class, @@ -256,6 +259,7 @@ impl TryFrom for Attribute { ATTR_PHANTOM => Attribute::Phantom, ATTR_PRIMARY_CREDENTIAL => Attribute::PrimaryCredential, ATTR_PRIVATE_COOKIE_KEY => Attribute::PrivateCookieKey, + ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry, ATTR_RADIUS_SECRET => Attribute::RadiusSecret, ATTR_REPLICATED => Attribute::Replicated, ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer, @@ -416,6 +420,8 @@ impl From for &'static str { Attribute::TestAttr => TEST_ATTR_TEST_ATTR, #[cfg(any(debug_assertions, test))] Attribute::Extra => TEST_ATTR_EXTRA, + Attribute::AuthSessionExpiry => ATTR_AUTH_SESSION_EXPIRY, + Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY, } } } diff --git a/server/lib/src/constants/mod.rs b/server/lib/src/constants/mod.rs index 399b4ca5a..d8dd81b3d 100644 --- a/server/lib/src/constants/mod.rs +++ b/server/lib/src/constants/mod.rs @@ -77,9 +77,9 @@ pub const MFAREG_SESSION_TIMEOUT: u64 = 300; pub const PW_MIN_LENGTH: usize = 10; // Default - sessions last for 1 hour. -pub const AUTH_SESSION_EXPIRY: u64 = 3600; +pub const DEFAULT_AUTH_SESSION_EXPIRY: u32 = 3600; // Default - privileges last for 10 minutes. -pub const AUTH_PRIVILEGE_EXPIRY: u64 = 600; +pub const DEFAULT_AUTH_PRIVILEGE_EXPIRY: u32 = 600; // Default - oauth refresh tokens last for 16 hours. pub const OAUTH_REFRESH_TOKEN_EXPIRY: u64 = 3600 * 8; diff --git a/server/lib/src/constants/schema.rs b/server/lib/src/constants/schema.rs index 39bb817b6..49b6c7f5a 100644 --- a/server/lib/src/constants/schema.rs +++ b/server/lib/src/constants/schema.rs @@ -193,6 +193,24 @@ pub static ref SCHEMA_ATTR_BADLIST_PASSWORD: SchemaAttribute = SchemaAttribute { ..Default::default() }; +pub static ref SCHEMA_ATTR_AUTH_SESSION_EXPIRY: SchemaAttribute = SchemaAttribute { + uuid: UUID_SCHEMA_ATTR_AUTH_SESSION_EXPIRY, + name: Attribute::AuthSessionExpiry.into(), + + description: "An expiration time for an authentication session.".to_string(), + syntax: SyntaxType::Uint32, + ..Default::default() +}; + +pub static ref SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY: SchemaAttribute = SchemaAttribute { + uuid: UUID_SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY, + name: Attribute::PrivilegeExpiry.into(), + + description: "An expiration time for a privileged authentication session.".to_string(), + syntax: SyntaxType::Uint32, + ..Default::default() +}; + pub static ref SCHEMA_ATTR_LOGINSHELL: SchemaAttribute = SchemaAttribute { uuid: UUID_SCHEMA_ATTR_LOGINSHELL, name: Attribute::LoginShell.into(), @@ -714,6 +732,8 @@ pub static ref SCHEMA_CLASS_SYSTEM_CONFIG: SchemaClass = SchemaClass { systemmay: vec![ Attribute::Description.into(), Attribute::BadlistPassword.into(), + Attribute::AuthSessionExpiry.into(), + Attribute::PrivilegeExpiry.into() ], ..Default::default() }; diff --git a/server/lib/src/constants/system_config.rs b/server/lib/src/constants/system_config.rs index 6fd6d51fd..2713c0262 100644 --- a/server/lib/src/constants/system_config.rs +++ b/server/lib/src/constants/system_config.rs @@ -2,6 +2,7 @@ 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 @@ -30,6 +31,14 @@ lazy_static! { "demo_badlist_shohfie3aeci2oobur0aru9uushah6EiPi2woh4hohngoighaiRuepieN3ongoo1" ) ), + ( + Attribute::AuthSessionExpiry.as_ref(), + Value::Uint32(DEFAULT_AUTH_SESSION_EXPIRY) + ), + ( + Attribute::PrivilegeExpiry.as_ref(), + Value::Uint32(DEFAULT_AUTH_PRIVILEGE_EXPIRY) + ), ( Attribute::BadlistPassword.as_ref(), Value::new_iutf8("100preteamare") diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs index 5fafccdaa..1daa14106 100644 --- a/server/lib/src/constants/uuids.rs +++ b/server/lib/src/constants/uuids.rs @@ -233,6 +233,10 @@ pub const UUID_SCHEMA_ATTR_SYNC_YIELD_AUTHORITY: Uuid = uuid!("00000000-0000-0000-0000-ffff00000138"); pub const UUID_SCHEMA_CLASS_CONFLICT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000139"); pub const UUID_SCHEMA_ATTR_SOURCE_UUID: Uuid = uuid!("00000000-0000-0000-0000-ffff00000140"); +pub const UUID_SCHEMA_ATTR_AUTH_SESSION_EXPIRY: Uuid = + uuid!("00000000-0000-0000-0000-ffff00000141"); +pub const UUID_SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY: Uuid = + uuid!("00000000-0000-0000-0000-ffff00000142"); // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. @@ -312,6 +316,8 @@ pub const UUID_IDM_HP_ACP_SYNC_ACCOUNT_MANAGE_PRIV_V1: Uuid = pub const UUID_IDM_ACP_ACCOUNT_MAIL_READ_PRIV_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000045"); pub const UUID_IDM_ACCOUNT_SELF_ACP_WRITE_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000046"); +pub const UUID_IDM_ACP_SYSTEM_CONFIG_SESSION_EXP_PRIV_V1: Uuid = + uuid!("00000000-0000-0000-0000-ffffff000047"); // End of system ranges pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe"); diff --git a/server/lib/src/idm/account.rs b/server/lib/src/idm/account.rs index a1ab48747..0734a9207 100644 --- a/server/lib/src/idm/account.rs +++ b/server/lib/src/idm/account.rs @@ -219,6 +219,7 @@ impl Account { session_id: Uuid, scope: SessionScope, ct: Duration, + auth_session_expiry: u32, ) -> Option { // TODO: Apply policy to this expiry time. // We have to remove the nanoseconds because when we transmit this / serialise it we drop @@ -226,9 +227,8 @@ impl Account { // ns value which breaks some checks. let ct = ct - Duration::from_nanos(ct.subsec_nanos() as u64); let issued_at = OffsetDateTime::UNIX_EPOCH + ct; - let expiry = - Some(OffsetDateTime::UNIX_EPOCH + ct + Duration::from_secs(AUTH_SESSION_EXPIRY)); + Some(OffsetDateTime::UNIX_EPOCH + ct + Duration::from_secs(auth_session_expiry as u64)); let (purpose, expiry) = match scope { // Issue an invalid/expired session. @@ -279,6 +279,7 @@ impl Account { session_expiry: Option, scope: SessionScope, ct: Duration, + auth_privilege_expiry: u32, ) -> Option { let issued_at = OffsetDateTime::UNIX_EPOCH + ct; @@ -294,7 +295,9 @@ impl Account { // Return a ReadWrite session with an inner expiry for the privileges { let expiry = Some( - OffsetDateTime::UNIX_EPOCH + ct + Duration::from_secs(AUTH_PRIVILEGE_EXPIRY), + OffsetDateTime::UNIX_EPOCH + + ct + + Duration::from_secs(auth_privilege_expiry.into()), ); ( UatPurpose::ReadWrite { expiry }, @@ -327,6 +330,7 @@ impl Account { expire: Option<&OffsetDateTime>, ) -> bool { let cot = OffsetDateTime::UNIX_EPOCH + ct; + trace!("Checking within valid time: {:?} {:?}", valid_from, expire); let vmin = if let Some(vft) = valid_from { // If current time greater than start time window @@ -539,7 +543,7 @@ impl Account { ) -> bool { // Remember, token expiry is checked by validate_and_parse_token_to_token. // If we wanted we could check other properties of the uat here? - // Alternatelly, we could always store LESS in the uat because of this? + // Alternatively, we could always store LESS in the uat because of this? let within_valid_window = Account::check_within_valid_time( ct, @@ -554,6 +558,7 @@ impl Account { // Anonymous does NOT record it's sessions, so we simply check the expiry time // of the token. This is already done for us as noted above. + trace!("{}", &uat); if uat.uuid == UUID_ANONYMOUS { security_debug!("Anonymous sessions do not have session records, session is valid."); @@ -845,7 +850,12 @@ mod tests { .expect("account must exist"); let session_id = uuid::Uuid::new_v4(); let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); // Check the ui hints are as expected. @@ -871,7 +881,12 @@ mod tests { .expect("account must exist"); let session_id = uuid::Uuid::new_v4(); let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_PRIVILEGE_EXPIRY, + ) .expect("Unable to create uat"); assert!(uat.ui_hints.len() == 2); @@ -902,7 +917,12 @@ mod tests { .expect("account must exist"); let session_id = uuid::Uuid::new_v4(); let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); assert!(uat.ui_hints.len() == 3); diff --git a/server/lib/src/idm/authsession.rs b/server/lib/src/idm/authsession.rs index 85d67f185..eae1c7277 100644 --- a/server/lib/src/idm/authsession.rs +++ b/server/lib/src/idm/authsession.rs @@ -37,6 +37,8 @@ use crate::prelude::*; use crate::value::Session; use time::OffsetDateTime; +use super::server::AccountPolicy; + // Each CredHandler takes one or more credentials and determines if the // handlers requirements can be 100% fulfilled. This is where MFA or other // auth policies would exist, but each credHandler has to be a whole @@ -332,7 +334,7 @@ impl CredHandler { } } - /// Validate a singule password credential of the account. + /// Validate a single password credential of the account. fn validate_password( cred: &AuthCredential, cred_id: Uuid, @@ -1005,8 +1007,8 @@ impl AuthSession { async_tx: &Sender, audit_tx: &Sender, webauthn: &Webauthn, - pw_badlist_set: Option<&HashSet>, uat_jwt_signer: &JwsSigner, + account_policy: &AccountPolicy, ) -> Result { let (next_state, response) = match &mut self.state { AuthSessionState::Init(_) | AuthSessionState::Success | AuthSessionState::Denied(_) => { @@ -1021,11 +1023,18 @@ impl AuthSession { self.account.uuid, async_tx, webauthn, - pw_badlist_set, + Some(&account_policy.pw_badlist_cache), ) { CredState::Success { auth_type, cred_id } => { // Issue the uat based on a set of factors. - let uat = self.issue_uat(&auth_type, time, async_tx, cred_id)?; + let uat = self.issue_uat( + &auth_type, + time, + async_tx, + cred_id, + account_policy.authsession_expiry, + account_policy.privilege_expiry, + )?; let jwt = Jws::new(uat); // Now encrypt and prepare the token for return to the client. @@ -1096,6 +1105,8 @@ impl AuthSession { time: Duration, async_tx: &Sender, cred_id: Uuid, + auth_session_expiry: u32, + auth_privilege_expiry: u32, ) -> Result { security_debug!("Successful cred handling"); match self.intent { @@ -1121,7 +1132,7 @@ impl AuthSession { let uat = self .account - .to_userauthtoken(session_id, scope, time) + .to_userauthtoken(session_id, scope, time, auth_session_expiry) .ok_or(OperationError::InvalidState)?; // Queue the session info write. @@ -1181,7 +1192,13 @@ impl AuthSession { let uat = self .account - .to_reissue_userauthtoken(session_id, session_expiry, scope, time) + .to_reissue_userauthtoken( + session_id, + session_expiry, + scope, + time, + auth_privilege_expiry, + ) .ok_or(OperationError::InvalidState)?; Ok(uat) @@ -1230,6 +1247,7 @@ mod tests { 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::{duration_from_epoch_now, readable_password_from_random}; @@ -1353,8 +1371,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(_)) => {} _ => panic!(), @@ -1377,8 +1395,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Success(_, AuthIssueSession::Token)) => {} _ => panic!(), @@ -1421,8 +1439,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == PW_BADLIST_MSG), _ => panic!(), @@ -1545,8 +1563,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -1571,8 +1589,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -1594,8 +1612,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), _ => panic!(), @@ -1619,8 +1637,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1631,8 +1649,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), @@ -1656,8 +1674,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1668,8 +1686,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Success(_, AuthIssueSession::Token)) => {} _ => panic!(), @@ -1733,8 +1751,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1745,8 +1763,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == PW_BADLIST_MSG), _ => panic!(), @@ -1890,8 +1908,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - None, &jws_signer, + &Default::default(), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -1918,8 +1936,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - None, &jws_signer, + &Default::default(), ) { Ok(AuthState::Success(_, AuthIssueSession::Token)) => {} _ => panic!(), @@ -1953,8 +1971,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - None, &jws_signer, + &Default::default(), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), @@ -2002,8 +2020,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - None, &jws_signer, + &Default::default(), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), @@ -2055,8 +2073,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -2079,8 +2097,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -2114,8 +2132,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), @@ -2144,8 +2162,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2156,8 +2174,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), @@ -2192,8 +2210,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2204,8 +2222,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Success(_, AuthIssueSession::Token)) => {} _ => panic!(), @@ -2273,8 +2291,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -2297,8 +2315,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), _ => panic!(), @@ -2330,8 +2348,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), @@ -2360,8 +2378,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2372,8 +2390,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), @@ -2402,8 +2420,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2414,8 +2432,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), @@ -2438,8 +2456,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2450,8 +2468,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Success(_, AuthIssueSession::Token)) => {} _ => panic!(), @@ -2480,8 +2498,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2492,8 +2510,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Success(_, AuthIssueSession::Token)) => {} _ => panic!(), @@ -2572,8 +2590,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -2595,8 +2613,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_BACKUPCODE_MSG), _ => panic!(), @@ -2619,8 +2637,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2631,8 +2649,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), @@ -2661,8 +2679,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2673,8 +2691,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Success(_, AuthIssueSession::Token)) => {} _ => panic!(), @@ -2705,8 +2723,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2717,8 +2735,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Success(_, AuthIssueSession::Token)) => {} _ => panic!(), @@ -2788,8 +2806,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2800,8 +2818,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Success(_, AuthIssueSession::Token)) => {} _ => panic!(), @@ -2824,8 +2842,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache.to_owned()), ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2836,8 +2854,8 @@ mod tests { &async_tx, &audit_tx, &webauthn, - Some(&pw_badlist_cache), &jws_signer, + &AccountPolicy::from_pw_badlist_cache(pw_badlist_cache), ) { Ok(AuthState::Success(_, AuthIssueSession::Token)) => {} _ => panic!(), diff --git a/server/lib/src/idm/credupdatesession.rs b/server/lib/src/idm/credupdatesession.rs index 6358ed12e..71d695177 100644 --- a/server/lib/src/idm/credupdatesession.rs +++ b/server/lib/src/idm/credupdatesession.rs @@ -1280,7 +1280,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> { // check a password badlist to eliminate more content // we check the password as "lower case" to help eliminate possibilities // also, when pw_badlist_cache is read from DB, it is read as Value (iutf8 lowercase) - if (*self.pw_badlist_cache).contains(&cleartext.to_lowercase()) { + if (self.account_policy.pw_badlist_cache).contains(&cleartext.to_lowercase()) { security_info!("Password found in badlist, rejecting"); Err(PasswordQuality::BadListed) } else { diff --git a/server/lib/src/idm/oauth2.rs b/server/lib/src/idm/oauth2.rs index 75ff200c7..adc96eac6 100644 --- a/server/lib/src/idm/oauth2.rs +++ b/server/lib/src/idm/oauth2.rs @@ -2118,7 +2118,12 @@ mod tests { .target_to_account(UUID_ADMIN) .expect("account must exist"); let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); // Need the uat first for expiry. @@ -2232,7 +2237,12 @@ mod tests { .target_to_account(UUID_ADMIN) .expect("account must exist"); let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); // Need the uat first for expiry. @@ -2287,7 +2297,12 @@ mod tests { .expect("account must exist"); let session_id = uuid::Uuid::new_v4(); let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); let ident = idms_prox_write .process_uat_to_identity(&uat, ct) @@ -2612,7 +2627,12 @@ mod tests { .expect("account must exist"); let session_id = uuid::Uuid::new_v4(); let uat2 = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); let ident2 = idms_prox_write .process_uat_to_identity(&uat2, ct) @@ -3212,7 +3232,12 @@ mod tests { .expect("account must exist"); let session_id = uuid::Uuid::new_v4(); let uat2 = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); let ident2 = idms_prox_write .process_uat_to_identity(&uat2, ct) diff --git a/server/lib/src/idm/server.rs b/server/lib/src/idm/server.rs index 85243d260..7349b3d07 100644 --- a/server/lib/src/idm/server.rs +++ b/server/lib/src/idm/server.rs @@ -69,6 +69,44 @@ pub struct DomainKeys { pub(crate) cookie_key: [u8; 64], } +#[derive(Clone)] +pub struct AccountPolicy { + pub(crate) privilege_expiry: u32, + pub(crate) authsession_expiry: u32, + pub(crate) pw_badlist_cache: HashSet, +} + +impl AccountPolicy { + pub fn new( + privilege_expiry: u32, + authsession_expiry: u32, + pw_badlist_cache: HashSet, + ) -> Self { + Self { + privilege_expiry, + authsession_expiry, + pw_badlist_cache, + } + } + + pub fn from_pw_badlist_cache(pw_badlist_cache: HashSet) -> Self { + Self { + pw_badlist_cache, + ..Default::default() + } + } +} + +impl Default for AccountPolicy { + fn default() -> Self { + Self { + privilege_expiry: DEFAULT_AUTH_PRIVILEGE_EXPIRY, + authsession_expiry: DEFAULT_AUTH_SESSION_EXPIRY, + pw_badlist_cache: Default::default(), // TODO: more thoughts on this + } + } +} + 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 @@ -87,9 +125,9 @@ pub struct IdmServer { audit_tx: Sender, /// [Webauthn] verifier/config webauthn: Webauthn, - pw_badlist_cache: Arc>>, 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. @@ -105,7 +143,7 @@ pub struct IdmServerAuthTransaction<'a> { pub(crate) async_tx: Sender, pub(crate) audit_tx: Sender, pub(crate) webauthn: &'a Webauthn, - pub(crate) pw_badlist_cache: CowCellReadTxn>, + pub(crate) account_policy: CowCellReadTxn, pub(crate) domain_keys: CowCellReadTxn, } @@ -113,7 +151,7 @@ pub struct IdmServerCredUpdateTransaction<'a> { pub(crate) _qs_read: QueryServerReadTransaction<'a>, // sid: Sid, pub(crate) webauthn: &'a Webauthn, - pub(crate) pw_badlist_cache: CowCellReadTxn>, + 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, @@ -135,7 +173,7 @@ pub struct IdmServerProxyWriteTransaction<'a> { pub(crate) sid: Sid, crypto_policy: &'a CryptoPolicy, webauthn: &'a Webauthn, - pw_badlist_cache: CowCellWriteTxn<'a, HashSet>, + account_policy: CowCellWriteTxn<'a, AccountPolicy>, pub(crate) domain_keys: CowCellWriteTxn<'a, DomainKeys>, pub(crate) oauth2rs: Oauth2ResourceServersWriteTransaction<'a>, } @@ -168,6 +206,8 @@ impl IdmServer { cookie_key, pw_badlist_set, oauth2rs_set, + privilege_expiry, + authsession_expiry, ) = { let mut qs_read = qs.read().await; ( @@ -179,6 +219,8 @@ impl IdmServer { qs_read.get_password_badlist()?, // Add a read/reload of all oauth2 configurations. qs_read.get_oauth2rs_set()?, + qs_read.get_privilege_expiry()?, + qs_read.get_authsession_expiry()?, ) }; @@ -253,7 +295,11 @@ impl IdmServer { async_tx, audit_tx, webauthn, - pw_badlist_cache: Arc::new(CowCell::new(pw_badlist_set)), + account_policy: Arc::new(CowCell::new(AccountPolicy::new( + privilege_expiry, + authsession_expiry, + pw_badlist_set, + ))), domain_keys, oauth2rs: Arc::new(oauth2rs), }, @@ -283,7 +329,7 @@ impl IdmServer { async_tx: self.async_tx.clone(), audit_tx: self.audit_tx.clone(), webauthn: &self.webauthn, - pw_badlist_cache: self.pw_badlist_cache.read(), + account_policy: self.account_policy.read(), domain_keys: self.domain_keys.read(), } } @@ -313,7 +359,7 @@ impl IdmServer { sid, crypto_policy: &self.crypto_policy, webauthn: &self.webauthn, - pw_badlist_cache: self.pw_badlist_cache.write(), + account_policy: self.account_policy.write(), domain_keys: self.domain_keys.write(), oauth2rs: self.oauth2rs.write(), } @@ -324,7 +370,7 @@ impl IdmServer { _qs_read: self.qs.read().await, // sid: Sid, webauthn: &self.webauthn, - pw_badlist_cache: self.pw_badlist_cache.read(), + 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, @@ -1120,7 +1166,6 @@ impl<'a> IdmServerAuthTransaction<'a> { // Process the credentials here as required. // Basically throw them at the auth_session and see what // falls out. - let pw_badlist_cache = Some(&(*self.pw_badlist_cache)); auth_session .validate_creds( &creds.cred, @@ -1128,8 +1173,8 @@ impl<'a> IdmServerAuthTransaction<'a> { &self.async_tx, &self.audit_tx, self.webauthn, - pw_badlist_cache, &self.domain_keys.uat_jwt_signer, + &self.account_policy, ) .map(|aus| { // Inspect the result: @@ -1563,7 +1608,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { // check a password badlist to eliminate more content // we check the password as "lower case" to help eliminate possibilities // also, when pw_badlist_cache is read from DB, it is read as Value (iutf8 lowercase) - if (*self.pw_badlist_cache).contains(&cleartext.to_lowercase()) { + if (self.account_policy.pw_badlist_cache).contains(&cleartext.to_lowercase()) { security_info!("Password found in badlist, rejecting"); Err(OperationError::PasswordQuality(vec![ PasswordFeedback::BadListed, @@ -1979,7 +2024,9 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { .get_changed_uuids() .contains(&UUID_SYSTEM_CONFIG) { - self.reload_password_badlist()?; + // TODO: probably it would be more efficient to introduce a single check for each of the possible system configs + // but that would require changing what uuid the operation is assigned + self.reload_system_account_policy()?; }; if self.qs_write.get_changed_ouath2() { self.qs_write @@ -2029,20 +2076,17 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { // Commit everything. self.oauth2rs.commit(); self.domain_keys.commit(); - self.pw_badlist_cache.commit(); + self.account_policy.commit(); self.cred_update_sessions.commit(); trace!("cred_update_session.commit"); self.qs_write.commit() } - fn reload_password_badlist(&mut self) -> Result<(), OperationError> { - match self.qs_write.get_password_badlist() { - Ok(badlist_entry) => { - *self.pw_badlist_cache = badlist_entry; - Ok(()) - } - Err(e) => Err(e), - } + fn reload_system_account_policy(&mut self) -> Result<(), OperationError> { + self.account_policy.pw_badlist_cache = self.qs_write.get_password_badlist()?; + self.account_policy.authsession_expiry = self.qs_write.get_authsession_expiry()?; + self.account_policy.privilege_expiry = self.qs_write.get_privilege_expiry()?; + Ok(()) } } @@ -2067,7 +2111,7 @@ mod tests { PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent, UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent, UnixUserTokenEvent, }; - use crate::idm::server::{IdmServer, IdmServerTransaction}; + use crate::idm::server::{IdmServer, IdmServerTransaction, Token}; use crate::idm::AuthState; use crate::modify::{Modify, ModifyList}; use crate::prelude::*; @@ -3396,7 +3440,7 @@ mod tests { #[idm_test] async fn test_idm_jwt_uat_expiry(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) { let ct = Duration::from_secs(TEST_CURRENT_TIME); - let expiry = ct + Duration::from_secs(AUTH_SESSION_EXPIRY + 1); + let expiry = ct + Duration::from_secs((DEFAULT_AUTH_SESSION_EXPIRY + 1).into()); // Do an authenticate init_admin_w_password(idms, TEST_PASSWORD) .await @@ -3431,8 +3475,8 @@ mod tests { _idms_delayed: &mut IdmServerDelayed, ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); - let expiry_a = ct + Duration::from_secs(AUTH_SESSION_EXPIRY + 1); - let expiry_b = ct + Duration::from_secs((AUTH_SESSION_EXPIRY + 1) * 2); + let expiry_a = ct + Duration::from_secs((DEFAULT_AUTH_SESSION_EXPIRY + 1).into()); + let expiry_b = ct + Duration::from_secs(((DEFAULT_AUTH_SESSION_EXPIRY + 1) * 2).into()); let session_a = Uuid::new_v4(); let session_b = Uuid::new_v4(); @@ -3533,7 +3577,7 @@ mod tests { let ct = Duration::from_secs(TEST_CURRENT_TIME); let post_grace = ct + GRACE_WINDOW + Duration::from_secs(1); - let expiry = ct + Duration::from_secs(AUTH_SESSION_EXPIRY + 1); + let expiry = ct + Duration::from_secs(DEFAULT_AUTH_SESSION_EXPIRY as u64 + 1); // Assert that our grace time is less than expiry, so we know the failure is due to // this. @@ -3592,6 +3636,137 @@ mod tests { } } + #[idm_test] + async fn test_idm_account_session_expiry( + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed, + ) { + let ct = Duration::from_secs(TEST_CURRENT_TIME); + + //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; + assert!(idms_prox_write.commit().is_ok()); + + // Start anonymous auth. + let mut idms_auth = idms.auth().await; + // Send the initial auth event for initialising the session + let anon_init = AuthEvent::anonymous_init(); + // Expect success + let r1 = idms_auth.auth(&anon_init, ct, Source::Internal).await; + /* Some weird lifetime things happen here ... */ + + let sid = match r1 { + Ok(ar) => { + let AuthResult { sessionid, state } = ar; + match state { + AuthState::Choose(mut conts) => { + // Should only be one auth mech + assert!(conts.len() == 1); + // And it should be anonymous + let m = conts.pop().expect("Should not fail"); + assert!(m == AuthMech::Anonymous); + } + _ => { + error!("A critical error has occurred! We have a non-continue result!"); + panic!(); + } + }; + // Now pass back the sessionid, we are good to continue. + sessionid + } + Err(e) => { + // Should not occur! + error!("A critical error has occurred! {:?}", e); + panic!(); + } + }; + + idms_auth.commit().expect("Must not fail"); + + let mut idms_auth = idms.auth().await; + let anon_begin = AuthEvent::begin_mech(sid, AuthMech::Anonymous); + + let r2 = idms_auth.auth(&anon_begin, ct, Source::Internal).await; + + match r2 { + Ok(ar) => { + let AuthResult { + sessionid: _, + state, + } = ar; + + match state { + AuthState::Continue(allowed) => { + // Check the uat. + assert!(allowed.len() == 1); + assert!(allowed.first() == Some(&AuthAllowed::Anonymous)); + } + _ => { + error!("A critical error has occurred! We have a non-continue result!"); + panic!(); + } + } + } + Err(e) => { + error!("A critical error has occurred! {:?}", e); + // Should not occur! + panic!(); + } + }; + + idms_auth.commit().expect("Must not fail"); + + let mut idms_auth = idms.auth().await; + // Now send the anonymous request, given the session id. + let anon_step = AuthEvent::cred_step_anonymous(sid); + + // Expect success + let r2 = idms_auth.auth(&anon_step, ct, Source::Internal).await; + + let token = match r2 { + Ok(ar) => { + let AuthResult { + sessionid: _, + state, + } = ar; + + match state { + AuthState::Success(uat, AuthIssueSession::Token) => uat, + _ => { + error!("A critical error has occurred! We have a non-succcess result!"); + panic!(); + } + } + } + Err(e) => { + error!("A critical error has occurred! {:?}", e); + // Should not occur! + panic!(); + } + }; + + idms_auth.commit().expect("Must not fail"); + + // Token_str to uat + // we have to do it this way because anonymous doesn't have an ideantity for which we cam get the expiry value + let Token::UserAuthToken(uat) = idms + .proxy_read() + .await + .validate_and_parse_token_to_token(Some(&token), ct) + .expect("Must not fail") else { + panic!("Unexpected auth token type for anonymous auth"); + }; + + debug!(?uat); + + assert!( + matches!(uat.expiry, Some(exp) if exp == OffsetDateTime::UNIX_EPOCH + ct + Duration::from_secs(new_authsession_expiry as u64)) + ); + } + #[idm_test] async fn test_idm_uat_claim_insertion(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) { let ct = Duration::from_secs(TEST_CURRENT_TIME); @@ -3609,7 +3784,12 @@ mod tests { // == anonymous let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); let ident = idms_prox_write .process_uat_to_identity(&uat, ct) @@ -3623,7 +3803,12 @@ mod tests { // == unixpassword let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); let ident = idms_prox_write .process_uat_to_identity(&uat, ct) @@ -3637,7 +3822,12 @@ mod tests { // == password let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); let ident = idms_prox_write .process_uat_to_identity(&uat, ct) @@ -3651,7 +3841,12 @@ mod tests { // == generatedpassword let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); let ident = idms_prox_write .process_uat_to_identity(&uat, ct) @@ -3665,7 +3860,12 @@ mod tests { // == webauthn let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); let ident = idms_prox_write .process_uat_to_identity(&uat, ct) @@ -3679,7 +3879,12 @@ mod tests { // == passwordmfa let uat = account - .to_userauthtoken(session_id, SessionScope::ReadWrite, ct) + .to_userauthtoken( + session_id, + SessionScope::ReadWrite, + ct, + DEFAULT_AUTH_SESSION_EXPIRY, + ) .expect("Unable to create uat"); let ident = idms_prox_write .process_uat_to_identity(&uat, ct) diff --git a/server/lib/src/plugins/protected.rs b/server/lib/src/plugins/protected.rs index b9cb105f5..56b546c4e 100644 --- a/server/lib/src/plugins/protected.rs +++ b/server/lib/src/plugins/protected.rs @@ -32,6 +32,8 @@ lazy_static! { m.insert("id_verification_eckey"); m.insert("badlist_password"); m.insert("domain_display_name"); + m.insert("authsession_expiry"); + m.insert("privilege_expiry"); m }; } diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index 1c4ea130f..9d7caa45c 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -490,6 +490,8 @@ impl<'a> QueryServerWriteTransaction<'a> { SCHEMA_ATTR_ACCOUNT_EXPIRE.clone().into(), SCHEMA_ATTR_ACCOUNT_VALID_FROM.clone().into(), SCHEMA_ATTR_API_TOKEN_SESSION.clone().into(), + SCHEMA_ATTR_AUTH_SESSION_EXPIRY.clone().into(), + SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY.clone().into(), SCHEMA_ATTR_BADLIST_PASSWORD.clone().into(), SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN.clone().into(), SCHEMA_ATTR_DEVICEKEYS.clone().into(), @@ -701,6 +703,7 @@ impl<'a> QueryServerWriteTransaction<'a> { E_IDM_ACP_RADIUS_SERVERS_V1.clone(), E_IDM_ACP_DOMAIN_ADMIN_PRIV_V1.clone(), E_IDM_ACP_SYSTEM_CONFIG_PRIV_V1.clone(), + E_IDM_ACP_SYSTEM_CONFIG_SESSION_EXP_PRIV_V1.clone(), E_IDM_ACP_PEOPLE_ACCOUNT_PASSWORD_IMPORT_PRIV_V1.clone(), E_IDM_ACP_PEOPLE_EXTEND_PRIV_V1.clone(), E_IDM_ACP_HP_PEOPLE_READ_PRIV_V1.clone(), diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs index 3adfd28e1..305cc4d38 100644 --- a/server/lib/src/server/mod.rs +++ b/server/lib/src/server/mod.rs @@ -820,6 +820,36 @@ 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("authsession_expiry") { + 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("privilege_expiry") { + 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, diff --git a/server/testkit/tests/default_entries.rs b/server/testkit/tests/default_entries.rs index 174f31586..ea6926d9f 100644 --- a/server/testkit/tests/default_entries.rs +++ b/server/testkit/tests/default_entries.rs @@ -323,6 +323,8 @@ async fn test_default_entries_rbac_admins_schema_entries(rsclient: KanidmClient) "domain_ssid", "gidnumber", "badlist_password", + "authsession_expiry", + "privilege_expiry", "loginshell", "unix_password", "nsuniqueid", diff --git a/server/testkit/tests/proto_v1_test.rs b/server/testkit/tests/proto_v1_test.rs index 9f1a886d8..d73f70561 100644 --- a/server/testkit/tests/proto_v1_test.rs +++ b/server/testkit/tests/proto_v1_test.rs @@ -1460,3 +1460,34 @@ async fn test_server_user_auth_reauthentication(rsclient: KanidmClient) { eprintln!("{:?} {:?}", now, uat.purpose); 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 ans = rsclient.system_authsession_expiry_get().await.unwrap(); + assert_eq!(authsession_expiry, ans); +} + +#[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 ans = rsclient.system_auth_privilege_expiry_get().await.unwrap(); + assert_eq!(authsession_expiry, ans); +} diff --git a/tools/cli/src/cli/lib.rs b/tools/cli/src/cli/lib.rs index 3ff2245ff..d7f08ce7c 100644 --- a/tools/cli/src/cli/lib.rs +++ b/tools/cli/src/cli/lib.rs @@ -31,6 +31,7 @@ pub mod raw; pub mod recycle; pub mod serviceaccount; pub mod session; +pub mod session_expiry; pub mod synch; mod webauthn; @@ -72,6 +73,8 @@ 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(), } } @@ -81,6 +84,8 @@ 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/session_expiry.rs b/tools/cli/src/cli/session_expiry.rs new file mode 100644 index 000000000..f68c360c6 --- /dev/null +++ b/tools/cli/src/cli/session_expiry.rs @@ -0,0 +1,73 @@ +use crate::common::OpType; + +use crate::{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) => eprintln!("{:?}", e), + } + } + 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) => eprintln!("{:?}", e), + } + } + } + } +} + +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) => eprintln!("{:?}", e), + } + } + 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) => eprintln!("{:?}", e), + } + } + } + } +} diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index d7e2ba113..6f1aeddd8 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -904,6 +904,36 @@ pub enum SynchOpt { }, } +#[derive(Debug, Subcommand)] +pub enum AuthSessionExpiryOpt { + #[clap[name = "get"]] + /// Show information about this system auth session expiry + Get(CommonOpt), + #[clap[name = "set"]] + /// Sets the system auth session expiry in seconds + Set { + #[clap(flatten)] + copt: CommonOpt, + #[clap(name = "expiry")] + expiry: u32, + }, +} + +#[derive(Debug, Subcommand)] +pub enum PrivilegedSessionExpiryOpt { + #[clap[name = "get"]] + /// Show information about this system privileged session expiry + Get(CommonOpt), + #[clap[name = "set"]] + /// Sets the system auth privilege session expiry in seconds + Set { + #[clap(flatten)] + copt: CommonOpt, + #[clap(name = "expiry")] + expiry: u32, + }, +} + #[derive(Debug, Subcommand)] pub enum SystemOpt { #[clap(name = "pw-badlist")] @@ -912,6 +942,18 @@ pub enum SystemOpt { #[clap(subcommand)] commands: PwBadlistOpt, }, + /// 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 {