20231019 1122 account policy basics (#2245)

---------

Co-authored-by: James Hodgkinson <james@terminaloutcomes.com>
This commit is contained in:
Firstyear 2023-10-22 21:16:42 +10:00 committed by GitHub
parent 684d72d09c
commit afe9d28754
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 873 additions and 396 deletions

2
Cargo.lock generated
View file

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

View file

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

View 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
```

View file

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

View file

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

View file

@ -40,6 +40,7 @@ use webauthn_rs_proto::{
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
};
mod group;
mod oauth;
mod person;
mod scim;

View file

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

View file

@ -6,6 +6,10 @@ edition = "2021"
[features]
tpm = ["dep:tss-esapi"]
[lib]
test = true
doctest = false
[dependencies]
argon2 = { workspace = true }
base64 = { workspace = true }

View file

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

View file

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

View file

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

View file

@ -16,6 +16,8 @@ repository = { workspace = true }
[lib]
name = "profiles"
path = "src/lib.rs"
test = false
doctest = false
[dependencies]
serde = { workspace = true, features = ["derive"] }

View file

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

View file

@ -8,5 +8,9 @@ license.workspace = true
homepage.workspace = true
repository.workspace = true
[lib]
test = true
doctest = false
[dependencies]
libc = { workspace = true }

View file

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

View file

@ -11,6 +11,10 @@ license = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
[lib]
test = true
doctest = true
[features]
wasm = ["webauthn-rs-proto/wasm"]

View file

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

View file

@ -16,6 +16,8 @@ repository = { workspace = true }
[[bin]]
name = "kanidmd"
path = "src/main.rs"
test = true
doctest = false
[dependencies]
kanidm_proto = { workspace = true }

View file

@ -5,6 +5,8 @@ edition = "2021"
[lib]
proc-macro = true
test = true
doctest = false
[dependencies]
proc-macro2 = { workspace = true }

View file

@ -14,6 +14,8 @@ repository = { workspace = true }
[lib]
name = "kanidmd_lib"
path = "src/lib.rs"
test = true
doctest = false
[[bench]]
name = "scaling_10k"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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!();
}
*/
}

View file

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

View file

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

View file

@ -4,6 +4,7 @@
//! is implemented.
pub mod account;
pub(crate) mod accountpolicy;
pub(crate) mod applinks;
pub mod audit;
pub(crate) mod authsession;

View file

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

View file

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

View file

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

View file

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

View file

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

View 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)
));
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,8 @@ edition = "2021"
[lib]
proc-macro = true
test = true
doctest = false
[dependencies]
proc-macro2 = { workspace = true }

View file

@ -14,6 +14,8 @@ repository = { workspace = true }
[lib]
name = "kanidmd_testkit"
path = "src/lib.rs"
test = true
doctest = false
[features]
default = []

View file

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

View file

@ -16,6 +16,8 @@ repository = "https://github.com/kanidm/kanidm/"
[lib]
crate-type = ["cdylib", "rlib"]
test = true
doctest = false
[dependencies]
gloo = { workspace = true }

View file

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

View file

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

View 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.");
}
}
}
}
}

View file

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

View file

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

View file

@ -1,3 +1,2 @@
pub mod badlist;
pub mod denied_names;
pub mod session_expiry;

View file

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

View file

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

View file

@ -14,6 +14,8 @@ repository = { workspace = true }
[[bin]]
name = "orca"
path = "src/main.rs"
test = true
doctest = false
[dependencies]
clap = { workspace = true }

View file

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