fix: unrecoverable error page doesn't include logo or domain name ()

This commit is contained in:
James Hodgkinson 2025-01-14 13:49:20 +10:00 committed by GitHub
parent 2d439c508f
commit 419c4a1827
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 152 additions and 42 deletions

View file

@ -88,6 +88,7 @@ webauthn-rs = { workspace = true, features = [
[dev-dependencies] [dev-dependencies]
walkdir = { workspace = true } walkdir = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
kanidmd_lib = { workspace = true, features = ["test"] }
[build-dependencies] [build-dependencies]
kanidm_build_profiles = { workspace = true } kanidm_build_profiles = { workspace = true }

View file

@ -43,7 +43,7 @@ pub(crate) async fn view_apps_get(
.qe_r_ref .qe_r_ref
.handle_list_applinks(client_auth_info, kopid.eventid) .handle_list_applinks(client_auth_info, kopid.eventid)
.await .await
.map_err(|old| HtmxError::new(&kopid, old))?; .map_err(|old| HtmxError::new(&kopid, old, domain_info.clone()))?;
Ok({ Ok({
( (

View file

@ -50,7 +50,7 @@ pub(crate) async fn view_enrol_get(
.qe_r_ref .qe_r_ref
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid) .handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
.await .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 time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0);
let can_rw = uat.purpose_readwrite_active(time); let can_rw = uat.purpose_readwrite_active(time);
@ -87,7 +87,7 @@ pub(crate) async fn view_enrol_get(
kopid.eventid, kopid.eventid,
) )
.await .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; let secret = cu_intent.token;

View file

@ -1,6 +1,7 @@
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Redirect, Response}; use axum::response::{IntoResponse, Redirect, Response};
use axum_htmx::{HxReswap, HxRetarget, SwapOption}; use axum_htmx::{HxReswap, HxRetarget, SwapOption};
use kanidmd_lib::idm::server::DomainInfoRead;
use utoipa::ToSchema; use utoipa::ToSchema;
use uuid::Uuid; use uuid::Uuid;
@ -21,19 +22,19 @@ use crate::https::views::UnrecoverableErrorView;
#[derive(Debug, ToSchema)] #[derive(Debug, ToSchema)]
pub(crate) enum HtmxError { pub(crate) enum HtmxError {
/// Something went wrong when doing things. /// Something went wrong when doing things.
OperationError(Uuid, OperationError), OperationError(Uuid, OperationError, DomainInfoRead),
} }
impl HtmxError { impl HtmxError {
pub(crate) fn new(kopid: &KOpId, operr: OperationError) -> Self { pub(crate) fn new(kopid: &KOpId, operr: OperationError, domain_info: DomainInfoRead) -> Self {
HtmxError::OperationError(kopid.eventid, operr) HtmxError::OperationError(kopid.eventid, operr, domain_info)
} }
} }
impl IntoResponse for HtmxError { impl IntoResponse for HtmxError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
match self { match self {
HtmxError::OperationError(kopid, inner) => { HtmxError::OperationError(kopid, inner, domain_info) => {
let body = serde_json::to_string(&inner).unwrap_or(inner.to_string()); let body = serde_json::to_string(&inner).unwrap_or(inner.to_string());
match &inner { match &inner {
OperationError::NotAuthenticated OperationError::NotAuthenticated
@ -58,6 +59,7 @@ impl IntoResponse for HtmxError {
UnrecoverableErrorView { UnrecoverableErrorView {
err_code: inner, err_code: inner,
operation_id: kopid, operation_id: kopid,
domain_info,
}, },
) )
.into_response(), .into_response(),

View file

@ -48,6 +48,7 @@ struct SessionContext {
after_auth_loc: Option<String>, after_auth_loc: Option<String>,
} }
#[derive(Clone)]
pub enum ReauthPurpose { pub enum ReauthPurpose {
ProfileSettings, ProfileSettings,
} }
@ -59,7 +60,7 @@ impl fmt::Display for ReauthPurpose {
} }
} }
} }
#[derive(Clone)]
pub enum LoginError { pub enum LoginError {
InvalidUsername, InvalidUsername,
} }
@ -71,16 +72,17 @@ impl fmt::Display for LoginError {
} }
} }
} }
#[derive(Clone)]
pub struct Reauth { pub struct Reauth {
pub username: String, pub username: String,
pub purpose: ReauthPurpose, pub purpose: ReauthPurpose,
} }
#[derive(Clone)]
pub struct Oauth2Ctx { pub struct Oauth2Ctx {
pub client_name: String, pub client_name: String,
} }
#[derive(Clone)]
pub struct LoginDisplayCtx { pub struct LoginDisplayCtx {
pub domain_info: DomainInfoRead, pub domain_info: DomainInfoRead,
// We only need this on the first re-auth screen to indicate what we are doing // 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>, State(state): State<ServerState>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation, VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
DomainInfo(domain_info): DomainInfo,
mut jar: CookieJar, mut jar: CookieJar,
) -> Response { ) -> Response {
let response = if let Err(err_code) = state let response = if let Err(err_code) = state
@ -170,6 +173,7 @@ pub async fn view_logout_get(
UnrecoverableErrorView { UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
} }
.into_response() .into_response()
} else { } else {
@ -232,7 +236,7 @@ pub async fn view_reauth_get(
ar, ar,
client_auth_info, client_auth_info,
session_context, session_context,
display_ctx, display_ctx.clone(),
) )
.await .await
{ {
@ -241,6 +245,7 @@ pub async fn view_reauth_get(
Err(err_code) => UnrecoverableErrorView { Err(err_code) => UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info: display_ctx.clone().domain_info,
} }
.into_response(), .into_response(),
} }
@ -249,6 +254,7 @@ pub async fn view_reauth_get(
Err(err_code) => UnrecoverableErrorView { Err(err_code) => UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info: display_ctx.domain_info,
} }
.into_response(), .into_response(),
} }
@ -275,6 +281,7 @@ pub async fn view_reauth_get(
Err(err_code) => UnrecoverableErrorView { Err(err_code) => UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info: display_ctx.domain_info,
} }
.into_response(), .into_response(),
} }
@ -358,6 +365,7 @@ pub async fn view_index_get(
Err(err_code) => UnrecoverableErrorView { Err(err_code) => UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
} }
.into_response(), .into_response(),
} }
@ -420,7 +428,7 @@ pub async fn view_login_begin_post(
}; };
let mut display_ctx = LoginDisplayCtx { let mut display_ctx = LoginDisplayCtx {
domain_info, domain_info: domain_info.clone(),
oauth2: None, oauth2: None,
reauth: None, reauth: None,
error: None, error: None,
@ -445,6 +453,7 @@ pub async fn view_login_begin_post(
Err(err_code) => UnrecoverableErrorView { Err(err_code) => UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
} }
.into_response(), .into_response(),
} }
@ -463,6 +472,7 @@ pub async fn view_login_begin_post(
_ => UnrecoverableErrorView { _ => UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
} }
.into_response(), .into_response(),
}, },
@ -503,7 +513,7 @@ pub async fn view_login_mech_choose_post(
.await; .await;
let display_ctx = LoginDisplayCtx { let display_ctx = LoginDisplayCtx {
domain_info, domain_info: domain_info.clone(),
oauth2: None, oauth2: None,
reauth: None, reauth: None,
error: None, error: None,
@ -528,6 +538,7 @@ pub async fn view_login_mech_choose_post(
Err(err_code) => UnrecoverableErrorView { Err(err_code) => UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
} }
.into_response(), .into_response(),
} }
@ -536,6 +547,7 @@ pub async fn view_login_mech_choose_post(
Err(err_code) => UnrecoverableErrorView { Err(err_code) => UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
} }
.into_response(), .into_response(),
} }
@ -662,7 +674,7 @@ pub async fn view_login_passkey_post(
} }
Err(e) => { Err(e) => {
error!(err = ?e, "Unable to deserialize credential submission"); 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(); .unwrap_or_default();
let display_ctx = LoginDisplayCtx { let display_ctx = LoginDisplayCtx {
domain_info, domain_info: domain_info.clone(),
oauth2: None, oauth2: None,
reauth: None, reauth: None,
error: None, error: None,
@ -720,7 +732,7 @@ async fn credential_step(
ar, ar,
client_auth_info, client_auth_info,
session_context, session_context,
display_ctx, display_ctx.clone(),
) )
.await .await
{ {
@ -729,6 +741,7 @@ async fn credential_step(
Err(err_code) => UnrecoverableErrorView { Err(err_code) => UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info: display_ctx.domain_info,
} }
.into_response(), .into_response(),
} }
@ -737,6 +750,7 @@ async fn credential_step(
Err(err_code) => UnrecoverableErrorView { Err(err_code) => UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
} }
.into_response(), .into_response(),
} }
@ -785,6 +799,7 @@ async fn view_login_step(
UnrecoverableErrorView { UnrecoverableErrorView {
err_code: OperationError::InvalidState, err_code: OperationError::InvalidState,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info: display_ctx.domain_info,
} }
.into_response() .into_response()
} }
@ -845,6 +860,7 @@ async fn view_login_step(
UnrecoverableErrorView { UnrecoverableErrorView {
err_code: OperationError::InvalidState, err_code: OperationError::InvalidState,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info: display_ctx.domain_info,
} }
.into_response() .into_response()
} }

View file

@ -9,7 +9,10 @@ use axum::{
use axum_htmx::HxRequestGuardLayer; use axum_htmx::HxRequestGuardLayer;
use constants::Urls; use constants::Urls;
use kanidmd_lib::prelude::{OperationError, Uuid}; use kanidmd_lib::{
idm::server::DomainInfoRead,
prelude::{OperationError, Uuid},
};
use crate::https::ServerState; use crate::https::ServerState;
@ -29,6 +32,8 @@ mod reset;
struct UnrecoverableErrorView { struct UnrecoverableErrorView {
err_code: OperationError, err_code: OperationError,
operation_id: Uuid, 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> { pub fn view_router() -> Router<ServerState> {
@ -130,3 +135,29 @@ where
.map(Some), .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);
}
}

View file

@ -104,6 +104,7 @@ async fn oauth2_auth_req(
UnrecoverableErrorView { UnrecoverableErrorView {
err_code: OperationError::UI0003InvalidOauth2Resume, err_code: OperationError::UI0003InvalidOauth2Resume,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
}, },
) )
.into_response(); .into_response();
@ -184,6 +185,7 @@ async fn oauth2_auth_req(
UnrecoverableErrorView { UnrecoverableErrorView {
err_code, err_code,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
}, },
) )
.into_response(), .into_response(),
@ -221,6 +223,7 @@ async fn oauth2_auth_req(
UnrecoverableErrorView { UnrecoverableErrorView {
err_code: OperationError::InvalidState, err_code: OperationError::InvalidState,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
}, },
) )
.into_response() .into_response()
@ -240,6 +243,7 @@ pub async fn view_consent_post(
State(server_state): State<ServerState>, State(server_state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation, VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
Form(consent_form): Form<ConsentForm>, Form(consent_form): Form<ConsentForm>,
) -> Result<Response, UnrecoverableErrorView> { ) -> Result<Response, UnrecoverableErrorView> {
@ -291,6 +295,7 @@ pub async fn view_consent_post(
Err(UnrecoverableErrorView { Err(UnrecoverableErrorView {
err_code: OperationError::InvalidState, err_code: OperationError::InvalidState,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
}) })
} }
} }

View file

@ -69,7 +69,7 @@ pub(crate) async fn view_profile_unlock_get(
.qe_r_ref .qe_r_ref
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid) .handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
.await .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 { let display_ctx = LoginDisplayCtx {
domain_info, domain_info,

View file

@ -209,6 +209,7 @@ pub(crate) async fn commit(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
let cu_session_token: CUSessionToken = get_cu_session(&jar).await?; let cu_session_token: CUSessionToken = get_cu_session(&jar).await?;
@ -216,7 +217,7 @@ pub(crate) async fn commit(
state state
.qe_w_ref .qe_w_ref
.handle_idmcredentialupdatecommit(cu_session_token, kopid.eventid) .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?; .await?;
// No longer need the cookie jar. // No longer need the cookie jar.
@ -230,6 +231,7 @@ pub(crate) async fn cancel_cred_update(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
let cu_session_token: CUSessionToken = get_cu_session(&jar).await?; let cu_session_token: CUSessionToken = get_cu_session(&jar).await?;
@ -237,7 +239,7 @@ pub(crate) async fn cancel_cred_update(
state state
.qe_w_ref .qe_w_ref
.handle_idmcredentialupdatecancel(cu_session_token, kopid.eventid) .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?; .await?;
// No longer need the cookie jar. // No longer need the cookie jar.
@ -256,6 +258,7 @@ pub(crate) async fn cancel_mfareg(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
let cu_session_token: CUSessionToken = get_cu_session(&jar).await?; let cu_session_token: CUSessionToken = get_cu_session(&jar).await?;
@ -263,7 +266,7 @@ pub(crate) async fn cancel_mfareg(
let cu_status = state let cu_status = state
.qe_r_ref .qe_r_ref
.handle_idmcredentialupdate(cu_session_token, CURequest::CancelMFAReg, kopid.eventid) .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?; .await?;
Ok(get_cu_partial_response(cu_status)) Ok(get_cu_partial_response(cu_status))
@ -274,6 +277,7 @@ pub(crate) async fn remove_alt_creds(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
let cu_session_token: CUSessionToken = get_cu_session(&jar).await?; 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 let cu_status = state
.qe_r_ref .qe_r_ref
.handle_idmcredentialupdate(cu_session_token, CURequest::PrimaryRemove, kopid.eventid) .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?; .await?;
Ok(get_cu_partial_response(cu_status)) Ok(get_cu_partial_response(cu_status))
@ -292,6 +296,7 @@ pub(crate) async fn remove_unixcred(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
let cu_session_token: CUSessionToken = get_cu_session(&jar).await?; let cu_session_token: CUSessionToken = get_cu_session(&jar).await?;
@ -303,7 +308,7 @@ pub(crate) async fn remove_unixcred(
CURequest::UnixPasswordRemove, CURequest::UnixPasswordRemove,
kopid.eventid, kopid.eventid,
) )
.map_err(|op_err| HtmxError::new(&kopid, op_err)) .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
.await?; .await?;
Ok(get_cu_partial_response(cu_status)) Ok(get_cu_partial_response(cu_status))
@ -314,6 +319,7 @@ pub(crate) async fn remove_totp(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
Form(totp): Form<TOTPRemoveData>, Form(totp): Form<TOTPRemoveData>,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
@ -326,7 +332,7 @@ pub(crate) async fn remove_totp(
CURequest::TotpRemove(totp.name), CURequest::TotpRemove(totp.name),
kopid.eventid, kopid.eventid,
) )
.map_err(|op_err| HtmxError::new(&kopid, op_err)) .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
.await?; .await?;
Ok(get_cu_partial_response(cu_status)) Ok(get_cu_partial_response(cu_status))
@ -337,6 +343,7 @@ pub(crate) async fn remove_passkey(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
Form(passkey): Form<PasskeyRemoveData>, Form(passkey): Form<PasskeyRemoveData>,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
@ -349,7 +356,7 @@ pub(crate) async fn remove_passkey(
CURequest::PasskeyRemove(passkey.uuid), CURequest::PasskeyRemove(passkey.uuid),
kopid.eventid, kopid.eventid,
) )
.map_err(|op_err| HtmxError::new(&kopid, op_err)) .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
.await?; .await?;
Ok(get_cu_partial_response(cu_status)) Ok(get_cu_partial_response(cu_status))
@ -358,8 +365,7 @@ pub(crate) async fn remove_passkey(
pub(crate) async fn finish_passkey( pub(crate) async fn finish_passkey(
State(state): State<ServerState>, State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, DomainInfo(domain_info): DomainInfo,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
jar: CookieJar, jar: CookieJar,
Form(passkey_create): Form<PasskeyCreateForm>, Form(passkey_create): Form<PasskeyCreateForm>,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
@ -377,7 +383,7 @@ pub(crate) async fn finish_passkey(
let cu_status = state let cu_status = state
.qe_r_ref .qe_r_ref
.handle_idmcredentialupdate(cu_session_token, cu_request, kopid.eventid) .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?; .await?;
Ok(get_cu_partial_response(cu_status)) Ok(get_cu_partial_response(cu_status))
@ -386,7 +392,7 @@ pub(crate) async fn finish_passkey(
error!("Bad request for passkey creation: {e}"); error!("Bad request for passkey creation: {e}");
Ok(( Ok((
StatusCode::UNPROCESSABLE_ENTITY, StatusCode::UNPROCESSABLE_ENTITY,
HtmxError::new(&kopid, OperationError::Backend).into_response(), HtmxError::new(&kopid, OperationError::Backend, domain_info).into_response(),
) )
.into_response()) .into_response())
} }
@ -398,6 +404,7 @@ pub(crate) async fn view_new_passkey(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
Form(init_form): Form<PasskeyInitForm>, Form(init_form): Form<PasskeyInitForm>,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
@ -410,7 +417,7 @@ pub(crate) async fn view_new_passkey(
let cu_status: CUStatus = state let cu_status: CUStatus = state
.qe_r_ref .qe_r_ref
.handle_idmcredentialupdate(cu_session_token, cu_req, kopid.eventid) .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?; .await?;
let response = match cu_status.mfaregstate { let response = match cu_status.mfaregstate {
@ -425,6 +432,7 @@ pub(crate) async fn view_new_passkey(
UnrecoverableErrorView { UnrecoverableErrorView {
err_code: OperationError::UI0001ChallengeSerialisation, err_code: OperationError::UI0001ChallengeSerialisation,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
} }
.into_response() .into_response()
} }
@ -432,6 +440,7 @@ pub(crate) async fn view_new_passkey(
_ => UnrecoverableErrorView { _ => UnrecoverableErrorView {
err_code: OperationError::UI0002InvalidState, err_code: OperationError::UI0002InvalidState,
operation_id: kopid.eventid, operation_id: kopid.eventid,
domain_info,
} }
.into_response(), .into_response(),
}; };
@ -449,8 +458,7 @@ pub(crate) async fn view_new_passkey(
pub(crate) async fn view_new_totp( pub(crate) async fn view_new_totp(
State(state): State<ServerState>, State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, DomainInfo(domain_info): DomainInfo,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
jar: CookieJar, jar: CookieJar,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
let cu_session_token = get_cu_session(&jar).await?; let cu_session_token = get_cu_session(&jar).await?;
@ -462,7 +470,7 @@ pub(crate) async fn view_new_totp(
.await .await
// TODO: better handling for invalid mfaregstate state, can be invalid if certain mfa flows were interrupted // 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 // 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 partial = if let CURegState::TotpCheck(secret) = cu_status.mfaregstate {
let uri = secret.to_uri(); let uri = secret.to_uri();
@ -491,6 +499,7 @@ pub(crate) async fn view_new_totp(
return Err(ErrorResponse::from(HtmxError::new( return Err(ErrorResponse::from(HtmxError::new(
&kopid, &kopid,
OperationError::CannotStartMFADuringOngoingMFASession, OperationError::CannotStartMFADuringOngoingMFASession,
domain_info,
))); )));
}; };
@ -502,6 +511,7 @@ pub(crate) async fn add_totp(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
new_totp_form: Form<NewTotp>, new_totp_form: Form<NewTotp>,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
@ -525,7 +535,7 @@ pub(crate) async fn add_totp(
) )
} }
.await .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 { let check = match &cu_status.mfaregstate {
CURegState::None => return Ok(get_cu_partial_response(cu_status)), 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( return Err(ErrorResponse::from(HtmxError::new(
&kopid, &kopid,
OperationError::InvalidState, OperationError::InvalidState,
domain_info,
))) )))
} }
}; };
@ -574,6 +585,7 @@ pub(crate) async fn view_new_pwd(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
opt_form: Option<Form<NewPassword>>, opt_form: Option<Form<NewPassword>>,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
@ -609,7 +621,13 @@ pub(crate) async fn view_new_pwd(
Err(OperationError::PasswordQuality(password_feedback)) => { Err(OperationError::PasswordQuality(password_feedback)) => {
(password_feedback, StatusCode::UNPROCESSABLE_ENTITY) (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 { } else {
(vec![], StatusCode::UNPROCESSABLE_ENTITY) (vec![], StatusCode::UNPROCESSABLE_ENTITY)
@ -641,7 +659,7 @@ pub(crate) async fn view_self_reset_get(
let uat: UserAuthToken = state let uat: UserAuthToken = state
.qe_r_ref .qe_r_ref
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid) .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?; .await?;
let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0); 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 let (cu_session_token, cu_status) = state
.qe_w_ref .qe_w_ref
.handle_idmcredentialupdate(client_auth_info, uat.uuid.to_string(), kopid.eventid) .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?; .await?;
let cu_resp = get_cu_response(domain_info, cu_status, true); 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>, Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar, jar: CookieJar,
opt_form: Option<Form<NewPassword>>, opt_form: Option<Form<NewPassword>>,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
@ -733,7 +752,13 @@ pub(crate) async fn view_set_unixcred(
Err(OperationError::PasswordQuality(password_feedback)) => { Err(OperationError::PasswordQuality(password_feedback)) => {
(password_feedback, StatusCode::UNPROCESSABLE_ENTITY) (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 { } else {
(vec![], StatusCode::UNPROCESSABLE_ENTITY) (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()); 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 // CU Session cookie is okay
@ -827,7 +854,7 @@ pub(crate) async fn view_reset_get(
.into_response()) .into_response())
} }
Err(op_err) => Err(ErrorResponse::from( Err(op_err) => Err(ErrorResponse::from(
HtmxError::new(&kopid, op_err).into_response(), HtmxError::new(&kopid, op_err, domain_info).into_response(),
)), )),
} }
} else { } else {

View file

@ -6,12 +6,24 @@
(% endblock %) (% endblock %)
(% block body %) (% 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> <h2>Error</h2>
<main id="main">
<p>An unrecoverable error occurred. Please contact your administrator with the details below.</p> <p>An unrecoverable error occurred. Please contact your administrator with the details below.</p>
<p>Operation ID: (( operation_id ))</p> <p>Operation ID: (( operation_id ))</p>
<p>Error Code: (( err_code ))</p> <p>Error Code: (( err_code ))</p>
<a href=((Urls::Ui))>Return</a> <a href=((Urls::Ui))>Return</a>
</main> </main>
(% endblock %)
(% endblock %)

View file

@ -22,6 +22,7 @@ default = []
dhat-heap = ["dep:dhat"] dhat-heap = ["dep:dhat"]
dhat-ad-hoc = ["dep:dhat"] dhat-ad-hoc = ["dep:dhat"]
dev-oauth2-device-flow = [] # still-in-development oauth2 device flow support dev-oauth2-device-flow = [] # still-in-development oauth2 device flow support
test = [] # Enable this for cross-package test features.
[dependencies] [dependencies]
base64 = { workspace = true } base64 = { workspace = true }

View file

@ -108,6 +108,21 @@ impl DomainInfo {
pub fn allow_easter_eggs(&self) -> bool { pub fn allow_easter_eggs(&self) -> bool {
self.d_allow_easter_eggs 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)] #[derive(Debug, Clone, PartialEq, Eq, Default)]