20240216 308 resource limits (#2559)

This adds account policy based resource limits to control the maximum
number of entries that an account may query
This commit is contained in:
Firstyear 2024-02-21 10:15:43 +10:00 committed by GitHub
parent 5701da8f23
commit 68d788a9f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 416 additions and 56 deletions

View file

@ -68,4 +68,28 @@ impl KanidmClient {
) )
.await .await
} }
pub async fn group_account_policy_limit_search_max_results(
&self,
id: &str,
maximum: u32,
) -> Result<(), ClientError> {
self.perform_put_request(
&format!("/v1/group/{}/_attr/limit_search_max_results", id),
vec![maximum.to_string()],
)
.await
}
pub async fn group_account_policy_limit_search_max_filter_test(
&self,
id: &str,
maximum: u32,
) -> Result<(), ClientError> {
self.perform_put_request(
&format!("/v1/group/{}/_attr/limit_search_max_filter_test", id),
vec![maximum.to_string()],
)
.await
}
} }

View file

@ -88,6 +88,8 @@ pub const ATTR_ENTRYDN: &str = "entrydn";
pub const ATTR_ENTRY_MANAGED_BY: &str = "entry_managed_by"; pub const ATTR_ENTRY_MANAGED_BY: &str = "entry_managed_by";
pub const ATTR_ENTRYUUID: &str = "entryuuid"; pub const ATTR_ENTRYUUID: &str = "entryuuid";
pub const ATTR_LDAP_KEYS: &str = "keys"; pub const ATTR_LDAP_KEYS: &str = "keys";
pub const ATTR_LIMIT_SEARCH_MAX_RESULTS: &str = "limit_search_max_results";
pub const ATTR_LIMIT_SEARCH_MAX_FILTER_TEST: &str = "limit_search_max_filter_test";
pub const ATTR_EXCLUDES: &str = "excludes"; pub const ATTR_EXCLUDES: &str = "excludes";
pub const ATTR_ES256_PRIVATE_KEY_DER: &str = "es256_private_key_der"; pub const ATTR_ES256_PRIVATE_KEY_DER: &str = "es256_private_key_der";
pub const ATTR_FERNET_PRIVATE_KEY_STR: &str = "fernet_private_key_str"; pub const ATTR_FERNET_PRIVATE_KEY_STR: &str = "fernet_private_key_str";

View file

@ -451,6 +451,7 @@ pub enum UatPurpose {
/// point onward! This means on updates, that sessions will invalidate in many /// point onward! This means on updates, that sessions will invalidate in many
/// cases. /// cases.
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
#[skip_serializing_none]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub struct UserAuthToken { pub struct UserAuthToken {
pub session_id: Uuid, pub session_id: Uuid,
@ -466,6 +467,9 @@ pub struct UserAuthToken {
pub spn: String, pub spn: String,
pub mail_primary: Option<String>, pub mail_primary: Option<String>,
pub ui_hints: BTreeSet<UiHint>, pub ui_hints: BTreeSet<UiHint>,
pub limit_search_max_results: Option<u64>,
pub limit_search_max_filter_test: Option<u64>,
} }
impl fmt::Display for UserAuthToken { impl fmt::Display for UserAuthToken {

View file

@ -66,9 +66,9 @@ impl Default for Limits {
fn default() -> Self { fn default() -> Self {
Limits { Limits {
unindexed_allow: false, unindexed_allow: false,
search_max_results: 256, search_max_results: DEFAULT_LIMIT_SEARCH_MAX_RESULTS as usize,
search_max_filter_test: 512, search_max_filter_test: DEFAULT_LIMIT_SEARCH_MAX_FILTER_TEST as usize,
filter_max_elements: 32, filter_max_elements: DEFAULT_LIMIT_FILTER_MAX_ELEMENTS as usize,
} }
} }
} }
@ -77,11 +77,20 @@ impl Limits {
pub fn unlimited() -> Self { pub fn unlimited() -> Self {
Limits { Limits {
unindexed_allow: true, unindexed_allow: true,
search_max_results: usize::MAX, search_max_results: usize::MAX >> 1,
search_max_filter_test: usize::MAX, search_max_filter_test: usize::MAX >> 1,
filter_max_elements: usize::MAX, filter_max_elements: usize::MAX,
} }
} }
pub fn api_token() -> Self {
Limits {
unindexed_allow: false,
search_max_results: DEFAULT_LIMIT_API_SEARCH_MAX_RESULTS as usize,
search_max_filter_test: DEFAULT_LIMIT_API_SEARCH_MAX_FILTER_TEST as usize,
filter_max_elements: DEFAULT_LIMIT_FILTER_MAX_ELEMENTS as usize,
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -532,6 +532,59 @@ lazy_static! {
}; };
} }
lazy_static! {
pub static ref IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6: BuiltinAcp = BuiltinAcp {
classes: vec![
EntryClass::Object,
EntryClass::AccessControlProfile,
EntryClass::AccessControlModify,
EntryClass::AccessControlSearch
],
name: "idm_acp_group_account_policy_manage",
uuid: UUID_IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE,
description: "Builtin IDM Control for management of account policy on groups",
receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_ACCOUNT_POLICY_ADMINS]),
target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
match_class_filter!(EntryClass::Group),
FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
])),
search_attrs: vec![
Attribute::Class,
Attribute::Name,
Attribute::Uuid,
Attribute::AuthSessionExpiry,
Attribute::AuthPasswordMinimumLength,
Attribute::CredentialTypeMinimum,
Attribute::PrivilegeExpiry,
Attribute::WebauthnAttestationCaList,
Attribute::LimitSearchMaxResults,
Attribute::LimitSearchMaxFilterTest,
],
modify_removed_attrs: vec![
Attribute::Class,
Attribute::AuthSessionExpiry,
Attribute::AuthPasswordMinimumLength,
Attribute::CredentialTypeMinimum,
Attribute::PrivilegeExpiry,
Attribute::WebauthnAttestationCaList,
Attribute::LimitSearchMaxResults,
Attribute::LimitSearchMaxFilterTest,
],
modify_present_attrs: vec![
Attribute::Class,
Attribute::AuthSessionExpiry,
Attribute::AuthPasswordMinimumLength,
Attribute::CredentialTypeMinimum,
Attribute::PrivilegeExpiry,
Attribute::WebauthnAttestationCaList,
Attribute::LimitSearchMaxResults,
Attribute::LimitSearchMaxFilterTest,
],
modify_classes: vec![EntryClass::AccountPolicy,],
..Default::default()
};
}
lazy_static! { lazy_static! {
pub static ref IDM_ACP_OAUTH2_MANAGE_V1: BuiltinAcp = BuiltinAcp { pub static ref IDM_ACP_OAUTH2_MANAGE_V1: BuiltinAcp = BuiltinAcp {
classes: vec![ classes: vec![

View file

@ -105,6 +105,8 @@ pub enum Attribute {
/// An LDAP Compatible sshkeys virtual attribute /// An LDAP Compatible sshkeys virtual attribute
LdapKeys, LdapKeys,
LegalName, LegalName,
LimitSearchMaxResults,
LimitSearchMaxFilterTest,
LoginShell, LoginShell,
Mail, Mail,
May, May,
@ -293,6 +295,8 @@ impl TryFrom<String> for Attribute {
ATTR_SSH_PUBLICKEY => Attribute::SshPublicKey, ATTR_SSH_PUBLICKEY => Attribute::SshPublicKey,
ATTR_LEGALNAME => Attribute::LegalName, ATTR_LEGALNAME => Attribute::LegalName,
ATTR_LOGINSHELL => Attribute::LoginShell, ATTR_LOGINSHELL => Attribute::LoginShell,
ATTR_LIMIT_SEARCH_MAX_RESULTS => Attribute::LimitSearchMaxResults,
ATTR_LIMIT_SEARCH_MAX_FILTER_TEST => Attribute::LimitSearchMaxFilterTest,
ATTR_MAIL => Attribute::Mail, ATTR_MAIL => Attribute::Mail,
ATTR_MAY => Attribute::May, ATTR_MAY => Attribute::May,
ATTR_MEMBER => Attribute::Member, ATTR_MEMBER => Attribute::Member,
@ -456,6 +460,8 @@ impl From<Attribute> for &'static str {
Attribute::LdapKeys => ATTR_LDAP_KEYS, Attribute::LdapKeys => ATTR_LDAP_KEYS,
Attribute::LdapSshPublicKey => ATTR_LDAP_SSHPUBLICKEY, Attribute::LdapSshPublicKey => ATTR_LDAP_SSHPUBLICKEY,
Attribute::LegalName => ATTR_LEGALNAME, Attribute::LegalName => ATTR_LEGALNAME,
Attribute::LimitSearchMaxResults => ATTR_LIMIT_SEARCH_MAX_RESULTS,
Attribute::LimitSearchMaxFilterTest => ATTR_LIMIT_SEARCH_MAX_FILTER_TEST,
Attribute::LoginShell => ATTR_LOGINSHELL, Attribute::LoginShell => ATTR_LOGINSHELL,
Attribute::Mail => ATTR_MAIL, Attribute::Mail => ATTR_MAIL,
Attribute::May => ATTR_MAY, Attribute::May => ATTR_MAY,

View file

@ -49,14 +49,15 @@ pub const DOMAIN_LEVEL_2: DomainVersion = 2;
pub const DOMAIN_LEVEL_3: DomainVersion = 3; pub const DOMAIN_LEVEL_3: DomainVersion = 3;
pub const DOMAIN_LEVEL_4: DomainVersion = 4; pub const DOMAIN_LEVEL_4: DomainVersion = 4;
pub const DOMAIN_LEVEL_5: DomainVersion = 5; pub const DOMAIN_LEVEL_5: DomainVersion = 5;
pub const DOMAIN_LEVEL_6: DomainVersion = 6;
// The minimum level that we can re-migrate from // The minimum level that we can re-migrate from
pub const DOMAIN_MIN_REMIGRATION_LEVEL: DomainVersion = DOMAIN_LEVEL_2; pub const DOMAIN_MIN_REMIGRATION_LEVEL: DomainVersion = DOMAIN_LEVEL_2;
// The minimum supported domain functional level // The minimum supported domain functional level
pub const DOMAIN_MIN_LEVEL: DomainVersion = DOMAIN_LEVEL_5; pub const DOMAIN_MIN_LEVEL: DomainVersion = DOMAIN_TGT_LEVEL;
// The target supported domain functional level // The target supported domain functional level
pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_5; pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_6;
// The maximum supported domain functional level // The maximum supported domain functional level
pub const DOMAIN_MAX_LEVEL: DomainVersion = DOMAIN_LEVEL_5; pub const DOMAIN_MAX_LEVEL: DomainVersion = DOMAIN_LEVEL_6;
// On test builds define to 60 seconds // On test builds define to 60 seconds
#[cfg(test)] #[cfg(test)]
@ -109,3 +110,19 @@ pub const OAUTH2_ACCESS_TOKEN_EXPIRY: u32 = 15 * 60;
/// The amount of time a suppliers clock can be "ahead" before /// The amount of time a suppliers clock can be "ahead" before
/// we warn about possible clock synchronisation issues. /// we warn about possible clock synchronisation issues.
pub const REPL_SUPPLIER_ADVANCE_WINDOW: Duration = Duration::from_secs(600); pub const REPL_SUPPLIER_ADVANCE_WINDOW: Duration = Duration::from_secs(600);
/// The default number of entries that a user may retrieve in a search
pub const DEFAULT_LIMIT_SEARCH_MAX_RESULTS: u64 = 1024;
/// The default number of entries than an api token may retrieve in a search;
pub const DEFAULT_LIMIT_API_SEARCH_MAX_RESULTS: u64 = u64::MAX >> 1;
/// the default number of entries that may be examined in a partially indexed
/// query.
pub const DEFAULT_LIMIT_SEARCH_MAX_FILTER_TEST: u64 = 2048;
/// the default number of entries that may be examined in a partially indexed
/// query by an api token.
pub const DEFAULT_LIMIT_API_SEARCH_MAX_FILTER_TEST: u64 = 16384;
/// The maximum number of items in a filter, regardless of nesting level.
pub const DEFAULT_LIMIT_FILTER_MAX_ELEMENTS: u64 = 32;
/// The maximum amount of recursion allowed in a filter.
pub const DEFAULT_LIMIT_FILTER_DEPTH_MAX: u64 = 12;

View file

@ -618,6 +618,26 @@ pub static ref SCHEMA_ATTR_CREDENTIAL_TYPE_MINIMUM: SchemaAttribute = SchemaAttr
..Default::default() ..Default::default()
}; };
pub static ref SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS_DL6: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS,
name: Attribute::LimitSearchMaxResults.into(),
description: "The maximum number of query results that may be returned in a single operation.".to_string(),
multivalue: false,
syntax: SyntaxType::Uint32,
..Default::default()
};
pub static ref SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST_DL6: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST,
name: Attribute::LimitSearchMaxFilterTest.into(),
description: "The maximum number of entries that may be examined in a partially indexed query".to_string(),
multivalue: false,
syntax: SyntaxType::Uint32,
..Default::default()
};
// === classes === // === classes ===
pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass { pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
@ -722,6 +742,23 @@ pub static ref SCHEMA_CLASS_ACCOUNT_POLICY: SchemaClass = SchemaClass {
..Default::default() ..Default::default()
}; };
pub static ref SCHEMA_CLASS_ACCOUNT_POLICY_DL6: 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(),
Attribute::AuthPasswordMinimumLength.into(),
Attribute::CredentialTypeMinimum.into(),
Attribute::WebauthnAttestationCaList.into(),
Attribute::LimitSearchMaxResults.into(),
Attribute::LimitSearchMaxFilterTest.into(),
],
systemsupplements: vec![Attribute::Group.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_ACCOUNT: SchemaClass = SchemaClass { pub static ref SCHEMA_CLASS_ACCOUNT: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_ACCOUNT, uuid: UUID_SCHEMA_CLASS_ACCOUNT,
name: EntryClass::Account.into(), name: EntryClass::Account.into(),

View file

@ -276,6 +276,10 @@ pub const UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000159"); uuid!("00000000-0000-0000-0000-ffff00000159");
pub const UUID_SCHEMA_ATTR_RECYCLEDDIRECTMEMBEROF: Uuid = pub const UUID_SCHEMA_ATTR_RECYCLEDDIRECTMEMBEROF: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000160"); uuid!("00000000-0000-0000-0000-ffff00000160");
pub const UUID_SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000161");
pub const UUID_SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000162");
// System and domain infos // System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations. // I'd like to strongly criticise william of the past for making poor choices about these allocations.

View file

@ -32,8 +32,6 @@ use crate::prelude::*;
use crate::schema::SchemaTransaction; use crate::schema::SchemaTransaction;
use crate::value::{IndexType, PartialValue}; use crate::value::{IndexType, PartialValue};
const FILTER_DEPTH_MAX: usize = 16;
// Default filter is safe, ignores all hidden types! // Default filter is safe, ignores all hidden types!
// This is &Value so we can lazy const then clone, but perhaps we can reconsider // This is &Value so we can lazy const then clone, but perhaps we can reconsider
@ -666,7 +664,7 @@ impl Filter<FilterInvalid> {
f: &ProtoFilter, f: &ProtoFilter,
qs: &mut QueryServerReadTransaction, qs: &mut QueryServerReadTransaction,
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
let depth = FILTER_DEPTH_MAX; let depth = DEFAULT_LIMIT_FILTER_DEPTH_MAX as usize;
let mut elems = ev.limits.filter_max_elements; let mut elems = ev.limits.filter_max_elements;
Ok(Filter { Ok(Filter {
state: FilterInvalid { state: FilterInvalid {
@ -681,7 +679,7 @@ impl Filter<FilterInvalid> {
f: &ProtoFilter, f: &ProtoFilter,
qs: &mut QueryServerWriteTransaction, qs: &mut QueryServerWriteTransaction,
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
let depth = FILTER_DEPTH_MAX; let depth = DEFAULT_LIMIT_FILTER_DEPTH_MAX as usize;
let mut elems = ev.limits.filter_max_elements; let mut elems = ev.limits.filter_max_elements;
Ok(Filter { Ok(Filter {
state: FilterInvalid { state: FilterInvalid {
@ -696,7 +694,7 @@ impl Filter<FilterInvalid> {
f: &LdapFilter, f: &LdapFilter,
qs: &mut QueryServerReadTransaction, qs: &mut QueryServerReadTransaction,
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
let depth = FILTER_DEPTH_MAX; let depth = DEFAULT_LIMIT_FILTER_DEPTH_MAX as usize;
let mut elems = ev.limits.filter_max_elements; let mut elems = ev.limits.filter_max_elements;
Ok(Filter { Ok(Filter {
state: FilterInvalid { state: FilterInvalid {
@ -1580,7 +1578,7 @@ mod tests {
use ldap3_proto::simple::LdapFilter; use ldap3_proto::simple::LdapFilter;
use crate::event::{CreateEvent, DeleteEvent}; use crate::event::{CreateEvent, DeleteEvent};
use crate::filter::{Filter, FilterInvalid, FILTER_DEPTH_MAX}; use crate::filter::{Filter, FilterInvalid, DEFAULT_LIMIT_FILTER_DEPTH_MAX};
use crate::prelude::*; use crate::prelude::*;
#[test] #[test]
@ -2104,12 +2102,12 @@ mod tests {
let mut r_txn = server.read().await; let mut r_txn = server.read().await;
let mut inv_proto = ProtoFilter::Pres(Attribute::Class.to_string()); let mut inv_proto = ProtoFilter::Pres(Attribute::Class.to_string());
for _i in 0..(FILTER_DEPTH_MAX + 1) { for _i in 0..(DEFAULT_LIMIT_FILTER_DEPTH_MAX + 1) {
inv_proto = ProtoFilter::And(vec![inv_proto]); inv_proto = ProtoFilter::And(vec![inv_proto]);
} }
let mut inv_ldap = LdapFilter::Present(Attribute::Class.to_string()); let mut inv_ldap = LdapFilter::Present(Attribute::Class.to_string());
for _i in 0..(FILTER_DEPTH_MAX + 1) { for _i in 0..(DEFAULT_LIMIT_FILTER_DEPTH_MAX + 1) {
inv_ldap = LdapFilter::And(vec![inv_ldap]); inv_ldap = LdapFilter::And(vec![inv_ldap]);
} }

View file

@ -273,7 +273,7 @@ impl Account {
session_id: Uuid, session_id: Uuid,
scope: SessionScope, scope: SessionScope,
ct: Duration, ct: Duration,
auth_session_expiry: u32, account_policy: &ResolvedAccountPolicy,
) -> Option<UserAuthToken> { ) -> Option<UserAuthToken> {
// TODO: Apply policy to this expiry time. // TODO: Apply policy to this expiry time.
// We have to remove the nanoseconds because when we transmit this / serialise it we drop // We have to remove the nanoseconds because when we transmit this / serialise it we drop
@ -281,10 +281,17 @@ impl Account {
// ns value which breaks some checks. // ns value which breaks some checks.
let ct = ct - Duration::from_nanos(ct.subsec_nanos() as u64); let ct = ct - Duration::from_nanos(ct.subsec_nanos() as u64);
let issued_at = OffsetDateTime::UNIX_EPOCH + ct; let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
let limit_search_max_results = account_policy.limit_search_max_results();
let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
// Note that currently the auth_session time comes from policy, but the already-privileged // Note that currently the auth_session time comes from policy, but the already-privileged
// session bound is hardcoded. // session bound is hardcoded.
let expiry = let expiry = Some(
Some(OffsetDateTime::UNIX_EPOCH + ct + Duration::from_secs(auth_session_expiry as u64)); OffsetDateTime::UNIX_EPOCH
+ ct
+ Duration::from_secs(account_policy.authsession_expiry() as u64),
);
let limited_expiry = Some( let limited_expiry = Some(
OffsetDateTime::UNIX_EPOCH OffsetDateTime::UNIX_EPOCH
+ ct + ct
@ -319,6 +326,8 @@ impl Account {
ui_hints: self.ui_hints.clone(), ui_hints: self.ui_hints.clone(),
// application: None, // application: None,
// groups: self.groups.iter().map(|g| g.to_proto()).collect(), // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
limit_search_max_results,
limit_search_max_filter_test,
}) })
} }
@ -331,10 +340,13 @@ impl Account {
session_expiry: Option<OffsetDateTime>, session_expiry: Option<OffsetDateTime>,
scope: SessionScope, scope: SessionScope,
ct: Duration, ct: Duration,
auth_privilege_expiry: u32, account_policy: &ResolvedAccountPolicy,
) -> Option<UserAuthToken> { ) -> Option<UserAuthToken> {
let issued_at = OffsetDateTime::UNIX_EPOCH + ct; let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
let limit_search_max_results = account_policy.limit_search_max_results();
let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
let (purpose, expiry) = match scope { let (purpose, expiry) = match scope {
SessionScope::Synchronise | SessionScope::ReadOnly | SessionScope::ReadWrite => { SessionScope::Synchronise | SessionScope::ReadOnly | SessionScope::ReadWrite => {
warn!( warn!(
@ -349,7 +361,7 @@ impl Account {
let expiry = Some( let expiry = Some(
OffsetDateTime::UNIX_EPOCH OffsetDateTime::UNIX_EPOCH
+ ct + ct
+ Duration::from_secs(auth_privilege_expiry.into()), + Duration::from_secs(account_policy.privilege_expiry().into()),
); );
( (
UatPurpose::ReadWrite { expiry }, UatPurpose::ReadWrite { expiry },
@ -373,6 +385,8 @@ impl Account {
ui_hints: self.ui_hints.clone(), ui_hints: self.ui_hints.clone(),
// application: None, // application: None,
// groups: self.groups.iter().map(|g| g.to_proto()).collect(), // groups: self.groups.iter().map(|g| g.to_proto()).collect(),
limit_search_max_results,
limit_search_max_filter_test,
}) })
} }
@ -909,6 +923,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::idm::account::Account; use crate::idm::account::Account;
use crate::idm::accountpolicy::ResolvedAccountPolicy;
use crate::prelude::*; use crate::prelude::*;
use kanidm_proto::v1::UiHint; use kanidm_proto::v1::UiHint;
@ -950,7 +965,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
@ -981,7 +996,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_PRIVILEGE_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
@ -1014,7 +1029,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");

View file

@ -1,15 +1,17 @@
use crate::prelude::*; use crate::prelude::*;
use crate::value::CredentialType; use crate::value::CredentialType;
use webauthn_rs::prelude::AttestationCaList; use webauthn_rs::prelude::AttestationCaList;
// use crate::idm::server::IdmServerProxyWriteTransaction;
#[derive(Clone)] #[derive(Clone)]
#[cfg_attr(test, derive(Default))]
pub(crate) struct AccountPolicy { pub(crate) struct AccountPolicy {
privilege_expiry: u32, privilege_expiry: u32,
authsession_expiry: u32, authsession_expiry: u32,
pw_min_length: u32, pw_min_length: u32,
credential_policy: CredentialType, credential_policy: CredentialType,
webauthn_att_ca_list: Option<AttestationCaList>, webauthn_att_ca_list: Option<AttestationCaList>,
limit_search_max_filter_test: Option<u64>,
limit_search_max_results: Option<u64>,
} }
impl From<&EntrySealedCommitted> for Option<AccountPolicy> { impl From<&EntrySealedCommitted> for Option<AccountPolicy> {
@ -41,12 +43,22 @@ impl From<&EntrySealedCommitted> for Option<AccountPolicy> {
.get_ava_webauthn_attestation_ca_list(Attribute::WebauthnAttestationCaList) .get_ava_webauthn_attestation_ca_list(Attribute::WebauthnAttestationCaList)
.cloned(); .cloned();
let limit_search_max_results = val
.get_ava_single_uint32(Attribute::LimitSearchMaxResults)
.map(|u| u as u64);
let limit_search_max_filter_test = val
.get_ava_single_uint32(Attribute::LimitSearchMaxFilterTest)
.map(|u| u as u64);
Some(AccountPolicy { Some(AccountPolicy {
privilege_expiry, privilege_expiry,
authsession_expiry, authsession_expiry,
pw_min_length, pw_min_length,
credential_policy, credential_policy,
webauthn_att_ca_list, webauthn_att_ca_list,
limit_search_max_filter_test,
limit_search_max_results,
}) })
} }
} }
@ -59,9 +71,24 @@ pub(crate) struct ResolvedAccountPolicy {
pw_min_length: u32, pw_min_length: u32,
credential_policy: CredentialType, credential_policy: CredentialType,
webauthn_att_ca_list: Option<AttestationCaList>, webauthn_att_ca_list: Option<AttestationCaList>,
limit_search_max_filter_test: Option<u64>,
limit_search_max_results: Option<u64>,
} }
impl ResolvedAccountPolicy { impl ResolvedAccountPolicy {
#[cfg(test)]
pub(crate) fn test_policy() -> Self {
ResolvedAccountPolicy {
privilege_expiry: DEFAULT_AUTH_PRIVILEGE_EXPIRY,
authsession_expiry: DEFAULT_AUTH_SESSION_EXPIRY,
pw_min_length: PW_MIN_LENGTH,
credential_policy: CredentialType::Any,
webauthn_att_ca_list: None,
limit_search_max_filter_test: Some(DEFAULT_LIMIT_SEARCH_MAX_FILTER_TEST),
limit_search_max_results: Some(DEFAULT_LIMIT_SEARCH_MAX_RESULTS),
}
}
pub(crate) fn fold_from<I>(iter: I) -> Self pub(crate) fn fold_from<I>(iter: I) -> Self
where where
I: Iterator<Item = AccountPolicy>, I: Iterator<Item = AccountPolicy>,
@ -73,6 +100,8 @@ impl ResolvedAccountPolicy {
pw_min_length: PW_MIN_LENGTH, pw_min_length: PW_MIN_LENGTH,
credential_policy: CredentialType::Any, credential_policy: CredentialType::Any,
webauthn_att_ca_list: None, webauthn_att_ca_list: None,
limit_search_max_filter_test: None,
limit_search_max_results: None,
}; };
iter.for_each(|acc_pol| { iter.for_each(|acc_pol| {
@ -96,6 +125,26 @@ impl ResolvedAccountPolicy {
accumulate.credential_policy = acc_pol.credential_policy accumulate.credential_policy = acc_pol.credential_policy
} }
if let Some(pol_lim) = acc_pol.limit_search_max_results {
if let Some(acc_lim) = accumulate.limit_search_max_results {
if pol_lim > acc_lim {
accumulate.limit_search_max_results = Some(pol_lim);
}
} else {
accumulate.limit_search_max_results = Some(pol_lim);
}
}
if let Some(pol_lim) = acc_pol.limit_search_max_filter_test {
if let Some(acc_lim) = accumulate.limit_search_max_filter_test {
if pol_lim > acc_lim {
accumulate.limit_search_max_filter_test = Some(pol_lim);
}
} else {
accumulate.limit_search_max_filter_test = Some(pol_lim);
}
}
if let Some(acc_pol_w_att_ca) = acc_pol.webauthn_att_ca_list { if let Some(acc_pol_w_att_ca) = acc_pol.webauthn_att_ca_list {
if let Some(res_w_att_ca) = accumulate.webauthn_att_ca_list.as_mut() { if let Some(res_w_att_ca) = accumulate.webauthn_att_ca_list.as_mut() {
res_w_att_ca.intersection(&acc_pol_w_att_ca); res_w_att_ca.intersection(&acc_pol_w_att_ca);
@ -127,6 +176,14 @@ impl ResolvedAccountPolicy {
pub(crate) fn webauthn_attestation_ca_list(&self) -> Option<&AttestationCaList> { pub(crate) fn webauthn_attestation_ca_list(&self) -> Option<&AttestationCaList> {
self.webauthn_att_ca_list.as_ref() self.webauthn_att_ca_list.as_ref()
} }
pub(crate) fn limit_search_max_results(&self) -> Option<u64> {
self.limit_search_max_results
}
pub(crate) fn limit_search_max_filter_test(&self) -> Option<u64> {
self.limit_search_max_filter_test
}
} }
#[cfg(test)] #[cfg(test)]
@ -205,6 +262,8 @@ jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
pw_min_length: 11, pw_min_length: 11,
credential_policy: CredentialType::Mfa, credential_policy: CredentialType::Mfa,
webauthn_att_ca_list: Some(att_ca_list_a), webauthn_att_ca_list: Some(att_ca_list_a),
limit_search_max_filter_test: Some(10),
limit_search_max_results: Some(10),
}; };
let mut att_ca_builder = AttestationCaListBuilder::new(); let mut att_ca_builder = AttestationCaListBuilder::new();
@ -224,6 +283,8 @@ jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
pw_min_length: 15, pw_min_length: 15,
credential_policy: CredentialType::Passkey, credential_policy: CredentialType::Passkey,
webauthn_att_ca_list: Some(att_ca_list_b), webauthn_att_ca_list: Some(att_ca_list_b),
limit_search_max_filter_test: Some(5),
limit_search_max_results: Some(15),
}; };
let rap = ResolvedAccountPolicy::fold_from([policy_a, policy_b].into_iter()); let rap = ResolvedAccountPolicy::fold_from([policy_a, policy_b].into_iter());
@ -232,6 +293,8 @@ jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
assert_eq!(rap.authsession_expiry(), 50); assert_eq!(rap.authsession_expiry(), 50);
assert_eq!(rap.pw_min_length(), 15); assert_eq!(rap.pw_min_length(), 15);
assert_eq!(rap.credential_policy, CredentialType::Passkey); assert_eq!(rap.credential_policy, CredentialType::Passkey);
assert_eq!(rap.limit_search_max_results(), Some(15));
assert_eq!(rap.limit_search_max_filter_test(), Some(10));
let mut att_ca_builder = AttestationCaListBuilder::new(); let mut att_ca_builder = AttestationCaListBuilder::new();

View file

@ -1275,14 +1275,7 @@ impl AuthSession {
) { ) {
CredState::Success { auth_type, cred_id } => { CredState::Success { auth_type, cred_id } => {
// Issue the uat based on a set of factors. // Issue the uat based on a set of factors.
let uat = self.issue_uat( let uat = self.issue_uat(&auth_type, time, async_tx, cred_id)?;
&auth_type,
time,
async_tx,
cred_id,
self.account_policy.authsession_expiry(),
self.account_policy.privilege_expiry(),
)?;
let jwt = Jws::into_json(&uat).map_err(|e| { let jwt = Jws::into_json(&uat).map_err(|e| {
admin_error!(?e, "Failed to serialise into Jws"); admin_error!(?e, "Failed to serialise into Jws");
@ -1357,8 +1350,6 @@ impl AuthSession {
time: Duration, time: Duration,
async_tx: &Sender<DelayedAction>, async_tx: &Sender<DelayedAction>,
cred_id: Uuid, cred_id: Uuid,
auth_session_expiry: u32,
auth_privilege_expiry: u32,
) -> Result<UserAuthToken, OperationError> { ) -> Result<UserAuthToken, OperationError> {
security_debug!("Successful cred handling"); security_debug!("Successful cred handling");
match self.intent { match self.intent {
@ -1392,7 +1383,7 @@ impl AuthSession {
let uat = self let uat = self
.account .account
.to_userauthtoken(session_id, scope, time, auth_session_expiry) .to_userauthtoken(session_id, scope, time, &self.account_policy)
.ok_or(OperationError::InvalidState)?; .ok_or(OperationError::InvalidState)?;
// Queue the session info write. // Queue the session info write.
@ -1454,7 +1445,7 @@ impl AuthSession {
session_expiry, session_expiry,
scope, scope,
time, time,
auth_privilege_expiry, &self.account_policy,
) )
.ok_or(OperationError::InvalidState)?; .ok_or(OperationError::InvalidState)?;

View file

@ -2555,6 +2555,7 @@ mod tests {
use kanidm_proto::v1::UserAuthToken; use kanidm_proto::v1::UserAuthToken;
use openssl::sha; use openssl::sha;
use crate::idm::accountpolicy::ResolvedAccountPolicy;
use crate::idm::oauth2::{AuthoriseResponse, Oauth2Error}; use crate::idm::oauth2::{AuthoriseResponse, Oauth2Error};
use crate::idm::server::{IdmServer, IdmServerTransaction}; use crate::idm::server::{IdmServer, IdmServerTransaction};
use crate::prelude::*; use crate::prelude::*;
@ -2715,7 +2716,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
@ -2843,7 +2844,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
@ -2906,7 +2907,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
let ident = idms_prox_write let ident = idms_prox_write
@ -3238,7 +3239,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
let ident2 = idms_prox_write let ident2 = idms_prox_write
@ -3858,7 +3859,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
let ident2 = idms_prox_write let ident2 = idms_prox_write

View file

@ -763,7 +763,17 @@ pub trait IdmServerTransaction<'a> {
} }
}; };
let limits = Limits::default(); let mut limits = Limits::default();
// Apply the limits from the uat
if let Some(lim) = uat.limit_search_max_results.and_then(|v| v.try_into().ok()) {
limits.search_max_results = lim;
}
if let Some(lim) = uat
.limit_search_max_filter_test
.and_then(|v| v.try_into().ok())
{
limits.search_max_filter_test = lim;
}
// #64: Now apply claims from the uat into the Entry // #64: Now apply claims from the uat into the Entry
// to allow filtering. // to allow filtering.
@ -806,7 +816,7 @@ pub trait IdmServerTransaction<'a> {
let scope = (&apit.purpose).into(); let scope = (&apit.purpose).into();
let limits = Limits::default(); let limits = Limits::api_token();
Ok(Identity { Ok(Identity {
origin: IdentType::User(IdentUser { entry }), origin: IdentType::User(IdentUser { entry }),
source, source,
@ -2160,6 +2170,7 @@ mod tests {
use crate::credential::{Credential, Password}; use crate::credential::{Credential, Password};
use crate::idm::account::DestroySessionTokenEvent; use crate::idm::account::DestroySessionTokenEvent;
use crate::idm::accountpolicy::ResolvedAccountPolicy;
use crate::idm::audit::AuditEvent; use crate::idm::audit::AuditEvent;
use crate::idm::delayed::{AuthSessionRecord, DelayedAction}; use crate::idm::delayed::{AuthSessionRecord, DelayedAction};
use crate::idm::event::{AuthEvent, AuthResult}; use crate::idm::event::{AuthEvent, AuthResult};
@ -3843,7 +3854,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
let ident = idms_prox_write let ident = idms_prox_write
@ -3862,7 +3873,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
let ident = idms_prox_write let ident = idms_prox_write
@ -3881,7 +3892,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
let ident = idms_prox_write let ident = idms_prox_write
@ -3900,7 +3911,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
let ident = idms_prox_write let ident = idms_prox_write
@ -3919,7 +3930,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
let ident = idms_prox_write let ident = idms_prox_write
@ -3938,7 +3949,7 @@ mod tests {
session_id, session_id,
SessionScope::ReadWrite, SessionScope::ReadWrite,
ct, ct,
DEFAULT_AUTH_SESSION_EXPIRY, &ResolvedAccountPolicy::test_policy(),
) )
.expect("Unable to create uat"); .expect("Unable to create uat");
let ident = idms_prox_write let ident = idms_prox_write
@ -3952,6 +3963,50 @@ mod tests {
assert!(!ident.has_claim("authclass_single")); assert!(!ident.has_claim("authclass_single"));
} }
#[idm_test]
async fn test_idm_uat_limits_account_policy(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await;
idms_prox_write
.qs_write
.internal_create(vec![E_TESTPERSON_1.clone()])
.expect("Failed to create test person");
// get an account.
let account = idms_prox_write
.target_to_account(UUID_TESTPERSON_1)
.expect("account must exist");
// Create a fake UATs
let session_id = uuid::Uuid::new_v4();
let uat = account
.to_userauthtoken(
session_id,
SessionScope::ReadWrite,
ct,
&ResolvedAccountPolicy::test_policy(),
)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct, Source::Internal)
.expect("Unable to process uat");
assert_eq!(
ident.limits().search_max_results,
DEFAULT_LIMIT_SEARCH_MAX_RESULTS as usize
);
assert_eq!(
ident.limits().search_max_filter_test,
DEFAULT_LIMIT_SEARCH_MAX_FILTER_TEST as usize
);
}
#[idm_test] #[idm_test]
async fn test_idm_jwt_uat_token_key_reload( async fn test_idm_jwt_uat_token_key_reload(
idms: &IdmServer, idms: &IdmServer,

View file

@ -135,6 +135,10 @@ impl Identity {
&self.source &self.source
} }
pub fn limits(&self) -> &Limits {
&self.limits
}
pub fn from_internal() -> Self { pub fn from_internal() -> Self {
Identity { Identity {
origin: IdentType::Internal, origin: IdentType::Internal,

View file

@ -860,6 +860,32 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.internal_batch_modify(modset.into_iter()) self.internal_batch_modify(modset.into_iter())
} }
/// Migration domain level 5 to 6 - support query limits in account policy.
pub fn migrate_domain_5_to_6(&mut self) -> Result<(), OperationError> {
let idm_schema_classes = [
SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS_DL6.clone().into(),
SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST_DL6.clone().into(),
SCHEMA_CLASS_ACCOUNT_POLICY_DL6.clone().into(),
];
idm_schema_classes
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry))
.map_err(|err| {
error!(?err, "migrate_domain_5_to_6 -> Error");
err
})?;
self.reload()?;
// Update access controls.
self.internal_migrate_or_create(IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6.clone().into())
.map_err(|err| {
error!(?err, "migrate_domain_5_to_6 -> Error");
err
})
}
#[instrument(level = "info", skip_all)] #[instrument(level = "info", skip_all)]
pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> { pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
admin_debug!("initialise_schema_core -> start ..."); admin_debug!("initialise_schema_core -> start ...");

View file

@ -9,6 +9,8 @@ impl GroupAccountPolicyOpt {
| GroupAccountPolicyOpt::CredentialTypeMinimum { copt, .. } | GroupAccountPolicyOpt::CredentialTypeMinimum { copt, .. }
| GroupAccountPolicyOpt::PasswordMinimumLength { copt, .. } | GroupAccountPolicyOpt::PasswordMinimumLength { copt, .. }
| GroupAccountPolicyOpt::WebauthnAttestationCaList { copt, .. } | GroupAccountPolicyOpt::WebauthnAttestationCaList { copt, .. }
| GroupAccountPolicyOpt::LimitSearchMaxResults { copt, .. }
| GroupAccountPolicyOpt::LimitSearchMaxFilterTest { copt, .. }
| GroupAccountPolicyOpt::PrivilegedSessionExpiry { copt, .. } => copt.debug, | GroupAccountPolicyOpt::PrivilegedSessionExpiry { copt, .. } => copt.debug,
} }
} }
@ -82,6 +84,36 @@ impl GroupAccountPolicyOpt {
println!("Updated webauthn attestation CA list."); println!("Updated webauthn attestation CA list.");
} }
} }
GroupAccountPolicyOpt::LimitSearchMaxResults {
name,
maximum,
copt,
} => {
let client = copt.to_client(OpType::Write).await;
if let Err(e) = client
.group_account_policy_limit_search_max_results(name, *maximum)
.await
{
handle_client_error(e, copt.output_mode);
} else {
println!("Updated search maximum results limit.");
}
}
GroupAccountPolicyOpt::LimitSearchMaxFilterTest {
name,
maximum,
copt,
} => {
let client = copt.to_client(OpType::Write).await;
if let Err(e) = client
.group_account_policy_limit_search_max_filter_test(name, *maximum)
.await
{
handle_client_error(e, copt.output_mode);
} else {
println!("Updated search maximum filter test limit.");
}
}
} }
} }
} }

View file

@ -152,7 +152,7 @@ pub enum GroupAccountPolicyOpt {
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, copt: CommonOpt,
}, },
/// Set the maximum time for session expiry /// Set the maximum time for session expiry in seconds.
#[clap(name = "auth-expiry")] #[clap(name = "auth-expiry")]
AuthSessionExpiry { AuthSessionExpiry {
name: String, name: String,
@ -161,7 +161,7 @@ pub enum GroupAccountPolicyOpt {
copt: CommonOpt, copt: CommonOpt,
}, },
/// Set the minimum credential class that members may authenticate with. Valid values /// Set the minimum credential class that members may authenticate with. Valid values
/// in order of weakest to strongest are: "any" "mfa" "passkey" "attested_passkey" /// in order of weakest to strongest are: "any" "mfa" "passkey" "attested_passkey".
#[clap(name = "credential-type-minimum")] #[clap(name = "credential-type-minimum")]
CredentialTypeMinimum { CredentialTypeMinimum {
name: String, name: String,
@ -170,7 +170,7 @@ pub enum GroupAccountPolicyOpt {
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, copt: CommonOpt,
}, },
/// Set the minimum length of passwords for accounts /// Set the minimum character length of passwords for accounts.
#[clap(name = "password-minimum-length")] #[clap(name = "password-minimum-length")]
PasswordMinimumLength { PasswordMinimumLength {
name: String, name: String,
@ -178,7 +178,7 @@ pub enum GroupAccountPolicyOpt {
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, copt: CommonOpt,
}, },
/// Set the maximum time for privilege session expiry /// Set the maximum time for privilege session expiry in seconds.
#[clap(name = "privilege-expiry")] #[clap(name = "privilege-expiry")]
PrivilegedSessionExpiry { PrivilegedSessionExpiry {
name: String, name: String,
@ -186,9 +186,9 @@ pub enum GroupAccountPolicyOpt {
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, copt: CommonOpt,
}, },
/// The the webauthn attestation ca list that should be enforced /// The WebAuthn attestation CA list that should be enforced
/// on members of this group. Prevents use of passkeys that are /// on members of this group. Prevents use of passkeys that are
/// in this list. To create this list, use `fido-mds-tool` /// not in this list. To create this list, use `fido-mds-tool`
/// from <https://crates.io/crates/fido-mds-tool> /// from <https://crates.io/crates/fido-mds-tool>
#[clap(name = "webauthn-attestation-ca-list")] #[clap(name = "webauthn-attestation-ca-list")]
WebauthnAttestationCaList { WebauthnAttestationCaList {
@ -197,6 +197,25 @@ pub enum GroupAccountPolicyOpt {
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, copt: CommonOpt,
}, },
/// Sets the maximum number of entries that may be returned in a
/// search operation.
#[clap(name = "limit-search-max-results")]
LimitSearchMaxResults {
name: String,
maximum: u32,
#[clap(flatten)]
copt: CommonOpt,
},
/// Sets the maximum number of entries that are examined during
/// a partially indexed search. This does not affect fully
/// indexed searches. If in doubt, set this to 1.5x limit-search-max-results
#[clap(name = "limit-search-max-filter-test")]
LimitSearchMaxFilterTest {
name: String,
maximum: u32,
#[clap(flatten)]
copt: CommonOpt,
},
} }
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]