mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
20231019 1122 account policy basics (#2245)
--------- Co-authored-by: James Hodgkinson <james@terminaloutcomes.com>
This commit is contained in:
parent
684d72d09c
commit
afe9d28754
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
86
book/src/account_policy.md
Normal file
86
book/src/account_policy.md
Normal file
|
@ -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 <group name>
|
||||
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 <group name> <seconds>
|
||||
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 <group name> <seconds>
|
||||
kanidm group account-policy privilege-expiry my_admin_group 900
|
||||
```
|
|
@ -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://<SERVER HOSTNAME/IP> -x -b '' -s base namingContexts
|
||||
|
|
|
@ -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 = [
|
||||
|
|
35
libs/client/src/group.rs
Normal file
35
libs/client/src/group.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ use webauthn_rs_proto::{
|
|||
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
|
||||
};
|
||||
|
||||
mod group;
|
||||
mod oauth;
|
||||
mod person;
|
||||
mod scim;
|
||||
|
|
|
@ -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<u32, ClientError> {
|
||||
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::<u32>()
|
||||
.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<u32, ClientError> {
|
||||
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::<u32>()
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,10 @@ edition = "2021"
|
|||
[features]
|
||||
tpm = ["dep:tss-esapi"]
|
||||
|
||||
[lib]
|
||||
test = true
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
argon2 = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
|
|
|
@ -84,11 +84,11 @@ impl From<OpenSSLErrorStack> 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
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -16,6 +16,8 @@ repository = { workspace = true }
|
|||
[lib]
|
||||
name = "profiles"
|
||||
path = "src/lib.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -8,5 +8,9 @@ license.workspace = true
|
|||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
test = true
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
libc = { workspace = true }
|
||||
|
|
|
@ -58,7 +58,6 @@ pub fn get_user_name_by_uid(uid: uid_t) -> Option<OsString> {
|
|||
Some(name)
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
/// just testing these literally don't panic
|
||||
fn test_get_effective_uid() {
|
||||
|
|
|
@ -11,6 +11,10 @@ license = { workspace = true }
|
|||
homepage = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
[lib]
|
||||
test = true
|
||||
doctest = true
|
||||
|
||||
[features]
|
||||
wasm = ["webauthn-rs-proto/wasm"]
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -16,6 +16,8 @@ repository = { workspace = true }
|
|||
[[bin]]
|
||||
name = "kanidmd"
|
||||
path = "src/main.rs"
|
||||
test = true
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
kanidm_proto = { workspace = true }
|
||||
|
|
|
@ -5,6 +5,8 @@ edition = "2021"
|
|||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
test = true
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { workspace = true }
|
||||
|
|
|
@ -14,6 +14,8 @@ repository = { workspace = true }
|
|||
[lib]
|
||||
name = "kanidmd_lib"
|
||||
path = "src/lib.rs"
|
||||
test = true
|
||||
doctest = false
|
||||
|
||||
[[bench]]
|
||||
name = "scaling_10k"
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -551,6 +551,7 @@ pub enum EntryClass {
|
|||
AccessControlProfile,
|
||||
AccessControlSearch,
|
||||
Account,
|
||||
AccountPolicy,
|
||||
AttributeType,
|
||||
Class,
|
||||
ClassType,
|
||||
|
@ -591,6 +592,7 @@ impl From<EntryClass> 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!(
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
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<EntrySealed, EntryCommitted>,
|
||||
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<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
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<EntryReduced, EntryCommitted>,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
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<EntrySealed, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
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
|
||||
|
|
153
server/lib/src/idm/accountpolicy.rs
Normal file
153
server/lib/src/idm/accountpolicy.rs
Normal file
|
@ -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<u32> 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<Option<AccountPolicy>> for &EntrySealedCommitted {
|
||||
fn into(self) -> Option<AccountPolicy> {
|
||||
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<I>(iter: I) -> Self
|
||||
where
|
||||
I: Iterator<Item = AccountPolicy>,
|
||||
{
|
||||
// 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!();
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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<UiHint>,
|
||||
}
|
||||
|
||||
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<Group> = 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<Vec<_>, _> = group_entries
|
||||
.iter()
|
||||
.map(|e| Group::try_from_entry(e.as_ref()))
|
||||
.collect();
|
||||
// Now convert the group entries to groups.
|
||||
let groups: Result<Vec<_>, _> = $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<EntryReduced, EntryCommitted>,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
try_from_account_e!(value, qs)
|
||||
qs: &mut TXN,
|
||||
) -> Result<Vec<Self>, 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<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
try_from_account_e!(value, qs)
|
||||
qs: &mut TXN,
|
||||
) -> Result<Vec<Self>, 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<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
try_from_account_e!(value, qs)
|
||||
pub(crate) fn try_from_account_entry_with_policy<'b, 'a, TXN>(
|
||||
value: &'b Entry<EntrySealed, EntryCommitted>,
|
||||
qs: &mut TXN,
|
||||
) -> Result<(Vec<Self>, 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<AccountPolicy> = entry.as_ref().into();
|
||||
acc_pol
|
||||
}));
|
||||
|
||||
let r_groups = upg_from_account_e!(value, groups)?;
|
||||
|
||||
Ok((r_groups, rap))
|
||||
}
|
||||
|
||||
pub fn try_from_entry(
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
//! is implemented.
|
||||
|
||||
pub mod account;
|
||||
pub(crate) mod accountpolicy;
|
||||
pub(crate) mod applinks;
|
||||
pub mod audit;
|
||||
pub(crate) mod authsession;
|
||||
|
|
|
@ -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)),
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Oauth2ResourceServers>,
|
||||
domain_keys: Arc<CowCell<DomainKeys>>,
|
||||
account_policy: Arc<CowCell<AccountPolicy>>,
|
||||
}
|
||||
|
||||
/// 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<DelayedAction>,
|
||||
pub(crate) audit_tx: Sender<AuditEvent>,
|
||||
pub(crate) webauthn: &'a Webauthn,
|
||||
pub(crate) account_policy: CowCellReadTxn<AccountPolicy>,
|
||||
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
|
||||
}
|
||||
|
||||
|
@ -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<AccountPolicy>,
|
||||
pub(crate) cred_update_sessions: BptreeMapReadTxn<'a, Uuid, CredentialUpdateSessionMutex>,
|
||||
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
|
||||
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.
|
||||
|
|
|
@ -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<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
) -> Result<Vec<Self>, 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<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
|
@ -495,13 +495,13 @@ impl UnixGroup {
|
|||
}
|
||||
*/
|
||||
|
||||
pub fn try_from_entry_reduced(
|
||||
pub(crate) fn try_from_entry_reduced(
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
try_from_group_e!(value)
|
||||
}
|
||||
|
||||
pub fn try_from_entry(
|
||||
pub(crate) fn try_from_entry(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
try_from_group_e!(value)
|
||||
|
|
143
server/lib/src/plugins/default_values.rs
Normal file
143
server/lib/src/plugins/default_values.rs
Normal file
|
@ -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<Entry<EntryInvalid, EntryNew>>,
|
||||
_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<EntrySealedCommitted>],
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
_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<EntrySealedCommitted>],
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
_me: &BatchModifyEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
Self::modify_inner(qs, cand)
|
||||
}
|
||||
}
|
||||
|
||||
impl DefaultValues {
|
||||
fn modify_inner<T: Clone + std::fmt::Debug>(
|
||||
_qs: &mut QueryServerWriteTransaction,
|
||||
cand: &mut [Entry<EntryInvalid, T>],
|
||||
) -> 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)
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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<EntrySealedCommitted>, EntryInvalidCommitted)>,
|
||||
affected_uuids: &mut Vec<Uuid>,
|
||||
expect: bool,
|
||||
|
@ -25,11 +24,16 @@ impl DynGroup {
|
|||
dyn_groups: &mut DynGroupCache,
|
||||
n_dyn_groups: &[&Entry<EntrySealed, EntryCommitted>],
|
||||
) -> 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<EntrySealed, EntryCommitted>],
|
||||
ident: &Identity,
|
||||
_ident: &Identity,
|
||||
) -> Result<Vec<Uuid>, 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<Entry<EntrySealed, EntryCommitted>>],
|
||||
cand: &[Entry<EntrySealed, EntryCommitted>],
|
||||
ident: &Identity,
|
||||
_ident: &Identity,
|
||||
) -> Result<Vec<Uuid>, 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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<EntryInitNew> = 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;
|
||||
|
||||
|
|
|
@ -869,42 +869,6 @@ pub trait QueryServerTransaction<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
fn get_authsession_expiry(&mut self) -> Result<u32, OperationError> {
|
||||
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<u32, OperationError> {
|
||||
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<Vec<Arc<EntrySealedCommitted>>, 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
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ edition = "2021"
|
|||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
test = true
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { workspace = true }
|
||||
|
|
|
@ -14,6 +14,8 @@ repository = { workspace = true }
|
|||
[lib]
|
||||
name = "kanidmd_testkit"
|
||||
path = "src/lib.rs"
|
||||
test = true
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -16,6 +16,8 @@ repository = "https://github.com/kanidm/kanidm/"
|
|||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
test = true
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
gloo = { workspace = true }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -361,6 +361,7 @@ pub fn prompt_for_username_get_username() -> Result<String, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// 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<String, String> {
|
|||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
47
tools/cli/src/cli/group/account_policy.rs
Normal file
47
tools/cli/src/cli/group/account_policy.rs
Normal file
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
pub mod badlist;
|
||||
pub mod denied_names;
|
||||
pub mod session_expiry;
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -14,6 +14,8 @@ repository = { workspace = true }
|
|||
[[bin]]
|
||||
name = "orca"
|
||||
path = "src/main.rs"
|
||||
test = true
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue