mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-06 00:55:04 +02:00
20240810 application passwords (#2968)
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:
parent
9f4cc984db
commit
239f4594dd
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -207,7 +207,7 @@ pub async fn view_reauth_get(
|
|||
.into_response(),
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn view_index_get(
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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}"))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,4 +24,4 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 %)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
||||
);
|
||||
|
|
|
@ -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");
|
||||
|
|
65
server/lib/src/credential/apppwd.rs
Normal file
65
server/lib/src/credential/apppwd.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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" ...
|
||||
|
|
728
server/lib/src/idm/application.rs
Normal file
728
server/lib/src/idm/application.rs
Normal 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(>e, 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());
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ...
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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(),
|
||||
];
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
|
|
319
server/lib/src/valueset/apppwd.rs
Normal file
319
server/lib/src/valueset/apppwd.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue