diff --git a/server/core/Cargo.toml b/server/core/Cargo.toml index bfe846abb..c0bab238d 100644 --- a/server/core/Cargo.toml +++ b/server/core/Cargo.toml @@ -88,6 +88,7 @@ webauthn-rs = { workspace = true, features = [ [dev-dependencies] walkdir = { workspace = true } tempfile = { workspace = true } +kanidmd_lib = { workspace = true, features = ["test"] } [build-dependencies] kanidm_build_profiles = { workspace = true } diff --git a/server/core/src/https/views/apps.rs b/server/core/src/https/views/apps.rs index a8eae210c..a68339524 100644 --- a/server/core/src/https/views/apps.rs +++ b/server/core/src/https/views/apps.rs @@ -43,7 +43,7 @@ pub(crate) async fn view_apps_get( .qe_r_ref .handle_list_applinks(client_auth_info, kopid.eventid) .await - .map_err(|old| HtmxError::new(&kopid, old))?; + .map_err(|old| HtmxError::new(&kopid, old, domain_info.clone()))?; Ok({ ( diff --git a/server/core/src/https/views/enrol.rs b/server/core/src/https/views/enrol.rs index abe35acab..ee92704c7 100644 --- a/server/core/src/https/views/enrol.rs +++ b/server/core/src/https/views/enrol.rs @@ -50,7 +50,7 @@ pub(crate) async fn view_enrol_get( .qe_r_ref .handle_whoami_uat(client_auth_info.clone(), kopid.eventid) .await - .map_err(|op_err| HtmxError::new(&kopid, op_err))?; + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?; let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0); let can_rw = uat.purpose_readwrite_active(time); @@ -87,7 +87,7 @@ pub(crate) async fn view_enrol_get( kopid.eventid, ) .await - .map_err(|op_err| HtmxError::new(&kopid, op_err))?; + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?; let secret = cu_intent.token; diff --git a/server/core/src/https/views/errors.rs b/server/core/src/https/views/errors.rs index dd1494623..3686772d8 100644 --- a/server/core/src/https/views/errors.rs +++ b/server/core/src/https/views/errors.rs @@ -1,6 +1,7 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Redirect, Response}; use axum_htmx::{HxReswap, HxRetarget, SwapOption}; +use kanidmd_lib::idm::server::DomainInfoRead; use utoipa::ToSchema; use uuid::Uuid; @@ -21,19 +22,19 @@ use crate::https::views::UnrecoverableErrorView; #[derive(Debug, ToSchema)] pub(crate) enum HtmxError { /// Something went wrong when doing things. - OperationError(Uuid, OperationError), + OperationError(Uuid, OperationError, DomainInfoRead), } impl HtmxError { - pub(crate) fn new(kopid: &KOpId, operr: OperationError) -> Self { - HtmxError::OperationError(kopid.eventid, operr) + pub(crate) fn new(kopid: &KOpId, operr: OperationError, domain_info: DomainInfoRead) -> Self { + HtmxError::OperationError(kopid.eventid, operr, domain_info) } } impl IntoResponse for HtmxError { fn into_response(self) -> Response { match self { - HtmxError::OperationError(kopid, inner) => { + HtmxError::OperationError(kopid, inner, domain_info) => { let body = serde_json::to_string(&inner).unwrap_or(inner.to_string()); match &inner { OperationError::NotAuthenticated @@ -58,6 +59,7 @@ impl IntoResponse for HtmxError { UnrecoverableErrorView { err_code: inner, operation_id: kopid, + domain_info, }, ) .into_response(), diff --git a/server/core/src/https/views/login.rs b/server/core/src/https/views/login.rs index 300a14798..140fa9aae 100644 --- a/server/core/src/https/views/login.rs +++ b/server/core/src/https/views/login.rs @@ -48,6 +48,7 @@ struct SessionContext { after_auth_loc: Option<String>, } +#[derive(Clone)] pub enum ReauthPurpose { ProfileSettings, } @@ -59,7 +60,7 @@ impl fmt::Display for ReauthPurpose { } } } - +#[derive(Clone)] pub enum LoginError { InvalidUsername, } @@ -71,16 +72,17 @@ impl fmt::Display for LoginError { } } } - +#[derive(Clone)] pub struct Reauth { pub username: String, pub purpose: ReauthPurpose, } - +#[derive(Clone)] pub struct Oauth2Ctx { pub client_name: String, } +#[derive(Clone)] pub struct LoginDisplayCtx { pub domain_info: DomainInfoRead, // We only need this on the first re-auth screen to indicate what we are doing @@ -160,6 +162,7 @@ pub async fn view_logout_get( State(state): State<ServerState>, VerifiedClientInformation(client_auth_info): VerifiedClientInformation, Extension(kopid): Extension<KOpId>, + DomainInfo(domain_info): DomainInfo, mut jar: CookieJar, ) -> Response { let response = if let Err(err_code) = state @@ -170,6 +173,7 @@ pub async fn view_logout_get( UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info, } .into_response() } else { @@ -232,7 +236,7 @@ pub async fn view_reauth_get( ar, client_auth_info, session_context, - display_ctx, + display_ctx.clone(), ) .await { @@ -241,6 +245,7 @@ pub async fn view_reauth_get( Err(err_code) => UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info: display_ctx.clone().domain_info, } .into_response(), } @@ -249,6 +254,7 @@ pub async fn view_reauth_get( Err(err_code) => UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info: display_ctx.domain_info, } .into_response(), } @@ -275,6 +281,7 @@ pub async fn view_reauth_get( Err(err_code) => UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info: display_ctx.domain_info, } .into_response(), } @@ -358,6 +365,7 @@ pub async fn view_index_get( Err(err_code) => UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info, } .into_response(), } @@ -420,7 +428,7 @@ pub async fn view_login_begin_post( }; let mut display_ctx = LoginDisplayCtx { - domain_info, + domain_info: domain_info.clone(), oauth2: None, reauth: None, error: None, @@ -445,6 +453,7 @@ pub async fn view_login_begin_post( Err(err_code) => UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info, } .into_response(), } @@ -463,6 +472,7 @@ pub async fn view_login_begin_post( _ => UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info, } .into_response(), }, @@ -503,7 +513,7 @@ pub async fn view_login_mech_choose_post( .await; let display_ctx = LoginDisplayCtx { - domain_info, + domain_info: domain_info.clone(), oauth2: None, reauth: None, error: None, @@ -528,6 +538,7 @@ pub async fn view_login_mech_choose_post( Err(err_code) => UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info, } .into_response(), } @@ -536,6 +547,7 @@ pub async fn view_login_mech_choose_post( Err(err_code) => UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info, } .into_response(), } @@ -662,7 +674,7 @@ pub async fn view_login_passkey_post( } Err(e) => { error!(err = ?e, "Unable to deserialize credential submission"); - HtmxError::new(&kopid, OperationError::SerdeJsonError).into_response() + HtmxError::new(&kopid, OperationError::SerdeJsonError, domain_info).into_response() } } } @@ -692,7 +704,7 @@ async fn credential_step( .unwrap_or_default(); let display_ctx = LoginDisplayCtx { - domain_info, + domain_info: domain_info.clone(), oauth2: None, reauth: None, error: None, @@ -720,7 +732,7 @@ async fn credential_step( ar, client_auth_info, session_context, - display_ctx, + display_ctx.clone(), ) .await { @@ -729,6 +741,7 @@ async fn credential_step( Err(err_code) => UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info: display_ctx.domain_info, } .into_response(), } @@ -737,6 +750,7 @@ async fn credential_step( Err(err_code) => UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info, } .into_response(), } @@ -785,6 +799,7 @@ async fn view_login_step( UnrecoverableErrorView { err_code: OperationError::InvalidState, operation_id: kopid.eventid, + domain_info: display_ctx.domain_info, } .into_response() } @@ -845,6 +860,7 @@ async fn view_login_step( UnrecoverableErrorView { err_code: OperationError::InvalidState, operation_id: kopid.eventid, + domain_info: display_ctx.domain_info, } .into_response() } diff --git a/server/core/src/https/views/mod.rs b/server/core/src/https/views/mod.rs index a2d2da324..e4b5bcfa1 100644 --- a/server/core/src/https/views/mod.rs +++ b/server/core/src/https/views/mod.rs @@ -9,7 +9,10 @@ use axum::{ use axum_htmx::HxRequestGuardLayer; use constants::Urls; -use kanidmd_lib::prelude::{OperationError, Uuid}; +use kanidmd_lib::{ + idm::server::DomainInfoRead, + prelude::{OperationError, Uuid}, +}; use crate::https::ServerState; @@ -29,6 +32,8 @@ mod reset; struct UnrecoverableErrorView { err_code: OperationError, operation_id: Uuid, + // This is an option because it's not always present in an "unrecoverable" situation + domain_info: DomainInfoRead, } pub fn view_router() -> Router<ServerState> { @@ -130,3 +135,29 @@ where .map(Some), } } + +#[cfg(test)] +mod tests { + use askama_axum::IntoResponse; + + use super::*; + #[tokio::test] + async fn test_unrecoverableerrorview() { + let domain_info = kanidmd_lib::server::DomainInfo::new_test(); + + let view = UnrecoverableErrorView { + err_code: OperationError::InvalidState, + operation_id: Uuid::new_v4(), + domain_info: domain_info.read(), + }; + + let error_html = view.render().expect("Failed to render"); + + assert!(error_html.contains(domain_info.read().display_name())); + + let response = view.into_response(); + + // TODO: this really should be an error code :( + assert_eq!(response.status(), 200); + } +} diff --git a/server/core/src/https/views/oauth2.rs b/server/core/src/https/views/oauth2.rs index b3d7ab37f..b4c15b6d9 100644 --- a/server/core/src/https/views/oauth2.rs +++ b/server/core/src/https/views/oauth2.rs @@ -104,6 +104,7 @@ async fn oauth2_auth_req( UnrecoverableErrorView { err_code: OperationError::UI0003InvalidOauth2Resume, operation_id: kopid.eventid, + domain_info, }, ) .into_response(); @@ -184,6 +185,7 @@ async fn oauth2_auth_req( UnrecoverableErrorView { err_code, operation_id: kopid.eventid, + domain_info, }, ) .into_response(), @@ -221,6 +223,7 @@ async fn oauth2_auth_req( UnrecoverableErrorView { err_code: OperationError::InvalidState, operation_id: kopid.eventid, + domain_info, }, ) .into_response() @@ -240,6 +243,7 @@ pub async fn view_consent_post( State(server_state): State<ServerState>, Extension(kopid): Extension<KOpId>, VerifiedClientInformation(client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, Form(consent_form): Form<ConsentForm>, ) -> Result<Response, UnrecoverableErrorView> { @@ -291,6 +295,7 @@ pub async fn view_consent_post( Err(UnrecoverableErrorView { err_code: OperationError::InvalidState, operation_id: kopid.eventid, + domain_info, }) } } diff --git a/server/core/src/https/views/profile.rs b/server/core/src/https/views/profile.rs index 42d635cb8..002abaaba 100644 --- a/server/core/src/https/views/profile.rs +++ b/server/core/src/https/views/profile.rs @@ -69,7 +69,7 @@ pub(crate) async fn view_profile_unlock_get( .qe_r_ref .handle_whoami_uat(client_auth_info.clone(), kopid.eventid) .await - .map_err(|op_err| HtmxError::new(&kopid, op_err))?; + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?; let display_ctx = LoginDisplayCtx { domain_info, diff --git a/server/core/src/https/views/reset.rs b/server/core/src/https/views/reset.rs index 7e6dd55d6..73ce55c32 100644 --- a/server/core/src/https/views/reset.rs +++ b/server/core/src/https/views/reset.rs @@ -209,6 +209,7 @@ pub(crate) async fn commit( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, ) -> axum::response::Result<Response> { let cu_session_token: CUSessionToken = get_cu_session(&jar).await?; @@ -216,7 +217,7 @@ pub(crate) async fn commit( state .qe_w_ref .handle_idmcredentialupdatecommit(cu_session_token, kopid.eventid) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info)) .await?; // No longer need the cookie jar. @@ -230,6 +231,7 @@ pub(crate) async fn cancel_cred_update( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, ) -> axum::response::Result<Response> { let cu_session_token: CUSessionToken = get_cu_session(&jar).await?; @@ -237,7 +239,7 @@ pub(crate) async fn cancel_cred_update( state .qe_w_ref .handle_idmcredentialupdatecancel(cu_session_token, kopid.eventid) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info)) .await?; // No longer need the cookie jar. @@ -256,6 +258,7 @@ pub(crate) async fn cancel_mfareg( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, ) -> axum::response::Result<Response> { let cu_session_token: CUSessionToken = get_cu_session(&jar).await?; @@ -263,7 +266,7 @@ pub(crate) async fn cancel_mfareg( let cu_status = state .qe_r_ref .handle_idmcredentialupdate(cu_session_token, CURequest::CancelMFAReg, kopid.eventid) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) .await?; Ok(get_cu_partial_response(cu_status)) @@ -274,6 +277,7 @@ pub(crate) async fn remove_alt_creds( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, ) -> axum::response::Result<Response> { let cu_session_token: CUSessionToken = get_cu_session(&jar).await?; @@ -281,7 +285,7 @@ pub(crate) async fn remove_alt_creds( let cu_status = state .qe_r_ref .handle_idmcredentialupdate(cu_session_token, CURequest::PrimaryRemove, kopid.eventid) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) .await?; Ok(get_cu_partial_response(cu_status)) @@ -292,6 +296,7 @@ pub(crate) async fn remove_unixcred( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, ) -> axum::response::Result<Response> { let cu_session_token: CUSessionToken = get_cu_session(&jar).await?; @@ -303,7 +308,7 @@ pub(crate) async fn remove_unixcred( CURequest::UnixPasswordRemove, kopid.eventid, ) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) .await?; Ok(get_cu_partial_response(cu_status)) @@ -314,6 +319,7 @@ pub(crate) async fn remove_totp( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, Form(totp): Form<TOTPRemoveData>, ) -> axum::response::Result<Response> { @@ -326,7 +332,7 @@ pub(crate) async fn remove_totp( CURequest::TotpRemove(totp.name), kopid.eventid, ) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) .await?; Ok(get_cu_partial_response(cu_status)) @@ -337,6 +343,7 @@ pub(crate) async fn remove_passkey( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, Form(passkey): Form<PasskeyRemoveData>, ) -> axum::response::Result<Response> { @@ -349,7 +356,7 @@ pub(crate) async fn remove_passkey( CURequest::PasskeyRemove(passkey.uuid), kopid.eventid, ) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) .await?; Ok(get_cu_partial_response(cu_status)) @@ -358,8 +365,7 @@ pub(crate) async fn remove_passkey( pub(crate) async fn finish_passkey( State(state): State<ServerState>, Extension(kopid): Extension<KOpId>, - HxRequest(_hx_request): HxRequest, - VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, Form(passkey_create): Form<PasskeyCreateForm>, ) -> axum::response::Result<Response> { @@ -377,7 +383,7 @@ pub(crate) async fn finish_passkey( let cu_status = state .qe_r_ref .handle_idmcredentialupdate(cu_session_token, cu_request, kopid.eventid) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) .await?; Ok(get_cu_partial_response(cu_status)) @@ -386,7 +392,7 @@ pub(crate) async fn finish_passkey( error!("Bad request for passkey creation: {e}"); Ok(( StatusCode::UNPROCESSABLE_ENTITY, - HtmxError::new(&kopid, OperationError::Backend).into_response(), + HtmxError::new(&kopid, OperationError::Backend, domain_info).into_response(), ) .into_response()) } @@ -398,6 +404,7 @@ pub(crate) async fn view_new_passkey( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, Form(init_form): Form<PasskeyInitForm>, ) -> axum::response::Result<Response> { @@ -410,7 +417,7 @@ pub(crate) async fn view_new_passkey( let cu_status: CUStatus = state .qe_r_ref .handle_idmcredentialupdate(cu_session_token, cu_req, kopid.eventid) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) .await?; let response = match cu_status.mfaregstate { @@ -425,6 +432,7 @@ pub(crate) async fn view_new_passkey( UnrecoverableErrorView { err_code: OperationError::UI0001ChallengeSerialisation, operation_id: kopid.eventid, + domain_info, } .into_response() } @@ -432,6 +440,7 @@ pub(crate) async fn view_new_passkey( _ => UnrecoverableErrorView { err_code: OperationError::UI0002InvalidState, operation_id: kopid.eventid, + domain_info, } .into_response(), }; @@ -449,8 +458,7 @@ pub(crate) async fn view_new_passkey( pub(crate) async fn view_new_totp( State(state): State<ServerState>, Extension(kopid): Extension<KOpId>, - HxRequest(_hx_request): HxRequest, - VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, ) -> axum::response::Result<Response> { let cu_session_token = get_cu_session(&jar).await?; @@ -462,7 +470,7 @@ pub(crate) async fn view_new_totp( .await // TODO: better handling for invalid mfaregstate state, can be invalid if certain mfa flows were interrupted // TODO: We should maybe automatically cancel the other MFA reg - .map_err(|op_err| HtmxError::new(&kopid, op_err))?; + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?; let partial = if let CURegState::TotpCheck(secret) = cu_status.mfaregstate { let uri = secret.to_uri(); @@ -491,6 +499,7 @@ pub(crate) async fn view_new_totp( return Err(ErrorResponse::from(HtmxError::new( &kopid, OperationError::CannotStartMFADuringOngoingMFASession, + domain_info, ))); }; @@ -502,6 +511,7 @@ pub(crate) async fn add_totp( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, new_totp_form: Form<NewTotp>, ) -> axum::response::Result<Response> { @@ -525,7 +535,7 @@ pub(crate) async fn add_totp( ) } .await - .map_err(|op_err| HtmxError::new(&kopid, op_err))?; + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?; let check = match &cu_status.mfaregstate { CURegState::None => return Ok(get_cu_partial_response(cu_status)), @@ -544,6 +554,7 @@ pub(crate) async fn add_totp( return Err(ErrorResponse::from(HtmxError::new( &kopid, OperationError::InvalidState, + domain_info, ))) } }; @@ -574,6 +585,7 @@ pub(crate) async fn view_new_pwd( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, opt_form: Option<Form<NewPassword>>, ) -> axum::response::Result<Response> { @@ -609,7 +621,13 @@ pub(crate) async fn view_new_pwd( Err(OperationError::PasswordQuality(password_feedback)) => { (password_feedback, StatusCode::UNPROCESSABLE_ENTITY) } - Err(operr) => return Err(ErrorResponse::from(HtmxError::new(&kopid, operr))), + Err(operr) => { + return Err(ErrorResponse::from(HtmxError::new( + &kopid, + operr, + domain_info, + ))) + } } } else { (vec![], StatusCode::UNPROCESSABLE_ENTITY) @@ -641,7 +659,7 @@ pub(crate) async fn view_self_reset_get( let uat: UserAuthToken = state .qe_r_ref .handle_whoami_uat(client_auth_info.clone(), kopid.eventid) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) .await?; let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0); @@ -651,7 +669,7 @@ pub(crate) async fn view_self_reset_get( let (cu_session_token, cu_status) = state .qe_w_ref .handle_idmcredentialupdate(client_auth_info, uat.uuid.to_string(), kopid.eventid) - .map_err(|op_err| HtmxError::new(&kopid, op_err)) + .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone())) .await?; let cu_resp = get_cu_response(domain_info, cu_status, true); @@ -698,6 +716,7 @@ pub(crate) async fn view_set_unixcred( Extension(kopid): Extension<KOpId>, HxRequest(_hx_request): HxRequest, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, + DomainInfo(domain_info): DomainInfo, jar: CookieJar, opt_form: Option<Form<NewPassword>>, ) -> axum::response::Result<Response> { @@ -733,7 +752,13 @@ pub(crate) async fn view_set_unixcred( Err(OperationError::PasswordQuality(password_feedback)) => { (password_feedback, StatusCode::UNPROCESSABLE_ENTITY) } - Err(operr) => return Err(ErrorResponse::from(HtmxError::new(&kopid, operr))), + Err(operr) => { + return Err(ErrorResponse::from(HtmxError::new( + &kopid, + operr, + domain_info, + ))) + } } } else { (vec![], StatusCode::UNPROCESSABLE_ENTITY) @@ -796,7 +821,9 @@ pub(crate) async fn view_reset_get( } return Ok((jar, Redirect::to(Urls::CredReset.as_ref())).into_response()); } - Err(op_err) => return Ok(HtmxError::new(&kopid, op_err).into_response()), + Err(op_err) => { + return Ok(HtmxError::new(&kopid, op_err, domain_info.clone()).into_response()) + } }; // CU Session cookie is okay @@ -827,7 +854,7 @@ pub(crate) async fn view_reset_get( .into_response()) } Err(op_err) => Err(ErrorResponse::from( - HtmxError::new(&kopid, op_err).into_response(), + HtmxError::new(&kopid, op_err, domain_info).into_response(), )), } } else { diff --git a/server/core/templates/unrecoverable_error.html b/server/core/templates/unrecoverable_error.html index 39d6b9098..7700b893a 100644 --- a/server/core/templates/unrecoverable_error.html +++ b/server/core/templates/unrecoverable_error.html @@ -6,12 +6,24 @@ (% endblock %) (% block body %) +<main id="main" class="m-auto align-items-center d-flex flex-column"> + (% if domain_info.image().is_some() %) + <img src="/ui/images/domain" + alt="(( domain_info.display_name() ))" class="kanidm_logo" /> + (% else %) + <img + src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))" + alt="(( domain_info.display_name() ))" class="kanidm_logo" /> + (% endif %) + <h3>(( domain_info.display_name() ))</h3> + + <h2>Error</h2> - <main id="main"> <p>An unrecoverable error occurred. Please contact your administrator with the details below.</p> <p>Operation ID: (( operation_id ))</p> <p>Error Code: (( err_code ))</p> <a href=((Urls::Ui))>Return</a> </main> -(% endblock %) + + (% endblock %) diff --git a/server/lib/Cargo.toml b/server/lib/Cargo.toml index f865213b4..7e6a0de55 100644 --- a/server/lib/Cargo.toml +++ b/server/lib/Cargo.toml @@ -22,6 +22,7 @@ default = [] dhat-heap = ["dep:dhat"] dhat-ad-hoc = ["dep:dhat"] dev-oauth2-device-flow = [] # still-in-development oauth2 device flow support +test = [] # Enable this for cross-package test features. [dependencies] base64 = { workspace = true } diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs index f8ab0fc81..5230138e0 100644 --- a/server/lib/src/server/mod.rs +++ b/server/lib/src/server/mod.rs @@ -108,6 +108,21 @@ impl DomainInfo { pub fn allow_easter_eggs(&self) -> bool { self.d_allow_easter_eggs } + + #[cfg(feature = "test")] + pub fn new_test() -> CowCell<Self> { + concread::cowcell::CowCell::new(Self { + d_uuid: Uuid::new_v4(), + d_name: "test domain".to_string(), + d_display: "Test Domain".to_string(), + d_vers: 1, + d_patch_level: 0, + d_devel_taint: false, + d_ldap_allow_unix_pw_bind: false, + d_allow_easter_eggs: false, + d_image: None, + }) + } } #[derive(Debug, Clone, PartialEq, Eq, Default)]