20240810 application passwords ()

Add the server side components for application passwords. This adds the needed datatypes and handling via the ldap components.

Admin tools will be in a follow up PR. 

Signed-off-by: Samuel Cabrero <scabrero@suse.de>
Co-authored-by: Samuel Cabrero <scabrero@suse.de>
This commit is contained in:
Firstyear 2024-08-20 16:44:37 +10:00 committed by GitHub
parent 9f4cc984db
commit 239f4594dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 2571 additions and 229 deletions

View file

@ -22,8 +22,8 @@ based authentication and remove the ability to bind with the UNIX password.
# User experience
The administrator configures two applications on their Kanidm instance. One is "mail" for a generic
SMTP+IMAP service. The other is HTTP basic auth to a legacy web server. Then the administrator
configures which users will be able to use application passwords for each application.
SMTP+IMAP service. The other is HTTP basic auth to a legacy web server. Applications have a linked
group to determine which users will be able to use application passwords for each application.
The mail services and web services are configured to point to Kanidm's LDAP gateway with a
customized search base DN.
@ -79,10 +79,10 @@ A new class for applications will be added. Each application will have a single
only members of this group will be able to bind with the application password for the associated
application.
Creating a new application will not create an associated group automatically. It will be the
administrator who will configure the association after creating the application and optionally a new
group. It will be possible to associate `idm_all_persons` to an application. Removing an application
will not delete the associated group nor its members.
Creating a new application will not create an associated group automatically, an existing group
must be provided. It will be possible to associate `idm_all_persons` to an application. Removing
an application will not delete the associated group nor its members. It will be possible to change
the linked group after creation.
When users are removed from a group associated to an application all of their application passwords
for the application will be disabled.
@ -150,6 +150,9 @@ We do not need temporary locks or holds - users can delete and recreate as neede
Since application passwords are related to applications, on delete of an application all entries
that have a bound application password should be removed from user accounts.
Trying to delete a group linked to an application will raise an error showing the user that
something still requires it.
### Access controls
The "Application administrators" group will manage the applications, and applications will allow

View file

@ -691,12 +691,10 @@ impl KanidmClient {
}
#[cfg(any(test, debug_assertions))]
if !matching {
if !std::env::var("KANIDM_DEV_YOLO").is_ok() {
eprintln!("⚠️ You're in debug/dev mode, so we're going to quit here.");
eprintln!("If you really must do this, set KANIDM_DEV_YOLO=1");
std::process::exit(1);
}
if !matching && std::env::var("KANIDM_DEV_YOLO").is_err() {
eprintln!("⚠️ You're in debug/dev mode, so we're going to quit here.");
eprintln!("If you really must do this, set KANIDM_DEV_YOLO=1");
std::process::exit(1);
}
// Check is done once, mark as no longer needing to occur

View file

@ -108,7 +108,7 @@ impl Into<OperationError> for CryptoError {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
#[allow(non_camel_case_types)]
pub enum DbPasswordV1 {
TPM_ARGON2ID {

View file

@ -67,6 +67,7 @@ pub const ATTR_ACP_RECEIVER: &str = "acp_receiver";
pub const ATTR_ACP_SEARCH_ATTR: &str = "acp_search_attr";
pub const ATTR_ACP_TARGET_SCOPE: &str = "acp_targetscope";
pub const ATTR_API_TOKEN_SESSION: &str = "api_token_session";
pub const ATTR_APPLICATION_PASSWORD: &str = "application_password";
pub const ATTR_ATTESTED_PASSKEYS: &str = "attested_passkeys";
pub const ATTR_ATTR: &str = "attr";
pub const ATTR_ATTRIBUTENAME: &str = "attributename";
@ -129,6 +130,7 @@ pub const ATTR_KEY_PROVIDER: &str = "key_provider";
pub const ATTR_LAST_MODIFIED_CID: &str = "last_modified_cid";
pub const ATTR_LDAP_ALLOW_UNIX_PW_BIND: &str = "ldap_allow_unix_pw_bind";
pub const ATTR_LEGALNAME: &str = "legalname";
pub const ATTR_LINKEDGROUP: &str = "linked_group";
pub const ATTR_LOGINSHELL: &str = "loginshell";
pub const ATTR_MAIL: &str = "mail";
pub const ATTR_MAY: &str = "may";

View file

@ -147,9 +147,13 @@ pub enum OperationError {
// Value Errors
VL0001ValueSshPublicKeyString,
// LDAP Errors
LD0001AnonymousNotAllowed,
// DB low level errors.
DB0001MismatchedRestoreVersion,
DB0002MismatchedRestoreVersion,
DB0003FilterResolveCacheBuild,
// SCIM
SC0001IncomingSshPublicKey,
@ -211,6 +215,10 @@ pub enum OperationError {
// Plugins
PL0001GidOverlapsSystemRange,
// Web UI
UI0001ChallengeSerialisation,
UI0002InvalidState,
}
impl PartialEq for OperationError {
@ -307,12 +315,14 @@ impl OperationError {
Self::VS0004CertificatePublicKeyDigest |
Self::VS0005CertificatePublicKeyDigest => Some("The certificates public key is unabled to be digested."),
Self::VL0001ValueSshPublicKeyString => None,
Self::LD0001AnonymousNotAllowed => Some("Anonymous is not allowed to access LDAP with this method."),
Self::SC0001IncomingSshPublicKey => None,
Self::MG0001InvalidReMigrationLevel => None,
Self::MG0002RaiseDomainLevelExceedsMaximum => None,
Self::MG0003ServerPhaseInvalidForMigration => None,
Self::DB0001MismatchedRestoreVersion => None,
Self::DB0002MismatchedRestoreVersion => None,
Self::DB0003FilterResolveCacheBuild => None,
Self::MG0004DomainLevelInDevelopment => None,
Self::MG0005GidConstraintsNotMet => None,
Self::MG0006SKConstraintsNotMet => Some("Migration Constraints Not Met - Security Keys should not be present."),
@ -364,6 +374,8 @@ impl OperationError {
Self::KP0043KeyObjectJweA128GCMEncryption => None,
Self::KP0044KeyObjectJwsPublicJwk => None,
Self::PL0001GidOverlapsSystemRange => None,
Self::UI0001ChallengeSerialisation => Some("The WebAuthn challenge was unable to be serialised."),
Self::UI0002InvalidState => Some("The credential update process returned an invalid state transition."),
}
}
}

View file

@ -207,7 +207,7 @@ pub async fn view_reauth_get(
.into_response(),
};
return Ok(res);
Ok(res)
}
pub async fn view_index_get(

View file

@ -141,13 +141,11 @@ async fn oauth2_auth_req(
match maybe_jar {
Ok(jar) => (jar, Redirect::to("/ui/login")).into_response(),
Err(err_code) => {
return HtmlTemplate(UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
})
.into_response();
}
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
})
.into_response(),
}
}
Err(Oauth2Error::AccessDenied) => {

View file

@ -27,9 +27,10 @@ use kanidm_proto::internal::{
use crate::https::extractors::VerifiedClientInformation;
use crate::https::middleware::KOpId;
use crate::https::views::errors::HtmxError;
use crate::https::views::HtmlTemplate;
use crate::https::ServerState;
use super::{HtmlTemplate, UnrecoverableErrorView};
#[derive(Template)]
#[template(path = "credentials_reset_form.html")]
struct ResetCredFormView {
@ -66,7 +67,7 @@ pub(crate) struct ResetTokenParam {
}
#[derive(Template)]
#[template(path = "cred_update/add_password_partial.html")]
#[template(path = "credential_update_add_password_partial.html")]
struct AddPasswordPartial {
check_res: PwdCheckResult,
}
@ -97,7 +98,7 @@ pub(crate) struct NewTotp {
}
#[derive(Template)]
#[template(path = "cred_update/add_passkey_partial.html")]
#[template(path = "credential_update_add_passkey_partial.html")]
struct AddPasskeyPartial {
// Passkey challenge for adding a new passkey
challenge: String,
@ -169,7 +170,7 @@ impl Display for TotpFeedback {
}
#[derive(Template)]
#[template(path = "cred_update/add_totp_partial.html")]
#[template(path = "credential_update_add_totp_partial.html")]
struct AddTotpPartial {
check_res: TotpCheckResult,
}
@ -367,17 +368,25 @@ pub(crate) async fn view_new_passkey(
let response = match cu_status.mfaregstate {
CURegState::Passkey(chal) | CURegState::AttestedPasskey(chal) => {
HtmlTemplate(AddPasskeyPartial {
challenge: serde_json::to_string(&chal).unwrap(),
class: init_form.class,
})
.into_response()
if let Ok(challenge) = serde_json::to_string(&chal) {
HtmlTemplate(AddPasskeyPartial {
challenge,
class: init_form.class,
})
.into_response()
} else {
HtmlTemplate(UnrecoverableErrorView {
err_code: OperationError::UI0001ChallengeSerialisation,
operation_id: kopid.eventid,
})
.into_response()
}
}
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
HtmxError::new(&kopid, OperationError::Backend).into_response(),
)
.into_response(),
_ => HtmlTemplate(UnrecoverableErrorView {
err_code: OperationError::UI0002InvalidState,
operation_id: kopid.eventid,
})
.into_response(),
};
let passkey_init_trigger =
@ -657,7 +666,7 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
..
} = cu_status;
return CredResetPartialView {
CredResetPartialView {
ext_cred_portal,
warnings,
attested_passkeys_state,
@ -666,19 +675,19 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
passkeys,
primary_state,
primary,
};
}
}
fn get_cu_partial_response(cu_status: CUStatus) -> Response {
let credentials_update_partial = get_cu_partial(cu_status);
return (
(
HxPushUrl(Uri::from_static("/ui/reset")),
HxRetarget("#credentialUpdateDynamicSection".to_string()),
HxReselect("#credentialUpdateDynamicSection".to_string()),
HxReswap(SwapOption::OuterHtml),
HtmlTemplate(credentials_update_partial),
)
.into_response();
.into_response()
}
fn get_cu_response(domain: String, cu_status: CUStatus) -> Response {
@ -710,34 +719,3 @@ async fn get_cu_session(jar: CookieJar) -> Result<CUSessionToken, Response> {
Err((StatusCode::FORBIDDEN, Redirect::to("/ui/reset")).into_response())
};
}
// Any filter defined in the module `filters` is accessible in your template.
mod filters {
pub fn blank_if<T: std::fmt::Display>(
implicit_arg: T,
condition: bool,
) -> ::askama::Result<String> {
blank_iff(implicit_arg, &condition)
}
pub fn ternary<T: std::fmt::Display, F: std::fmt::Display>(
implicit_arg: &bool,
true_case: T,
false_case: F,
) -> ::askama::Result<String> {
if *implicit_arg {
Ok(format!("{true_case}"))
} else {
Ok(format!("{false_case}"))
}
}
pub fn blank_iff<T: std::fmt::Display>(
implicit_arg: T,
condition: &bool,
) -> ::askama::Result<String> {
return if *condition {
Ok("".into())
} else {
Ok(format!("{implicit_arg}"))
};
}
}

View file

@ -7,8 +7,12 @@
(% if let PwdCheckResult::Failure with { pwd_equal, warnings } = check_res %)
(% let pwd_equal = pwd_equal.clone() %)
(% let potentially_invalid_input_class = "is-invalid"|blank_if(warnings.len() == 0) %)
(% let potentially_invalid_reinput_class = "is-invalid"|blank_iff(pwd_equal) %)
(% if !warnings.is_empty() %)
(% let potentially_invalid_input_class = "is-invalid" %)
(% endif %)
(% if pwd_equal %)
(% let potentially_invalid_reinput_class = "is-invalid" %)
(% endif %)
(% endif %)
<label for="new-password" class="form-label">Enter New Password</label>

View file

@ -23,8 +23,12 @@
(% if let TotpCheckResult::Failure with { wrong_code, broken_app, warnings } = check_res %)
(% let wrong_code = wrong_code.clone() %)
(% let broken_app = broken_app.clone() %)
(% let potentially_invalid_name_class = "is-invalid"|blank_if(warnings.len() == 0) %)
(% let potentially_invalid_check_class = "is-invalid"|blank_iff(wrong_code) %)
(% if !warnings.is_empty() %)
(% let potentially_invalid_name_class = "is-invalid" %)
(% endif %)
(% if wrong_code %)
(% let potentially_invalid_check_class = "is-invalid" %)
(% endif %)
(% endif %)
<label for="new-totp-name" class="form-label">Enter a name for your TOTP</label>

View file

@ -23,7 +23,11 @@
name="token"
autofocus
aria-describedby="unknown-reset-token-validation-feedback"
class='form-control (( "is-invalid"|blank_iff(!wrong_code) ))'
(% if wrong_code %)
class='form-control is-invalid'
(% else %)
class='form-control'
(% endif %)
>
(% if wrong_code %)
<div id="unknown-reset-token-validation-feedback" class="invalid-feedback">
@ -45,4 +49,4 @@
</button>
</p>
</main>
(% endblock %)
(% endblock %)

View file

@ -15,8 +15,12 @@
<hr class="my-4" >
(% for warning in warnings %)
(% let is_danger = [CURegWarning::WebauthnAttestationUnsatisfiable, CURegWarning::Unsatisfiable].contains(warning) %)
(% if is_danger %)
<div class='alert alert-danger' role="alert">
(% else %)
<div class='alert alert-warning' role="alert">
(% endif %)
<div class='alert alert-(( is_danger|ternary("danger", "warning") ))' role="alert">
(% match warning %)
(% when CURegWarning::MfaRequired %)
Multi-Factor Authentication is required for your account. Either add TOTP or remove your password in favour of passkeys to submit.
@ -97,7 +101,9 @@
type="submit"
hx-post="/ui/api/cu_commit"
hx-boost="false"
(( "disabled"|blank_if(warnings.len() == 0) ))
(% if !warnings.is_empty() %)
disabled
(% endif %)
>Submit Changes</button>
</span>
</div>

View file

@ -2,6 +2,8 @@
set -e
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# This script based on the developer readme and allows you to run a test server.
if [ -z "$KANI_CARGO_OPTS" ]; then
@ -20,14 +22,14 @@ fi
mkdir -p "${KANI_TMP}"/client_ca
CONFIG_FILE=${CONFIG_FILE:="../../examples/insecure_server.toml"}
CONFIG_FILE=${CONFIG_FILE:="${SCRIPT_DIR}/../../examples/insecure_server.toml"}
if [ ! -f "${CONFIG_FILE}" ]; then
SCRIPT_DIR="$(dirname -a "$0")"
echo "Couldn't find configuration file at ${CONFIG_FILE}, please ensure you're running this script from its base directory (${SCRIPT_DIR})."
exit 1
fi
pushd "${SCRIPT_DIR}" > /dev/null 2>&1
if [ -n "${1}" ]; then
COMMAND=$*
#shellcheck disable=SC2086
@ -38,4 +40,4 @@ else
#shellcheck disable=SC2086
cargo run ${KANI_CARGO_OPTS} --bin kanidmd -- server -c "${CONFIG_FILE}"
fi
popd > /dev/null 2>&1

View file

@ -337,16 +337,6 @@ pub struct DbValueCredV1 {
pub data: DbCred,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum DbApiToken {
V1 {
#[serde(rename = "u")]
uuid: Uuid,
#[serde(rename = "s")]
secret: DbPasswordV1,
},
}
#[derive(Serialize, Deserialize, Debug)]
pub enum DbValuePasskeyV1 {
V4 { u: Uuid, t: String, k: PasskeyV4 },
@ -663,6 +653,20 @@ pub enum DbValueCertificate {
V1 { certificate_der: Vec<u8> },
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum DbValueApplicationPassword {
V1 {
#[serde(rename = "u")]
refer: Uuid,
#[serde(rename = "a")]
application_refer: Uuid,
#[serde(rename = "l")]
label: String,
#[serde(rename = "p")]
password: DbPasswordV1,
},
}
#[derive(Serialize, Deserialize, Debug)]
pub enum DbValueV1 {
#[serde(rename = "U8")]
@ -822,6 +826,8 @@ pub enum DbValueSetV2 {
HexString(Vec<String>),
#[serde(rename = "X509")]
Certificate(Vec<DbValueCertificate>),
#[serde(rename = "AP")]
ApplicationPassword(Vec<DbValueApplicationPassword>),
}
impl DbValueSetV2 {
@ -874,6 +880,7 @@ impl DbValueSetV2 {
DbValueSetV2::WebauthnAttestationCaList { ca_list } => ca_list.len(),
DbValueSetV2::KeyInternal(set) => set.len(),
DbValueSetV2::Certificate(set) => set.len(),
DbValueSetV2::ApplicationPassword(set) => set.len(),
}
}

View file

@ -1076,7 +1076,7 @@ lazy_static! {
lazy_static! {
pub static ref IDM_ACP_SELF_READ_V1: BuiltinAcp = BuiltinAcp {
name: "idm_acp_self_read",
uuid: UUID_IDM_ACP_SELF_READ_V1,
uuid: UUID_IDM_ACP_SELF_READ,
description:
"Builtin IDM Control for self read - required for whoami and many other functions",
classes: vec![
@ -1111,6 +1111,45 @@ lazy_static! {
};
}
lazy_static! {
pub static ref IDM_ACP_SELF_READ_DL8: BuiltinAcp = BuiltinAcp {
name: "idm_acp_self_read",
uuid: UUID_IDM_ACP_SELF_READ,
description:
"Builtin IDM Control for self read - required for whoami and many other functions",
classes: vec![
EntryClass::Object,
EntryClass::AccessControlProfile,
EntryClass::AccessControlSearch,
],
receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_ALL_ACCOUNTS]),
target: BuiltinAcpTarget::Filter(ProtoFilter::SelfUuid),
search_attrs: vec![
Attribute::Class,
Attribute::Name,
Attribute::Spn,
Attribute::DisplayName,
Attribute::LegalName,
Attribute::Class,
Attribute::MemberOf,
Attribute::Mail,
Attribute::RadiusSecret,
Attribute::GidNumber,
Attribute::LoginShell,
Attribute::Uuid,
Attribute::SyncParentUuid,
Attribute::AccountExpire,
Attribute::AccountValidFrom,
Attribute::PrimaryCredential,
Attribute::UserAuthTokenSession,
Attribute::PassKeys,
Attribute::AttestedPasskeys,
Attribute::ApplicationPassword,
],
..Default::default()
};
}
lazy_static! {
pub static ref IDM_ACP_SELF_WRITE_V1: BuiltinAcp = BuiltinAcp{
name: "idm_acp_self_write",
@ -1133,6 +1172,7 @@ lazy_static! {
Attribute::PassKeys,
Attribute::AttestedPasskeys,
Attribute::UserAuthTokenSession,
Attribute::ApplicationPassword,
],
modify_present_attrs: vec![
Attribute::DisplayName,
@ -1143,6 +1183,7 @@ lazy_static! {
Attribute::UnixPassword,
Attribute::PassKeys,
Attribute::AttestedPasskeys,
Attribute::ApplicationPassword,
],
..Default::default()
};
@ -1181,6 +1222,42 @@ lazy_static! {
};
}
lazy_static! {
pub static ref IDM_ACP_SELF_WRITE_DL8: BuiltinAcp = BuiltinAcp{
name: "idm_acp_self_write",
uuid: UUID_IDM_ACP_SELF_WRITE_V1,
classes: vec![
EntryClass::Object,
EntryClass::AccessControlProfile,
EntryClass::AccessControlModify,
],
description: "Builtin IDM Control for self write - required for people to update their own credentials in line with best practices.",
receiver: BuiltinAcpReceiver::Group ( vec![UUID_IDM_ALL_PERSONS] ),
target: BuiltinAcpTarget::Filter(ProtoFilter::SelfUuid),
modify_removed_attrs: vec![
Attribute::RadiusSecret,
Attribute::PrimaryCredential,
Attribute::SshPublicKey,
Attribute::UnixPassword,
Attribute::PassKeys,
Attribute::AttestedPasskeys,
Attribute::UserAuthTokenSession,
Attribute::ApplicationPassword,
],
modify_present_attrs: vec![
Attribute::RadiusSecret,
Attribute::PrimaryCredential,
Attribute::SshPublicKey,
Attribute::UnixPassword,
Attribute::PassKeys,
Attribute::AttestedPasskeys,
Attribute::ApplicationPassword,
Attribute::ApplicationPassword,
],
..Default::default()
};
}
lazy_static! {
pub static ref IDM_ACP_SELF_NAME_WRITE_V1: BuiltinAcp = BuiltinAcp{
name: "idm_acp_self_name_write",
@ -2054,3 +2131,129 @@ lazy_static! {
..Default::default()
};
}
lazy_static! {
pub static ref IDM_ACP_APPLICATION_MANAGE_DL8: BuiltinAcp = BuiltinAcp{
classes: vec![
EntryClass::Object,
EntryClass::AccessControlProfile,
EntryClass::AccessControlCreate,
EntryClass::AccessControlDelete,
EntryClass::AccessControlModify,
EntryClass::AccessControlSearch
],
name: "idm_acp_application_manage",
uuid: UUID_IDM_ACP_APPLICATION_MANAGE,
description: "Builtin IDM Control for creating and deleting applications in the directory",
receiver: BuiltinAcpReceiver::Group ( vec![UUID_IDM_APPLICATION_ADMINS] ),
// Any application
target: BuiltinAcpTarget::Filter( ProtoFilter::And(vec![
match_class_filter!(EntryClass::Application),
FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
])),
search_attrs: vec![
Attribute::Class,
Attribute::Uuid,
Attribute::Name,
Attribute::Description,
Attribute::DisplayName,
Attribute::Mail,
Attribute::UnixPassword,
Attribute::ApiTokenSession,
Attribute::UserAuthTokenSession,
Attribute::LinkedGroup,
Attribute::EntryManagedBy,
],
create_attrs: vec![
Attribute::Class,
Attribute::Uuid,
Attribute::Name,
Attribute::Description,
Attribute::DisplayName,
Attribute::Mail,
Attribute::LinkedGroup,
Attribute::EntryManagedBy,
],
create_classes: vec![
EntryClass::Object,
EntryClass::Account,
EntryClass::ServiceAccount,
EntryClass::Application,
],
modify_present_attrs: vec![
Attribute::Name,
Attribute::Description,
Attribute::DisplayName,
Attribute::Mail,
Attribute::UnixPassword,
Attribute::ApiTokenSession,
Attribute::LinkedGroup,
Attribute::EntryManagedBy,
],
modify_removed_attrs: vec![
Attribute::Name,
Attribute::Description,
Attribute::DisplayName,
Attribute::Mail,
Attribute::UnixPassword,
Attribute::ApiTokenSession,
Attribute::UserAuthTokenSession,
Attribute::LinkedGroup,
Attribute::EntryManagedBy,
],
..Default::default()
};
}
lazy_static! {
pub static ref IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8: BuiltinAcp = BuiltinAcp {
classes: vec![
EntryClass::Object,
EntryClass::AccessControlProfile,
EntryClass::AccessControlModify,
EntryClass::AccessControlSearch
],
name: "idm_acp_application_entry_manager",
uuid: UUID_IDM_ACP_APPLICATION_ENTRY_MANAGER,
description: "Builtin IDM Control for allowing EntryManager to read and modify applications",
receiver: BuiltinAcpReceiver::EntryManager,
// Applications that belong to the Entry Manager.
target: BuiltinAcpTarget::Filter( ProtoFilter::And(vec![
match_class_filter!(EntryClass::Application),
FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
])),
search_attrs: vec![
Attribute::Class,
Attribute::Uuid,
Attribute::Name,
Attribute::DisplayName,
Attribute::Mail,
Attribute::UnixPassword,
Attribute::ApiTokenSession,
Attribute::UserAuthTokenSession,
Attribute::Description,
Attribute::LinkedGroup,
Attribute::EntryManagedBy,
],
modify_present_attrs: vec![
Attribute::Name,
Attribute::Description,
Attribute::DisplayName,
Attribute::Mail,
Attribute::UnixPassword,
Attribute::ApiTokenSession,
Attribute::LinkedGroup,
],
modify_removed_attrs: vec![
Attribute::Name,
Attribute::Description,
Attribute::DisplayName,
Attribute::Mail,
Attribute::UnixPassword,
Attribute::ApiTokenSession,
Attribute::UserAuthTokenSession,
Attribute::LinkedGroup,
],
..Default::default()
};
}

View file

@ -52,6 +52,7 @@ pub enum Attribute {
AcpSearchAttr,
AcpTargetScope,
ApiTokenSession,
ApplicationPassword,
AttestedPasskeys,
Attr,
AttributeName,
@ -116,6 +117,7 @@ pub enum Attribute {
LegalName,
LimitSearchMaxResults,
LimitSearchMaxFilterTest,
LinkedGroup,
LoginShell,
Mail,
May,
@ -244,6 +246,7 @@ impl<'a> TryFrom<&'a str> for Attribute {
ATTR_ACP_SEARCH_ATTR => Attribute::AcpSearchAttr,
ATTR_ACP_TARGET_SCOPE => Attribute::AcpTargetScope,
ATTR_API_TOKEN_SESSION => Attribute::ApiTokenSession,
ATTR_APPLICATION_PASSWORD => Attribute::ApplicationPassword,
ATTR_ATTESTED_PASSKEYS => Attribute::AttestedPasskeys,
ATTR_ATTR => Attribute::Attr,
ATTR_ATTRIBUTENAME => Attribute::AttributeName,
@ -305,6 +308,7 @@ impl<'a> TryFrom<&'a str> for Attribute {
ATTR_LDAP_KEYS => Attribute::LdapKeys,
ATTR_SSH_PUBLICKEY => Attribute::SshPublicKey,
ATTR_LEGALNAME => Attribute::LegalName,
ATTR_LINKEDGROUP => Attribute::LinkedGroup,
ATTR_LOGINSHELL => Attribute::LoginShell,
ATTR_LIMIT_SEARCH_MAX_RESULTS => Attribute::LimitSearchMaxResults,
ATTR_LIMIT_SEARCH_MAX_FILTER_TEST => Attribute::LimitSearchMaxFilterTest,
@ -420,6 +424,7 @@ impl From<Attribute> for &'static str {
Attribute::AcpSearchAttr => ATTR_ACP_SEARCH_ATTR,
Attribute::AcpTargetScope => ATTR_ACP_TARGET_SCOPE,
Attribute::ApiTokenSession => ATTR_API_TOKEN_SESSION,
Attribute::ApplicationPassword => ATTR_APPLICATION_PASSWORD,
Attribute::AttestedPasskeys => ATTR_ATTESTED_PASSKEYS,
Attribute::Attr => ATTR_ATTR,
Attribute::AttributeName => ATTR_ATTRIBUTENAME,
@ -483,6 +488,7 @@ impl From<Attribute> for &'static str {
Attribute::LegalName => ATTR_LEGALNAME,
Attribute::LimitSearchMaxResults => ATTR_LIMIT_SEARCH_MAX_RESULTS,
Attribute::LimitSearchMaxFilterTest => ATTR_LIMIT_SEARCH_MAX_FILTER_TEST,
Attribute::LinkedGroup => ATTR_LINKEDGROUP,
Attribute::LoginShell => ATTR_LOGINSHELL,
Attribute::Mail => ATTR_MAIL,
Attribute::May => ATTR_MAY,
@ -621,6 +627,7 @@ pub enum EntryClass {
AccessControlTargetScope,
Account,
AccountPolicy,
Application,
AttributeType,
Builtin,
Class,
@ -675,6 +682,7 @@ impl From<EntryClass> for &'static str {
EntryClass::AccessControlTargetScope => "access_control_target_scope",
EntryClass::Account => "account",
EntryClass::AccountPolicy => "account_policy",
EntryClass::Application => "application",
EntryClass::AttributeType => "attributetype",
EntryClass::Builtin => ENTRYCLASS_BUILTIN,
EntryClass::Class => ATTR_CLASS,

View file

@ -407,6 +407,15 @@ lazy_static! {
],
..Default::default()
};
pub static ref BUILTIN_GROUP_APPLICATION_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_application_admins",
uuid: UUID_IDM_APPLICATION_ADMINS,
description: "Builtin Application Administration Group.",
entry_managed_by: Some(UUID_IDM_ADMINS),
members: vec![UUID_IDM_ADMINS],
..Default::default()
};
}
/// Make a list of all the non-admin BuiltinGroup's that are created by default, doing it in a standard-ish way so we can use it around the platform
@ -426,6 +435,7 @@ pub fn idm_builtin_non_admin_groups() -> Vec<&'static BuiltinGroup> {
&BUILTIN_GROUP_PEOPLE_PII_READ,
&BUILTIN_GROUP_PEOPLE_ON_BOARDING,
&BUILTIN_GROUP_SERVICE_ACCOUNT_ADMINS,
&BUILTIN_GROUP_APPLICATION_ADMINS,
&IDM_GROUP_ADMINS_V1,
&IDM_ALL_PERSONS,
&IDM_ALL_ACCOUNTS,

View file

@ -769,6 +769,16 @@ pub static ref SCHEMA_ATTR_REFERS_DL7: SchemaAttribute = SchemaAttribute {
..Default::default()
};
pub static ref SCHEMA_ATTR_LINKED_GROUP_DL8: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LINKED_GROUP,
name: Attribute::LinkedGroup.into(),
description: "A reference linking a group to an entry".to_string(),
multivalue: false,
syntax: SyntaxType::ReferenceUuid,
..Default::default()
};
pub static ref SCHEMA_ATTR_CERTIFICATE_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_CERTIFICATE,
name: Attribute::Certificate.into(),
@ -778,6 +788,16 @@ pub static ref SCHEMA_ATTR_CERTIFICATE_DL7: SchemaAttribute = SchemaAttribute {
..Default::default()
};
pub static ref SCHEMA_ATTR_APPLICATION_PASSWORD_DL8: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_APPLICATION_PASSWORD,
name: Attribute::ApplicationPassword.into(),
description: "A set of application passwords".to_string(),
multivalue: true,
syntax: SyntaxType::ApplicationPassword,
..Default::default()
};
// === classes ===
pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
@ -819,7 +839,34 @@ pub static ref SCHEMA_CLASS_PERSON_DL5: SchemaClass = SchemaClass {
systemmust: vec![
Attribute::IdVerificationEcKey.into()
],
systemexcludes: vec![EntryClass::ServiceAccount.into()],
systemexcludes: vec![EntryClass::ServiceAccount.into(), EntryClass::Application.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_PERSON_DL8: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_PERSON,
name: EntryClass::Person.into(),
description: "Object representation of a person".to_string(),
sync_allowed: true,
systemmay: vec![
Attribute::PrimaryCredential.into(),
Attribute::PassKeys.into(),
Attribute::AttestedPasskeys.into(),
Attribute::CredentialUpdateIntentToken.into(),
Attribute::SshPublicKey.into(),
Attribute::RadiusSecret.into(),
Attribute::OAuth2ConsentScopeMap.into(),
Attribute::UserAuthTokenSession.into(),
Attribute::OAuth2Session.into(),
Attribute::Mail.into(),
Attribute::LegalName.into(),
Attribute::ApplicationPassword.into(),
],
systemmust: vec![
Attribute::IdVerificationEcKey.into()
],
systemexcludes: vec![EntryClass::ServiceAccount.into(), EntryClass::Application.into()],
..Default::default()
};
@ -1291,4 +1338,15 @@ pub static ref SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7: SchemaClass = SchemaClass {
..Default::default()
};
pub static ref SCHEMA_CLASS_APPLICATION_DL8: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_APPLICATION,
name: EntryClass::Application.into(),
description: "The class representing an application".to_string(),
systemmust: vec![Attribute::Name.into(), Attribute::LinkedGroup.into()],
systemmay: vec![Attribute::Description.into()],
systemsupplements: vec![EntryClass::ServiceAccount.into()],
..Default::default()
};
);

View file

@ -68,6 +68,7 @@ pub const UUID_IDM_SERVICE_ACCOUNT_ADMINS: Uuid = uuid!("00000000-0000-0000-0000
pub const UUID_IDM_ACCOUNT_POLICY_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000047");
pub const UUID_IDM_PEOPLE_SELF_NAME_WRITE: Uuid = uuid!("00000000-0000-0000-0000-000000000048");
pub const UUID_IDM_CLIENT_CERTIFICATE_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000049");
pub const UUID_IDM_APPLICATION_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000050");
//
pub const UUID_IDM_HIGH_PRIVILEGE: Uuid = uuid!("00000000-0000-0000-0000-000000001000");
@ -311,6 +312,10 @@ pub const UUID_SCHEMA_CLASS_CLIENT_CERTIFICATE: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000179");
pub const UUID_SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000180");
pub const UUID_SCHEMA_CLASS_APPLICATION: Uuid = uuid!("00000000-0000-0000-0000-ffff00000181");
pub const UUID_SCHEMA_ATTR_LINKED_GROUP: Uuid = uuid!("00000000-0000-0000-0000-ffff00000182");
pub const UUID_SCHEMA_ATTR_APPLICATION_PASSWORD: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000183");
// System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
@ -324,7 +329,7 @@ pub const UUID_DOMAIN_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000025")
// skip 00 / 01 - see system info
pub const UUID_IDM_ACP_RECYCLE_BIN_SEARCH_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000002");
pub const UUID_IDM_ACP_RECYCLE_BIN_REVIVE_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000003");
pub const UUID_IDM_ACP_SELF_READ_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000004");
pub const UUID_IDM_ACP_SELF_READ: Uuid = uuid!("00000000-0000-0000-0000-ffffff000004");
pub const UUID_IDM_ACP_ALL_ACCOUNTS_POSIX_READ_V1: Uuid =
uuid!("00000000-0000-0000-0000-ffffff000006");
pub const UUID_IDM_ACP_PEOPLE_PII_READ_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000007");
@ -427,6 +432,10 @@ pub const UUID_KEY_PROVIDER_INTERNAL: Uuid = uuid!("00000000-0000-0000-0000-ffff
pub const UUID_IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER: Uuid =
uuid!("00000000-0000-0000-0000-ffffff000071");
pub const UUID_IDM_ACP_APPLICATION_ENTRY_MANAGER: Uuid =
uuid!("00000000-0000-0000-0000-ffffff000072");
pub const UUID_IDM_ACP_APPLICATION_MANAGE: Uuid = uuid!("00000000-0000-0000-0000-ffffff000073");
// End of system ranges
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");
pub const UUID_ANONYMOUS: Uuid = uuid!("00000000-0000-0000-0000-ffffffffffff");

View file

@ -0,0 +1,65 @@
use crate::credential::{CryptoPolicy, Password};
use crate::prelude::*;
use kanidm_proto::internal::OperationError;
use std::cmp::Ordering;
use std::fmt;
#[derive(Clone)]
pub struct ApplicationPassword {
pub uuid: Uuid,
pub(crate) application: Uuid,
pub(crate) label: String,
pub(crate) password: Password,
}
impl fmt::Debug for ApplicationPassword {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ApplicationPassword")
.field("uuid", &self.uuid)
.field("application", &self.application)
.field("label", &self.label)
.finish()
}
}
impl ApplicationPassword {
pub fn new(
application: Uuid,
label: &str,
cleartext: &str,
policy: &CryptoPolicy,
) -> Result<ApplicationPassword, OperationError> {
let pw = Password::new(policy, cleartext).map_err(|e| {
error!(crypto_err = ?e);
e.into()
})?;
let ap = ApplicationPassword {
uuid: Uuid::new_v4(),
application,
label: label.to_string(),
password: pw,
};
Ok(ap)
}
}
impl PartialEq for ApplicationPassword {
fn eq(&self, other: &Self) -> bool {
self.uuid == other.uuid
|| (self.application == other.application && self.label == other.label)
}
}
impl Eq for ApplicationPassword {}
impl PartialOrd for ApplicationPassword {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ApplicationPassword {
fn cmp(&self, other: &Self) -> Ordering {
self.uuid.cmp(&other.uuid)
}
}

View file

@ -11,6 +11,7 @@ use webauthn_rs_core::proto::{Credential as WebauthnCredential, CredentialV3};
use crate::be::dbvalue::{DbBackupCodeV1, DbCred};
use crate::repl::proto::{ReplBackupCodeV1, ReplCredV1, ReplPasskeyV4V1, ReplSecurityKeyV4V1};
pub mod apppwd;
pub mod softlock;
pub mod totp;

View file

@ -50,6 +50,7 @@ use webauthn_rs::prelude::{
use crate::be::dbentry::{DbEntry, DbEntryVers};
use crate::be::dbvalue::DbValueSetV2;
use crate::be::{IdxKey, IdxSlope};
use crate::credential::apppwd::ApplicationPassword;
use crate::credential::Credential;
use crate::filter::{Filter, FilterInvalid, FilterResolved, FilterValidResolved};
use crate::idm::ldap::ldap_vattr_map;
@ -2933,6 +2934,15 @@ impl<VALID, STATE> Entry<VALID, STATE> {
.and_then(|vs| vs.as_webauthn_attestation_ca_list())
}
pub fn get_ava_application_password(
&self,
attr: Attribute,
) -> Option<&BTreeMap<Uuid, Vec<ApplicationPassword>>> {
self.attrs
.get(attr.as_ref())
.and_then(|vs| vs.as_application_password_map())
}
#[inline(always)]
/// Return a single security principle name, if valid to transform this value.
pub(crate) fn generate_spn(&self, domain_name: &str) -> Option<Value> {

View file

@ -14,10 +14,12 @@ use webauthn_rs::prelude::{
use super::accountpolicy::ResolvedAccountPolicy;
use crate::constants::UUID_ANONYMOUS;
use crate::credential::softlock::CredSoftLockPolicy;
use crate::credential::Credential;
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};
use crate::prelude::*;
@ -65,6 +67,7 @@ pub struct Account {
pub mail: Vec<String>,
pub credential_update_intent_tokens: BTreeMap<String, IntentTokenState>,
pub(crate) unix_extn: Option<UnixExtensions>,
pub apps_pwds: BTreeMap<Uuid, Vec<ApplicationPassword>>,
}
macro_rules! try_from_entry {
@ -197,6 +200,11 @@ macro_rules! try_from_entry {
None
};
let apps_pwds = $value
.get_ava_application_password(Attribute::ApplicationPassword)
.cloned()
.unwrap_or_default();
Ok(Account {
uuid,
name,
@ -215,6 +223,7 @@ macro_rules! try_from_entry {
mail,
credential_update_intent_tokens,
unix_extn,
apps_pwds,
})
}};
}
@ -738,6 +747,53 @@ impl Account {
}
}
}
pub(crate) fn verify_application_password(
&self,
application: &Application,
cleartext: &str,
) -> Result<Option<LdapBoundToken>, OperationError> {
if let Some(v) = self.apps_pwds.get(&application.uuid) {
for ap in v.iter() {
let password_verified = ap.password.verify(cleartext).map_err(|e| {
error!(crypto_err = ?e);
e.into()
})?;
if password_verified {
let session_id = uuid::Uuid::new_v4();
security_info!(
"Starting session {} for {} {}",
session_id,
self.spn,
self.uuid
);
return Ok(Some(LdapBoundToken {
spn: self.spn.clone(),
session_id,
effective_session: LdapSession::ApplicationPasswordBind(
application.uuid,
self.uuid,
),
}));
}
}
}
Ok(None)
}
pub(crate) fn generate_application_password_mod(
&self,
application: Uuid,
label: &str,
cleartext: &str,
policy: &CryptoPolicy,
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
let ap = ApplicationPassword::new(application, label, cleartext, policy)?;
let vap = Value::ApplicationPassword(ap);
Ok(ModifyList::new_append(Attribute::ApplicationPassword, vap))
}
}
// Need to also add a "to UserAuthToken" ...

View file

@ -0,0 +1,728 @@
use super::ldap::{LdapBoundToken, LdapSession};
use crate::idm::account::Account;
use crate::idm::event::LdapApplicationAuthEvent;
use crate::idm::server::{IdmServerAuthTransaction, IdmServerTransaction};
use crate::prelude::*;
use concread::cowcell::*;
use hashbrown::HashMap;
use kanidm_proto::internal::OperationError;
use std::sync::Arc;
use uuid::Uuid;
#[derive(Clone)]
pub(crate) struct Application {
pub uuid: Uuid,
pub name: String,
pub linked_group: Uuid,
}
impl Application {
#[cfg(test)]
pub(crate) fn try_from_entry_ro(
value: &Entry<EntrySealed, EntryCommitted>,
_qs: &mut QueryServerReadTransaction,
) -> Result<Self, OperationError> {
if !value.attribute_equality(Attribute::Class, &EntryClass::Application.to_partialvalue()) {
return Err(OperationError::InvalidAccountState(
"Missing class: application".to_string(),
));
}
let uuid = value.get_uuid();
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 linked_group = value
.get_ava_single_refer(Attribute::LinkedGroup)
.ok_or_else(|| {
OperationError::InvalidAccountState(format!(
"Missing attribute: {}",
Attribute::LinkedGroup
))
})?;
Ok(Application {
name,
uuid,
linked_group,
})
}
}
#[derive(Clone)]
struct LdapApplicationsInner {
set: HashMap<String, Application>,
}
pub struct LdapApplications {
inner: CowCell<LdapApplicationsInner>,
}
pub struct LdapApplicationsReadTransaction {
inner: CowCellReadTxn<LdapApplicationsInner>,
}
pub struct LdapApplicationsWriteTransaction<'a> {
inner: CowCellWriteTxn<'a, LdapApplicationsInner>,
}
impl<'a> LdapApplicationsWriteTransaction<'a> {
pub fn reload(&mut self, value: Vec<Arc<EntrySealedCommitted>>) -> Result<(), OperationError> {
let app_set: Result<HashMap<_, _>, _> = value
.into_iter()
.map(|ent| {
if !ent.attribute_equality(Attribute::Class, &EntryClass::Application.into()) {
error!("Missing class application");
return Err(OperationError::InvalidEntryState);
}
let uuid = ent.get_uuid();
let name = ent
.get_ava_single_iname(Attribute::Name)
.map(str::to_string)
.ok_or(OperationError::InvalidValueState)?;
let linked_group = ent
.get_ava_single_refer(Attribute::LinkedGroup)
.ok_or(OperationError::InvalidValueState)?;
let app = Application {
uuid,
name: name.clone(),
linked_group,
};
Ok((name, app))
})
.collect();
let new_inner = LdapApplicationsInner { set: app_set? };
self.inner.replace(new_inner);
Ok(())
}
pub fn commit(self) {
self.inner.commit();
}
}
impl LdapApplications {
pub fn read(&self) -> LdapApplicationsReadTransaction {
LdapApplicationsReadTransaction {
inner: self.inner.read(),
}
}
pub fn write(&self) -> LdapApplicationsWriteTransaction {
LdapApplicationsWriteTransaction {
inner: self.inner.write(),
}
}
}
impl TryFrom<Vec<Arc<EntrySealedCommitted>>> for LdapApplications {
type Error = OperationError;
fn try_from(value: Vec<Arc<EntrySealedCommitted>>) -> Result<Self, Self::Error> {
let apps = LdapApplications {
inner: CowCell::new(LdapApplicationsInner {
set: HashMap::new(),
}),
};
let mut apps_wr = apps.write();
apps_wr.reload(value)?;
apps_wr.commit();
Ok(apps)
}
}
impl<'a> IdmServerAuthTransaction<'a> {
pub async fn application_auth_ldap(
&mut self,
lae: &LdapApplicationAuthEvent,
ct: Duration,
) -> Result<Option<LdapBoundToken>, OperationError> {
let usr_entry = self.get_qs_txn().internal_search_uuid(lae.target)?;
let account: Account =
Account::try_from_entry_ro(&usr_entry, &mut self.qs_read).map_err(|e| {
error!("Failed to search account {:?}", e);
e
})?;
if account.is_anonymous() {
return Err(OperationError::InvalidUuid);
}
if !account.is_within_valid_time(ct) {
security_info!("Account has expired or is not yet valid, not allowing to proceed");
return Err(OperationError::SessionExpired);
}
let application = self
.applications
.inner
.set
.get(&lae.application)
.ok_or_else(|| {
info!("Application {:?} not found", lae.application);
OperationError::NoMatchingEntries
})?;
// Check linked group membership
let is_memberof = usr_entry
.get_ava_refer(Attribute::MemberOf)
.map(|member_of_set| member_of_set.contains(&application.linked_group))
.unwrap_or_default();
if !is_memberof {
debug!(
"User {:?} not member of application {}:{:?} linked group {:?}",
account.uuid, application.name, application.uuid, application.linked_group,
);
return Ok(None);
}
match account.verify_application_password(application, lae.cleartext.as_str())? {
Some(_) => {
let session_id = Uuid::new_v4();
security_info!(
"Starting session {} for {} {} with application {}:{:?}",
session_id,
account.spn,
account.uuid,
application.name,
application.uuid,
);
Ok(Some(LdapBoundToken {
spn: account.spn,
session_id,
effective_session: LdapSession::UnixBind(account.uuid),
}))
}
None => {
security_info!("Account does not have a configured application password.");
Ok(None)
}
}
}
}
#[derive(Debug)]
pub struct GenerateApplicationPasswordEvent {
pub ident: Identity,
pub target: Uuid,
pub application: Uuid,
pub label: String,
}
impl GenerateApplicationPasswordEvent {
pub fn from_parts(
ident: Identity,
target: Uuid,
application: Uuid,
label: String,
) -> Result<Self, OperationError> {
Ok(GenerateApplicationPasswordEvent {
ident,
target,
application,
label,
})
}
pub fn new_internal(target: Uuid, application: Uuid, label: String) -> Self {
GenerateApplicationPasswordEvent {
ident: Identity::from_internal(),
target,
application,
label,
}
}
}
#[cfg(test)]
mod tests {
use crate::event::CreateEvent;
use crate::idm::account::Account;
use crate::idm::application::Application;
use crate::idm::application::GenerateApplicationPasswordEvent;
use crate::idm::server::IdmServerTransaction;
use crate::idm::serviceaccount::{DestroyApiTokenEvent, GenerateApiTokenEvent};
use crate::prelude::*;
use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
use kanidm_proto::internal::ApiToken as ProtoApiToken;
use std::time::Duration;
const TEST_CURRENT_TIME: u64 = 6000;
// Tests that only the correct combinations of [Account, Person, Application and
// ServiceAccount] classes are allowed.
#[idm_test]
async fn test_idm_application_excludes(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
// ServiceAccount, Application and Person not allowed together
let test_grp_name = "testgroup1";
let test_grp_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(test_grp_name)),
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
);
let test_entry_uuid = Uuid::new_v4();
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("test_app_name")),
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
(Attribute::Description, Value::new_utf8s("test_app_desc")),
(
Attribute::DisplayName,
Value::new_utf8s("test_app_dispname")
),
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(!cr.is_ok());
// Application and Person not allowed together
let test_grp_name = "testgroup1";
let test_grp_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(test_grp_name)),
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
);
let test_entry_uuid = Uuid::new_v4();
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("test_app_name")),
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
(Attribute::Description, Value::new_utf8s("test_app_desc")),
(
Attribute::DisplayName,
Value::new_utf8s("test_app_dispname")
),
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(!cr.is_ok());
// Supplements not satisfied, Application supplements ServiceAccount
let test_grp_name = "testgroup1";
let test_grp_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(test_grp_name)),
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
);
let test_entry_uuid = Uuid::new_v4();
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname("test_app_name")),
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
(Attribute::Description, Value::new_utf8s("test_app_desc")),
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(!cr.is_ok());
// Supplements not satisfied, Application supplements ServiceAccount
let test_grp_name = "testgroup1";
let test_grp_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(test_grp_name)),
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
);
let test_entry_uuid = Uuid::new_v4();
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname("test_app_name")),
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
(Attribute::Description, Value::new_utf8s("test_app_desc")),
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(!cr.is_ok());
// Supplements satisfied, Application supplements ServiceAccount
let test_grp_name = "testgroup1";
let test_grp_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(test_grp_name)),
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
);
let test_entry_uuid = Uuid::new_v4();
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Name, Value::new_iname("test_app_name")),
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
(Attribute::Description, Value::new_utf8s("test_app_desc")),
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(cr.is_ok());
}
// Tests it is not possible to create an applicatin without the linked group attribute
#[idm_test]
async fn test_idm_application_no_linked_group(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let test_entry_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname("test_app_name")),
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
(Attribute::Description, Value::new_utf8s("test_app_desc")),
(
Attribute::DisplayName,
Value::new_utf8s("test_app_dispname")
)
);
let ce = CreateEvent::new_internal(vec![e1]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(!cr.is_ok());
}
// Tests creating an applicatin with a real linked group attribute
#[idm_test]
async fn test_idm_application_linked_group(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let test_entry_name = "test_app_name";
let test_entry_uuid = Uuid::new_v4();
let test_grp_name = "testgroup1";
let test_grp_uuid = Uuid::new_v4();
{
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(test_grp_name)),
(Attribute::Uuid, Value::Uuid(test_grp_uuid))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname(test_entry_name)),
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
(Attribute::Description, Value::new_utf8s("test_app_desc")),
(
Attribute::DisplayName,
Value::new_utf8s("test_app_dispname")
),
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(cr.is_ok());
let cr = idms_prox_write.qs_write.commit();
assert!(cr.is_ok());
}
{
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let app = idms_prox_read
.qs_read
.internal_search_uuid(test_entry_uuid)
.and_then(|entry| {
Application::try_from_entry_ro(&entry, &mut idms_prox_read.qs_read)
})
.map_err(|e| {
trace!("Error: {:?}", e);
e
});
assert!(app.is_ok());
let app = app.unwrap();
assert_eq!(app.name, "test_app_name");
assert_eq!(app.uuid, test_entry_uuid);
assert_eq!(app.linked_group, test_grp_uuid);
}
// Test reference integrity. An attempt to remove a linked group blocks
// the group being deleted
{
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::Uuid,
PartialValue::Uuid(test_grp_uuid)
)));
let mut idms_proxy_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
assert!(idms_proxy_write.qs_write.delete(&de).is_err());
}
{
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::Uuid,
PartialValue::Uuid(test_entry_uuid)
)));
let mut idms_proxy_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
assert!(idms_proxy_write.qs_write.delete(&de).is_ok());
assert!(idms_proxy_write.qs_write.commit().is_ok());
}
{
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::Uuid,
PartialValue::Uuid(test_grp_uuid)
)));
let mut idms_proxy_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
assert!(idms_proxy_write.qs_write.delete(&de).is_ok());
assert!(idms_proxy_write.qs_write.commit().is_ok());
}
}
#[idm_test]
async fn test_idm_application_delete(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let test_usr_name = "testuser1";
let test_usr_uuid = Uuid::new_v4();
let test_app_name = "testapp1";
let test_app_uuid = Uuid::new_v4();
let test_grp_name = "testgroup1";
let test_grp_uuid = Uuid::new_v4();
{
let ct = duration_from_epoch_now();
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname(test_usr_name)),
(Attribute::Uuid, Value::Uuid(test_usr_uuid)),
(Attribute::Description, Value::new_utf8s(test_usr_name)),
(Attribute::DisplayName, Value::new_utf8s(test_usr_name))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(test_grp_name)),
(Attribute::Uuid, Value::Uuid(test_grp_uuid)),
(Attribute::Member, Value::Refer(test_usr_uuid))
);
let e3 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname(test_app_name)),
(Attribute::Uuid, Value::Uuid(test_app_uuid)),
(Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
);
let ce = CreateEvent::new_internal(vec![e1, e2, e3]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(cr.is_ok());
let ev = GenerateApplicationPasswordEvent {
ident: Identity::from_internal(),
target: test_usr_uuid,
application: test_app_uuid,
label: "label".to_string(),
};
idms_prox_write
.generate_application_password(&ev)
.expect("Failed to create application password");
let cr = idms_prox_write.qs_write.commit();
assert!(cr.is_ok());
}
{
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let account = idms_prox_read
.qs_read
.internal_search_uuid(test_usr_uuid)
.and_then(|entry| Account::try_from_entry_ro(&entry, &mut idms_prox_read.qs_read))
.map_err(|e| {
trace!("Error: {:?}", e);
e
})
.expect("Failed to search for account");
assert!(account.apps_pwds.values().count() > 0);
}
// Test reference integrity. If app is removed linked application passwords must go
{
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::Uuid,
PartialValue::Uuid(test_app_uuid)
)));
let mut idms_proxy_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
assert!(idms_proxy_write.qs_write.delete(&de).is_ok());
assert!(idms_proxy_write.qs_write.commit().is_ok());
}
{
let mut idms_prox_read = idms.proxy_read().await.unwrap();
assert!(idms_prox_read
.qs_read
.internal_search_uuid(test_app_uuid)
.is_err());
}
{
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let account = idms_prox_read
.qs_read
.internal_search_uuid(test_usr_uuid)
.and_then(|entry| Account::try_from_entry_ro(&entry, &mut idms_prox_read.qs_read))
.map_err(|e| {
trace!("Error: {:?}", e);
e
})
.expect("Failed to search for account");
assert!(account.apps_pwds.values().count() == 0);
}
}
// Test apitoken for application entries
#[idm_test]
async fn test_idm_application_api_token(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let past_grc = Duration::from_secs(TEST_CURRENT_TIME + 1) + AUTH_TOKEN_GRACE_WINDOW;
let exp = Duration::from_secs(TEST_CURRENT_TIME + 6000);
let post_exp = Duration::from_secs(TEST_CURRENT_TIME + 6010);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let test_entry_uuid = Uuid::new_v4();
let test_group_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("test_group")),
(Attribute::Uuid, Value::Uuid(test_group_uuid))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname("test_app_name")),
(Attribute::Uuid, Value::Uuid(test_entry_uuid)),
(Attribute::Description, Value::new_utf8s("test_app_desc")),
(Attribute::LinkedGroup, Value::Refer(test_group_uuid))
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(cr.is_ok());
let gte = GenerateApiTokenEvent::new_internal(test_entry_uuid, "TestToken", Some(exp));
let api_token = idms_prox_write
.service_account_generate_api_token(&gte, ct)
.expect("failed to generate new api token");
trace!(?api_token);
// Deserialise it.
let jws_verifier = JwsDangerReleaseWithoutVerify::default();
let apitoken_inner = jws_verifier
.verify(&api_token)
.unwrap()
.from_json::<ProtoApiToken>()
.unwrap();
let ident = idms_prox_write
.validate_client_auth_info_to_ident(api_token.clone().into(), ct)
.expect("Unable to verify api token.");
assert!(ident.get_uuid() == Some(test_entry_uuid));
// Check the expiry
assert!(
idms_prox_write
.validate_client_auth_info_to_ident(api_token.clone().into(), post_exp)
.expect_err("Should not succeed")
== OperationError::SessionExpired
);
// Delete session
let dte =
DestroyApiTokenEvent::new_internal(apitoken_inner.account_id, apitoken_inner.token_id);
assert!(idms_prox_write
.service_account_destroy_api_token(&dte)
.is_ok());
// Within gracewindow?
// This is okay, because we are within the gracewindow.
let ident = idms_prox_write
.validate_client_auth_info_to_ident(api_token.clone().into(), ct)
.expect("Unable to verify api token.");
assert!(ident.get_uuid() == Some(test_entry_uuid));
// Past gracewindow?
assert!(
idms_prox_write
.validate_client_auth_info_to_ident(api_token.clone().into(), past_grc)
.expect_err("Should not succeed")
== OperationError::SessionExpired
);
assert!(idms_prox_write.commit().is_ok());
}
}

View file

@ -276,6 +276,22 @@ impl LdapTokenAuthEvent {
}
}
pub struct LdapApplicationAuthEvent {
pub application: String,
pub target: Uuid,
pub cleartext: String,
}
impl LdapApplicationAuthEvent {
pub fn new(app_name: &str, usr_uuid: Uuid, cleartext: String) -> Result<Self, OperationError> {
Ok(LdapApplicationAuthEvent {
application: app_name.to_string(),
target: usr_uuid,
cleartext,
})
}
}
#[derive(Debug)]
pub struct AuthEventStepInit {
pub username: String,

View file

@ -9,14 +9,14 @@ use compact_jwt::JwsCompact;
use kanidm_proto::constants::*;
use kanidm_proto::internal::{ApiToken, UserAuthToken};
use ldap3_proto::simple::*;
use regex::Regex;
use regex::{Regex, RegexBuilder};
use std::net::IpAddr;
use tracing::trace;
use uuid::Uuid;
use crate::event::SearchEvent;
use crate::idm::event::{LdapAuthEvent, LdapTokenAuthEvent};
use crate::idm::server::{IdmServer, IdmServerTransaction};
use crate::idm::event::{LdapApplicationAuthEvent, LdapAuthEvent, LdapTokenAuthEvent};
use crate::idm::server::{IdmServer, IdmServerAuthTransaction, IdmServerTransaction};
use crate::prelude::*;
// Clippy doesn't like Bind here. But proto needs unboxed ldapmsg,
@ -38,6 +38,7 @@ pub enum LdapSession {
UnixBind(Uuid),
UserAuthToken(UserAuthToken),
ApiToken(ApiToken),
ApplicationPasswordBind(Uuid, Uuid),
}
#[derive(Debug, Clone)]
@ -65,12 +66,13 @@ pub struct LdapServer {
enum LdapBindTarget {
Account(Uuid),
ApiToken,
Application(String, Uuid),
}
impl LdapServer {
pub async fn new(idms: &IdmServer) -> Result<Self, OperationError> {
// let ct = duration_from_epoch_now();
let mut idms_prox_read = idms.proxy_read().await.unwrap();
let mut idms_prox_read = idms.proxy_read().await?;
// This is the rootdse path.
// get the domain_info item
let domain_entry = idms_prox_read
@ -87,11 +89,31 @@ impl LdapServer {
})
.ok_or(OperationError::InvalidEntryState)?;
let dnre = Regex::new(format!("^((?P<attr>[^=]+)=(?P<val>[^=]+),)?{basedn}$").as_str())
.map_err(|_| OperationError::InvalidEntryState)?;
// It is necessary to swap greed to avoid the first group "<attr>=<val>" matching the
// next group "app=<app>", son one can use "app=app1,dc=test,dc=net" as search base:
// Greedy (app=app1,dc=test,dc=net):
// Match 1 - app=app1,dc=test,dc=net
// Group 1 - app=app1,
// Group <attr> - app
// Group <val> - app1
// Group 6 - dc=test,dc=net
// Ungreedy (app=app1,dc=test,dc=net):
// Match 1 - app=app1,dc=test,dc=net
// Group 4 - app=app1,
// Group <app> - app1
// Group 6 - dc=test,dc=net
let dnre = RegexBuilder::new(
format!("^((?P<attr>[^=,]+)=(?P<val>[^=,]+),)?(app=(?P<app>[^=,]+),)?({basedn})$")
.as_str(),
)
.swap_greed(true)
.build()
.map_err(|_| OperationError::InvalidEntryState)?;
let binddnre = Regex::new(format!("^(([^=,]+)=)?(?P<val>[^=,]+)(,{basedn})?$").as_str())
.map_err(|_| OperationError::InvalidEntryState)?;
let binddnre = Regex::new(
format!("^((([^=,]+)=)?(?P<val>[^=,]+))(,app=(?P<app>[^=,]+))?(,{basedn})?$").as_str(),
)
.map_err(|_| OperationError::InvalidEntryState)?;
let rootdse = LdapSearchResultEntry {
dn: "".to_string(),
@ -299,7 +321,7 @@ impl LdapServer {
admin_info!(attr = ?k_attrs, "LDAP Search Request Mapped Attrs");
let ct = duration_from_epoch_now();
let mut idm_read = idms.proxy_read().await.unwrap();
let mut idm_read = idms.proxy_read().await?;
// Now start the txn - we need it for resolving filter components.
// join the filter, with ext_filter
@ -406,41 +428,8 @@ impl LdapServer {
);
let ct = duration_from_epoch_now();
let mut idm_auth = idms.auth().await.unwrap();
let target: LdapBindTarget = if dn.is_empty() {
if pw.is_empty() {
LdapBindTarget::Account(UUID_ANONYMOUS)
} else {
// This is the path to access api-token logins.
LdapBindTarget::ApiToken
}
} else if dn == "dn=token" {
// Is the passed dn requesting token auth?
// We use dn= here since these are attr=value, and dn is a phantom so it will
// never be present or match a real value. We also make it an ava so that clients
// that over-zealously validate dn syntax are happy.
LdapBindTarget::ApiToken
} else {
let rdn = self
.binddnre
.captures(dn)
.and_then(|caps| caps.name("val"))
.map(|v| v.as_str().to_string())
.ok_or(OperationError::NoMatchingEntries)?;
if rdn.is_empty() {
// That's weird ...
return Err(OperationError::NoMatchingEntries);
}
let uuid = idm_auth.qs_read.name_to_uuid(rdn.as_str()).map_err(|e| {
request_error!(err = ?e, ?rdn, "Error resolving rdn to target");
e
})?;
LdapBindTarget::Account(uuid)
};
let mut idm_auth = idms.auth().await?;
let target = self.bind_target_from_bind_dn(&mut idm_auth, dn, pw).await?;
let result = match target {
LdapBindTarget::Account(uuid) => {
@ -456,6 +445,11 @@ impl LdapServer {
let lae = LdapTokenAuthEvent::from_parts(jwsc)?;
idm_auth.token_auth_ldap(&lae, ct).await?
}
LdapBindTarget::Application(ref app_name, usr_uuid) => {
let lae =
LdapApplicationAuthEvent::new(app_name.as_str(), usr_uuid, pw.to_string())?;
idm_auth.application_auth_ldap(&lae, ct).await?
}
};
idm_auth.commit()?;
@ -507,7 +501,7 @@ impl LdapServer {
};
let ct = duration_from_epoch_now();
let mut idm_read = idms.proxy_read().await.unwrap();
let mut idm_read = idms.proxy_read().await?;
// Now start the txn - we need it for resolving filter components.
// join the filter, with ext_filter
@ -698,6 +692,63 @@ impl LdapServer {
},
} // end match server op
}
async fn bind_target_from_bind_dn<'a>(
&self,
idm_auth: &mut IdmServerAuthTransaction<'a>,
dn: &str,
pw: &str,
) -> Result<LdapBindTarget, OperationError> {
if dn.is_empty() {
if pw.is_empty() {
return Ok(LdapBindTarget::Account(UUID_ANONYMOUS));
} else {
// This is the path to access api-token logins.
return Ok(LdapBindTarget::ApiToken);
}
} else if dn == "dn=token" {
// Is the passed dn requesting token auth?
// We use dn= here since these are attr=value, and dn is a phantom so it will
// never be present or match a real value. We also make it an ava so that clients
// that over-zealously validate dn syntax are happy.
return Ok(LdapBindTarget::ApiToken);
}
if let Some(captures) = self.binddnre.captures(dn) {
if let Some(usr) = captures.name("val") {
let usr = usr.as_str();
if usr.is_empty() {
error!("Failed to parse user name from bind DN, it is empty (capture group is {:#?})", captures.name("val"));
return Err(OperationError::NoMatchingEntries);
}
let usr_uuid = idm_auth.qs_read.name_to_uuid(usr).map_err(|e| {
error!(err = ?e, ?usr, "Error resolving rdn to target");
e
})?;
if let Some(app) = captures.name("app") {
let app = app.as_str();
if app.is_empty() {
error!("Failed to parse application name from bind DN, it is empty (capture group is {:#?})", captures.name("app"));
return Err(OperationError::NoMatchingEntries);
}
return Ok(LdapBindTarget::Application(app.to_string(), usr_uuid));
}
return Ok(LdapBindTarget::Account(usr_uuid));
}
}
error!(
"Failed to parse bind DN, no captures. Bind DN was {:?})",
dn
);
Err(OperationError::NoMatchingEntries)
}
}
fn ldap_domain_to_dc(input: &str) -> String {
@ -800,7 +851,8 @@ mod tests {
use ldap3_proto::simple::*;
use super::{LdapServer, LdapSession};
use crate::idm::event::UnixPasswordChangeEvent;
use crate::idm::application::GenerateApplicationPasswordEvent;
use crate::idm::event::{LdapApplicationAuthEvent, UnixPasswordChangeEvent};
use crate::idm::serviceaccount::GenerateApiTokenEvent;
const TEST_PASSWORD: &str = "ntaoeuntnaoeuhraohuercahu😍";
@ -997,6 +1049,578 @@ mod tests {
assert!(ldaps.do_bind(idms, "claire", "test").await.is_err());
}
#[idm_test]
async fn test_ldap_application_dnre(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
let testdn = format!("app=app1,{0}", ldaps.basedn);
let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
assert!(captures.name("app").is_some());
assert!(captures.name("attr").is_none());
assert!(captures.name("val").is_none());
let testdn = format!("uid=foo,app=app1,{0}", ldaps.basedn);
let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
assert!(captures.name("app").is_some());
assert!(captures.name("attr").is_some());
assert!(captures.name("val").is_some());
let testdn = format!("uid=foo,{0}", ldaps.basedn);
let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
assert!(captures.name("app").is_none());
assert!(captures.name("attr").is_some());
assert!(captures.name("val").is_some());
}
#[idm_test]
async fn test_ldap_application_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
let usr_uuid = Uuid::new_v4();
let grp_uuid = Uuid::new_v4();
let app_uuid = Uuid::new_v4();
let app_name = "testapp1";
// Setup person, group and application
{
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(usr_uuid)),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup1")),
(Attribute::Uuid, Value::Uuid(grp_uuid))
);
let e3 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname(app_name)),
(Attribute::Uuid, Value::Uuid(app_uuid)),
(Attribute::LinkedGroup, Value::Refer(grp_uuid))
);
let ct = duration_from_epoch_now();
let mut server_txn = idms.proxy_write(ct).await.unwrap();
assert!(server_txn
.qs_write
.internal_create(vec![e1, e2, e3])
.and_then(|_| server_txn.commit())
.is_ok());
}
// Setup the anonymous login
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
assert!(anon_t.effective_session == LdapSession::UnixBind(UUID_ANONYMOUS));
// Searches under application base DN must show same content
let sr = SearchRequest {
msgid: 1,
base: format!("app={app_name},dc=example,dc=com"),
scope: LdapSearchScope::Subtree,
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
attrs: vec!["*".to_string()],
};
let r1 = ldaps
.do_search(idms, &sr, &anon_t, Source::Internal)
.await
.unwrap();
let sr = SearchRequest {
msgid: 1,
base: format!("dc=example,dc=com"),
scope: LdapSearchScope::Subtree,
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
attrs: vec!["*".to_string()],
};
let r2 = ldaps
.do_search(idms, &sr, &anon_t, Source::Internal)
.await
.unwrap();
assert!(r1.len() > 0);
assert!(r1.len() == r2.len());
}
#[idm_test]
async fn test_ldap_application_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
let usr_uuid = Uuid::new_v4();
let grp_uuid = Uuid::new_v4();
let app_uuid = Uuid::new_v4();
// Setup person, group and application
{
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(usr_uuid)),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup1")),
(Attribute::Uuid, Value::Uuid(grp_uuid))
);
let e3 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname("testapp1")),
(Attribute::Uuid, Value::Uuid(app_uuid)),
(Attribute::LinkedGroup, Value::Refer(grp_uuid))
);
let ct = duration_from_epoch_now();
let mut server_txn = idms.proxy_write(ct).await.unwrap();
assert!(server_txn
.qs_write
.internal_create(vec![e1, e2, e3])
.and_then(|_| server_txn.commit())
.is_ok());
}
// No session, user not member of linked group
let res = ldaps
.do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_none());
{
let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
assert!(idms_prox_write
.qs_write
.internal_modify_uuid(grp_uuid, &ml)
.is_ok());
assert!(idms_prox_write.commit().is_ok());
}
// No session, user does not have app password for testapp1
let res = ldaps
.do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_none());
let pass1: String;
let pass2: String;
let pass3: String;
{
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
let ev = GenerateApplicationPasswordEvent::new_internal(
usr_uuid,
app_uuid,
"apppwd1".to_string(),
);
pass1 = idms_prox_write
.generate_application_password(&ev)
.expect("Failed to generate application password");
let ev = GenerateApplicationPasswordEvent::new_internal(
usr_uuid,
app_uuid,
"apppwd2".to_string(),
);
pass2 = idms_prox_write
.generate_application_password(&ev)
.expect("Failed to generate application password");
assert!(idms_prox_write.commit().is_ok());
// Application password overwritten on duplicated label
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
let ev = GenerateApplicationPasswordEvent::new_internal(
usr_uuid,
app_uuid,
"apppwd2".to_string(),
);
pass3 = idms_prox_write
.generate_application_password(&ev)
.expect("Failed to generate application password");
assert!(idms_prox_write.commit().is_ok());
}
// Got session, app password valid
let res = ldaps
.do_bind(
idms,
"spn=testperson1,app=testapp1,dc=example,dc=com",
pass1.as_str(),
)
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_some());
// No session, app password overwritten
let res = ldaps
.do_bind(
idms,
"spn=testperson1,app=testapp1,dc=example,dc=com",
pass2.as_str(),
)
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_none());
// Got session, app password overwritten
let res = ldaps
.do_bind(
idms,
"spn=testperson1,app=testapp1,dc=example,dc=com",
pass3.as_str(),
)
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_some());
// No session, invalid app password
let res = ldaps
.do_bind(
idms,
"spn=testperson1,app=testapp1,dc=example,dc=com",
"FOO",
)
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_none());
}
#[idm_test]
async fn test_ldap_application_linked_group(
idms: &IdmServer,
_idms_delayed: &IdmServerDelayed,
) {
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
let usr_uuid = Uuid::new_v4();
let usr_name = "testuser1";
let grp1_uuid = Uuid::new_v4();
let grp1_name = "testgroup1";
let grp2_uuid = Uuid::new_v4();
let grp2_name = "testgroup2";
let app1_uuid = Uuid::new_v4();
let app1_name = "testapp1";
let app2_uuid = Uuid::new_v4();
let app2_name = "testapp2";
// Setup person, groups and applications
{
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname(usr_name)),
(Attribute::Uuid, Value::Uuid(usr_uuid)),
(Attribute::Description, Value::new_utf8s(usr_name)),
(Attribute::DisplayName, Value::new_utf8s(usr_name))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(grp1_name)),
(Attribute::Uuid, Value::Uuid(grp1_uuid)),
(Attribute::Member, Value::Refer(usr_uuid))
);
let e3 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(grp2_name)),
(Attribute::Uuid, Value::Uuid(grp2_uuid))
);
let e4 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname(app1_name)),
(Attribute::Uuid, Value::Uuid(app1_uuid)),
(Attribute::LinkedGroup, Value::Refer(grp1_uuid))
);
let e5 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname(app2_name)),
(Attribute::Uuid, Value::Uuid(app2_uuid)),
(Attribute::LinkedGroup, Value::Refer(grp2_uuid))
);
let ct = duration_from_epoch_now();
let mut server_txn = idms.proxy_write(ct).await.unwrap();
assert!(server_txn
.qs_write
.internal_create(vec![e1, e2, e3, e4, e5])
.and_then(|_| server_txn.commit())
.is_ok());
}
let pass_app1: String;
let pass_app2: String;
{
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
let ev = GenerateApplicationPasswordEvent::new_internal(
usr_uuid,
app1_uuid,
"label".to_string(),
);
pass_app1 = idms_prox_write
.generate_application_password(&ev)
.expect("Failed to generate application password");
// It is possible to generate an application password even if the
// user is not member of the linked group
let ev = GenerateApplicationPasswordEvent::new_internal(
usr_uuid,
app2_uuid,
"label".to_string(),
);
pass_app2 = idms_prox_write
.generate_application_password(&ev)
.expect("Failed to generate application password");
assert!(idms_prox_write.commit().is_ok());
}
// Got session, app password valid
let res = ldaps
.do_bind(
idms,
format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
pass_app1.as_str(),
)
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_some());
// No session, not member
let res = ldaps
.do_bind(
idms,
format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
pass_app2.as_str(),
)
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_none());
// Add user to grp2
{
let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
assert!(idms_prox_write
.qs_write
.internal_modify_uuid(grp2_uuid, &ml)
.is_ok());
assert!(idms_prox_write.commit().is_ok());
}
// Got session, app password valid
let res = ldaps
.do_bind(
idms,
format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
pass_app2.as_str(),
)
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_some());
// No session, wrong app
let res = ldaps
.do_bind(
idms,
format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
pass_app2.as_str(),
)
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_none());
// Bind error, app not exists
{
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::Uuid,
PartialValue::Uuid(app2_uuid)
)));
assert!(idms_prox_write.qs_write.delete(&de).is_ok());
assert!(idms_prox_write.commit().is_ok());
}
let res = ldaps
.do_bind(
idms,
format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
pass_app2.as_str(),
)
.await;
assert!(res.is_err());
}
// For testing the timeouts
// We need times on this scale
// not yet valid <-> valid from time <-> current_time <-> expire time <-> expired
const TEST_CURRENT_TIME: u64 = 6000;
const TEST_NOT_YET_VALID_TIME: u64 = TEST_CURRENT_TIME - 240;
const TEST_VALID_FROM_TIME: u64 = TEST_CURRENT_TIME - 120;
const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;
async fn set_account_valid_time(idms: &IdmServer, acct: Uuid) {
let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_EXPIRE_TIME));
let me = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(acct))),
ModifyList::new_list(vec![
Modify::Present(Attribute::AccountExpire.into(), v_expire),
Modify::Present(Attribute::AccountValidFrom.into(), v_valid_from),
]),
);
assert!(idms_write.qs_write.modify(&me).is_ok());
idms_write.commit().expect("Must not fail");
}
#[idm_test]
async fn test_ldap_application_valid_from_expire(
idms: &IdmServer,
_idms_delayed: &IdmServerDelayed,
) {
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
let usr_uuid = Uuid::new_v4();
let usr_name = "testuser1";
let grp1_uuid = Uuid::new_v4();
let grp1_name = "testgroup1";
let app1_uuid = Uuid::new_v4();
let app1_name = "testapp1";
let pass_app1: String;
// Setup person, group, application and app password
{
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname(usr_name)),
(Attribute::Uuid, Value::Uuid(usr_uuid)),
(Attribute::Description, Value::new_utf8s(usr_name)),
(Attribute::DisplayName, Value::new_utf8s(usr_name))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(grp1_name)),
(Attribute::Uuid, Value::Uuid(grp1_uuid)),
(Attribute::Member, Value::Refer(usr_uuid))
);
let e3 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname(app1_name)),
(Attribute::Uuid, Value::Uuid(app1_uuid)),
(Attribute::LinkedGroup, Value::Refer(grp1_uuid))
);
let ct = duration_from_epoch_now();
let mut server_txn = idms.proxy_write(ct).await.unwrap();
assert!(server_txn
.qs_write
.internal_create(vec![e1, e2, e3])
.and_then(|_| server_txn.commit())
.is_ok());
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
let ev = GenerateApplicationPasswordEvent::new_internal(
usr_uuid,
app1_uuid,
"label".to_string(),
);
pass_app1 = idms_prox_write
.generate_application_password(&ev)
.expect("Failed to generate application password");
assert!(idms_prox_write.commit().is_ok());
}
// Got session, app password valid
let res = ldaps
.do_bind(
idms,
format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
pass_app1.as_str(),
)
.await;
assert!(res.is_ok());
assert!(res.unwrap().is_some());
// Any account that is not yet valid / expired can't auth.
// Set the valid bounds high/low
// TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
set_account_valid_time(idms, usr_uuid).await;
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
let time = Duration::from_secs(TEST_CURRENT_TIME);
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
let mut idms_auth = idms.auth().await.unwrap();
let lae = LdapApplicationAuthEvent::new(app1_name, usr_uuid, pass_app1)
.expect("Failed to build auth event");
let r1 = idms_auth
.application_auth_ldap(&lae, time_low)
.await
.expect_err("Authentication succeeded");
assert!(r1 == OperationError::SessionExpired);
let r1 = idms_auth
.application_auth_ldap(&lae, time)
.await
.expect("Failed auth");
assert!(r1.is_some());
let r1 = idms_auth
.application_auth_ldap(&lae, time_high)
.await
.expect_err("Authentication succeeded");
assert!(r1 == OperationError::SessionExpired);
}
macro_rules! assert_entry_contains {
(
$entry:expr,

View file

@ -5,6 +5,7 @@
pub mod account;
pub(crate) mod accountpolicy;
pub(crate) mod application;
pub(crate) mod applinks;
pub mod audit;
pub(crate) mod authsession;

View file

@ -25,6 +25,10 @@ use super::event::ReadBackupCodeEvent;
use super::ldap::{LdapBoundToken, LdapSession};
use crate::credential::{softlock::CredSoftLock, Credential};
use crate::idm::account::Account;
use crate::idm::application::{
GenerateApplicationPasswordEvent, LdapApplications, LdapApplicationsReadTransaction,
LdapApplicationsWriteTransaction,
};
use crate::idm::audit::AuditEvent;
use crate::idm::authsession::{AuthSession, AuthSessionData};
use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
@ -77,6 +81,7 @@ pub struct IdmServer {
/// [Webauthn] verifier/config
webauthn: Webauthn,
oauth2rs: Arc<Oauth2ResourceServers>,
applications: Arc<LdapApplications>,
}
/// Contains methods that require writes, but in the context of writing to the idm in memory structures (maybe the query server too). This is things like authentication.
@ -92,6 +97,7 @@ pub struct IdmServerAuthTransaction<'a> {
pub(crate) async_tx: Sender<DelayedAction>,
pub(crate) audit_tx: Sender<AuditEvent>,
pub(crate) webauthn: &'a Webauthn,
pub(crate) applications: LdapApplicationsReadTransaction,
}
pub struct IdmServerCredUpdateTransaction<'a> {
@ -118,6 +124,7 @@ pub struct IdmServerProxyWriteTransaction<'a> {
crypto_policy: &'a CryptoPolicy,
webauthn: &'a Webauthn,
pub(crate) oauth2rs: Oauth2ResourceServersWriteTransaction<'a>,
pub(crate) applications: LdapApplicationsWriteTransaction<'a>,
}
pub struct IdmServerDelayed {
@ -145,7 +152,7 @@ impl IdmServer {
let (audit_tx, audit_rx) = unbounded();
// Get the domain name, as the relying party id.
let (rp_id, rp_name, domain_level, oauth2rs_set) = {
let (rp_id, rp_name, domain_level, oauth2rs_set, application_set) = {
let mut qs_read = qs.read().await?;
(
qs_read.get_domain_name().to_string(),
@ -153,6 +160,7 @@ impl IdmServer {
qs_read.get_domain_version(),
// Add a read/reload of all oauth2 configurations.
qs_read.get_oauth2rs_set()?,
qs_read.get_applications_set()?,
)
};
@ -193,6 +201,11 @@ impl IdmServer {
e
})?;
let applications = LdapApplications::try_from(application_set).map_err(|e| {
admin_error!("Failed to load ldap applications - {:?}", e);
e
})?;
Ok((
IdmServer {
session_ticket: Semaphore::new(1),
@ -205,6 +218,7 @@ impl IdmServer {
audit_tx,
webauthn,
oauth2rs: Arc::new(oauth2rs),
applications: Arc::new(applications),
},
IdmServerDelayed { async_rx },
IdmServerAudit { audit_rx },
@ -228,6 +242,7 @@ impl IdmServer {
async_tx: self.async_tx.clone(),
audit_tx: self.audit_tx.clone(),
webauthn: &self.webauthn,
applications: self.applications.read(),
})
}
@ -260,6 +275,7 @@ impl IdmServer {
crypto_policy: &self.crypto_policy,
webauthn: &self.webauthn,
oauth2rs: self.oauth2rs.write(),
applications: self.applications.write(),
})
}
@ -820,6 +836,67 @@ pub trait IdmServerTransaction<'a> {
.ok_or(OperationError::InvalidState)
}
fn process_ldap_uuid_to_identity(
&mut self,
uuid: &Uuid,
ct: Duration,
source: Source,
) -> Result<Identity, OperationError> {
let entry = self
.get_qs_txn()
.internal_search_uuid(*uuid)
.map_err(|err| {
error!(?err, ?uuid, "Failed to search user by uuid");
err
})?;
let (account, account_policy) =
Account::try_from_entry_with_policy(entry.as_ref(), self.get_qs_txn())?;
if !account.is_within_valid_time(ct) {
info!("Account is expired or not yet valid.");
return Err(OperationError::SessionExpired);
}
// Good to go
let anon_entry = if *uuid == UUID_ANONYMOUS {
// We already have it.
entry
} else {
// Pull the anon entry for mapping the identity.
self.get_qs_txn()
.internal_search_uuid(UUID_ANONYMOUS)
.map_err(|err| {
error!(
?err,
"Unable to search anonymous user for privilege bounding."
);
err
})?
};
let mut limits = Limits::default();
let session_id = Uuid::new_v4();
// Update limits from account policy
if let Some(max_results) = account_policy.limit_search_max_results() {
limits.search_max_results = max_results as usize;
}
if let Some(max_filter) = account_policy.limit_search_max_filter_test() {
limits.search_max_filter_test = max_filter as usize;
}
// Users via LDAP are always only granted anonymous rights unless
// they auth with an api-token
Ok(Identity {
origin: IdentType::User(IdentUser { entry: anon_entry }),
source,
session_id,
scope: AccessScope::ReadOnly,
limits,
})
}
#[instrument(level = "debug", skip_all)]
fn validate_ldap_session(
&mut self,
@ -828,48 +905,8 @@ pub trait IdmServerTransaction<'a> {
ct: Duration,
) -> Result<Identity, OperationError> {
match session {
LdapSession::UnixBind(uuid) => {
let anon_entry = self
.get_qs_txn()
.internal_search_uuid(UUID_ANONYMOUS)
.map_err(|e| {
admin_error!("Failed to validate ldap session -> {:?}", e);
e
})?;
let entry = if *uuid == UUID_ANONYMOUS {
anon_entry.clone()
} else {
self.get_qs_txn().internal_search_uuid(*uuid).map_err(|e| {
admin_error!("Failed to start auth ldap -> {:?}", e);
e
})?
};
if Account::check_within_valid_time(
ct,
entry
.get_ava_single_datetime(Attribute::AccountValidFrom)
.as_ref(),
entry
.get_ava_single_datetime(Attribute::AccountExpire)
.as_ref(),
) {
// Good to go
let limits = Limits::default();
let session_id = Uuid::new_v4();
Ok(Identity {
origin: IdentType::User(IdentUser { entry: anon_entry }),
source,
session_id,
scope: AccessScope::ReadOnly,
limits,
})
} else {
// Nope, expired
Err(OperationError::SessionExpired)
}
LdapSession::UnixBind(uuid) | LdapSession::ApplicationPasswordBind(_, uuid) => {
self.process_ldap_uuid_to_identity(uuid, ct, source)
}
LdapSession::UserAuthToken(uat) => self.process_uat_to_identity(uat, ct, source),
LdapSession::ApiToken(apit) => {
@ -2059,6 +2096,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
#[instrument(level = "debug", skip_all)]
pub fn commit(mut self) -> Result<(), OperationError> {
if self.qs_write.get_changed_app() {
self.qs_write
.get_applications_set()
.and_then(|application_set| self.applications.reload(application_set))?;
}
if self.qs_write.get_changed_oauth2() {
let domain_level = self.qs_write.get_domain_version();
self.qs_write
@ -2069,12 +2111,54 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
}
// Commit everything.
self.applications.commit();
self.oauth2rs.commit();
self.cred_update_sessions.commit();
trace!("cred_update_session.commit");
self.qs_write.commit()
}
#[instrument(level = "debug", skip_all)]
pub fn generate_application_password(
&mut self,
ev: &GenerateApplicationPasswordEvent,
) -> Result<String, OperationError> {
let account = self.target_to_account(ev.target)?;
// This is intended to be read/copied by a human
let cleartext = readable_password_from_random();
// Create a modlist from the change
let modlist = account
.generate_application_password_mod(
ev.application,
ev.label.as_str(),
cleartext.as_str(),
self.crypto_policy,
)
.map_err(|e| {
admin_error!("Unable to generate application password mod {:?}", e);
e
})?;
trace!(?modlist, "processing change");
// Apply it
self.qs_write
.impersonate_modify(
// Filter as executed
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(ev.target))),
// Filter as intended (acp)
&filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(ev.target))),
&modlist,
// Provide the event to impersonate
&ev.ident,
)
.map_err(|e| {
error!(error = ?e);
e
})
.map(|_| cleartext)
}
}
// Need tests of the sessions and the auth ...

View file

@ -23,10 +23,6 @@ macro_rules! try_from_entry {
));
}
let spn = $value.get_ava_single_proto_string(Attribute::Spn).ok_or(
OperationError::InvalidAccountState(format!("Missing attribute: {}", Attribute::Spn)),
)?;
let jws_key = $value
.get_ava_single_jws_key_es256(Attribute::JwsEs256PrivateKey)
.cloned()
@ -48,7 +44,6 @@ macro_rules! try_from_entry {
let uuid = $value.get_uuid().clone();
Ok(ServiceAccount {
spn,
uuid,
valid_from,
expire,
@ -59,7 +54,6 @@ macro_rules! try_from_entry {
}
pub struct ServiceAccount {
pub spn: String,
pub uuid: Uuid,
pub valid_from: Option<OffsetDateTime>,

View file

@ -242,6 +242,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.changed_flags.insert(ChangeFlag::OAUTH2)
}
if !self.changed_flags.contains(ChangeFlag::APPLICATION)
&& cand
.iter()
.chain(pre_cand.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
{
self.changed_flags.insert(ChangeFlag::APPLICATION)
}
if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
&& cand
.iter()
@ -629,6 +638,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
| ChangeFlag::ACP
| ChangeFlag::OAUTH2
| ChangeFlag::DOMAIN
| ChangeFlag::APPLICATION
| ChangeFlag::SYSTEM_CONFIG
| ChangeFlag::SYNC_AGREEMENT
| ChangeFlag::KEY_MATERIAL,

View file

@ -1,6 +1,7 @@
use super::cid::Cid;
use super::entry::EntryChangeState;
use super::entry::State;
use crate::be::dbvalue::DbValueApplicationPassword;
use crate::be::dbvalue::DbValueCertificate;
use crate::be::dbvalue::DbValueImage;
use crate::be::dbvalue::DbValueKeyInternal;
@ -443,6 +444,9 @@ pub enum ReplAttrV1 {
Certificate {
set: Vec<DbValueCertificate>,
},
ApplicationPassword {
set: Vec<DbValueApplicationPassword>,
},
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]

View file

@ -242,6 +242,9 @@ impl SchemaAttribute {
}
SyntaxType::WebauthnAttestationCaList => false,
SyntaxType::ApplicationPassword => {
matches!(v, PartialValue::Uuid(_)) || matches!(v, PartialValue::Refer(_))
}
};
if r {
Ok(())
@ -305,6 +308,7 @@ impl SchemaAttribute {
SyntaxType::KeyInternal => matches!(v, Value::KeyInternal { .. }),
SyntaxType::HexString => matches!(v, Value::HexString(_)),
SyntaxType::Certificate => matches!(v, Value::Certificate(_)),
SyntaxType::ApplicationPassword => matches!(v, Value::ApplicationPassword(..)),
};
if r {
Ok(())
@ -781,7 +785,9 @@ impl<'a> SchemaWriteTransaction<'a> {
a.syntax == SyntaxType::OauthScopeMap ||
a.syntax == SyntaxType::OauthClaimMap ||
// So that when an rs is removed we trigger removal of the sessions.
a.syntax == SyntaxType::Oauth2Session
a.syntax == SyntaxType::Oauth2Session ||
// When an application is removed we trigger removal of passwords
a.syntax == SyntaxType::ApplicationPassword
// May not need to be a ref type since it doesn't have external links/impact?
// || a.syntax == SyntaxType::Session
{

View file

@ -207,6 +207,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_flags.contains(ChangeFlag::APPLICATION)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
{
self.changed_flags.insert(ChangeFlag::APPLICATION)
}
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& norm_cand
.iter()

View file

@ -112,6 +112,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
{
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_flags.contains(ChangeFlag::APPLICATION)
&& commit_cand
.iter()
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
{
self.changed_flags.insert(ChangeFlag::APPLICATION)
}
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& commit_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())

View file

@ -115,6 +115,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
{
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_flags.contains(ChangeFlag::APPLICATION)
&& del_cand
.iter()
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
{
self.changed_flags.insert(ChangeFlag::APPLICATION)
}
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& del_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())

View file

@ -594,6 +594,39 @@ impl<'a> QueryServerWriteTransaction<'a> {
// =========== Apply changes ==============
let idm_schema_classes = [
SCHEMA_ATTR_LINKED_GROUP_DL8.clone().into(),
SCHEMA_ATTR_APPLICATION_PASSWORD_DL8.clone().into(),
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
SCHEMA_CLASS_PERSON_DL8.clone().into(),
];
idm_schema_classes
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry))
.map_err(|err| {
error!(?err, "migrate_domain_6_to_7 -> Error");
err
})?;
self.reload()?;
// Update access controls.
let idm_data = [
IDM_ACP_SELF_READ_DL8.clone().into(),
IDM_ACP_SELF_WRITE_DL8.clone().into(),
IDM_ACP_APPLICATION_MANAGE_DL8.clone().into(),
IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8.clone().into(),
];
idm_data
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry))
.map_err(|err| {
error!(?err, "migrate_domain_6_to_7 -> Error");
err
})?;
Ok(())
}
@ -888,12 +921,12 @@ impl<'a> QueryServerWriteTransaction<'a> {
IDM_ACP_SERVICE_ACCOUNT_MANAGE_V1.clone(),
// DL4
// DL5
IDM_ACP_OAUTH2_MANAGE_DL5.clone().into(),
IDM_ACP_OAUTH2_MANAGE_DL5.clone(),
// DL6
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6.clone().into(),
IDM_ACP_PEOPLE_CREATE_DL6.clone().into(),
IDM_ACP_GROUP_MANAGE_DL6.clone().into(),
IDM_ACP_ACCOUNT_MAIL_READ_DL6.clone().into(),
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6.clone(),
IDM_ACP_PEOPLE_CREATE_DL6.clone(),
IDM_ACP_GROUP_MANAGE_DL6.clone(),
IDM_ACP_ACCOUNT_MAIL_READ_DL6.clone(),
IDM_ACP_DOMAIN_ADMIN_DL6.clone(),
];

View file

@ -131,6 +131,7 @@ bitflags::bitflags! {
const SYSTEM_CONFIG = 0b0001_0000;
const SYNC_AGREEMENT = 0b0010_0000;
const KEY_MATERIAL = 0b0100_0000;
const APPLICATION = 0b1000_0000;
}
}
@ -664,6 +665,7 @@ pub trait QueryServerTransaction<'a> {
.ok_or_else(|| OperationError::InvalidAttribute("Invalid hex string syntax".to_string())),
SyntaxType::Certificate => Value::new_certificate_s(value)
.ok_or_else(|| OperationError::InvalidAttribute("Invalid x509 certificate syntax".to_string())),
SyntaxType::ApplicationPassword => Err(OperationError::InvalidAttribute("ApplicationPassword values can not be supplied through modification".to_string())),
}
}
None => {
@ -719,7 +721,8 @@ pub trait QueryServerTransaction<'a> {
| SyntaxType::OauthScopeMap
| SyntaxType::Session
| SyntaxType::ApiToken
| SyntaxType::Oauth2Session => {
| SyntaxType::Oauth2Session
| SyntaxType::ApplicationPassword => {
let un = self.name_to_uuid(value).unwrap_or(UUID_DOES_NOT_EXIST);
Ok(PartialValue::Refer(un))
}
@ -978,6 +981,13 @@ pub trait QueryServerTransaction<'a> {
)))
}
fn get_applications_set(&mut self) -> Result<Vec<Arc<EntrySealedCommitted>>, OperationError> {
self.internal_search(filter!(f_eq(
Attribute::Class,
EntryClass::Application.into(),
)))
}
#[instrument(level = "debug", skip_all)]
fn consumer_get_state(&mut self) -> Result<ReplRuvRange, OperationError> {
// Get the current state of "where we are up to"
@ -1261,9 +1271,7 @@ impl QueryServer {
let s_uuid = wr.get_db_s_uuid()?;
let d_uuid = wr.get_db_d_uuid()?;
let ts_max = wr.get_db_ts_max(curtime)?;
#[allow(clippy::expect_used)]
wr.commit()
.expect("Critical - unable to commit db_s_uuid or db_d_uuid");
wr.commit()?;
(s_uuid, d_uuid, ts_max)
};
@ -1298,13 +1306,15 @@ impl QueryServer {
let phase = Arc::new(CowCell::new(ServerPhase::Bootstrap));
#[allow(clippy::expect_used)]
let resolve_filter_cache = Arc::new(
ARCacheBuilder::new()
.set_size(RESOLVE_FILTER_CACHE_MAX, RESOLVE_FILTER_CACHE_LOCAL)
.set_reader_quiesce(true)
.build()
.expect("Failed to build resolve_filter_cache"),
.ok_or_else(|| {
error!("Failed to build filter resolve cache");
OperationError::DB0003FilterResolveCacheBuild
})?,
);
let key_providers = Arc::new(KeyProviders::default());
@ -1343,10 +1353,12 @@ impl QueryServer {
// us from competing with writers on the db tickets. This tilts us to write prioritising
// on db operations by always making sure a writer can get a db ticket.
let read_ticket = if cfg!(test) {
#[allow(clippy::expect_used)]
self.read_tickets
.try_acquire()
.expect("unable to acquire db_ticket for qsr")
.inspect_err(|err| {
error!(?err, "Unable to acquire read ticket!");
})
.ok()?
} else {
let fut = tokio::time::timeout(
Duration::from_millis(DB_LOCK_ACQUIRE_TIMEOUT_MILLIS),
@ -1371,16 +1383,20 @@ impl QueryServer {
// and read ticket holders, OR pool_size == 1, and we are waiting on the writer to now
// complete.
let db_ticket = if cfg!(test) {
#[allow(clippy::expect_used)]
self.db_tickets
.try_acquire()
.expect("unable to acquire db_ticket for qsr")
.inspect_err(|err| {
error!(?err, "Unable to acquire database ticket!");
})
.ok()?
} else {
#[allow(clippy::expect_used)]
self.db_tickets
.acquire()
.await
.expect("unable to acquire db_ticket for qsr")
.inspect_err(|err| {
error!(?err, "Unable to acquire database ticket!");
})
.ok()?
};
Some((read_ticket, db_ticket))
@ -1397,16 +1413,12 @@ impl QueryServer {
let schema = self.schema.read();
let cid_max = self.cid_max.read();
#[allow(clippy::expect_used)]
let trim_cid = cid_max
.sub_secs(CHANGELOG_MAX_AGE)
.expect("unable to generate trim cid");
let trim_cid = cid_max.sub_secs(CHANGELOG_MAX_AGE)?;
let be_txn = self.be.read()?;
Ok(QueryServerReadTransaction {
be_txn: self
.be
.read()
.expect("unable to create backend read transaction"),
be_txn,
schema,
d_info: self.d_info.read(),
system_config: self.system_config.read(),
@ -1422,11 +1434,13 @@ impl QueryServer {
#[instrument(level = "debug", skip_all)]
async fn write_acquire_ticket(&self) -> Option<(SemaphorePermit<'_>, SemaphorePermit<'_>)> {
// Guarantee we are the only writer on the thread pool
#[allow(clippy::expect_used)]
let write_ticket = if cfg!(test) {
self.write_ticket
.try_acquire()
.expect("unable to acquire writer_ticket for qsw")
.inspect_err(|err| {
error!(?err, "Unable to acquire write ticket!");
})
.ok()?
} else {
let fut = tokio::time::timeout(
Duration::from_millis(DB_LOCK_ACQUIRE_TIMEOUT_MILLIS),
@ -1450,16 +1464,20 @@ impl QueryServer {
// *must* be available because pool_size >= 2 and the only other are readers, or
// pool_size == 1 and we are waiting on a single reader to now complete
let db_ticket = if cfg!(test) {
#[allow(clippy::expect_used)]
self.db_tickets
.try_acquire()
.expect("unable to acquire db_ticket for qsw")
.inspect_err(|err| {
error!(?err, "Unable to acquire write db_ticket!");
})
.ok()?
} else {
#[allow(clippy::expect_used)]
self.db_tickets
.acquire()
.await
.expect("unable to acquire db_ticket for qsw")
.inspect_err(|err| {
error!(?err, "Unable to acquire write db_ticket!");
})
.ok()?
};
Some((write_ticket, db_ticket))
@ -1489,10 +1507,7 @@ impl QueryServer {
// Update the cid now.
*cid = Cid::new_lamport(cid.s_uuid, curtime, &cid.ts);
#[allow(clippy::expect_used)]
let trim_cid = cid
.sub_secs(CHANGELOG_MAX_AGE)
.expect("unable to generate trim cid");
let trim_cid = cid.sub_secs(CHANGELOG_MAX_AGE)?;
Ok(QueryServerWriteTransaction {
// I think this is *not* needed, because commit is mut self which should
@ -2060,6 +2075,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.be_txn.upgrade_reindex(v)
}
#[inline]
pub(crate) fn get_changed_app(&self) -> bool {
self.changed_flags.contains(ChangeFlag::APPLICATION)
}
#[inline]
pub(crate) fn get_changed_oauth2(&self) -> bool {
self.changed_flags.contains(ChangeFlag::OAUTH2)

View file

@ -214,6 +214,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_flags.contains(ChangeFlag::APPLICATION)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
{
self.changed_flags.insert(ChangeFlag::APPLICATION)
}
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& norm_cand
.iter()
@ -415,6 +424,16 @@ impl<'a> QueryServerWriteTransaction<'a> {
{
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_flags.contains(ChangeFlag::APPLICATION)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
{
self.changed_flags.insert(ChangeFlag::APPLICATION)
}
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& norm_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())

View file

@ -34,7 +34,7 @@ use webauthn_rs::prelude::{
use crate::be::dbentry::DbIdentSpn;
use crate::be::dbvalue::DbValueOauthClaimMapJoinV1;
use crate::credential::{totp::Totp, Credential};
use crate::credential::{apppwd::ApplicationPassword, totp::Totp, Credential};
use crate::prelude::*;
use crate::repl::cid::Cid;
use crate::server::identity::IdentityId;
@ -276,6 +276,7 @@ pub enum SyntaxType {
KeyInternal = 38,
HexString = 39,
Certificate = 40,
ApplicationPassword = 41,
}
impl TryFrom<&str> for SyntaxType {
@ -325,6 +326,7 @@ impl TryFrom<&str> for SyntaxType {
"KEY_INTERNAL" => Ok(SyntaxType::KeyInternal),
"HEX_STRING" => Ok(SyntaxType::HexString),
"CERTIFICATE" => Ok(SyntaxType::Certificate),
"APPLICATION_PASSWORD" => Ok(SyntaxType::ApplicationPassword),
_ => Err(()),
}
}
@ -374,6 +376,7 @@ impl fmt::Display for SyntaxType {
SyntaxType::KeyInternal => "KEY_INTERNAL",
SyntaxType::HexString => "HEX_STRING",
SyntaxType::Certificate => "CERTIFICATE",
SyntaxType::ApplicationPassword => "APPLICATION_PASSWORD",
})
}
}
@ -1227,6 +1230,7 @@ pub enum Value {
HexString(String),
Certificate(Box<Certificate>),
ApplicationPassword(ApplicationPassword),
}
impl PartialEq for Value {
@ -2041,6 +2045,10 @@ impl Value {
Value::AuditLogString(_, s) => {
Value::validate_str_escapes(s) && Value::validate_singleline(s)
}
Value::ApplicationPassword(ap) => {
Value::validate_str_escapes(&ap.label) && Value::validate_singleline(&ap.label)
}
// These have stricter validators so not needed.
Value::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s),
Value::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,

View file

@ -0,0 +1,319 @@
use crate::be::dbvalue::{DbValueApplicationPassword, DbValueSetV2};
use crate::credential::{apppwd::ApplicationPassword, Password};
use crate::prelude::*;
use crate::repl::proto::ReplAttrV1;
use crate::schema::SchemaAttribute;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct ValueSetApplicationPassword {
// The map key is application's UUID
// The value is a vector instead of BTreeSet to use
// PartialValue::Refer instead of having to implement
// PartialValue::ApplicationPassword. For example
// btreeset.remove takes a full ApplicationPassword
// struct.
map: BTreeMap<Uuid, Vec<ApplicationPassword>>,
}
impl ValueSetApplicationPassword {
pub fn new(ap: ApplicationPassword) -> Box<Self> {
let mut map: BTreeMap<Uuid, Vec<ApplicationPassword>> = BTreeMap::new();
map.entry(ap.application).or_default().push(ap);
Box::new(ValueSetApplicationPassword { map })
}
fn from_dbv_iter(
data: impl Iterator<Item = DbValueApplicationPassword>,
) -> Result<ValueSet, OperationError> {
let mut map: BTreeMap<Uuid, Vec<ApplicationPassword>> = BTreeMap::new();
for ap in data {
let ap = match ap {
DbValueApplicationPassword::V1 {
refer,
application_refer,
label,
password,
} => {
let password = Password::try_from(password)
.map_err(|()| OperationError::InvalidValueState)?;
ApplicationPassword {
uuid: refer,
application: application_refer,
label,
password,
}
}
};
map.entry(ap.application).or_default().push(ap);
}
Ok(Box::new(ValueSetApplicationPassword { map }))
}
pub fn from_dbvs2(data: Vec<DbValueApplicationPassword>) -> Result<ValueSet, OperationError> {
Self::from_dbv_iter(data.into_iter())
}
pub fn from_repl_v1(data: &[DbValueApplicationPassword]) -> Result<ValueSet, OperationError> {
Self::from_dbv_iter(data.iter().cloned())
}
fn to_vec_dbvs(&self) -> Vec<DbValueApplicationPassword> {
self.map
.iter()
.flat_map(|(_, v)| {
v.iter().map(|ap| DbValueApplicationPassword::V1 {
refer: ap.uuid,
application_refer: ap.application,
label: ap.label.clone(),
password: ap.password.to_dbpasswordv1(),
})
})
.collect()
}
}
impl ValueSetT for ValueSetApplicationPassword {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
Value::ApplicationPassword(ap) => {
let application_entries = self.map.entry(ap.application).or_default();
if let Some(application_entry) = application_entries
.iter_mut()
.find(|entry_app_password| *entry_app_password == &ap)
{
// Overwrite on duplicated labels for the same application.
application_entry.password = ap.password;
} else {
// Or just add it.
application_entries.push(ap);
}
Ok(true)
}
_ => Err(OperationError::InvalidValueState),
}
}
fn clear(&mut self) {
self.map.clear();
}
fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
match pv {
PartialValue::Refer(u) => {
// Deletes all passwords for the referred application
self.map.remove(u).is_some()
}
PartialValue::Uuid(u) => {
// Delete specific application password
// TODO Migrate to extract_if when available
let mut removed = false;
self.map.retain(|_, v| {
let prev = v.len();
// Check the innel vec of passwords related to this application.
v.retain(|y| y.uuid != *u);
let post = v.len();
removed |= post < prev;
// Is the apppwd set for this application id now empty?
!v.is_empty()
});
removed
}
_ => false,
}
}
fn contains(&self, pv: &PartialValue) -> bool {
match pv {
PartialValue::Uuid(u) => self.map.values().any(|v| v.iter().any(|ap| ap.uuid == *u)),
PartialValue::Refer(u) => self
.map
.values()
.any(|v| v.iter().any(|ap| ap.application == *u)),
_ => false,
}
}
fn substring(&self, _pv: &PartialValue) -> bool {
false
}
fn startswith(&self, _pv: &PartialValue) -> bool {
false
}
fn endswith(&self, _pv: &PartialValue) -> bool {
false
}
fn lessthan(&self, _pv: &PartialValue) -> bool {
false
}
fn len(&self) -> usize {
let mut count = 0;
for v in self.map.values() {
count += v.len();
}
count
}
fn generate_idx_eq_keys(&self) -> Vec<String> {
self.map
.keys()
.map(|u| u.as_hyphenated().to_string())
.collect()
}
fn syntax(&self) -> SyntaxType {
SyntaxType::ApplicationPassword
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
self.map.iter().all(|(_, v)| {
v.iter().all(|ap| {
Value::validate_str_escapes(ap.label.as_str())
&& Value::validate_singleline(ap.label.as_str())
})
})
}
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
Box::new(self.map.iter().flat_map(|(_, v)| {
v.iter()
.map(|ap| format!("App: {} Label: {}", ap.application, ap.label))
}))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
let data = self.to_vec_dbvs();
DbValueSetV2::ApplicationPassword(data)
}
fn to_repl_v1(&self) -> ReplAttrV1 {
let set = self.to_vec_dbvs();
ReplAttrV1::ApplicationPassword { set }
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
Box::new(
self.map
.iter()
.flat_map(|(_, v)| v.iter().map(|ap| ap.uuid))
.map(PartialValue::Refer),
)
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
Box::new(
self.map
.iter()
.flat_map(|(_, v)| v.iter().map(|ap| Value::ApplicationPassword(ap.clone()))),
)
}
fn equal(&self, other: &ValueSet) -> bool {
if let Some(other) = other.as_application_password_map() {
&self.map == other
} else {
debug_assert!(false);
false
}
}
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
if let Some(b) = other.as_application_password_map() {
mergemaps!(self.map, b)
} else {
debug_assert!(false);
Err(OperationError::InvalidValueState)
}
}
fn as_application_password_map(&self) -> Option<&BTreeMap<Uuid, Vec<ApplicationPassword>>> {
Some(&self.map)
}
fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
// This is what ties us as a type that can be refint checked.
Some(Box::new(self.map.keys().copied()))
}
}
#[cfg(test)]
mod tests {
use crate::credential::{apppwd::ApplicationPassword, Password};
use crate::prelude::*;
use crate::valueset::ValueSetApplicationPassword;
use kanidm_lib_crypto::CryptoPolicy;
// Test the remove operation, removing all application passwords for an
// applicaiton should also remove the KV pair.
#[test]
fn test_valueset_application_password_remove() {
let app1_uuid = Uuid::new_v4();
let app2_uuid = Uuid::new_v4();
let ap1_uuid = Uuid::new_v4();
let ap2_uuid = Uuid::new_v4();
let ap3_uuid = Uuid::new_v4();
let ap1: ApplicationPassword = ApplicationPassword {
uuid: ap1_uuid,
application: app1_uuid,
label: "apppwd1".to_string(),
password: Password::new_pbkdf2(&CryptoPolicy::minimum(), "apppwd1")
.expect("Failed to create password"),
};
let ap2: ApplicationPassword = ApplicationPassword {
uuid: ap2_uuid,
application: app1_uuid,
label: "apppwd2".to_string(),
password: Password::new_pbkdf2(&CryptoPolicy::minimum(), "apppwd2")
.expect("Failed to create password"),
};
let ap3: ApplicationPassword = ApplicationPassword {
uuid: ap3_uuid,
application: app2_uuid,
label: "apppwd3".to_string(),
password: Password::new_pbkdf2(&CryptoPolicy::minimum(), "apppwd3")
.expect("Failed to create password"),
};
let mut vs: ValueSet = ValueSetApplicationPassword::new(ap1);
assert!(vs.len() == 1);
let res = vs
.insert_checked(Value::ApplicationPassword(ap2))
.expect("Failed to insert");
assert!(res);
assert!(vs.len() == 2);
let res = vs
.insert_checked(Value::ApplicationPassword(ap3))
.expect("Failed to insert");
assert!(res);
assert!(vs.len() == 3);
let res = vs.remove(&PartialValue::Uuid(Uuid::new_v4()), &Cid::new_zero());
assert!(!res);
assert!(vs.len() == 3);
let res = vs.remove(&PartialValue::Uuid(ap1_uuid), &Cid::new_zero());
assert!(res);
assert!(vs.len() == 2);
let res = vs.remove(&PartialValue::Uuid(ap3_uuid), &Cid::new_zero());
assert!(res);
assert!(vs.len() == 1);
let res = vs.remove(&PartialValue::Uuid(ap2_uuid), &Cid::new_zero());
assert!(res);
assert!(vs.len() == 0);
let res = vs.as_application_password_map().unwrap();
assert!(res.keys().len() == 0);
}
}

View file

@ -18,7 +18,7 @@ use webauthn_rs::prelude::Passkey as PasskeyV4;
use kanidm_proto::internal::{Filter as ProtoFilter, UiHint};
use crate::be::dbvalue::DbValueSetV2;
use crate::credential::{totp::Totp, Credential};
use crate::credential::{apppwd::ApplicationPassword, totp::Totp, Credential};
use crate::prelude::*;
use crate::repl::{cid::Cid, proto::ReplAttrV1};
use crate::schema::SchemaAttribute;
@ -26,6 +26,7 @@ use crate::server::keys::KeyId;
use crate::value::{Address, ApiToken, CredentialType, IntentTokenState, Oauth2Session, Session};
pub use self::address::{ValueSetAddress, ValueSetEmailAddress};
use self::apppwd::ValueSetApplicationPassword;
pub use self::auditlogstring::{ValueSetAuditLogString, AUDIT_LOG_STRING_CAPACITY};
pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary};
pub use self::bool::ValueSetBool;
@ -63,6 +64,7 @@ pub use self::utf8::ValueSetUtf8;
pub use self::uuid::{ValueSetRefer, ValueSetUuid};
mod address;
mod apppwd;
mod auditlogstring;
mod binary;
mod bool;
@ -392,6 +394,11 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
None
}
fn as_application_password_map(&self) -> Option<&BTreeMap<Uuid, Vec<ApplicationPassword>>> {
debug_assert!(false);
None
}
fn to_value_single(&self) -> Option<Value> {
if self.len() != 1 {
None
@ -708,6 +715,7 @@ pub fn from_result_value_iter(
Value::Certificate(c) => ValueSetCertificate::new(c)?,
Value::WebauthnAttestationCaList(_)
| Value::PhoneNumber(_, _)
| Value::ApplicationPassword(_)
| Value::Passkey(_, _, _)
| Value::AttestedPasskey(_, _, _)
| Value::TotpSecret(_, _)
@ -802,6 +810,7 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
debug_assert!(false);
return Err(OperationError::InvalidValueState);
}
Value::ApplicationPassword(ap) => ValueSetApplicationPassword::new(ap),
};
for v in iter {
@ -862,6 +871,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
DbValueSetV2::KeyInternal(set) => ValueSetKeyInternal::from_dbvs2(set),
DbValueSetV2::HexString(set) => ValueSetHexString::from_dbvs2(set),
DbValueSetV2::Certificate(set) => ValueSetCertificate::from_dbvs2(set),
DbValueSetV2::ApplicationPassword(set) => ValueSetApplicationPassword::from_dbvs2(set),
}
}
@ -915,5 +925,6 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
ReplAttrV1::KeyInternal { set } => ValueSetKeyInternal::from_repl_v1(set),
ReplAttrV1::HexString { set } => ValueSetHexString::from_repl_v1(set),
ReplAttrV1::Certificate { set } => ValueSetCertificate::from_repl_v1(set),
ReplAttrV1::ApplicationPassword { set } => ValueSetApplicationPassword::from_repl_v1(set),
}
}