mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Feat: Adding POSIX Password fallback (#3067)
* Added Schema for credential fallback * Added account polcity management to ac migration * Refactored Ldap & Unix auth to be common * removed unused methods and renamed unused fields * Fixed LDAP missing Anonymous logic * Added CLI argument for configuring primary cred fallback
This commit is contained in:
parent
2dbeeaaedb
commit
dc4a438c31
|
@ -43,6 +43,7 @@
|
|||
- Wei Jian Gan (weijiangan)
|
||||
- adamcstephens
|
||||
- Chris Olstrom (colstrom)
|
||||
- Christopher-Robin (cebbinghaus)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
|
|
@ -109,6 +109,18 @@ impl KanidmClient {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn group_account_policy_allow_primary_cred_fallback(
|
||||
&self,
|
||||
id: &str,
|
||||
allow: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
self.perform_put_request(
|
||||
&format!("/v1/group/{}/_attr/allow_primary_cred_fallback", id),
|
||||
vec![allow.to_string()],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_group_purge_mail(&self, id: &str) -> Result<(), ClientError> {
|
||||
self.idm_group_purge_attr(id, "mail").await
|
||||
}
|
||||
|
|
|
@ -172,6 +172,7 @@ pub enum Attribute {
|
|||
Uuid,
|
||||
Version,
|
||||
WebauthnAttestationCaList,
|
||||
AllowPrimaryCredFallback,
|
||||
|
||||
#[cfg(any(debug_assertions, test, feature = "test"))]
|
||||
NonExist,
|
||||
|
@ -385,6 +386,7 @@ impl Attribute {
|
|||
Attribute::Uuid => ATTR_UUID,
|
||||
Attribute::Version => ATTR_VERSION,
|
||||
Attribute::WebauthnAttestationCaList => ATTR_WEBAUTHN_ATTESTATION_CA_LIST,
|
||||
Attribute::AllowPrimaryCredFallback => ATTR_ALLOW_PRIMARY_CRED_FALLBACK,
|
||||
|
||||
#[cfg(any(debug_assertions, test, feature = "test"))]
|
||||
Attribute::NonExist => TEST_ATTR_NON_EXIST,
|
||||
|
@ -564,6 +566,7 @@ impl Attribute {
|
|||
ATTR_UUID => Attribute::Uuid,
|
||||
ATTR_VERSION => Attribute::Version,
|
||||
ATTR_WEBAUTHN_ATTESTATION_CA_LIST => Attribute::WebauthnAttestationCaList,
|
||||
ATTR_ALLOW_PRIMARY_CRED_FALLBACK => Attribute::AllowPrimaryCredFallback,
|
||||
|
||||
#[cfg(any(debug_assertions, test, feature = "test"))]
|
||||
TEST_ATTR_NON_EXIST => Attribute::NonExist,
|
||||
|
|
|
@ -212,6 +212,7 @@ pub const ATTR_USERPASSWORD: &str = "userpassword";
|
|||
pub const ATTR_UUID: &str = "uuid";
|
||||
pub const ATTR_VERSION: &str = "version";
|
||||
pub const ATTR_WEBAUTHN_ATTESTATION_CA_LIST: &str = "webauthn_attestation_ca_list";
|
||||
pub const ATTR_ALLOW_PRIMARY_CRED_FALLBACK: &str = "allow_primary_cred_fallback";
|
||||
|
||||
pub const OAUTH2_SCOPE_EMAIL: &str = ATTR_EMAIL;
|
||||
pub const OAUTH2_SCOPE_GROUPS: &str = "groups";
|
||||
|
|
|
@ -544,6 +544,62 @@ lazy_static! {
|
|||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8: 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,
|
||||
Attribute::AllowPrimaryCredFallback,
|
||||
],
|
||||
modify_removed_attrs: vec![
|
||||
Attribute::Class,
|
||||
Attribute::AuthSessionExpiry,
|
||||
Attribute::AuthPasswordMinimumLength,
|
||||
Attribute::CredentialTypeMinimum,
|
||||
Attribute::PrivilegeExpiry,
|
||||
Attribute::WebauthnAttestationCaList,
|
||||
Attribute::LimitSearchMaxResults,
|
||||
Attribute::LimitSearchMaxFilterTest,
|
||||
Attribute::AllowPrimaryCredFallback,
|
||||
],
|
||||
modify_present_attrs: vec![
|
||||
Attribute::Class,
|
||||
Attribute::AuthSessionExpiry,
|
||||
Attribute::AuthPasswordMinimumLength,
|
||||
Attribute::CredentialTypeMinimum,
|
||||
Attribute::PrivilegeExpiry,
|
||||
Attribute::WebauthnAttestationCaList,
|
||||
Attribute::LimitSearchMaxResults,
|
||||
Attribute::LimitSearchMaxFilterTest,
|
||||
Attribute::AllowPrimaryCredFallback,
|
||||
],
|
||||
modify_classes: vec![EntryClass::AccountPolicy,],
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref IDM_ACP_OAUTH2_MANAGE_DL4: BuiltinAcp = BuiltinAcp {
|
||||
classes: vec![
|
||||
|
|
|
@ -779,6 +779,16 @@ pub static ref SCHEMA_ATTR_LINKED_GROUP_DL8: SchemaAttribute = SchemaAttribute {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK_DL8: SchemaAttribute = SchemaAttribute {
|
||||
uuid: UUID_SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK,
|
||||
name: Attribute::AllowPrimaryCredFallback,
|
||||
description: "Allow fallback to primary password if no POSIX password exists".to_string(),
|
||||
|
||||
multivalue: false,
|
||||
syntax: SyntaxType::Boolean,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref SCHEMA_ATTR_CERTIFICATE_DL7: SchemaAttribute = SchemaAttribute {
|
||||
uuid: UUID_SCHEMA_ATTR_CERTIFICATE,
|
||||
name: Attribute::Certificate,
|
||||
|
@ -933,6 +943,25 @@ pub static ref SCHEMA_CLASS_ACCOUNT_POLICY_DL6: SchemaClass = SchemaClass {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref SCHEMA_CLASS_ACCOUNT_POLICY_DL8: 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,
|
||||
Attribute::PrivilegeExpiry,
|
||||
Attribute::AuthPasswordMinimumLength,
|
||||
Attribute::CredentialTypeMinimum,
|
||||
Attribute::WebauthnAttestationCaList,
|
||||
Attribute::LimitSearchMaxResults,
|
||||
Attribute::LimitSearchMaxFilterTest,
|
||||
Attribute::AllowPrimaryCredFallback,
|
||||
],
|
||||
systemsupplements: vec![Attribute::Group.into()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref SCHEMA_CLASS_ACCOUNT: SchemaClass = SchemaClass {
|
||||
uuid: UUID_SCHEMA_CLASS_ACCOUNT,
|
||||
name: EntryClass::Account.into(),
|
||||
|
|
|
@ -319,6 +319,8 @@ pub const UUID_SCHEMA_ATTR_LINKED_GROUP: Uuid = uuid!("00000000-0000-0000-0000-f
|
|||
pub const UUID_SCHEMA_ATTR_APPLICATION_PASSWORD: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000183");
|
||||
pub const UUID_SCHEMA_ATTR_CREATED_AT_CID: Uuid = uuid!("00000000-0000-0000-0000-ffff00000184");
|
||||
pub const UUID_SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000185");
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::time::Duration;
|
|||
use kanidm_proto::internal::{
|
||||
BackupCodesView, CredentialStatus, UatPurpose, UiHint, UserAuthToken,
|
||||
};
|
||||
use kanidm_proto::v1::{UatStatus, UatStatusState};
|
||||
use kanidm_proto::v1::{UatStatus, UatStatusState, UnixGroupToken, UnixUserToken};
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::prelude::{
|
||||
|
@ -12,13 +12,16 @@ use webauthn_rs::prelude::{
|
|||
};
|
||||
|
||||
use super::accountpolicy::ResolvedAccountPolicy;
|
||||
use super::group::{
|
||||
load_all_groups_from_account_entry, load_all_groups_from_account_entry_reduced,
|
||||
load_all_groups_from_account_entry_with_policy, Group, UnixGroup,
|
||||
};
|
||||
use crate::constants::UUID_ANONYMOUS;
|
||||
use crate::credential::softlock::CredSoftLockPolicy;
|
||||
use crate::credential::{apppwd::ApplicationPassword, Credential};
|
||||
use crate::entry::{Entry, EntryCommitted, EntryReduced, EntrySealed};
|
||||
use crate::event::SearchEvent;
|
||||
use crate::idm::application::Application;
|
||||
use crate::idm::group::Group;
|
||||
use crate::idm::ldap::{LdapBoundToken, LdapSession};
|
||||
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
|
||||
use crate::modify::{ModifyInvalid, ModifyList};
|
||||
|
@ -31,9 +34,10 @@ use sshkey_attest::proto::PublicKey as SshPublicKey;
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct UnixExtensions {
|
||||
ucred: Option<Credential>,
|
||||
_shell: Option<String>,
|
||||
shell: Option<String>,
|
||||
sshkeys: BTreeMap<String, SshPublicKey>,
|
||||
_gidnumber: u32,
|
||||
gidnumber: u32,
|
||||
groups: Vec<UnixGroup>,
|
||||
}
|
||||
|
||||
impl UnixExtensions {
|
||||
|
@ -71,7 +75,7 @@ pub struct Account {
|
|||
}
|
||||
|
||||
macro_rules! try_from_entry {
|
||||
($value:expr, $groups:expr) => {{
|
||||
($value:expr, $groups:expr, $unix_groups:expr) => {{
|
||||
// Check the classes
|
||||
if !$value.attribute_equality(Attribute::Class, &EntryClass::Account.to_partialvalue()) {
|
||||
return Err(OperationError::InvalidAccountState(format!(
|
||||
|
@ -177,11 +181,11 @@ macro_rules! try_from_entry {
|
|||
.get_ava_single_credential(Attribute::UnixPassword)
|
||||
.cloned();
|
||||
|
||||
let _shell = $value
|
||||
let shell = $value
|
||||
.get_ava_single_iutf8(Attribute::LoginShell)
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let _gidnumber = $value
|
||||
let gidnumber = $value
|
||||
.get_ava_single_uint32(Attribute::GidNumber)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
|
@ -190,11 +194,14 @@ macro_rules! try_from_entry {
|
|||
))
|
||||
})?;
|
||||
|
||||
let groups = $unix_groups;
|
||||
|
||||
Some(UnixExtensions {
|
||||
ucred,
|
||||
_shell,
|
||||
shell,
|
||||
sshkeys,
|
||||
_gidnumber,
|
||||
gidnumber,
|
||||
groups,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
@ -233,13 +240,18 @@ impl Account {
|
|||
self.unix_extn.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn primary(&self) -> Option<&Credential> {
|
||||
self.primary.as_ref()
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub(crate) fn try_from_entry_ro(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let groups = Group::try_from_account_entry(value, qs)?;
|
||||
try_from_entry!(value, groups)
|
||||
let (groups, unix_groups) = load_all_groups_from_account_entry(value, qs)?;
|
||||
|
||||
try_from_entry!(value, groups, unix_groups)
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
|
@ -250,8 +262,10 @@ impl Account {
|
|||
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))
|
||||
let ((groups, unix_groups), rap) =
|
||||
load_all_groups_from_account_entry_with_policy(value, qs)?;
|
||||
|
||||
try_from_entry!(value, groups, unix_groups).map(|acct| (acct, rap))
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
|
@ -259,8 +273,9 @@ impl Account {
|
|||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let groups = Group::try_from_account_entry(value, qs)?;
|
||||
try_from_entry!(value, groups)
|
||||
let (groups, unix_groups) = load_all_groups_from_account_entry(value, qs)?;
|
||||
|
||||
try_from_entry!(value, groups, unix_groups)
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
|
@ -268,8 +283,8 @@ impl Account {
|
|||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let groups = Group::try_from_account_entry_reduced(value, qs)?;
|
||||
try_from_entry!(value, groups)
|
||||
let (groups, unix_groups) = load_all_groups_from_account_entry_reduced(value, qs)?;
|
||||
try_from_entry!(value, groups, unix_groups)
|
||||
}
|
||||
|
||||
/// Given the session_id and other metadata, create a user authentication token
|
||||
|
@ -794,6 +809,35 @@ impl Account {
|
|||
let vap = Value::ApplicationPassword(ap);
|
||||
Ok(ModifyList::new_append(Attribute::ApplicationPassword, vap))
|
||||
}
|
||||
|
||||
pub(crate) fn to_unixusertoken(&self, ct: Duration) -> Result<UnixUserToken, OperationError> {
|
||||
let (gidnumber, shell, sshkeys, groups) = match &self.unix_extn {
|
||||
Some(ue) => {
|
||||
let sshkeys: Vec<String> = ue.sshkeys.keys().cloned().collect();
|
||||
(ue.gidnumber, ue.shell.clone(), sshkeys, ue.groups.clone())
|
||||
}
|
||||
None => {
|
||||
return Err(OperationError::InvalidAccountState(format!(
|
||||
"Missing class: {}",
|
||||
EntryClass::PosixAccount
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let groups: Vec<UnixGroupToken> = groups.iter().map(|g| g.to_unixgrouptoken()).collect();
|
||||
|
||||
Ok(UnixUserToken {
|
||||
name: self.name.clone(),
|
||||
spn: self.spn.clone(),
|
||||
displayname: self.displayname.clone(),
|
||||
gidnumber,
|
||||
uuid: self.uuid,
|
||||
shell: shell.clone(),
|
||||
groups,
|
||||
sshkeys,
|
||||
valid: self.is_within_valid_time(ct),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Need to also add a "to UserAuthToken" ...
|
||||
|
|
|
@ -12,6 +12,7 @@ pub(crate) struct AccountPolicy {
|
|||
webauthn_att_ca_list: Option<AttestationCaList>,
|
||||
limit_search_max_filter_test: Option<u64>,
|
||||
limit_search_max_results: Option<u64>,
|
||||
allow_primary_cred_fallback: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<&EntrySealedCommitted> for Option<AccountPolicy> {
|
||||
|
@ -51,6 +52,9 @@ impl From<&EntrySealedCommitted> for Option<AccountPolicy> {
|
|||
.get_ava_single_uint32(Attribute::LimitSearchMaxFilterTest)
|
||||
.map(|u| u as u64);
|
||||
|
||||
let allow_primary_cred_fallback =
|
||||
val.get_ava_single_bool(Attribute::AllowPrimaryCredFallback);
|
||||
|
||||
Some(AccountPolicy {
|
||||
privilege_expiry,
|
||||
authsession_expiry,
|
||||
|
@ -59,6 +63,7 @@ impl From<&EntrySealedCommitted> for Option<AccountPolicy> {
|
|||
webauthn_att_ca_list,
|
||||
limit_search_max_filter_test,
|
||||
limit_search_max_results,
|
||||
allow_primary_cred_fallback,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +78,7 @@ pub(crate) struct ResolvedAccountPolicy {
|
|||
webauthn_att_ca_list: Option<AttestationCaList>,
|
||||
limit_search_max_filter_test: Option<u64>,
|
||||
limit_search_max_results: Option<u64>,
|
||||
allow_primary_cred_fallback: Option<bool>,
|
||||
}
|
||||
|
||||
impl ResolvedAccountPolicy {
|
||||
|
@ -86,6 +92,7 @@ impl ResolvedAccountPolicy {
|
|||
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),
|
||||
allow_primary_cred_fallback: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,6 +109,7 @@ impl ResolvedAccountPolicy {
|
|||
webauthn_att_ca_list: None,
|
||||
limit_search_max_filter_test: None,
|
||||
limit_search_max_results: None,
|
||||
allow_primary_cred_fallback: None,
|
||||
};
|
||||
|
||||
iter.for_each(|acc_pol| {
|
||||
|
@ -152,6 +160,14 @@ impl ResolvedAccountPolicy {
|
|||
accumulate.webauthn_att_ca_list = Some(acc_pol_w_att_ca);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(allow_primary_cred_fallback) = acc_pol.allow_primary_cred_fallback {
|
||||
accumulate.allow_primary_cred_fallback =
|
||||
match accumulate.allow_primary_cred_fallback {
|
||||
Some(acc_fallback) => Some(allow_primary_cred_fallback && acc_fallback),
|
||||
None => Some(allow_primary_cred_fallback),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
accumulate
|
||||
|
@ -184,6 +200,10 @@ impl ResolvedAccountPolicy {
|
|||
pub(crate) fn limit_search_max_filter_test(&self) -> Option<u64> {
|
||||
self.limit_search_max_filter_test
|
||||
}
|
||||
|
||||
pub(crate) fn allow_primary_cred_fallback(&self) -> Option<bool> {
|
||||
self.allow_primary_cred_fallback
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -264,6 +284,7 @@ jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
|
|||
webauthn_att_ca_list: Some(att_ca_list_a),
|
||||
limit_search_max_filter_test: Some(10),
|
||||
limit_search_max_results: Some(10),
|
||||
allow_primary_cred_fallback: None,
|
||||
};
|
||||
|
||||
let mut att_ca_builder = AttestationCaListBuilder::new();
|
||||
|
@ -285,6 +306,7 @@ jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
|
|||
webauthn_att_ca_list: Some(att_ca_list_b),
|
||||
limit_search_max_filter_test: Some(5),
|
||||
limit_search_max_results: Some(15),
|
||||
allow_primary_cred_fallback: Some(false),
|
||||
};
|
||||
|
||||
let rap = ResolvedAccountPolicy::fold_from([policy_a, policy_b].into_iter());
|
||||
|
@ -295,6 +317,7 @@ jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
|
|||
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));
|
||||
assert_eq!(rap.allow_primary_cred_fallback(), Some(false));
|
||||
|
||||
let mut att_ca_builder = AttestationCaListBuilder::new();
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::iter;
|
||||
|
||||
use kanidm_proto::internal::{Group as ProtoGroup, UiHint};
|
||||
use kanidm_proto::v1::UnixGroupToken;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::accountpolicy::{AccountPolicy, ResolvedAccountPolicy};
|
||||
|
@ -8,14 +10,6 @@ use crate::entry::{Entry, EntryCommitted, EntryReduced, EntrySealed};
|
|||
use crate::prelude::*;
|
||||
use crate::value::PartialValue;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Group {
|
||||
spn: String,
|
||||
uuid: Uuid,
|
||||
// We'll probably add policy and claims later to this
|
||||
pub ui_hints: BTreeSet<UiHint>,
|
||||
}
|
||||
|
||||
macro_rules! entry_groups {
|
||||
($value:expr, $qs:expr) => {{
|
||||
match $value.get_ava_as_refuuid(Attribute::MemberOf) {
|
||||
|
@ -40,6 +34,119 @@ macro_rules! entry_groups {
|
|||
}};
|
||||
}
|
||||
|
||||
macro_rules! load_all_groups_from_iter {
|
||||
($value:expr, $group_iter:expr) => {{
|
||||
let mut groups: Vec<Group> = vec![];
|
||||
let mut unix_groups: Vec<UnixGroup> = vec![];
|
||||
|
||||
let is_unix_account = $value.attribute_equality(
|
||||
Attribute::Class,
|
||||
&EntryClass::PosixAccount.to_partialvalue(),
|
||||
);
|
||||
|
||||
// Setup the user private group
|
||||
let spn = $value.get_ava_single_proto_string(Attribute::Spn).ok_or(
|
||||
OperationError::InvalidAccountState(format!("Missing attribute: {}", Attribute::Spn)),
|
||||
)?;
|
||||
|
||||
let uuid = $value.get_uuid();
|
||||
|
||||
// We could allow ui hints on the user direct in the future?
|
||||
let ui_hints = BTreeSet::default();
|
||||
|
||||
groups.push(Group {
|
||||
spn: spn.clone(),
|
||||
uuid: uuid.clone(),
|
||||
ui_hints,
|
||||
});
|
||||
|
||||
if is_unix_account {
|
||||
let name = $value.get_ava_single_proto_string(Attribute::Name).ok_or(
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Name
|
||||
)),
|
||||
)?;
|
||||
|
||||
let gidnumber = $value.get_ava_single_uint32(Attribute::GidNumber).ok_or(
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::GidNumber
|
||||
)),
|
||||
)?;
|
||||
|
||||
unix_groups.push(UnixGroup {
|
||||
name,
|
||||
spn,
|
||||
gidnumber,
|
||||
uuid,
|
||||
});
|
||||
}
|
||||
|
||||
for group_entry in $group_iter {
|
||||
let group = Group::try_from_entry(group_entry.as_ref())?;
|
||||
groups.push(group);
|
||||
|
||||
if is_unix_account
|
||||
&& group_entry
|
||||
.attribute_equality(Attribute::Class, &EntryClass::PosixGroup.to_partialvalue())
|
||||
{
|
||||
let unix_group = UnixGroup::try_from_entry(group_entry.as_ref())?;
|
||||
unix_groups.push(unix_group);
|
||||
}
|
||||
}
|
||||
|
||||
(groups, unix_groups)
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) fn load_all_groups_from_account_entry<'a, T>(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
qs: &mut T,
|
||||
) -> Result<(Vec<Group>, Vec<UnixGroup>), OperationError>
|
||||
where
|
||||
T: QueryServerTransaction<'a>,
|
||||
{
|
||||
let group_iter = entry_groups!(value, qs);
|
||||
Ok(load_all_groups_from_iter!(value, group_iter))
|
||||
}
|
||||
|
||||
pub(crate) fn load_all_groups_from_account_entry_with_policy<'a, T>(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
qs: &mut T,
|
||||
) -> Result<((Vec<Group>, Vec<UnixGroup>), ResolvedAccountPolicy), OperationError>
|
||||
where
|
||||
T: QueryServerTransaction<'a>,
|
||||
{
|
||||
let group_iter = entry_groups!(value, qs);
|
||||
|
||||
let rap = ResolvedAccountPolicy::fold_from(group_iter.iter().filter_map(|entry| {
|
||||
let acc_pol: Option<AccountPolicy> = entry.as_ref().into();
|
||||
acc_pol
|
||||
}));
|
||||
|
||||
Ok((load_all_groups_from_iter!(value, group_iter), rap))
|
||||
}
|
||||
|
||||
pub(crate) fn load_all_groups_from_account_entry_reduced<'a, T>(
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
qs: &mut T,
|
||||
) -> Result<(Vec<Group>, Vec<UnixGroup>), OperationError>
|
||||
where
|
||||
T: QueryServerTransaction<'a>,
|
||||
{
|
||||
let group_iter = entry_groups!(value, qs);
|
||||
Ok(load_all_groups_from_iter!(value, group_iter))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Group {
|
||||
spn: String,
|
||||
uuid: Uuid,
|
||||
// We'll probably add policy and claims later to this
|
||||
pub ui_hints: BTreeSet<UiHint>,
|
||||
}
|
||||
|
||||
macro_rules! upg_from_account_e {
|
||||
($value:expr, $groups:expr) => {{
|
||||
// Setup the user private group
|
||||
|
@ -99,26 +206,6 @@ impl Group {
|
|||
upg_from_account_e!(value, groups)
|
||||
}
|
||||
|
||||
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(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
|
@ -164,3 +251,195 @@ impl Group {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct UnixGroup {
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
pub gidnumber: u32,
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
macro_rules! try_from_group_e {
|
||||
($value:expr) => {{
|
||||
// We could be looking at a user for their UPG, OR a true group.
|
||||
|
||||
if !(($value.attribute_equality(Attribute::Class, &EntryClass::Account.to_partialvalue())
|
||||
&& $value.attribute_equality(
|
||||
Attribute::Class,
|
||||
&EntryClass::PosixAccount.to_partialvalue(),
|
||||
))
|
||||
|| ($value.attribute_equality(Attribute::Class, &EntryClass::Group.to_partialvalue())
|
||||
&& $value.attribute_equality(
|
||||
Attribute::Class,
|
||||
&EntryClass::PosixGroup.to_partialvalue(),
|
||||
)))
|
||||
{
|
||||
return Err(OperationError::InvalidAccountState(format!(
|
||||
"Missing {}: {} && {} OR {} && {}",
|
||||
Attribute::Class,
|
||||
Attribute::Account,
|
||||
EntryClass::PosixAccount,
|
||||
Attribute::Group,
|
||||
EntryClass::PosixGroup,
|
||||
)));
|
||||
}
|
||||
|
||||
let name = $value
|
||||
.get_ava_single_iname(Attribute::Name)
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Name
|
||||
))
|
||||
})?;
|
||||
|
||||
let spn = $value
|
||||
.get_ava_single_proto_string(Attribute::Spn)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Spn
|
||||
))
|
||||
})?;
|
||||
|
||||
let uuid = $value.get_uuid();
|
||||
|
||||
let gidnumber = $value
|
||||
.get_ava_single_uint32(Attribute::GidNumber)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::GidNumber
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(UnixGroup {
|
||||
name,
|
||||
spn,
|
||||
gidnumber,
|
||||
uuid,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! try_from_account_group_e {
|
||||
($value:expr, $qs:expr) => {{
|
||||
// First synthesise the self-group from the account.
|
||||
// We have already checked these, but paranoia is better than
|
||||
// complacency.
|
||||
if !$value.attribute_equality(Attribute::Class, &EntryClass::Account.to_partialvalue()) {
|
||||
return Err(OperationError::InvalidAccountState(format!(
|
||||
"Missing class: {}",
|
||||
EntryClass::Account
|
||||
)));
|
||||
}
|
||||
|
||||
if !$value.attribute_equality(
|
||||
Attribute::Class,
|
||||
&EntryClass::PosixAccount.to_partialvalue(),
|
||||
) {
|
||||
return Err(OperationError::InvalidAccountState(format!(
|
||||
"Missing class: {}",
|
||||
EntryClass::PosixAccount
|
||||
)));
|
||||
}
|
||||
|
||||
let name = $value
|
||||
.get_ava_single_iname(Attribute::Name)
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Name
|
||||
))
|
||||
})?;
|
||||
|
||||
let spn = $value
|
||||
.get_ava_single_proto_string(Attribute::Spn)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Spn
|
||||
))
|
||||
})?;
|
||||
|
||||
let uuid = $value.get_uuid();
|
||||
|
||||
let gidnumber = $value
|
||||
.get_ava_single_uint32(Attribute::GidNumber)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::GidNumber
|
||||
))
|
||||
})?;
|
||||
|
||||
// This is the user private group.
|
||||
let upg = UnixGroup {
|
||||
name,
|
||||
spn,
|
||||
gidnumber,
|
||||
uuid,
|
||||
};
|
||||
|
||||
match $value.get_ava_as_refuuid(Attribute::MemberOf) {
|
||||
Some(riter) => {
|
||||
let f = filter!(f_and!([
|
||||
f_eq(Attribute::Class, EntryClass::PosixGroup.into()),
|
||||
f_eq(Attribute::Class, EntryClass::Group.into()),
|
||||
f_or(
|
||||
riter
|
||||
.map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
|
||||
.collect()
|
||||
)
|
||||
]));
|
||||
let group_entries: Vec<_> = $qs.internal_search(f)?;
|
||||
let groups: Result<Vec<_>, _> = iter::once(Ok(upg))
|
||||
.chain(
|
||||
group_entries
|
||||
.iter()
|
||||
.map(|e| UnixGroup::try_from_entry(e.as_ref())),
|
||||
)
|
||||
.collect();
|
||||
groups
|
||||
}
|
||||
None => {
|
||||
// No memberof, no groups!
|
||||
Ok(vec![upg])
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
impl UnixGroup {
|
||||
#[allow(dead_code)]
|
||||
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(crate) fn try_from_entry_reduced(
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
try_from_group_e!(value)
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_entry(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
try_from_group_e!(value)
|
||||
}
|
||||
|
||||
pub(crate) fn to_unixgrouptoken(&self) -> UnixGroupToken {
|
||||
UnixGroupToken {
|
||||
name: self.name.clone(),
|
||||
spn: self.spn.clone(),
|
||||
uuid: self.uuid,
|
||||
gidnumber: self.gidnumber,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ pub(crate) mod reauth;
|
|||
pub mod scim;
|
||||
pub mod server;
|
||||
pub mod serviceaccount;
|
||||
pub(crate) mod unix;
|
||||
|
||||
use crate::server::identity::Source;
|
||||
use compact_jwt::JwsCompact;
|
||||
|
|
|
@ -46,6 +46,7 @@ use crate::idm::event::{
|
|||
RegenerateRadiusSecretEvent, UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent,
|
||||
UnixUserTokenEvent,
|
||||
};
|
||||
use crate::idm::group::UnixGroup;
|
||||
use crate::idm::oauth2::{
|
||||
Oauth2ResourceServers, Oauth2ResourceServersReadTransaction,
|
||||
Oauth2ResourceServersWriteTransaction,
|
||||
|
@ -53,7 +54,6 @@ use crate::idm::oauth2::{
|
|||
use crate::idm::radius::RadiusAccount;
|
||||
use crate::idm::scim::SyncAccount;
|
||||
use crate::idm::serviceaccount::ServiceAccount;
|
||||
use crate::idm::unix::{UnixGroup, UnixUserAccount};
|
||||
use crate::idm::AuthState;
|
||||
use crate::prelude::*;
|
||||
use crate::server::keys::KeyProvidersTransaction;
|
||||
|
@ -1275,113 +1275,116 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn auth_with_unix_pass(
|
||||
&mut self,
|
||||
id: Uuid,
|
||||
cleartext: &str,
|
||||
ct: Duration,
|
||||
) -> Result<Option<Account>, OperationError> {
|
||||
let entry = match self.qs_read.internal_search_uuid(id) {
|
||||
Ok(entry) => entry,
|
||||
Err(e) => {
|
||||
admin_error!("Failed to start auth unix -> {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let (account, acp) =
|
||||
Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_read)?;
|
||||
|
||||
if !account.is_within_valid_time(ct) {
|
||||
security_info!("Account is expired or not yet valid.");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let cred = if acp.allow_primary_cred_fallback() == Some(true) {
|
||||
account
|
||||
.unix_extn()
|
||||
.and_then(|extn| extn.ucred())
|
||||
.or_else(|| account.primary())
|
||||
} else {
|
||||
account.unix_extn().and_then(|extn| extn.ucred())
|
||||
};
|
||||
|
||||
let (cred, cred_id, cred_slock_policy) = match cred {
|
||||
None => {
|
||||
if acp.allow_primary_cred_fallback() == Some(true) {
|
||||
security_info!("Account does not have a POSIX or primary password configured.");
|
||||
} else {
|
||||
security_info!("Account does not have a POSIX password configured.");
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
Some(cred) => (cred, cred.uuid, cred.softlock_policy()),
|
||||
};
|
||||
|
||||
// The credential should only ever be a password
|
||||
let Ok(password) = cred.password_ref() else {
|
||||
error!("User's UNIX or primary credential is not a password, can't authenticate!");
|
||||
return Err(OperationError::InvalidState);
|
||||
};
|
||||
|
||||
let slock_ref = {
|
||||
let softlock_read = self.softlocks.read();
|
||||
if let Some(slock_ref) = softlock_read.get(&cred_id) {
|
||||
slock_ref.clone()
|
||||
} else {
|
||||
let _session_ticket = self.session_ticket.acquire().await;
|
||||
let mut softlock_write = self.softlocks.write();
|
||||
let slock = Arc::new(Mutex::new(CredSoftLock::new(cred_slock_policy)));
|
||||
softlock_write.insert(cred_id, slock.clone());
|
||||
softlock_write.commit();
|
||||
slock
|
||||
}
|
||||
};
|
||||
|
||||
let mut slock = slock_ref.lock().await;
|
||||
|
||||
slock.apply_time_step(ct);
|
||||
|
||||
if !slock.is_valid() {
|
||||
security_info!("Account is softlocked.");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Check the provided password against the stored hash
|
||||
let valid = password.verify(cleartext).map_err(|e| {
|
||||
error!(crypto_err = ?e);
|
||||
e.into()
|
||||
})?;
|
||||
|
||||
if !valid {
|
||||
// Update it.
|
||||
slock.record_failure(ct);
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
security_info!("Successfully authenticated with unix (or primary) password");
|
||||
if password.requires_upgrade() {
|
||||
self.async_tx
|
||||
.send(DelayedAction::UnixPwUpgrade(UnixPasswordUpgrade {
|
||||
target_uuid: id,
|
||||
existing_password: cleartext.to_string(),
|
||||
}))
|
||||
.map_err(|_| {
|
||||
admin_error!("failed to queue delayed action - unix password upgrade");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(Some(account))
|
||||
}
|
||||
|
||||
pub async fn auth_unix(
|
||||
&mut self,
|
||||
uae: &UnixUserAuthEvent,
|
||||
ct: Duration,
|
||||
) -> Result<Option<UnixUserToken>, OperationError> {
|
||||
// Get the entry/target we are working on.
|
||||
let account = self
|
||||
.qs_read
|
||||
.internal_search_uuid(uae.target)
|
||||
.and_then(|account_entry| {
|
||||
UnixUserAccount::try_from_entry_ro(account_entry.as_ref(), &mut self.qs_read)
|
||||
})
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to start auth unix -> {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
if !account.is_within_valid_time(ct) {
|
||||
security_info!("Account is not within valid time period");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let maybe_slock_ref = match account.unix_cred_uuid_and_policy() {
|
||||
Some((cred_uuid, policy)) => {
|
||||
let softlock_read = self.softlocks.read();
|
||||
let slock_ref = match softlock_read.get(&cred_uuid) {
|
||||
Some(slock_ref) => slock_ref.clone(),
|
||||
None => {
|
||||
let _session_ticket = self.session_ticket.acquire().await;
|
||||
let mut softlock_write = self.softlocks.write();
|
||||
let slock = Arc::new(Mutex::new(CredSoftLock::new(policy)));
|
||||
softlock_write.insert(cred_uuid, slock.clone());
|
||||
softlock_write.commit();
|
||||
slock
|
||||
}
|
||||
};
|
||||
Some(slock_ref)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let maybe_slock = if let Some(s) = maybe_slock_ref.as_ref() {
|
||||
Some(s.lock().await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let maybe_valid = if let Some(mut slock) = maybe_slock {
|
||||
// Apply the current time.
|
||||
slock.apply_time_step(ct);
|
||||
// Now check the results
|
||||
if slock.is_valid() {
|
||||
Some(slock)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Validate the unix_pw - this checks the account/cred lock states.
|
||||
let res = if let Some(mut slock) = maybe_valid {
|
||||
// Account is unlocked, can proceed.
|
||||
account
|
||||
.verify_unix_credential(uae.cleartext.as_str(), &self.async_tx, ct)
|
||||
.inspect(|res| {
|
||||
if res.is_none() {
|
||||
// Update it.
|
||||
slock.record_failure(ct);
|
||||
};
|
||||
})
|
||||
} else {
|
||||
// Account is slocked!
|
||||
security_info!("Account is softlocked.");
|
||||
Ok(None)
|
||||
};
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn token_auth_ldap(
|
||||
&mut self,
|
||||
lae: &LdapTokenAuthEvent,
|
||||
ct: Duration,
|
||||
) -> Result<Option<LdapBoundToken>, OperationError> {
|
||||
match self.validate_and_parse_token_to_token(&lae.token, ct)? {
|
||||
Token::UserAuthToken(uat) => {
|
||||
let spn = uat.spn.clone();
|
||||
Ok(Some(LdapBoundToken {
|
||||
session_id: uat.session_id,
|
||||
spn,
|
||||
effective_session: LdapSession::UserAuthToken(uat),
|
||||
}))
|
||||
}
|
||||
Token::ApiToken(apit, entry) => {
|
||||
let spn = entry
|
||||
.get_ava_single_proto_string(Attribute::Spn)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: spn".to_string())
|
||||
})?;
|
||||
|
||||
Ok(Some(LdapBoundToken {
|
||||
session_id: apit.token_id,
|
||||
spn,
|
||||
effective_session: LdapSession::ApiToken(apit),
|
||||
}))
|
||||
}
|
||||
}
|
||||
Ok(self
|
||||
.auth_with_unix_pass(uae.target, &uae.cleartext, ct)
|
||||
.await?
|
||||
.and_then(|acc| acc.to_unixusertoken(ct).ok()))
|
||||
}
|
||||
|
||||
pub async fn auth_ldap(
|
||||
|
@ -1389,14 +1392,14 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
lae: &LdapAuthEvent,
|
||||
ct: Duration,
|
||||
) -> Result<Option<LdapBoundToken>, OperationError> {
|
||||
if lae.target == UUID_ANONYMOUS {
|
||||
let account_entry = self.qs_read.internal_search_uuid(lae.target).map_err(|e| {
|
||||
admin_error!("Failed to start auth ldap -> {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
// if anonymous
|
||||
if lae.target == UUID_ANONYMOUS {
|
||||
let account = Account::try_from_entry_ro(account_entry.as_ref(), &mut self.qs_read)?;
|
||||
|
||||
// Check if the anon account has been locked.
|
||||
if !account.is_within_valid_time(ct) {
|
||||
security_info!("Account is not within valid time period");
|
||||
|
@ -1422,58 +1425,13 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
security_info!("Bind not allowed through Unix passwords.");
|
||||
return Ok(None);
|
||||
}
|
||||
let account =
|
||||
UnixUserAccount::try_from_entry_ro(account_entry.as_ref(), &mut self.qs_read)?;
|
||||
|
||||
if !account.is_within_valid_time(ct) {
|
||||
security_info!("Account is not within valid time period");
|
||||
return Ok(None);
|
||||
}
|
||||
let auth = self
|
||||
.auth_with_unix_pass(lae.target, &lae.cleartext, ct)
|
||||
.await?;
|
||||
|
||||
let maybe_slock_ref = match account.unix_cred_uuid_and_policy() {
|
||||
Some((cred_uuid, policy)) => {
|
||||
let softlock_read = self.softlocks.read();
|
||||
let slock_ref = match softlock_read.get(&cred_uuid) {
|
||||
Some(slock_ref) => slock_ref.clone(),
|
||||
None => {
|
||||
let _session_ticket = self.session_ticket.acquire().await;
|
||||
let mut softlock_write = self.softlocks.write();
|
||||
let slock = Arc::new(Mutex::new(CredSoftLock::new(policy)));
|
||||
softlock_write.insert(cred_uuid, slock.clone());
|
||||
softlock_write.commit();
|
||||
slock
|
||||
}
|
||||
};
|
||||
Ok(slock_ref)
|
||||
}
|
||||
None => Err(false),
|
||||
};
|
||||
|
||||
let maybe_slock = match maybe_slock_ref.as_ref() {
|
||||
Ok(s) => Ok(s.lock().await),
|
||||
Err(cred_state) => Err(cred_state),
|
||||
};
|
||||
|
||||
let maybe_valid = match maybe_slock {
|
||||
Ok(mut slock) => {
|
||||
// Apply the current time.
|
||||
slock.apply_time_step(ct);
|
||||
// Now check the results
|
||||
if slock.is_valid() {
|
||||
Ok(slock)
|
||||
} else {
|
||||
Err(true)
|
||||
}
|
||||
}
|
||||
Err(cred_state) => Err(*cred_state),
|
||||
};
|
||||
|
||||
match maybe_valid {
|
||||
Ok(mut slock) => {
|
||||
if account
|
||||
.verify_unix_credential(lae.cleartext.as_str(), &self.async_tx, ct)?
|
||||
.is_some()
|
||||
{
|
||||
match auth {
|
||||
Some(account) => {
|
||||
let session_id = Uuid::new_v4();
|
||||
security_info!(
|
||||
"Starting session {} for {} {}",
|
||||
|
@ -1487,20 +1445,41 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
session_id,
|
||||
effective_session: LdapSession::UnixBind(account.uuid),
|
||||
}))
|
||||
} else {
|
||||
// PW failure, update softlock.
|
||||
slock.record_failure(ct);
|
||||
Ok(None)
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
Err(true) => {
|
||||
security_info!("Account is softlocked.");
|
||||
Ok(None)
|
||||
}
|
||||
Err(false) => {
|
||||
security_info!("Account does not have a configured posix password.");
|
||||
Ok(None)
|
||||
|
||||
pub async fn token_auth_ldap(
|
||||
&mut self,
|
||||
lae: &LdapTokenAuthEvent,
|
||||
ct: Duration,
|
||||
) -> Result<Option<LdapBoundToken>, OperationError> {
|
||||
match self.validate_and_parse_token_to_token(&lae.token, ct)? {
|
||||
Token::UserAuthToken(uat) => {
|
||||
let spn = uat.spn.clone();
|
||||
Ok(Some(LdapBoundToken {
|
||||
session_id: uat.session_id,
|
||||
spn,
|
||||
effective_session: LdapSession::UserAuthToken(uat),
|
||||
}))
|
||||
}
|
||||
Token::ApiToken(apit, entry) => {
|
||||
let spn = entry
|
||||
.get_ava_single_proto_string(Attribute::Spn)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Spn
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Some(LdapBoundToken {
|
||||
session_id: apit.token_id,
|
||||
spn,
|
||||
effective_session: LdapSession::ApiToken(apit),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1518,6 +1497,35 @@ impl<'a> IdmServerTransaction<'a> for IdmServerProxyReadTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn gen_password_mod(
|
||||
cleartext: &str,
|
||||
crypto_policy: &CryptoPolicy,
|
||||
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
|
||||
let new_cred = Credential::new_password_only(crypto_policy, cleartext)?;
|
||||
let cred_value = Value::new_credential("unix", new_cred);
|
||||
Ok(ModifyList::new_purge_and_set(
|
||||
Attribute::UnixPassword,
|
||||
cred_value,
|
||||
))
|
||||
}
|
||||
|
||||
fn gen_password_upgrade_mod(
|
||||
unix_cred: &Credential,
|
||||
cleartext: &str,
|
||||
crypto_policy: &CryptoPolicy,
|
||||
) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
|
||||
if let Some(new_cred) = unix_cred.upgrade_password(crypto_policy, cleartext)? {
|
||||
let cred_value = Value::new_credential("primary", new_cred);
|
||||
Ok(Some(ModifyList::new_purge_and_set(
|
||||
Attribute::UnixPassword,
|
||||
cred_value,
|
||||
)))
|
||||
} else {
|
||||
// No action, not the same pw
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||
pub fn jws_public_jwk(&mut self, key_id: &str) -> Result<Jwk, OperationError> {
|
||||
self.qs_read
|
||||
|
@ -1556,9 +1564,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
let account = self
|
||||
.qs_read
|
||||
.impersonate_search_uuid(uute.target, &uute.ident)
|
||||
.and_then(|account_entry| {
|
||||
UnixUserAccount::try_from_entry_ro(&account_entry, &mut self.qs_read)
|
||||
})
|
||||
.and_then(|account_entry| Account::try_from_entry_ro(&account_entry, &mut self.qs_read))
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to start unix user token -> {:?}", e);
|
||||
e
|
||||
|
@ -1579,7 +1585,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
admin_error!("Failed to start unix group token {:?}", e);
|
||||
e
|
||||
})?;
|
||||
group.to_unixgrouptoken()
|
||||
Ok(group.to_unixgrouptoken())
|
||||
}
|
||||
|
||||
pub fn get_credentialstatus(
|
||||
|
@ -1786,13 +1792,20 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
.internal_search_uuid(pce.target)
|
||||
.and_then(|account_entry| {
|
||||
// Assert the account is unix and valid.
|
||||
UnixUserAccount::try_from_entry_rw(&account_entry, &mut self.qs_write)
|
||||
Account::try_from_entry_rw(&account_entry, &mut self.qs_write)
|
||||
})
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to start set unix account password {:?}", e);
|
||||
e
|
||||
})?;
|
||||
// Ask if tis all good - this step checks pwpolicy and such
|
||||
|
||||
// Account is not a unix account
|
||||
if account.unix_extn().is_none() {
|
||||
return Err(OperationError::InvalidAccountState(format!(
|
||||
"Missing class: {}",
|
||||
EntryClass::PosixAccount
|
||||
)));
|
||||
}
|
||||
|
||||
// Deny the change if the account is anonymous!
|
||||
if account.is_anonymous() {
|
||||
|
@ -1800,9 +1813,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
return Err(OperationError::SystemProtectedObject);
|
||||
}
|
||||
|
||||
let modlist = account
|
||||
.gen_password_mod(pce.cleartext.as_str(), self.crypto_policy)
|
||||
.map_err(|e| {
|
||||
let modlist =
|
||||
gen_password_mod(pce.cleartext.as_str(), self.crypto_policy).map_err(|e| {
|
||||
admin_error!(?e, "Unable to generate password change modlist");
|
||||
e
|
||||
})?;
|
||||
|
@ -1969,24 +1981,37 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
.qs_write
|
||||
.internal_search_uuid(pwu.target_uuid)
|
||||
.and_then(|account_entry| {
|
||||
UnixUserAccount::try_from_entry_rw(&account_entry, &mut self.qs_write)
|
||||
Account::try_from_entry_rw(&account_entry, &mut self.qs_write)
|
||||
})
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to start unix pw upgrade -> {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
let maybe_modlist =
|
||||
account.gen_password_upgrade_mod(pwu.existing_password.as_str(), self.crypto_policy)?;
|
||||
let cred = match account.unix_extn() {
|
||||
Some(ue) => ue.ucred(),
|
||||
None => {
|
||||
return Err(OperationError::InvalidAccountState(format!(
|
||||
"Missing class: {}",
|
||||
EntryClass::PosixAccount
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(modlist) = maybe_modlist {
|
||||
self.qs_write.internal_modify(
|
||||
// No credential no problem
|
||||
let Some(cred) = cred else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let maybe_modlist =
|
||||
gen_password_upgrade_mod(cred, pwu.existing_password.as_str(), self.crypto_policy)?;
|
||||
|
||||
match maybe_modlist {
|
||||
Some(modlist) => self.qs_write.internal_modify(
|
||||
&filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(pwu.target_uuid))),
|
||||
&modlist,
|
||||
)
|
||||
} else {
|
||||
// No action needed, it's probably been changed/updated already.
|
||||
Ok(())
|
||||
),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2188,9 +2213,10 @@ mod tests {
|
|||
use crate::idm::delayed::{AuthSessionRecord, DelayedAction};
|
||||
use crate::idm::event::{AuthEvent, AuthResult};
|
||||
use crate::idm::event::{
|
||||
PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
|
||||
LdapAuthEvent, PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
|
||||
UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent, UnixUserTokenEvent,
|
||||
};
|
||||
|
||||
use crate::idm::server::{IdmServer, IdmServerTransaction, Token};
|
||||
use crate::idm::AuthState;
|
||||
use crate::modify::{Modify, ModifyList};
|
||||
|
@ -4125,4 +4151,132 @@ mod tests {
|
|||
|
||||
// Any checks?
|
||||
}
|
||||
|
||||
async fn idm_fallback_auth_fixture(
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
has_posix_password: bool,
|
||||
allow_primary_cred_fallback: Option<bool>,
|
||||
expected: Option<()>,
|
||||
) {
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
let target_uuid = Uuid::new_v4();
|
||||
let p = CryptoPolicy::minimum();
|
||||
|
||||
{
|
||||
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
|
||||
|
||||
if let Some(allow_primary_cred_fallback) = allow_primary_cred_fallback {
|
||||
idms_prox_write
|
||||
.qs_write
|
||||
.internal_modify_uuid(
|
||||
UUID_IDM_ALL_ACCOUNTS,
|
||||
&ModifyList::new_purge_and_set(
|
||||
Attribute::AllowPrimaryCredFallback,
|
||||
Value::new_bool(allow_primary_cred_fallback),
|
||||
),
|
||||
)
|
||||
.expect("Unable to change default session exp");
|
||||
}
|
||||
|
||||
let mut e = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Uuid, Value::Uuid(target_uuid)),
|
||||
(Attribute::Name, Value::new_iname("kevin")),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Kevin")),
|
||||
(Attribute::Class, EntryClass::PosixAccount.to_value()),
|
||||
(
|
||||
Attribute::PrimaryCredential,
|
||||
Value::Cred(
|
||||
"primary".to_string(),
|
||||
Credential::new_password_only(&p, "banana").unwrap()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if has_posix_password {
|
||||
e.add_ava(
|
||||
Attribute::UnixPassword,
|
||||
Value::Cred(
|
||||
"unix".to_string(),
|
||||
Credential::new_password_only(&p, "kampai").unwrap(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let ce = CreateEvent::new_internal(vec![e]);
|
||||
let cr = idms_prox_write.qs_write.create(&ce);
|
||||
assert!(cr.is_ok());
|
||||
idms_prox_write.commit().expect("Must not fail");
|
||||
}
|
||||
|
||||
let result = idms
|
||||
.auth()
|
||||
.await
|
||||
.unwrap()
|
||||
.auth_ldap(
|
||||
&LdapAuthEvent {
|
||||
target: target_uuid,
|
||||
cleartext: if has_posix_password {
|
||||
"kampai".to_string()
|
||||
} else {
|
||||
"banana".to_string()
|
||||
},
|
||||
},
|
||||
ct,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
if let Some(_) = expected {
|
||||
assert!(result.unwrap().is_some());
|
||||
} else {
|
||||
assert!(result.unwrap().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[idm_test]
|
||||
async fn test_idm_fallback_auth_no_pass_none_fallback(
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
) {
|
||||
idm_fallback_auth_fixture(idms, _idms_delayed, false, None, None).await;
|
||||
}
|
||||
#[idm_test]
|
||||
async fn test_idm_fallback_auth_pass_none_fallback(
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
) {
|
||||
idm_fallback_auth_fixture(idms, _idms_delayed, true, None, Some(())).await;
|
||||
}
|
||||
#[idm_test]
|
||||
async fn test_idm_fallback_auth_no_pass_true_fallback(
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
) {
|
||||
idm_fallback_auth_fixture(idms, _idms_delayed, false, Some(true), Some(())).await;
|
||||
}
|
||||
#[idm_test]
|
||||
async fn test_idm_fallback_auth_pass_true_fallback(
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
) {
|
||||
idm_fallback_auth_fixture(idms, _idms_delayed, true, Some(true), Some(())).await;
|
||||
}
|
||||
#[idm_test]
|
||||
async fn test_idm_fallback_auth_no_pass_false_fallback(
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
) {
|
||||
idm_fallback_auth_fixture(idms, _idms_delayed, false, Some(false), None).await;
|
||||
}
|
||||
#[idm_test]
|
||||
async fn test_idm_fallback_auth_pass_false_fallback(
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
) {
|
||||
idm_fallback_auth_fixture(idms, _idms_delayed, true, Some(false), Some(())).await;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,518 +0,0 @@
|
|||
use std::iter;
|
||||
// use crossbeam::channel::Sender;
|
||||
use std::time::Duration;
|
||||
|
||||
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::mpsc::UnboundedSender as Sender;
|
||||
use uuid::Uuid;
|
||||
|
||||
use kanidm_lib_crypto::CryptoPolicy;
|
||||
|
||||
use crate::credential::softlock::CredSoftLockPolicy;
|
||||
use crate::credential::Credential;
|
||||
use crate::idm::delayed::{DelayedAction, UnixPasswordUpgrade};
|
||||
use crate::modify::{ModifyInvalid, ModifyList};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct UnixUserAccount {
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
pub displayname: String,
|
||||
pub uuid: Uuid,
|
||||
pub valid_from: Option<OffsetDateTime>,
|
||||
pub expire: Option<OffsetDateTime>,
|
||||
pub radius_secret: Option<String>,
|
||||
pub mail: Vec<String>,
|
||||
|
||||
cred: Option<Credential>,
|
||||
pub shell: Option<String>,
|
||||
pub sshkeys: Vec<String>,
|
||||
pub gidnumber: u32,
|
||||
pub groups: Vec<UnixGroup>,
|
||||
}
|
||||
|
||||
macro_rules! try_from_entry {
|
||||
($value:expr, $groups:expr) => {{
|
||||
if !$value.attribute_equality(Attribute::Class, &EntryClass::Account.to_partialvalue()) {
|
||||
return Err(OperationError::InvalidAccountState(
|
||||
"Missing class: account".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !$value.attribute_equality(
|
||||
Attribute::Class,
|
||||
&EntryClass::PosixAccount.to_partialvalue(),
|
||||
) {
|
||||
return Err(OperationError::InvalidAccountState(
|
||||
"Missing class: posixaccount".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let name = $value
|
||||
.get_ava_single_iname(Attribute::Name)
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Name
|
||||
))
|
||||
})?;
|
||||
|
||||
let spn = $value
|
||||
.get_ava_single_proto_string(Attribute::Spn)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Spn
|
||||
))
|
||||
})?;
|
||||
|
||||
let uuid = $value.get_uuid();
|
||||
|
||||
let displayname = $value
|
||||
.get_ava_single_utf8(Attribute::DisplayName)
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::DisplayName
|
||||
))
|
||||
})?;
|
||||
|
||||
let gidnumber = $value
|
||||
.get_ava_single_uint32(Attribute::GidNumber)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::GidNumber
|
||||
))
|
||||
})?;
|
||||
|
||||
let shell = $value
|
||||
.get_ava_single_iutf8(Attribute::LoginShell)
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let sshkeys = $value
|
||||
.get_ava_iter_sshpubkeys(Attribute::SshPublicKey)
|
||||
.map(|i| i.map(|s| s.to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let cred = $value
|
||||
.get_ava_single_credential(Attribute::UnixPassword)
|
||||
.cloned();
|
||||
|
||||
let radius_secret = $value
|
||||
.get_ava_single_secret(Attribute::RadiusSecret)
|
||||
.map(str::to_string);
|
||||
|
||||
let mail = $value
|
||||
.get_ava_iter_mail(Attribute::Mail)
|
||||
.map(|i| i.map(str::to_string).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let valid_from = $value.get_ava_single_datetime(Attribute::AccountValidFrom);
|
||||
|
||||
let expire = $value.get_ava_single_datetime(Attribute::AccountExpire);
|
||||
|
||||
Ok(UnixUserAccount {
|
||||
name,
|
||||
spn,
|
||||
uuid,
|
||||
displayname,
|
||||
gidnumber,
|
||||
shell,
|
||||
sshkeys,
|
||||
groups: $groups,
|
||||
cred,
|
||||
valid_from,
|
||||
expire,
|
||||
radius_secret,
|
||||
mail,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
impl UnixUserAccount {
|
||||
pub(crate) fn try_from_entry_rw(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let groups = UnixGroup::try_from_account_entry_rw(value, qs)?;
|
||||
try_from_entry!(value, groups)
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_entry_ro(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let groups = UnixGroup::try_from_account_entry_ro(value, qs)?;
|
||||
try_from_entry!(value, groups)
|
||||
}
|
||||
|
||||
pub(crate) fn to_unixusertoken(&self, ct: Duration) -> Result<UnixUserToken, OperationError> {
|
||||
let groups: Result<Vec<_>, _> = self.groups.iter().map(|g| g.to_unixgrouptoken()).collect();
|
||||
let groups = groups?;
|
||||
|
||||
Ok(UnixUserToken {
|
||||
name: self.name.clone(),
|
||||
spn: self.spn.clone(),
|
||||
displayname: self.displayname.clone(),
|
||||
gidnumber: self.gidnumber,
|
||||
uuid: self.uuid,
|
||||
shell: self.shell.clone(),
|
||||
groups,
|
||||
sshkeys: self.sshkeys.clone(),
|
||||
valid: self.is_within_valid_time(ct),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unix_cred_uuid_and_policy(&self) -> Option<(Uuid, CredSoftLockPolicy)> {
|
||||
self.cred
|
||||
.as_ref()
|
||||
.map(|cred| (cred.uuid, cred.softlock_policy()))
|
||||
}
|
||||
|
||||
pub fn is_anonymous(&self) -> bool {
|
||||
self.uuid == UUID_ANONYMOUS
|
||||
}
|
||||
|
||||
pub(crate) fn gen_password_mod(
|
||||
&self,
|
||||
cleartext: &str,
|
||||
crypto_policy: &CryptoPolicy,
|
||||
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
|
||||
let ncred = Credential::new_password_only(crypto_policy, cleartext)?;
|
||||
let vcred = Value::new_credential("unix", ncred);
|
||||
Ok(ModifyList::new_purge_and_set(
|
||||
Attribute::UnixPassword,
|
||||
vcred,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn gen_password_upgrade_mod(
|
||||
&self,
|
||||
cleartext: &str,
|
||||
crypto_policy: &CryptoPolicy,
|
||||
) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
|
||||
match &self.cred {
|
||||
// Change the cred
|
||||
Some(ucred) => {
|
||||
if let Some(ncred) = ucred.upgrade_password(crypto_policy, cleartext)? {
|
||||
let vcred = Value::new_credential("primary", ncred);
|
||||
Ok(Some(ModifyList::new_purge_and_set(
|
||||
Attribute::UnixPassword,
|
||||
vcred,
|
||||
)))
|
||||
} else {
|
||||
// No action, not the same pw
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
// Nothing to do.
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_within_valid_time(&self, ct: Duration) -> bool {
|
||||
let cot = OffsetDateTime::UNIX_EPOCH + ct;
|
||||
|
||||
let vmin = if let Some(vft) = &self.valid_from {
|
||||
// If current time greater than start time window
|
||||
vft < &cot
|
||||
} else {
|
||||
// We have no time, not expired.
|
||||
true
|
||||
};
|
||||
let vmax = if let Some(ext) = &self.expire {
|
||||
// If exp greater than ct then expired.
|
||||
&cot < ext
|
||||
} else {
|
||||
// If not present, we are not expired
|
||||
true
|
||||
};
|
||||
// Mix the results
|
||||
vmin && vmax
|
||||
}
|
||||
|
||||
// Get related inputs, such as account name, email, etc.
|
||||
pub fn related_inputs(&self) -> Vec<&str> {
|
||||
let mut inputs = Vec::with_capacity(4 + self.mail.len());
|
||||
self.mail.iter().for_each(|m| {
|
||||
inputs.push(m.as_str());
|
||||
});
|
||||
inputs.push(self.name.as_str());
|
||||
inputs.push(self.spn.as_str());
|
||||
inputs.push(self.displayname.as_str());
|
||||
if let Some(s) = self.radius_secret.as_deref() {
|
||||
inputs.push(s);
|
||||
}
|
||||
inputs
|
||||
}
|
||||
|
||||
pub(crate) fn verify_unix_credential(
|
||||
&self,
|
||||
cleartext: &str,
|
||||
async_tx: &Sender<DelayedAction>,
|
||||
ct: Duration,
|
||||
) -> Result<Option<UnixUserToken>, OperationError> {
|
||||
// Is the cred locked?
|
||||
// NOW checked by the caller!
|
||||
|
||||
/*
|
||||
if !self.is_within_valid_time(ct) {
|
||||
lsecurity!(au, "Account is not within valid time period");
|
||||
return Ok(None);
|
||||
}
|
||||
*/
|
||||
|
||||
// is the cred some or none?
|
||||
match &self.cred {
|
||||
Some(cred) => {
|
||||
cred.password_ref().and_then(|pw| {
|
||||
let valid = pw.verify(cleartext).map_err(|e| {
|
||||
error!(crypto_err = ?e);
|
||||
e.into()
|
||||
})?;
|
||||
if valid {
|
||||
security_info!("Successful unix cred handling");
|
||||
if pw.requires_upgrade() {
|
||||
async_tx
|
||||
.send(DelayedAction::UnixPwUpgrade(UnixPasswordUpgrade {
|
||||
target_uuid: self.uuid,
|
||||
existing_password: cleartext.to_string(),
|
||||
}))
|
||||
.map_err(|_| {
|
||||
admin_error!(
|
||||
"failed to queue delayed action - unix password upgrade"
|
||||
);
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
}
|
||||
|
||||
// Technically this means we check the times twice, but that doesn't
|
||||
// seem like a big deal when we want to short cut return on invalid.
|
||||
Some(self.to_unixusertoken(ct)).transpose()
|
||||
} else {
|
||||
// Failed to auth
|
||||
security_info!("Failed unix cred handling (denied)");
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
}
|
||||
// They don't have a unix cred, fail the auth.
|
||||
None => {
|
||||
security_info!("Failed unix cred handling (no cred present)");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct UnixGroup {
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
pub gidnumber: u32,
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
macro_rules! try_from_group_e {
|
||||
($value:expr) => {{
|
||||
// We could be looking at a user for their UPG, OR a true group.
|
||||
|
||||
if !(($value.attribute_equality(Attribute::Class, &EntryClass::Account.to_partialvalue())
|
||||
&& $value.attribute_equality(
|
||||
Attribute::Class,
|
||||
&EntryClass::PosixAccount.to_partialvalue(),
|
||||
))
|
||||
|| ($value.attribute_equality(Attribute::Class, &EntryClass::Group.to_partialvalue())
|
||||
&& $value.attribute_equality(
|
||||
Attribute::Class,
|
||||
&EntryClass::PosixGroup.to_partialvalue(),
|
||||
)))
|
||||
{
|
||||
return Err(OperationError::InvalidAccountState(format!(
|
||||
"Missing {}: {} && {} OR {} && {}",
|
||||
Attribute::Class,
|
||||
Attribute::Account,
|
||||
EntryClass::PosixAccount,
|
||||
Attribute::Group,
|
||||
EntryClass::PosixGroup,
|
||||
)));
|
||||
}
|
||||
|
||||
let name = $value
|
||||
.get_ava_single_iname(Attribute::Name)
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Name
|
||||
))
|
||||
})?;
|
||||
|
||||
let spn = $value
|
||||
.get_ava_single_proto_string(Attribute::Spn)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Spn
|
||||
))
|
||||
})?;
|
||||
|
||||
let uuid = $value.get_uuid();
|
||||
|
||||
let gidnumber = $value
|
||||
.get_ava_single_uint32(Attribute::GidNumber)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::GidNumber
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(UnixGroup {
|
||||
name,
|
||||
spn,
|
||||
gidnumber,
|
||||
uuid,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! try_from_account_group_e {
|
||||
($value:expr, $qs:expr) => {{
|
||||
// First synthesise the self-group from the account.
|
||||
// We have already checked these, but paranoia is better than
|
||||
// complacency.
|
||||
if !$value.attribute_equality(Attribute::Class, &EntryClass::Account.to_partialvalue()) {
|
||||
return Err(OperationError::InvalidAccountState(format!(
|
||||
"Missing class: {}",
|
||||
EntryClass::Account
|
||||
)));
|
||||
}
|
||||
|
||||
if !$value.attribute_equality(
|
||||
Attribute::Class,
|
||||
&EntryClass::PosixAccount.to_partialvalue(),
|
||||
) {
|
||||
return Err(OperationError::InvalidAccountState(format!(
|
||||
"Missing class: {}",
|
||||
EntryClass::PosixAccount
|
||||
)));
|
||||
}
|
||||
|
||||
let name = $value
|
||||
.get_ava_single_iname(Attribute::Name)
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Name
|
||||
))
|
||||
})?;
|
||||
|
||||
let spn = $value
|
||||
.get_ava_single_proto_string(Attribute::Spn)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::Spn
|
||||
))
|
||||
})?;
|
||||
|
||||
let uuid = $value.get_uuid();
|
||||
|
||||
let gidnumber = $value
|
||||
.get_ava_single_uint32(Attribute::GidNumber)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState(format!(
|
||||
"Missing attribute: {}",
|
||||
Attribute::GidNumber
|
||||
))
|
||||
})?;
|
||||
|
||||
// This is the user private group.
|
||||
let upg = UnixGroup {
|
||||
name,
|
||||
spn,
|
||||
gidnumber,
|
||||
uuid,
|
||||
};
|
||||
|
||||
match $value.get_ava_as_refuuid(Attribute::MemberOf) {
|
||||
Some(riter) => {
|
||||
let f = filter!(f_and!([
|
||||
f_eq(Attribute::Class, EntryClass::PosixGroup.into()),
|
||||
f_eq(Attribute::Class, EntryClass::Group.into()),
|
||||
f_or(
|
||||
riter
|
||||
.map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
|
||||
.collect()
|
||||
)
|
||||
]));
|
||||
let group_entries: Vec<_> = $qs.internal_search(f)?;
|
||||
let groups: Result<Vec<_>, _> = iter::once(Ok(upg))
|
||||
.chain(
|
||||
group_entries
|
||||
.iter()
|
||||
.map(|e| UnixGroup::try_from_entry(e.as_ref())),
|
||||
)
|
||||
.collect();
|
||||
groups
|
||||
}
|
||||
None => {
|
||||
// No memberof, no groups!
|
||||
Ok(vec![upg])
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
impl UnixGroup {
|
||||
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(crate) fn try_from_account_entry_ro(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
try_from_account_group_e!(value, qs)
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn try_from_account_entry_red_ro(
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
qs: &mut QueryServerReadTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
try_from_account_group_e!(au, value, qs)
|
||||
}
|
||||
*/
|
||||
|
||||
pub(crate) fn try_from_entry_reduced(
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
try_from_group_e!(value)
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_entry(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
try_from_group_e!(value)
|
||||
}
|
||||
|
||||
pub(crate) fn to_unixgrouptoken(&self) -> Result<UnixGroupToken, OperationError> {
|
||||
Ok(UnixGroupToken {
|
||||
name: self.name.clone(),
|
||||
spn: self.spn.clone(),
|
||||
uuid: self.uuid,
|
||||
gidnumber: self.gidnumber,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -584,6 +584,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
|
||||
SCHEMA_CLASS_PERSON_DL8.clone().into(),
|
||||
SCHEMA_CLASS_DOMAIN_INFO_DL8.clone().into(),
|
||||
SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK_DL8.clone().into(),
|
||||
SCHEMA_CLASS_ACCOUNT_POLICY_DL8.clone().into(),
|
||||
];
|
||||
|
||||
idm_schema_classes
|
||||
|
@ -608,6 +610,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
BUILTIN_IDM_MAIL_SERVERS_DL8.clone().try_into()?,
|
||||
IDM_ACP_MAIL_SERVERS_DL8.clone().into(),
|
||||
IDM_ACP_DOMAIN_ADMIN_DL8.clone().into(),
|
||||
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8.clone().into(),
|
||||
];
|
||||
|
||||
idm_data
|
||||
|
|
|
@ -11,6 +11,7 @@ impl GroupAccountPolicyOpt {
|
|||
| GroupAccountPolicyOpt::WebauthnAttestationCaList { copt, .. }
|
||||
| GroupAccountPolicyOpt::LimitSearchMaxResults { copt, .. }
|
||||
| GroupAccountPolicyOpt::LimitSearchMaxFilterTest { copt, .. }
|
||||
| GroupAccountPolicyOpt::AllowPrimaryCredFallback { copt, .. }
|
||||
| GroupAccountPolicyOpt::PrivilegedSessionExpiry { copt, .. } => copt.debug,
|
||||
}
|
||||
}
|
||||
|
@ -114,6 +115,17 @@ impl GroupAccountPolicyOpt {
|
|||
println!("Updated search maximum filter test limit.");
|
||||
}
|
||||
}
|
||||
GroupAccountPolicyOpt::AllowPrimaryCredFallback { name, allow, copt } => {
|
||||
let client = copt.to_client(OpType::Write).await;
|
||||
if let Err(e) = client
|
||||
.group_account_policy_allow_primary_cred_fallback(name, *allow)
|
||||
.await
|
||||
{
|
||||
handle_client_error(e, copt.output_mode);
|
||||
} else {
|
||||
println!("Updated primary credential fallback policy.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -235,6 +235,16 @@ pub enum GroupAccountPolicyOpt {
|
|||
#[clap(flatten)]
|
||||
copt: CommonOpt,
|
||||
},
|
||||
/// Sets whether during login the primary password can be used
|
||||
/// as a fallback if no posix password has been defined
|
||||
#[clap(name = "allow-primary-cred-fallback")]
|
||||
AllowPrimaryCredFallback {
|
||||
name: String,
|
||||
#[clap(name = "allow", action = clap::ArgAction::Set)]
|
||||
allow: bool,
|
||||
#[clap(flatten)]
|
||||
copt: CommonOpt,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
|
|
Loading…
Reference in a new issue