mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
20240828 Support Larger Images, Allow Custom Domain Icons (#3016)
Allow setting custom domain icons.
This commit is contained in:
parent
e5a5de8de3
commit
95fc6fc5bf
68
libs/client/src/domain.rs
Normal file
68
libs/client/src/domain.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use crate::{ClientError, KanidmClient};
|
||||||
|
use kanidm_proto::internal::ImageValue;
|
||||||
|
use reqwest::multipart;
|
||||||
|
|
||||||
|
impl KanidmClient {
|
||||||
|
/// Clear the current domain logo/image
|
||||||
|
pub async fn idm_domain_delete_image(&self) -> Result<(), ClientError> {
|
||||||
|
self.perform_delete_request("/v1/domain/_image").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add or update the domain logo/image
|
||||||
|
pub async fn idm_domain_update_image(&self, image: ImageValue) -> Result<(), ClientError> {
|
||||||
|
let file_content_type = image.filetype.as_content_type_str();
|
||||||
|
|
||||||
|
let file_data = match multipart::Part::bytes(image.contents.clone())
|
||||||
|
.file_name(image.filename)
|
||||||
|
.mime_str(file_content_type)
|
||||||
|
{
|
||||||
|
Ok(part) => part,
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"Failed to generate multipart body from image data: {:}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return Err(ClientError::SystemError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let form = multipart::Form::new().part("image", file_data);
|
||||||
|
|
||||||
|
// send it
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(self.make_url("/v1/domain/_image"))
|
||||||
|
.multipart(form);
|
||||||
|
|
||||||
|
let response = {
|
||||||
|
let tguard = self.bearer_token.read().await;
|
||||||
|
if let Some(token) = &(*tguard) {
|
||||||
|
response.bearer_auth(token)
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let response = response
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| self.handle_response_error(err))?;
|
||||||
|
self.expect_version(&response).await;
|
||||||
|
|
||||||
|
let opid = self.get_kopid_from_response(&response);
|
||||||
|
|
||||||
|
match response.status() {
|
||||||
|
reqwest::StatusCode::OK => {}
|
||||||
|
unexpect => {
|
||||||
|
return Err(ClientError::Http(
|
||||||
|
unexpect,
|
||||||
|
response.json().await.ok(),
|
||||||
|
opid,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientError::JsonDecode(e, opid))
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,7 @@ use webauthn_rs_proto::{
|
||||||
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
|
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod domain;
|
||||||
mod group;
|
mod group;
|
||||||
mod oauth;
|
mod oauth;
|
||||||
mod person;
|
mod person;
|
||||||
|
|
|
@ -40,7 +40,7 @@ use kanidmd_lib::{
|
||||||
AuthoriseResponse, JwkKeySet, Oauth2Error, Oauth2Rfc8414MetadataResponse,
|
AuthoriseResponse, JwkKeySet, Oauth2Error, Oauth2Rfc8414MetadataResponse,
|
||||||
OidcDiscoveryResponse, OidcToken,
|
OidcDiscoveryResponse, OidcToken,
|
||||||
},
|
},
|
||||||
idm::server::IdmServerTransaction,
|
idm::server::{DomainInfoRead, IdmServerTransaction},
|
||||||
idm::serviceaccount::ListApiTokenEvent,
|
idm::serviceaccount::ListApiTokenEvent,
|
||||||
idm::ClientAuthInfo,
|
idm::ClientAuthInfo,
|
||||||
};
|
};
|
||||||
|
@ -389,7 +389,7 @@ impl QueryServerReadV1 {
|
||||||
&self,
|
&self,
|
||||||
client_auth_info: ClientAuthInfo,
|
client_auth_info: ClientAuthInfo,
|
||||||
rs: Filter<FilterInvalid>,
|
rs: Filter<FilterInvalid>,
|
||||||
) -> Result<ImageValue, OperationError> {
|
) -> Result<Option<ImageValue>, OperationError> {
|
||||||
let mut idms_prox_read = self.idms.proxy_read().await?;
|
let mut idms_prox_read = self.idms.proxy_read().await?;
|
||||||
let ct = duration_from_epoch_now();
|
let ct = duration_from_epoch_now();
|
||||||
|
|
||||||
|
@ -409,17 +409,9 @@ impl QueryServerReadV1 {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let entries = idms_prox_read.qs_read.search(&search)?;
|
let entries = idms_prox_read.qs_read.search(&search)?;
|
||||||
if entries.is_empty() {
|
Ok(entries
|
||||||
return Err(OperationError::NoMatchingEntries);
|
.first()
|
||||||
}
|
.and_then(|entry| entry.get_ava_single_image(Attribute::Image)))
|
||||||
let entry = match entries.first() {
|
|
||||||
Some(entry) => entry,
|
|
||||||
None => return Err(OperationError::NoMatchingEntries),
|
|
||||||
};
|
|
||||||
match entry.get_ava_single_image(Attribute::Image) {
|
|
||||||
Some(image) => Ok(image),
|
|
||||||
None => Err(OperationError::NoMatchingEntries),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(
|
#[instrument(
|
||||||
|
@ -1554,16 +1546,6 @@ impl QueryServerReadV1 {
|
||||||
idms_prox_read.list_applinks(&ident)
|
idms_prox_read.list_applinks(&ident)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(
|
|
||||||
level = "info",
|
|
||||||
skip_all,
|
|
||||||
fields(uuid = ?eventid)
|
|
||||||
)]
|
|
||||||
pub async fn get_domain_display_name(&self, eventid: Uuid) -> Result<String, OperationError> {
|
|
||||||
let idms_prox_read = self.idms.proxy_read().await?;
|
|
||||||
Ok(idms_prox_read.qs_read.get_domain_display_name().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(
|
#[instrument(
|
||||||
level = "info",
|
level = "info",
|
||||||
skip_all,
|
skip_all,
|
||||||
|
@ -1633,4 +1615,8 @@ impl QueryServerReadV1 {
|
||||||
};
|
};
|
||||||
Some(res)
|
Some(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn domain_info_read(&self) -> DomainInfoRead {
|
||||||
|
self.idms.domain_read()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1182,64 +1182,36 @@ impl QueryServerWriteV1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all)]
|
#[instrument(level = "debug", skip_all)]
|
||||||
pub async fn handle_oauth2_rs_image_delete(
|
pub async fn handle_image_update(
|
||||||
&self,
|
&self,
|
||||||
client_auth_info: ClientAuthInfo,
|
client_auth_info: ClientAuthInfo,
|
||||||
rs: Filter<FilterInvalid>,
|
request_filter: Filter<FilterInvalid>,
|
||||||
|
image: Option<ImageValue>,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
let mut idms_prox_write = self.idms.proxy_write(duration_from_epoch_now()).await?;
|
|
||||||
let ct = duration_from_epoch_now();
|
let ct = duration_from_epoch_now();
|
||||||
|
let mut idms_prox_write = self.idms.proxy_write(ct).await?;
|
||||||
|
|
||||||
let ident = idms_prox_write
|
let ident = idms_prox_write
|
||||||
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||||
.map_err(|e| {
|
.inspect_err(|err| {
|
||||||
admin_error!(err = ?e, "Invalid identity in handle_oauth2_rs_image_delete");
|
error!(?err, "Invalid identity in handle_image_update");
|
||||||
e
|
|
||||||
})?;
|
|
||||||
let ml = ModifyList::new_purge(Attribute::Image);
|
|
||||||
let mdf = match ModifyEvent::from_internal_parts(ident, &ml, &rs, &idms_prox_write.qs_write)
|
|
||||||
{
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(e) => {
|
|
||||||
admin_error!(err = ?e, "Failed to begin modify during handle_oauth2_rs_image_delete");
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
idms_prox_write
|
|
||||||
.qs_write
|
|
||||||
.modify(&mdf)
|
|
||||||
.and_then(|_| idms_prox_write.commit().map(|_| ()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all)]
|
|
||||||
pub async fn handle_oauth2_rs_image_update(
|
|
||||||
&self,
|
|
||||||
client_auth_info: ClientAuthInfo,
|
|
||||||
rs: Filter<FilterInvalid>,
|
|
||||||
image: ImageValue,
|
|
||||||
) -> Result<(), OperationError> {
|
|
||||||
let mut idms_prox_write = self.idms.proxy_write(duration_from_epoch_now()).await?;
|
|
||||||
let ct = duration_from_epoch_now();
|
|
||||||
|
|
||||||
let ident = idms_prox_write
|
|
||||||
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
|
||||||
.map_err(|e| {
|
|
||||||
admin_error!(err = ?e, "Invalid identity in handle_oauth2_rs_image_update");
|
|
||||||
e
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let ml = ModifyList::new_purge_and_set(Attribute::Image, Value::Image(image));
|
let modlist = if let Some(image) = image {
|
||||||
|
ModifyList::new_purge_and_set(Attribute::Image, Value::Image(image))
|
||||||
let mdf = match ModifyEvent::from_internal_parts(ident, &ml, &rs, &idms_prox_write.qs_write)
|
} else {
|
||||||
{
|
ModifyList::new_purge(Attribute::Image)
|
||||||
Ok(m) => m,
|
|
||||||
Err(e) => {
|
|
||||||
admin_error!(err = ?e, "Failed to begin modify during handle_oauth2_rs_image_update");
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!(?mdf, "Begin modify event");
|
let mdf = ModifyEvent::from_internal_parts(
|
||||||
|
ident,
|
||||||
|
&modlist,
|
||||||
|
&request_filter,
|
||||||
|
&idms_prox_write.qs_write,
|
||||||
|
)
|
||||||
|
.inspect_err(|err| {
|
||||||
|
error!(?err, "Failed to begin modify during handle_image_update");
|
||||||
|
})?;
|
||||||
|
|
||||||
idms_prox_write
|
idms_prox_write
|
||||||
.qs_write
|
.qs_write
|
||||||
|
|
|
@ -163,6 +163,9 @@ impl Modify for SecurityAddon {
|
||||||
super::v1::domain_attr_get,
|
super::v1::domain_attr_get,
|
||||||
super::v1::domain_attr_put,
|
super::v1::domain_attr_put,
|
||||||
super::v1::domain_attr_delete,
|
super::v1::domain_attr_delete,
|
||||||
|
super::v1_domain::image_post,
|
||||||
|
super::v1_domain::image_delete,
|
||||||
|
|
||||||
super::v1::group_id_unix_token_get,
|
super::v1::group_id_unix_token_get,
|
||||||
super::v1::group_id_unix_post,
|
super::v1::group_id_unix_post,
|
||||||
super::v1::group_get,
|
super::v1::group_get,
|
||||||
|
|
|
@ -14,6 +14,8 @@ use axum_extra::extract::cookie::CookieJar;
|
||||||
use kanidm_proto::constants::X_FORWARDED_FOR;
|
use kanidm_proto::constants::X_FORWARDED_FOR;
|
||||||
use kanidm_proto::internal::COOKIE_BEARER_TOKEN;
|
use kanidm_proto::internal::COOKIE_BEARER_TOKEN;
|
||||||
use kanidmd_lib::prelude::{ClientAuthInfo, ClientCertInfo, Source};
|
use kanidmd_lib::prelude::{ClientAuthInfo, ClientCertInfo, Source};
|
||||||
|
// Re-export
|
||||||
|
pub use kanidmd_lib::idm::server::DomainInfoRead;
|
||||||
|
|
||||||
use compact_jwt::JwsCompact;
|
use compact_jwt::JwsCompact;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -181,6 +183,22 @@ impl FromRequestParts<ServerState> for VerifiedClientInformation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DomainInfo(pub DomainInfoRead);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FromRequestParts<ServerState> for DomainInfo {
|
||||||
|
type Rejection = (StatusCode, &'static str);
|
||||||
|
|
||||||
|
// Need to skip all to prevent leaking tokens to logs.
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
|
async fn from_request_parts(
|
||||||
|
_parts: &mut Parts,
|
||||||
|
state: &ServerState,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
Ok(DomainInfo(state.qe_r_ref.domain_info_read()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ClientConnInfo {
|
pub struct ClientConnInfo {
|
||||||
pub addr: SocketAddr,
|
pub addr: SocketAddr,
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
//! Builds a Progressive Web App Manifest page.
|
//! Builds a Progressive Web App Manifest page.
|
||||||
|
|
||||||
use axum::extract::State;
|
|
||||||
use axum::http::header::CONTENT_TYPE;
|
use axum::http::header::CONTENT_TYPE;
|
||||||
use axum::http::HeaderValue;
|
use axum::http::HeaderValue;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::Extension;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::skip_serializing_none;
|
use serde_with::skip_serializing_none;
|
||||||
|
|
||||||
use super::middleware::KOpId;
|
use crate::https::extractors::DomainInfo;
|
||||||
// Thanks to the webmanifest crate for a lot of this code
|
|
||||||
use super::ServerState;
|
|
||||||
|
|
||||||
/// The MIME type for `.webmanifest` files.
|
/// The MIME type for `.webmanifest` files.
|
||||||
const MIME_TYPE_MANIFEST: &str = "application/manifest+json;charset=utf-8";
|
const MIME_TYPE_MANIFEST: &str = "application/manifest+json;charset=utf-8";
|
||||||
|
@ -150,15 +145,8 @@ pub fn manifest_data(host_req: Option<&str>, domain_display_name: String) -> Man
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a manifest.json file for progressive web app usage
|
/// Generates a manifest.json file for progressive web app usage
|
||||||
pub(crate) async fn manifest(
|
pub(crate) async fn manifest(DomainInfo(domain_info): DomainInfo) -> impl IntoResponse {
|
||||||
State(state): State<ServerState>,
|
let domain_display_name = domain_info.display_name().to_string();
|
||||||
Extension(kopid): Extension<KOpId>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let domain_display_name = state
|
|
||||||
.qe_r_ref
|
|
||||||
.get_domain_display_name(kopid.eventid)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
|
||||||
// TODO: fix the None here to make it the request host
|
// TODO: fix the None here to make it the request host
|
||||||
let manifest_string =
|
let manifest_string =
|
||||||
match serde_json::to_string_pretty(&manifest_data(None, domain_display_name)) {
|
match serde_json::to_string_pretty(&manifest_data(None, domain_display_name)) {
|
||||||
|
|
|
@ -20,7 +20,7 @@ pub async fn dont_cache_me(request: Request<Body>, next: Next) -> Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a cache control header of 300 seconds to the response headers.
|
/// Adds a cache control header of 300 seconds to the response headers.
|
||||||
pub async fn cache_me(request: Request<Body>, next: Next) -> Response {
|
pub async fn cache_me_short(request: Request<Body>, next: Next) -> Response {
|
||||||
let mut response = next.run(request).await;
|
let mut response = next.run(request).await;
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
header::CACHE_CONTROL,
|
header::CACHE_CONTROL,
|
||||||
|
|
|
@ -11,6 +11,7 @@ mod tests;
|
||||||
pub(crate) mod trace;
|
pub(crate) mod trace;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod v1;
|
mod v1;
|
||||||
|
mod v1_domain;
|
||||||
mod v1_oauth2;
|
mod v1_oauth2;
|
||||||
mod v1_scim;
|
mod v1_scim;
|
||||||
mod views;
|
mod views;
|
||||||
|
@ -270,12 +271,18 @@ pub async fn create_https_server(
|
||||||
// Create a spa router that captures everything at ui without key extraction.
|
// Create a spa router that captures everything at ui without key extraction.
|
||||||
if cfg!(feature = "ui_htmx") {
|
if cfg!(feature = "ui_htmx") {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route("/ui/images/oauth2/:rs_name", get(oauth2::oauth2_image_get))
|
||||||
|
.route("/ui/images/domain", get(v1_domain::image_get))
|
||||||
|
// Layers only apply to routes that are *already* added, not the ones
|
||||||
|
// added after.
|
||||||
|
.layer(middleware::compression::new())
|
||||||
|
.layer(from_fn(middleware::caching::cache_me_short))
|
||||||
.route("/", get(|| async { Redirect::to("/ui") }))
|
.route("/", get(|| async { Redirect::to("/ui") }))
|
||||||
.nest("/ui", views::view_router())
|
.nest("/ui", views::view_router())
|
||||||
.layer(middleware::compression::new())
|
|
||||||
.route("/ui/images/oauth2/:rs_name", get(oauth2::oauth2_image_get))
|
|
||||||
} else {
|
} else {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route("/ui/images/oauth2/:rs_name", get(oauth2::oauth2_image_get))
|
||||||
|
.layer(middleware::compression::new())
|
||||||
// Direct users to the base app page. If a login is required,
|
// Direct users to the base app page. If a login is required,
|
||||||
// then views will take care of redirection.
|
// then views will take care of redirection.
|
||||||
.route("/", get(|| async { Redirect::temporary("/ui") }))
|
.route("/", get(|| async { Redirect::temporary("/ui") }))
|
||||||
|
@ -288,8 +295,6 @@ pub async fn create_https_server(
|
||||||
.nest("/ui/oauth2", ui::spa_router_login_flows())
|
.nest("/ui/oauth2", ui::spa_router_login_flows())
|
||||||
// admin app
|
// admin app
|
||||||
.nest("/ui/admin", ui::spa_router_admin())
|
.nest("/ui/admin", ui::spa_router_admin())
|
||||||
.layer(middleware::compression::new())
|
|
||||||
.route("/ui/images/oauth2/:rs_name", get(oauth2::oauth2_image_get))
|
|
||||||
// skip_route_check
|
// skip_route_check
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -329,7 +334,7 @@ pub async fn create_https_server(
|
||||||
.nest_service("/pkg", ServeDir::new(pkg_path).precompressed_br())
|
.nest_service("/pkg", ServeDir::new(pkg_path).precompressed_br())
|
||||||
.layer(middleware::compression::new())
|
.layer(middleware::compression::new())
|
||||||
}
|
}
|
||||||
.layer(from_fn(middleware::caching::cache_me));
|
.layer(from_fn(middleware::caching::cache_me_short));
|
||||||
|
|
||||||
app.merge(pkg_router)
|
app.merge(pkg_router)
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,21 +107,21 @@ pub(crate) async fn oauth2_image_get(
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
Ok(image) => (
|
Ok(Some(image)) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
[(CONTENT_TYPE, image.filetype.as_content_type_str())],
|
[(CONTENT_TYPE, image.filetype.as_content_type_str())],
|
||||||
image.contents,
|
image.contents,
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
Err(err) => {
|
Ok(None) => {
|
||||||
admin_debug!(
|
warn!(?rs_name, "No image set for oauth2 client");
|
||||||
"Unable to get image for oauth2 resource server {}: {:?}",
|
|
||||||
rs_name,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
// TODO: a 404 probably isn't perfect but it's not the worst
|
|
||||||
(StatusCode::NOT_FOUND, "").into_response()
|
(StatusCode::NOT_FOUND, "").into_response()
|
||||||
}
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err, "Unable to get image for oauth2 client");
|
||||||
|
// TODO: a 404 probably isn't perfect but it's not the worst
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "").into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,12 @@ use axum::http::header::CONTENT_TYPE;
|
||||||
use axum::http::HeaderValue;
|
use axum::http::HeaderValue;
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::{Extension, Router};
|
use axum::Router;
|
||||||
|
|
||||||
use super::middleware::KOpId;
|
|
||||||
use super::ServerState;
|
use super::ServerState;
|
||||||
|
|
||||||
|
use crate::https::extractors::{DomainInfo, DomainInfoRead};
|
||||||
|
|
||||||
// when you want to put big text at the top of the page
|
// when you want to put big text at the top of the page
|
||||||
pub const CSS_PAGE_HEADER: &str = "d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-0 pb-0 mb-3 border-bottom";
|
pub const CSS_PAGE_HEADER: &str = "d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-0 pb-0 mb-3 border-bottom";
|
||||||
|
|
||||||
|
@ -40,36 +41,30 @@ pub(crate) fn spa_router_login_flows() -> Router<ServerState> {
|
||||||
|
|
||||||
pub(crate) async fn ui_handler_user_ui(
|
pub(crate) async fn ui_handler_user_ui(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
DomainInfo(domain_info): DomainInfo,
|
||||||
) -> Response<String> {
|
) -> Response<String> {
|
||||||
ui_handler_generic(state, kopid, "wasmloader_user.js").await
|
ui_handler_generic(state, "wasmloader_user.js", domain_info).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn ui_handler_admin(
|
pub(crate) async fn ui_handler_admin(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
DomainInfo(domain_info): DomainInfo,
|
||||||
) -> Response<String> {
|
) -> Response<String> {
|
||||||
ui_handler_generic(state, kopid, "wasmloader_admin.js").await
|
ui_handler_generic(state, "wasmloader_admin.js", domain_info).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn ui_handler_login_flows(
|
pub(crate) async fn ui_handler_login_flows(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
DomainInfo(domain_info): DomainInfo,
|
||||||
) -> Response<String> {
|
) -> Response<String> {
|
||||||
ui_handler_generic(state, kopid, "wasmloader_login_flows.js").await
|
ui_handler_generic(state, "wasmloader_login_flows.js", domain_info).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn ui_handler_generic(
|
pub(crate) async fn ui_handler_generic(
|
||||||
state: ServerState,
|
state: ServerState,
|
||||||
kopid: KOpId,
|
|
||||||
wasmloader: &str,
|
wasmloader: &str,
|
||||||
|
domain_info: DomainInfoRead,
|
||||||
) -> Response<String> {
|
) -> Response<String> {
|
||||||
let domain_display_name = state
|
|
||||||
.qe_r_ref
|
|
||||||
.get_domain_display_name(kopid.eventid)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// let's get the tags we want to load the javascript files
|
// let's get the tags we want to load the javascript files
|
||||||
let mut jsfiles: Vec<String> = state
|
let mut jsfiles: Vec<String> = state
|
||||||
.js_files
|
.js_files
|
||||||
|
@ -85,7 +80,7 @@ pub(crate) async fn ui_handler_generic(
|
||||||
|
|
||||||
let body = format!(
|
let body = format!(
|
||||||
include_str!("ui_html.html"),
|
include_str!("ui_html.html"),
|
||||||
domain_display_name.as_str(),
|
domain_info.display_name(),
|
||||||
jstags,
|
jstags,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ use kanidmd_lib::prelude::*;
|
||||||
use kanidmd_lib::value::PartialValue;
|
use kanidmd_lib::value::PartialValue;
|
||||||
|
|
||||||
use super::errors::WebError;
|
use super::errors::WebError;
|
||||||
use super::middleware::caching::{cache_me, dont_cache_me};
|
use super::middleware::caching::{cache_me_short, dont_cache_me};
|
||||||
use super::middleware::KOpId;
|
use super::middleware::KOpId;
|
||||||
use super::ServerState;
|
use super::ServerState;
|
||||||
use crate::https::apidocs::response_schema::{ApiResponseWithout200, DefaultApiResponse};
|
use crate::https::apidocs::response_schema::{ApiResponseWithout200, DefaultApiResponse};
|
||||||
|
@ -3090,7 +3090,7 @@ fn cacheable_routes(state: ServerState) -> Router<ServerState> {
|
||||||
"/v1/account/:id/_radius/_token",
|
"/v1/account/:id/_radius/_token",
|
||||||
get(account_id_radius_token_get),
|
get(account_id_radius_token_get),
|
||||||
)
|
)
|
||||||
.layer(from_fn(cache_me))
|
.layer(from_fn(cache_me_short))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3347,6 +3347,10 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
|
||||||
.route("/v1/credential/_cancel", post(credential_update_cancel))
|
.route("/v1/credential/_cancel", post(credential_update_cancel))
|
||||||
// domain-things
|
// domain-things
|
||||||
.route("/v1/domain", get(domain_get))
|
.route("/v1/domain", get(domain_get))
|
||||||
|
.route(
|
||||||
|
"/v1/domain/_image",
|
||||||
|
post(super::v1_domain::image_post).delete(super::v1_domain::image_delete),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/domain/_attr/:attr",
|
"/v1/domain/_attr/:attr",
|
||||||
get(domain_attr_get)
|
get(domain_attr_get)
|
||||||
|
|
140
server/core/src/https/v1_domain.rs
Normal file
140
server/core/src/https/v1_domain.rs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
use super::apidocs::response_schema::DefaultApiResponse;
|
||||||
|
use super::errors::WebError;
|
||||||
|
use super::ServerState;
|
||||||
|
use crate::https::extractors::DomainInfo;
|
||||||
|
use crate::https::extractors::VerifiedClientInformation;
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum::Json;
|
||||||
|
use axum::{
|
||||||
|
http::header::CONTENT_TYPE,
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use kanidm_proto::internal::{ImageType, ImageValue};
|
||||||
|
use kanidmd_lib::prelude::*;
|
||||||
|
use kanidmd_lib::valueset::image::ImageValueThings;
|
||||||
|
use sketching::admin_error;
|
||||||
|
|
||||||
|
pub(crate) async fn image_get(DomainInfo(domain_info): DomainInfo) -> Response {
|
||||||
|
match domain_info.image() {
|
||||||
|
Some(image) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
[(CONTENT_TYPE, image.filetype.as_content_type_str())],
|
||||||
|
image.contents.clone(),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
None => {
|
||||||
|
warn!("No image set for domain");
|
||||||
|
(StatusCode::NOT_FOUND, "").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/v1/domain/_image",
|
||||||
|
responses(
|
||||||
|
DefaultApiResponse,
|
||||||
|
),
|
||||||
|
security(("token_jwt" = [])),
|
||||||
|
tag = "v1/domain",
|
||||||
|
operation_id = "domain_image_delete"
|
||||||
|
)]
|
||||||
|
pub(crate) async fn image_delete(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
) -> Result<Json<()>, WebError> {
|
||||||
|
let f_uuid = filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO)));
|
||||||
|
|
||||||
|
state
|
||||||
|
.qe_w_ref
|
||||||
|
.handle_image_update(client_auth_info, f_uuid, None)
|
||||||
|
.await
|
||||||
|
.map(Json::from)
|
||||||
|
.map_err(WebError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/domain/_image",
|
||||||
|
responses(
|
||||||
|
DefaultApiResponse,
|
||||||
|
),
|
||||||
|
security(("token_jwt" = [])),
|
||||||
|
tag = "v1/domain",
|
||||||
|
operation_id = "domain_image_post"
|
||||||
|
)]
|
||||||
|
/// API endpoint for creating/replacing the image associated with an OAuth2 Resource Server.
|
||||||
|
///
|
||||||
|
/// It requires a multipart form with the image file, and the content type must be one of the
|
||||||
|
/// [VALID_IMAGE_UPLOAD_CONTENT_TYPES].
|
||||||
|
pub(crate) async fn image_post(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
mut multipart: axum::extract::Multipart,
|
||||||
|
) -> Result<Json<()>, WebError> {
|
||||||
|
// because we might not get an image
|
||||||
|
let mut image: Option<ImageValue> = None;
|
||||||
|
|
||||||
|
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
|
||||||
|
let filename = field.file_name().map(|f| f.to_string()).clone();
|
||||||
|
if let Some(filename) = filename {
|
||||||
|
let content_type = field.content_type().map(|f| f.to_string()).clone();
|
||||||
|
|
||||||
|
let content_type = match content_type {
|
||||||
|
Some(val) => {
|
||||||
|
if VALID_IMAGE_UPLOAD_CONTENT_TYPES.contains(&val.as_str()) {
|
||||||
|
val
|
||||||
|
} else {
|
||||||
|
debug!("Invalid content type: {}", val);
|
||||||
|
return Err(OperationError::InvalidRequestState.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
debug!("No content type header provided");
|
||||||
|
return Err(OperationError::InvalidRequestState.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data = match field.bytes().await {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(_e) => return Err(OperationError::InvalidRequestState.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let filetype = match ImageType::try_from_content_type(&content_type) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(_err) => return Err(OperationError::InvalidRequestState.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
image = Some(ImageValue {
|
||||||
|
filetype,
|
||||||
|
filename: filename.to_string(),
|
||||||
|
contents: data.to_vec(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match image {
|
||||||
|
Some(image) => {
|
||||||
|
let image_validation_result = image.validate_image();
|
||||||
|
match image_validation_result {
|
||||||
|
Err(err) => {
|
||||||
|
admin_error!("Invalid image uploaded: {:?}", err);
|
||||||
|
Err(WebError::from(OperationError::InvalidRequestState))
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
let f_uuid =
|
||||||
|
filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO)));
|
||||||
|
state
|
||||||
|
.qe_w_ref
|
||||||
|
.handle_image_update(client_auth_info, f_uuid, Some(image))
|
||||||
|
.await
|
||||||
|
.map(Json::from)
|
||||||
|
.map_err(WebError::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(WebError::from(OperationError::InvalidAttribute(
|
||||||
|
"No image included, did you mean to use the DELETE method?".to_string(),
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
|
@ -478,7 +478,7 @@ pub(crate) async fn oauth2_id_image_delete(
|
||||||
) -> Result<Json<()>, WebError> {
|
) -> Result<Json<()>, WebError> {
|
||||||
state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_oauth2_rs_image_delete(client_auth_info, oauth2_id(&rs_name))
|
.handle_image_update(client_auth_info, oauth2_id(&rs_name), None)
|
||||||
.await
|
.await
|
||||||
.map(Json::from)
|
.map(Json::from)
|
||||||
.map_err(WebError::from)
|
.map_err(WebError::from)
|
||||||
|
@ -553,10 +553,10 @@ pub(crate) async fn oauth2_id_image_post(
|
||||||
Err(WebError::from(OperationError::InvalidRequestState))
|
Err(WebError::from(OperationError::InvalidRequestState))
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let rs_name = oauth2_id(&rs_name);
|
let rs_filter = oauth2_id(&rs_name);
|
||||||
state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_oauth2_rs_image_update(client_auth_info, rs_name, image)
|
.handle_image_update(client_auth_info, rs_filter, Some(image))
|
||||||
.await
|
.await
|
||||||
.map(Json::from)
|
.map(Json::from)
|
||||||
.map_err(WebError::from)
|
.map_err(WebError::from)
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
use super::{cookies, empty_string_as_none, HtmlTemplate, UnrecoverableErrorView};
|
use super::{cookies, empty_string_as_none, HtmlTemplate, UnrecoverableErrorView};
|
||||||
use crate::https::{extractors::VerifiedClientInformation, middleware::KOpId, ServerState};
|
use crate::https::{
|
||||||
|
extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation},
|
||||||
|
middleware::KOpId,
|
||||||
|
ServerState,
|
||||||
|
};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::State,
|
extract::State,
|
||||||
|
@ -43,6 +47,7 @@ struct SessionContext {
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "login.html")]
|
#[template(path = "login.html")]
|
||||||
struct LoginView {
|
struct LoginView {
|
||||||
|
domain_custom_image: bool,
|
||||||
username: String,
|
username: String,
|
||||||
remember_me: bool,
|
remember_me: bool,
|
||||||
}
|
}
|
||||||
|
@ -55,6 +60,7 @@ pub struct Mech<'a> {
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "login_mech_choose.html")]
|
#[template(path = "login_mech_choose.html")]
|
||||||
struct LoginMechView<'a> {
|
struct LoginMechView<'a> {
|
||||||
|
domain_custom_image: bool,
|
||||||
mechs: Vec<Mech<'a>>,
|
mechs: Vec<Mech<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +74,7 @@ enum LoginTotpError {
|
||||||
#[derive(Template, Default)]
|
#[derive(Template, Default)]
|
||||||
#[template(path = "login_totp.html")]
|
#[template(path = "login_totp.html")]
|
||||||
struct LoginTotpView {
|
struct LoginTotpView {
|
||||||
|
domain_custom_image: bool,
|
||||||
totp: String,
|
totp: String,
|
||||||
errors: LoginTotpError,
|
errors: LoginTotpError,
|
||||||
}
|
}
|
||||||
|
@ -75,16 +82,20 @@ struct LoginTotpView {
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "login_password.html")]
|
#[template(path = "login_password.html")]
|
||||||
struct LoginPasswordView {
|
struct LoginPasswordView {
|
||||||
|
domain_custom_image: bool,
|
||||||
password: String,
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "login_backupcode.html")]
|
#[template(path = "login_backupcode.html")]
|
||||||
struct LoginBackupCodeView {}
|
struct LoginBackupCodeView {
|
||||||
|
domain_custom_image: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "login_webauthn.html")]
|
#[template(path = "login_webauthn.html")]
|
||||||
struct LoginWebauthnView {
|
struct LoginWebauthnView {
|
||||||
|
domain_custom_image: bool,
|
||||||
// Control if we are rendering in security key or passkey mode.
|
// Control if we are rendering in security key or passkey mode.
|
||||||
passkey: bool,
|
passkey: bool,
|
||||||
// chal: RequestChallengeResponse,
|
// chal: RequestChallengeResponse,
|
||||||
|
@ -94,6 +105,7 @@ struct LoginWebauthnView {
|
||||||
#[derive(Template, Default)]
|
#[derive(Template, Default)]
|
||||||
#[template(path = "login_denied.html")]
|
#[template(path = "login_denied.html")]
|
||||||
struct LoginDeniedView {
|
struct LoginDeniedView {
|
||||||
|
domain_custom_image: bool,
|
||||||
reason: String,
|
reason: String,
|
||||||
operation_id: Uuid,
|
operation_id: Uuid,
|
||||||
}
|
}
|
||||||
|
@ -129,6 +141,7 @@ pub async fn view_reauth_get(
|
||||||
kopid: KOpId,
|
kopid: KOpId,
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
return_location: &str,
|
return_location: &str,
|
||||||
|
domain_info: DomainInfoRead,
|
||||||
) -> axum::response::Result<Response> {
|
) -> axum::response::Result<Response> {
|
||||||
let session_valid_result = state
|
let session_valid_result = state
|
||||||
.qe_r_ref
|
.qe_r_ref
|
||||||
|
@ -165,6 +178,7 @@ pub async fn view_reauth_get(
|
||||||
ar,
|
ar,
|
||||||
client_auth_info,
|
client_auth_info,
|
||||||
session_context,
|
session_context,
|
||||||
|
domain_info,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -194,7 +208,10 @@ pub async fn view_reauth_get(
|
||||||
|
|
||||||
let remember_me = !username.is_empty();
|
let remember_me = !username.is_empty();
|
||||||
|
|
||||||
|
let domain_custom_image = domain_info.image().is_some();
|
||||||
|
|
||||||
HtmlTemplate(LoginView {
|
HtmlTemplate(LoginView {
|
||||||
|
domain_custom_image,
|
||||||
username,
|
username,
|
||||||
remember_me,
|
remember_me,
|
||||||
})
|
})
|
||||||
|
@ -213,6 +230,7 @@ pub async fn view_reauth_get(
|
||||||
pub async fn view_index_get(
|
pub async fn view_index_get(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
DomainInfo(domain_info): DomainInfo,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
@ -236,7 +254,10 @@ pub async fn view_index_get(
|
||||||
|
|
||||||
let remember_me = !username.is_empty();
|
let remember_me = !username.is_empty();
|
||||||
|
|
||||||
|
let domain_custom_image = domain_info.image().is_some();
|
||||||
|
|
||||||
HtmlTemplate(LoginView {
|
HtmlTemplate(LoginView {
|
||||||
|
domain_custom_image,
|
||||||
username,
|
username,
|
||||||
remember_me,
|
remember_me,
|
||||||
})
|
})
|
||||||
|
@ -265,6 +286,7 @@ pub async fn view_login_begin_post(
|
||||||
State(state): State<ServerState>,
|
State(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(login_begin_form): Form<LoginBeginForm>,
|
Form(login_begin_form): Form<LoginBeginForm>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
@ -315,6 +337,7 @@ pub async fn view_login_begin_post(
|
||||||
ar,
|
ar,
|
||||||
client_auth_info,
|
client_auth_info,
|
||||||
session_context,
|
session_context,
|
||||||
|
domain_info,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -345,6 +368,7 @@ pub async fn view_login_mech_choose_post(
|
||||||
State(state): State<ServerState>,
|
State(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(login_mech_form): Form<LoginMechForm>,
|
Form(login_mech_form): Form<LoginMechForm>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
@ -378,6 +402,7 @@ pub async fn view_login_mech_choose_post(
|
||||||
ar,
|
ar,
|
||||||
client_auth_info,
|
client_auth_info,
|
||||||
session_context,
|
session_context,
|
||||||
|
domain_info,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -408,13 +433,16 @@ pub async fn view_login_totp_post(
|
||||||
State(state): State<ServerState>,
|
State(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(login_totp_form): Form<LoginTotpForm>,
|
Form(login_totp_form): Form<LoginTotpForm>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
// trim leading and trailing white space.
|
// trim leading and trailing white space.
|
||||||
let Ok(totp) = u32::from_str(login_totp_form.totp.trim()) else {
|
let Ok(totp) = u32::from_str(login_totp_form.totp.trim()) else {
|
||||||
|
let domain_custom_image = domain_info.image().is_some();
|
||||||
// If not an int, we need to re-render with an error
|
// If not an int, we need to re-render with an error
|
||||||
return HtmlTemplate(LoginTotpView {
|
return HtmlTemplate(LoginTotpView {
|
||||||
|
domain_custom_image,
|
||||||
totp: String::default(),
|
totp: String::default(),
|
||||||
errors: LoginTotpError::Syntax,
|
errors: LoginTotpError::Syntax,
|
||||||
})
|
})
|
||||||
|
@ -422,7 +450,7 @@ pub async fn view_login_totp_post(
|
||||||
};
|
};
|
||||||
|
|
||||||
let auth_cred = AuthCredential::Totp(totp);
|
let auth_cred = AuthCredential::Totp(totp);
|
||||||
credential_step(state, kopid, jar, client_auth_info, auth_cred).await
|
credential_step(state, kopid, jar, client_auth_info, auth_cred, domain_info).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
@ -434,11 +462,12 @@ pub async fn view_login_pw_post(
|
||||||
State(state): State<ServerState>,
|
State(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(login_pw_form): Form<LoginPwForm>,
|
Form(login_pw_form): Form<LoginPwForm>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let auth_cred = AuthCredential::Password(login_pw_form.password);
|
let auth_cred = AuthCredential::Password(login_pw_form.password);
|
||||||
credential_step(state, kopid, jar, client_auth_info, auth_cred).await
|
credential_step(state, kopid, jar, client_auth_info, auth_cred, domain_info).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
@ -450,35 +479,38 @@ pub async fn view_login_backupcode_post(
|
||||||
State(state): State<ServerState>,
|
State(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(login_bc_form): Form<LoginBackupCodeForm>,
|
Form(login_bc_form): Form<LoginBackupCodeForm>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
// People (like me) may copy-paste the bc with whitespace that causes issues. Trim it now.
|
// People (like me) may copy-paste the bc with whitespace that causes issues. Trim it now.
|
||||||
let trimmed = login_bc_form.backupcode.trim().to_string();
|
let trimmed = login_bc_form.backupcode.trim().to_string();
|
||||||
let auth_cred = AuthCredential::BackupCode(trimmed);
|
let auth_cred = AuthCredential::BackupCode(trimmed);
|
||||||
credential_step(state, kopid, jar, client_auth_info, auth_cred).await
|
credential_step(state, kopid, jar, client_auth_info, auth_cred, domain_info).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn view_login_passkey_post(
|
pub async fn view_login_passkey_post(
|
||||||
State(state): State<ServerState>,
|
State(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,
|
||||||
Json(assertion): Json<Box<PublicKeyCredential>>,
|
Json(assertion): Json<Box<PublicKeyCredential>>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let auth_cred = AuthCredential::Passkey(assertion);
|
let auth_cred = AuthCredential::Passkey(assertion);
|
||||||
credential_step(state, kopid, jar, client_auth_info, auth_cred).await
|
credential_step(state, kopid, jar, client_auth_info, auth_cred, domain_info).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn view_login_seckey_post(
|
pub async fn view_login_seckey_post(
|
||||||
State(state): State<ServerState>,
|
State(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,
|
||||||
Json(assertion): Json<Box<PublicKeyCredential>>,
|
Json(assertion): Json<Box<PublicKeyCredential>>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let auth_cred = AuthCredential::SecurityKey(assertion);
|
let auth_cred = AuthCredential::SecurityKey(assertion);
|
||||||
credential_step(state, kopid, jar, client_auth_info, auth_cred).await
|
credential_step(state, kopid, jar, client_auth_info, auth_cred, domain_info).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn credential_step(
|
async fn credential_step(
|
||||||
|
@ -487,6 +519,7 @@ async fn credential_step(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
client_auth_info: ClientAuthInfo,
|
client_auth_info: ClientAuthInfo,
|
||||||
auth_cred: AuthCredential,
|
auth_cred: AuthCredential,
|
||||||
|
domain_info: DomainInfoRead,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let session_context =
|
let session_context =
|
||||||
cookies::get_signed::<SessionContext>(&state, &jar, COOKIE_AUTH_SESSION_ID)
|
cookies::get_signed::<SessionContext>(&state, &jar, COOKIE_AUTH_SESSION_ID)
|
||||||
|
@ -514,6 +547,7 @@ async fn credential_step(
|
||||||
ar,
|
ar,
|
||||||
client_auth_info,
|
client_auth_info,
|
||||||
session_context,
|
session_context,
|
||||||
|
domain_info,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -542,6 +576,7 @@ async fn view_login_step(
|
||||||
auth_result: AuthResult,
|
auth_result: AuthResult,
|
||||||
client_auth_info: ClientAuthInfo,
|
client_auth_info: ClientAuthInfo,
|
||||||
mut session_context: SessionContext,
|
mut session_context: SessionContext,
|
||||||
|
domain_info: DomainInfoRead,
|
||||||
) -> Result<Response, OperationError> {
|
) -> Result<Response, OperationError> {
|
||||||
trace!(?auth_result);
|
trace!(?auth_result);
|
||||||
|
|
||||||
|
@ -551,6 +586,8 @@ async fn view_login_step(
|
||||||
} = auth_result;
|
} = auth_result;
|
||||||
session_context.id = Some(sessionid);
|
session_context.id = Some(sessionid);
|
||||||
|
|
||||||
|
let domain_custom_image = domain_info.image().is_some();
|
||||||
|
|
||||||
let mut safety = 3;
|
let mut safety = 3;
|
||||||
|
|
||||||
// Unlike the api version, only set the cookie.
|
// Unlike the api version, only set the cookie.
|
||||||
|
@ -610,7 +647,11 @@ async fn view_login_step(
|
||||||
name: m,
|
name: m,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
HtmlTemplate(LoginMechView { mechs }).into_response()
|
HtmlTemplate(LoginMechView {
|
||||||
|
domain_custom_image,
|
||||||
|
mechs,
|
||||||
|
})
|
||||||
|
.into_response()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// break acts as return in a loop.
|
// break acts as return in a loop.
|
||||||
|
@ -642,16 +683,19 @@ async fn view_login_step(
|
||||||
})
|
})
|
||||||
.into_response(),
|
.into_response(),
|
||||||
AuthAllowed::Password => HtmlTemplate(LoginPasswordView {
|
AuthAllowed::Password => HtmlTemplate(LoginPasswordView {
|
||||||
|
domain_custom_image,
|
||||||
password: session_context.password.clone().unwrap_or_default(),
|
password: session_context.password.clone().unwrap_or_default(),
|
||||||
})
|
})
|
||||||
.into_response(),
|
.into_response(),
|
||||||
AuthAllowed::BackupCode => {
|
AuthAllowed::BackupCode => HtmlTemplate(LoginBackupCodeView {
|
||||||
HtmlTemplate(LoginBackupCodeView {}).into_response()
|
domain_custom_image,
|
||||||
}
|
})
|
||||||
|
.into_response(),
|
||||||
AuthAllowed::SecurityKey(chal) => {
|
AuthAllowed::SecurityKey(chal) => {
|
||||||
let chal_json = serde_json::to_string(&chal)
|
let chal_json = serde_json::to_string(&chal)
|
||||||
.map_err(|_| OperationError::SerdeJsonError)?;
|
.map_err(|_| OperationError::SerdeJsonError)?;
|
||||||
HtmlTemplate(LoginWebauthnView {
|
HtmlTemplate(LoginWebauthnView {
|
||||||
|
domain_custom_image,
|
||||||
passkey: false,
|
passkey: false,
|
||||||
chal: chal_json,
|
chal: chal_json,
|
||||||
})
|
})
|
||||||
|
@ -661,6 +705,7 @@ async fn view_login_step(
|
||||||
let chal_json = serde_json::to_string(&chal)
|
let chal_json = serde_json::to_string(&chal)
|
||||||
.map_err(|_| OperationError::SerdeJsonError)?;
|
.map_err(|_| OperationError::SerdeJsonError)?;
|
||||||
HtmlTemplate(LoginWebauthnView {
|
HtmlTemplate(LoginWebauthnView {
|
||||||
|
domain_custom_image,
|
||||||
passkey: true,
|
passkey: true,
|
||||||
chal: chal_json,
|
chal: chal_json,
|
||||||
})
|
})
|
||||||
|
@ -738,6 +783,7 @@ async fn view_login_step(
|
||||||
jar = jar.remove(Cookie::from(COOKIE_AUTH_SESSION_ID));
|
jar = jar.remove(Cookie::from(COOKIE_AUTH_SESSION_ID));
|
||||||
|
|
||||||
break HtmlTemplate(LoginDeniedView {
|
break HtmlTemplate(LoginDeniedView {
|
||||||
|
domain_custom_image,
|
||||||
reason,
|
reason,
|
||||||
operation_id: kopid.eventid,
|
operation_id: kopid.eventid,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::https::extractors::VerifiedClientInformation;
|
use crate::https::extractors::{DomainInfo, VerifiedClientInformation};
|
||||||
use crate::https::middleware::KOpId;
|
use crate::https::middleware::KOpId;
|
||||||
use crate::https::views::errors::HtmxError;
|
use crate::https::views::errors::HtmxError;
|
||||||
use crate::https::views::HtmlTemplate;
|
use crate::https::views::HtmlTemplate;
|
||||||
|
@ -73,8 +73,17 @@ pub(crate) async fn view_profile_get(
|
||||||
pub(crate) async fn view_profile_unlock_get(
|
pub(crate) async fn view_profile_unlock_get(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
DomainInfo(domain_info): DomainInfo,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
) -> axum::response::Result<Response> {
|
) -> axum::response::Result<Response> {
|
||||||
super::login::view_reauth_get(state, client_auth_info, kopid, jar, "/ui/profile").await
|
super::login::view_reauth_get(
|
||||||
|
state,
|
||||||
|
client_auth_info,
|
||||||
|
kopid,
|
||||||
|
jar,
|
||||||
|
"/ui/profile",
|
||||||
|
domain_info,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ use kanidm_proto::internal::{
|
||||||
UserAuthToken, COOKIE_CU_SESSION_TOKEN,
|
UserAuthToken, COOKIE_CU_SESSION_TOKEN,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::https::extractors::VerifiedClientInformation;
|
use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation};
|
||||||
use crate::https::middleware::KOpId;
|
use crate::https::middleware::KOpId;
|
||||||
use crate::https::views::errors::HtmxError;
|
use crate::https::views::errors::HtmxError;
|
||||||
use crate::https::ServerState;
|
use crate::https::ServerState;
|
||||||
|
@ -34,14 +34,14 @@ use super::{HtmlTemplate, UnrecoverableErrorView};
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "credentials_reset_form.html")]
|
#[template(path = "credentials_reset_form.html")]
|
||||||
struct ResetCredFormView {
|
struct ResetCredFormView {
|
||||||
domain: String,
|
domain_info: DomainInfoRead,
|
||||||
wrong_code: bool,
|
wrong_code: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "credentials_reset.html")]
|
#[template(path = "credentials_reset.html")]
|
||||||
struct CredResetView {
|
struct CredResetView {
|
||||||
domain: String,
|
domain_info: DomainInfoRead,
|
||||||
names: String,
|
names: String,
|
||||||
credentials_update_partial: CredResetPartialView,
|
credentials_update_partial: CredResetPartialView,
|
||||||
}
|
}
|
||||||
|
@ -571,6 +571,7 @@ pub(crate) async fn view_self_reset_get(
|
||||||
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,
|
||||||
mut jar: CookieJar,
|
mut jar: CookieJar,
|
||||||
) -> axum::response::Result<Response> {
|
) -> axum::response::Result<Response> {
|
||||||
let uat: UserAuthToken = state
|
let uat: UserAuthToken = state
|
||||||
|
@ -589,13 +590,7 @@ pub(crate) async fn view_self_reset_get(
|
||||||
.map_err(|op_err| HtmxError::new(&kopid, op_err))
|
.map_err(|op_err| HtmxError::new(&kopid, op_err))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let domain_display_name = state
|
let cu_resp = get_cu_response(domain_info, cu_status);
|
||||||
.qe_r_ref
|
|
||||||
.get_domain_display_name(kopid.eventid)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let cu_resp = get_cu_response(domain_display_name, cu_status);
|
|
||||||
|
|
||||||
jar = add_cu_cookie(jar, &state, cu_session_token);
|
jar = add_cu_cookie(jar, &state, cu_session_token);
|
||||||
Ok((jar, cu_resp).into_response())
|
Ok((jar, cu_resp).into_response())
|
||||||
|
@ -606,6 +601,7 @@ pub(crate) async fn view_self_reset_get(
|
||||||
kopid,
|
kopid,
|
||||||
jar,
|
jar,
|
||||||
"/ui/update_credentials",
|
"/ui/update_credentials",
|
||||||
|
domain_info,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -629,14 +625,10 @@ pub(crate) async fn view_reset_get(
|
||||||
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,
|
||||||
Query(params): Query<ResetTokenParam>,
|
Query(params): Query<ResetTokenParam>,
|
||||||
mut jar: CookieJar,
|
mut jar: CookieJar,
|
||||||
) -> axum::response::Result<Response> {
|
) -> axum::response::Result<Response> {
|
||||||
let domain_display_name = state
|
|
||||||
.qe_r_ref
|
|
||||||
.get_domain_display_name(kopid.eventid)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
|
||||||
let push_url = HxPushUrl(Uri::from_static("/ui/reset"));
|
let push_url = HxPushUrl(Uri::from_static("/ui/reset"));
|
||||||
let cookie = jar.get(COOKIE_CU_SESSION_TOKEN);
|
let cookie = jar.get(COOKIE_CU_SESSION_TOKEN);
|
||||||
if let Some(cookie) = cookie {
|
if let Some(cookie) = cookie {
|
||||||
|
@ -669,7 +661,7 @@ pub(crate) async fn view_reset_get(
|
||||||
};
|
};
|
||||||
|
|
||||||
// CU Session cookie is okay
|
// CU Session cookie is okay
|
||||||
let cu_resp = get_cu_response(domain_display_name, cu_status);
|
let cu_resp = get_cu_response(domain_info, cu_status);
|
||||||
Ok(cu_resp)
|
Ok(cu_resp)
|
||||||
} else if let Some(token) = params.token {
|
} else if let Some(token) = params.token {
|
||||||
// We have a reset token and want to create a new session
|
// We have a reset token and want to create a new session
|
||||||
|
@ -679,14 +671,14 @@ pub(crate) async fn view_reset_get(
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((cu_session_token, cu_status)) => {
|
Ok((cu_session_token, cu_status)) => {
|
||||||
let cu_resp = get_cu_response(domain_display_name, cu_status);
|
let cu_resp = get_cu_response(domain_info, cu_status);
|
||||||
|
|
||||||
jar = add_cu_cookie(jar, &state, cu_session_token);
|
jar = add_cu_cookie(jar, &state, cu_session_token);
|
||||||
Ok((jar, cu_resp).into_response())
|
Ok((jar, cu_resp).into_response())
|
||||||
}
|
}
|
||||||
Err(OperationError::SessionExpired) | Err(OperationError::Wait(_)) => {
|
Err(OperationError::SessionExpired) | Err(OperationError::Wait(_)) => {
|
||||||
let cred_form_view = ResetCredFormView {
|
let cred_form_view = ResetCredFormView {
|
||||||
domain: domain_display_name.clone(),
|
domain_info,
|
||||||
wrong_code: true,
|
wrong_code: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -699,7 +691,7 @@ pub(crate) async fn view_reset_get(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let cred_form_view = ResetCredFormView {
|
let cred_form_view = ResetCredFormView {
|
||||||
domain: domain_display_name.clone(),
|
domain_info,
|
||||||
wrong_code: false,
|
wrong_code: false,
|
||||||
};
|
};
|
||||||
// We don't have any credential, show reset token input form
|
// We don't have any credential, show reset token input form
|
||||||
|
@ -744,7 +736,7 @@ fn get_cu_partial_response(cu_status: CUStatus) -> Response {
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cu_response(domain: String, cu_status: CUStatus) -> Response {
|
fn get_cu_response(domain_info: DomainInfoRead, cu_status: CUStatus) -> Response {
|
||||||
let spn = cu_status.spn.clone();
|
let spn = cu_status.spn.clone();
|
||||||
let displayname = cu_status.displayname.clone();
|
let displayname = cu_status.displayname.clone();
|
||||||
let (username, _domain) = spn.split_once('@').unwrap_or(("", &spn));
|
let (username, _domain) = spn.split_once('@').unwrap_or(("", &spn));
|
||||||
|
@ -753,7 +745,7 @@ fn get_cu_response(domain: String, cu_status: CUStatus) -> Response {
|
||||||
(
|
(
|
||||||
HxPushUrl(Uri::from_static("/ui/reset")),
|
HxPushUrl(Uri::from_static("/ui/reset")),
|
||||||
HtmlTemplate(CredResetView {
|
HtmlTemplate(CredResetView {
|
||||||
domain,
|
domain_info,
|
||||||
names,
|
names,
|
||||||
credentials_update_partial,
|
credentials_update_partial,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -556,7 +556,7 @@ pub async fn domain_rename_core(config: &Configuration) {
|
||||||
let new_domain_name = config.domain.as_str();
|
let new_domain_name = config.domain.as_str();
|
||||||
|
|
||||||
// make sure we're actually changing the domain name...
|
// make sure we're actually changing the domain name...
|
||||||
match qs.read().await.and_then(|mut qs| qs.get_db_domain_name()) {
|
match qs.read().await.map(|qs| qs.get_domain_name().to_string()) {
|
||||||
Ok(old_domain_name) => {
|
Ok(old_domain_name) => {
|
||||||
admin_info!(?old_domain_name, ?new_domain_name);
|
admin_info!(?old_domain_name, ?new_domain_name);
|
||||||
if old_domain_name == new_domain_name {
|
if old_domain_name == new_domain_name {
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="py-3 text-center">
|
<div class="py-3 text-center">
|
||||||
<h3>Updating Credentials</h3>
|
<h3>Updating Credentials</h3>
|
||||||
<p>(( names ))</p>
|
<p>(( names ))</p>
|
||||||
<p>(( domain ))</p>
|
<p>(( domain_info.display_name() ))</p>
|
||||||
</div>
|
</div>
|
||||||
(( credentials_update_partial|safe ))
|
(( credentials_update_partial|safe ))
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<center>
|
<center>
|
||||||
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
|
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
|
||||||
<h2>Credential Reset</h2>
|
<h2>Credential Reset</h2>
|
||||||
<h3>(( domain ))</h3>
|
<h3>(( domain_info.display_name() ))</h3>
|
||||||
</center>
|
</center>
|
||||||
<form class="mb-3">
|
<form class="mb-3">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -8,7 +8,11 @@
|
||||||
(% block body %)
|
(% block body %)
|
||||||
<main id="main" class="flex-shrink-0 form-signin">
|
<main id="main" class="flex-shrink-0 form-signin">
|
||||||
<center>
|
<center>
|
||||||
|
(% if domain_custom_image %)
|
||||||
|
<img src="/ui/images/domain" alt="Kanidm" class="kanidm_logo"/>
|
||||||
|
(% else %)
|
||||||
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
|
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
|
||||||
|
(% endif %)
|
||||||
<h3>Kanidm</h3>
|
<h3>Kanidm</h3>
|
||||||
</center>
|
</center>
|
||||||
<div id="login-form-container" class="container">
|
<div id="login-form-container" class="container">
|
||||||
|
|
|
@ -907,6 +907,61 @@ lazy_static! {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref IDM_ACP_DOMAIN_ADMIN_DL8: BuiltinAcp = BuiltinAcp {
|
||||||
|
classes: vec![
|
||||||
|
EntryClass::Object,
|
||||||
|
EntryClass::AccessControlProfile,
|
||||||
|
EntryClass::AccessControlModify,
|
||||||
|
EntryClass::AccessControlSearch
|
||||||
|
],
|
||||||
|
name: "idm_acp_domain_admin",
|
||||||
|
uuid: UUID_IDM_ACP_DOMAIN_ADMIN_V1,
|
||||||
|
description: "Builtin IDM Control for granting domain info administration locally",
|
||||||
|
receiver: BuiltinAcpReceiver::Group(vec![UUID_DOMAIN_ADMINS]),
|
||||||
|
target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
|
||||||
|
ProtoFilter::Eq(
|
||||||
|
Attribute::Uuid.to_string(),
|
||||||
|
STR_UUID_DOMAIN_INFO.to_string()
|
||||||
|
),
|
||||||
|
FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
|
||||||
|
])),
|
||||||
|
search_attrs: vec![
|
||||||
|
Attribute::Class,
|
||||||
|
Attribute::Name,
|
||||||
|
Attribute::Uuid,
|
||||||
|
Attribute::DomainDisplayName,
|
||||||
|
Attribute::DomainName,
|
||||||
|
Attribute::DomainLdapBasedn,
|
||||||
|
Attribute::DomainSsid,
|
||||||
|
Attribute::DomainUuid,
|
||||||
|
Attribute::KeyInternalData,
|
||||||
|
Attribute::LdapAllowUnixPwBind,
|
||||||
|
Attribute::Version,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
modify_removed_attrs: vec![
|
||||||
|
Attribute::DomainDisplayName,
|
||||||
|
Attribute::DomainSsid,
|
||||||
|
Attribute::DomainLdapBasedn,
|
||||||
|
Attribute::LdapAllowUnixPwBind,
|
||||||
|
Attribute::KeyActionRevoke,
|
||||||
|
Attribute::KeyActionRotate,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
modify_present_attrs: vec![
|
||||||
|
Attribute::DomainDisplayName,
|
||||||
|
Attribute::DomainLdapBasedn,
|
||||||
|
Attribute::DomainSsid,
|
||||||
|
Attribute::LdapAllowUnixPwBind,
|
||||||
|
Attribute::KeyActionRevoke,
|
||||||
|
Attribute::KeyActionRotate,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref IDM_ACP_SYNC_ACCOUNT_MANAGE_V1: BuiltinAcp = BuiltinAcp {
|
pub static ref IDM_ACP_SYNC_ACCOUNT_MANAGE_V1: BuiltinAcp = BuiltinAcp {
|
||||||
classes: vec![
|
classes: vec![
|
||||||
|
|
|
@ -1113,6 +1113,29 @@ pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL7: SchemaClass = SchemaClass {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL8: SchemaClass = SchemaClass {
|
||||||
|
uuid: UUID_SCHEMA_CLASS_DOMAIN_INFO,
|
||||||
|
name: EntryClass::DomainInfo.into(),
|
||||||
|
description: "Local domain information and configuration".to_string(),
|
||||||
|
|
||||||
|
systemmay: vec![
|
||||||
|
Attribute::DomainSsid.into(),
|
||||||
|
Attribute::DomainLdapBasedn.into(),
|
||||||
|
Attribute::LdapAllowUnixPwBind.into(),
|
||||||
|
Attribute::Image.into(),
|
||||||
|
Attribute::PatchLevel.into(),
|
||||||
|
Attribute::DomainDevelopmentTaint.into(),
|
||||||
|
],
|
||||||
|
systemmust: vec![
|
||||||
|
Attribute::Name.into(),
|
||||||
|
Attribute::DomainUuid.into(),
|
||||||
|
Attribute::DomainName.into(),
|
||||||
|
Attribute::DomainDisplayName.into(),
|
||||||
|
Attribute::Version.into(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
pub static ref SCHEMA_CLASS_POSIXGROUP: SchemaClass = SchemaClass {
|
pub static ref SCHEMA_CLASS_POSIXGROUP: SchemaClass = SchemaClass {
|
||||||
uuid: UUID_SCHEMA_CLASS_POSIXGROUP,
|
uuid: UUID_SCHEMA_CLASS_POSIXGROUP,
|
||||||
name: EntryClass::PosixGroup.into(),
|
name: EntryClass::PosixGroup.into(),
|
||||||
|
|
|
@ -6,6 +6,7 @@ use kanidm_lib_crypto::CryptoPolicy;
|
||||||
|
|
||||||
use compact_jwt::{Jwk, JwsCompact};
|
use compact_jwt::{Jwk, JwsCompact};
|
||||||
use concread::bptree::{BptreeMap, BptreeMapReadTxn, BptreeMapWriteTxn};
|
use concread::bptree::{BptreeMap, BptreeMapReadTxn, BptreeMapWriteTxn};
|
||||||
|
use concread::cowcell::CowCellReadTxn;
|
||||||
use concread::hashmap::HashMap;
|
use concread::hashmap::HashMap;
|
||||||
use kanidm_proto::internal::{
|
use kanidm_proto::internal::{
|
||||||
ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, ScimSyncToken,
|
ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, ScimSyncToken,
|
||||||
|
@ -56,12 +57,15 @@ use crate::idm::unix::{UnixGroup, UnixUserAccount};
|
||||||
use crate::idm::AuthState;
|
use crate::idm::AuthState;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::server::keys::KeyProvidersTransaction;
|
use crate::server::keys::KeyProvidersTransaction;
|
||||||
|
use crate::server::DomainInfo;
|
||||||
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
|
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
|
||||||
use crate::value::{Session, SessionState};
|
use crate::value::{Session, SessionState};
|
||||||
|
|
||||||
pub(crate) type AuthSessionMutex = Arc<Mutex<AuthSession>>;
|
pub(crate) type AuthSessionMutex = Arc<Mutex<AuthSession>>;
|
||||||
pub(crate) type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;
|
pub(crate) type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;
|
||||||
|
|
||||||
|
pub type DomainInfoRead = CowCellReadTxn<DomainInfo>;
|
||||||
|
|
||||||
pub struct IdmServer {
|
pub struct IdmServer {
|
||||||
// There is a good reason to keep this single thread - it
|
// There is a good reason to keep this single thread - it
|
||||||
// means that limits to sessions can be easily applied and checked to
|
// means that limits to sessions can be easily applied and checked to
|
||||||
|
@ -246,6 +250,14 @@ impl IdmServer {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Begin a fast (low cost) read of the servers domain info. It is important to note
|
||||||
|
/// this does not conflict with any other type of transaction type and may safely
|
||||||
|
/// beheld over other transaction boundaries.
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
|
pub fn domain_read(&self) -> DomainInfoRead {
|
||||||
|
self.qs.d_info.read()
|
||||||
|
}
|
||||||
|
|
||||||
/// Read from the database, in a transaction.
|
/// Read from the database, in a transaction.
|
||||||
#[instrument(level = "debug", skip_all)]
|
#[instrument(level = "debug", skip_all)]
|
||||||
pub async fn proxy_read(&self) -> Result<IdmServerProxyReadTransaction<'_>, OperationError> {
|
pub async fn proxy_read(&self) -> Result<IdmServerProxyReadTransaction<'_>, OperationError> {
|
||||||
|
|
|
@ -34,6 +34,7 @@ lazy_static! {
|
||||||
Attribute::BadlistPassword,
|
Attribute::BadlistPassword,
|
||||||
Attribute::DeniedName,
|
Attribute::DeniedName,
|
||||||
Attribute::DomainDisplayName,
|
Attribute::DomainDisplayName,
|
||||||
|
Attribute::Image,
|
||||||
// modification of account policy values for dyngroup.
|
// modification of account policy values for dyngroup.
|
||||||
Attribute::AuthSessionExpiry,
|
Attribute::AuthSessionExpiry,
|
||||||
Attribute::PrivilegeExpiry,
|
Attribute::PrivilegeExpiry,
|
||||||
|
|
|
@ -599,6 +599,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
SCHEMA_ATTR_APPLICATION_PASSWORD_DL8.clone().into(),
|
SCHEMA_ATTR_APPLICATION_PASSWORD_DL8.clone().into(),
|
||||||
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
|
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
|
||||||
SCHEMA_CLASS_PERSON_DL8.clone().into(),
|
SCHEMA_CLASS_PERSON_DL8.clone().into(),
|
||||||
|
SCHEMA_CLASS_DOMAIN_INFO_DL8.clone().into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
idm_schema_classes
|
idm_schema_classes
|
||||||
|
@ -622,6 +623,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
BUILTIN_GROUP_MAIL_SERVICE_ADMINS_DL8.clone().try_into()?,
|
BUILTIN_GROUP_MAIL_SERVICE_ADMINS_DL8.clone().try_into()?,
|
||||||
BUILTIN_IDM_MAIL_SERVERS_DL8.clone().try_into()?,
|
BUILTIN_IDM_MAIL_SERVERS_DL8.clone().try_into()?,
|
||||||
IDM_ACP_MAIL_SERVERS_DL8.clone().into(),
|
IDM_ACP_MAIL_SERVERS_DL8.clone().into(),
|
||||||
|
IDM_ACP_DOMAIN_ADMIN_DL8.clone().into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
idm_data
|
idm_data
|
||||||
|
|
|
@ -11,7 +11,7 @@ use std::collections::BTreeSet;
|
||||||
use tokio::sync::{Semaphore, SemaphorePermit};
|
use tokio::sync::{Semaphore, SemaphorePermit};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use kanidm_proto::internal::{DomainInfo as ProtoDomainInfo, UiHint};
|
use kanidm_proto::internal::{DomainInfo as ProtoDomainInfo, ImageValue, UiHint};
|
||||||
|
|
||||||
use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction};
|
use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction};
|
||||||
// We use so many, we just import them all ...
|
// We use so many, we just import them all ...
|
||||||
|
@ -66,6 +66,8 @@ pub(crate) enum ServerPhase {
|
||||||
Running,
|
Running,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Domain Information. This should not contain sensitive information, the data within
|
||||||
|
/// this structure may be used for public presentation.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct DomainInfo {
|
pub struct DomainInfo {
|
||||||
pub(crate) d_uuid: Uuid,
|
pub(crate) d_uuid: Uuid,
|
||||||
|
@ -75,6 +77,26 @@ pub struct DomainInfo {
|
||||||
pub(crate) d_patch_level: u32,
|
pub(crate) d_patch_level: u32,
|
||||||
pub(crate) d_devel_taint: bool,
|
pub(crate) d_devel_taint: bool,
|
||||||
pub(crate) d_ldap_allow_unix_pw_bind: bool,
|
pub(crate) d_ldap_allow_unix_pw_bind: bool,
|
||||||
|
// In future this should be image reference instead of the image itself.
|
||||||
|
d_image: Option<ImageValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DomainInfo {
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
self.d_name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_name(&self) -> &str {
|
||||||
|
self.d_display.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn devel_taint(&self) -> bool {
|
||||||
|
self.d_devel_taint
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn image(&self) -> Option<&ImageValue> {
|
||||||
|
self.d_image.as_ref()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
@ -209,6 +231,8 @@ pub trait QueryServerTransaction<'a> {
|
||||||
|
|
||||||
fn get_domain_display_name(&self) -> &str;
|
fn get_domain_display_name(&self) -> &str;
|
||||||
|
|
||||||
|
fn get_domain_image_value(&self) -> Option<ImageValue>;
|
||||||
|
|
||||||
fn get_resolve_filter_cache(&mut self) -> &mut ResolveFilterCacheReadTxn<'a>;
|
fn get_resolve_filter_cache(&mut self) -> &mut ResolveFilterCacheReadTxn<'a>;
|
||||||
|
|
||||||
// Because of how borrowck in rust works, if we need to get two inner types we have to get them
|
// Because of how borrowck in rust works, if we need to get two inner types we have to get them
|
||||||
|
@ -891,19 +915,8 @@ pub trait QueryServerTransaction<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pull the domain name from the database
|
fn get_db_domain(&mut self) -> Result<Arc<EntrySealedCommitted>, OperationError> {
|
||||||
fn get_db_domain_name(&mut self) -> Result<String, OperationError> {
|
|
||||||
self.internal_search_uuid(UUID_DOMAIN_INFO)
|
self.internal_search_uuid(UUID_DOMAIN_INFO)
|
||||||
.and_then(|e| {
|
|
||||||
trace!(?e);
|
|
||||||
e.get_ava_single_iname(Attribute::DomainName)
|
|
||||||
.map(str::to_string)
|
|
||||||
.ok_or(OperationError::InvalidEntryState)
|
|
||||||
})
|
|
||||||
.map_err(|e| {
|
|
||||||
admin_error!(?e, "Error getting domain name");
|
|
||||||
e
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_domain_key_object_handle(&self) -> Result<Arc<KeyObject>, OperationError> {
|
fn get_domain_key_object_handle(&self) -> Result<Arc<KeyObject>, OperationError> {
|
||||||
|
@ -1105,6 +1118,10 @@ impl<'a> QueryServerTransaction<'a> for QueryServerReadTransaction<'a> {
|
||||||
fn get_domain_display_name(&self) -> &str {
|
fn get_domain_display_name(&self) -> &str {
|
||||||
&self.d_info.d_display
|
&self.d_info.d_display
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_domain_image_value(&self) -> Option<ImageValue> {
|
||||||
|
self.d_info.d_image.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> QueryServerReadTransaction<'a> {
|
impl<'a> QueryServerReadTransaction<'a> {
|
||||||
|
@ -1257,6 +1274,10 @@ impl<'a> QueryServerTransaction<'a> for QueryServerWriteTransaction<'a> {
|
||||||
fn get_domain_display_name(&self) -> &str {
|
fn get_domain_display_name(&self) -> &str {
|
||||||
&self.d_info.d_display
|
&self.d_info.d_display
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_domain_image_value(&self) -> Option<ImageValue> {
|
||||||
|
self.d_info.d_image.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QueryServer {
|
impl QueryServer {
|
||||||
|
@ -1294,6 +1315,7 @@ impl QueryServer {
|
||||||
// Automatically derive our current taint mode based on the PRERELEASE setting.
|
// Automatically derive our current taint mode based on the PRERELEASE setting.
|
||||||
d_devel_taint: option_env!("KANIDM_PRE_RELEASE").is_some(),
|
d_devel_taint: option_env!("KANIDM_PRE_RELEASE").is_some(),
|
||||||
d_ldap_allow_unix_pw_bind: false,
|
d_ldap_allow_unix_pw_bind: false,
|
||||||
|
d_image: None,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let cid = Cid::new_lamport(s_uuid, curtime, &ts_max);
|
let cid = Cid::new_lamport(s_uuid, curtime, &ts_max);
|
||||||
|
@ -1840,20 +1862,6 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_db_domain_display_name(&mut self) -> Result<String, OperationError> {
|
|
||||||
self.internal_search_uuid(UUID_DOMAIN_INFO)
|
|
||||||
.and_then(|e| {
|
|
||||||
trace!(?e);
|
|
||||||
e.get_ava_single_utf8(Attribute::DomainDisplayName)
|
|
||||||
.map(str::to_string)
|
|
||||||
.ok_or(OperationError::InvalidEntryState)
|
|
||||||
})
|
|
||||||
.map_err(|e| {
|
|
||||||
admin_error!(?e, "Error getting domain display name");
|
|
||||||
e
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all)]
|
#[instrument(level = "debug", skip_all)]
|
||||||
pub(crate) fn reload_key_material(&mut self) -> Result<(), OperationError> {
|
pub(crate) fn reload_key_material(&mut self) -> Result<(), OperationError> {
|
||||||
let filt = filter!(f_eq(Attribute::Class, EntryClass::KeyProvider.into()));
|
let filt = filter!(f_eq(Attribute::Class, EntryClass::KeyProvider.into()));
|
||||||
|
@ -1988,16 +1996,26 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
/// Pulls the domain name from the database and updates the DomainInfo data in memory
|
/// Pulls the domain name from the database and updates the DomainInfo data in memory
|
||||||
#[instrument(level = "debug", skip_all)]
|
#[instrument(level = "debug", skip_all)]
|
||||||
pub(crate) fn reload_domain_info(&mut self) -> Result<(), OperationError> {
|
pub(crate) fn reload_domain_info(&mut self) -> Result<(), OperationError> {
|
||||||
let domain_name = self.get_db_domain_name()?;
|
let domain_entry = self.get_db_domain()?;
|
||||||
let display_name = self.get_db_domain_display_name()?;
|
|
||||||
let domain_ldap_allow_unix_pw_bind = match self.get_domain_ldap_allow_unix_pw_bind() {
|
let domain_name = domain_entry
|
||||||
Ok(v) => v,
|
.get_ava_single_iname(Attribute::DomainName)
|
||||||
_ => {
|
.map(str::to_string)
|
||||||
admin_warn!("Defaulting ldap_allow_unix_pw_bind to true");
|
.ok_or(OperationError::InvalidEntryState)?;
|
||||||
true
|
|
||||||
}
|
let display_name = domain_entry
|
||||||
};
|
.get_ava_single_utf8(Attribute::DomainDisplayName)
|
||||||
|
.map(str::to_string)
|
||||||
|
.ok_or(OperationError::InvalidEntryState)?;
|
||||||
|
|
||||||
|
let domain_ldap_allow_unix_pw_bind = domain_entry
|
||||||
|
.get_ava_single_bool(Attribute::LdapAllowUnixPwBind)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
let domain_image = domain_entry.get_ava_single_image(Attribute::Image);
|
||||||
|
|
||||||
let domain_uuid = self.be_txn.get_db_d_uuid()?;
|
let domain_uuid = self.be_txn.get_db_d_uuid()?;
|
||||||
|
|
||||||
let mut_d_info = self.d_info.get_mut();
|
let mut_d_info = self.d_info.get_mut();
|
||||||
mut_d_info.d_ldap_allow_unix_pw_bind = domain_ldap_allow_unix_pw_bind;
|
mut_d_info.d_ldap_allow_unix_pw_bind = domain_ldap_allow_unix_pw_bind;
|
||||||
if mut_d_info.d_uuid != domain_uuid {
|
if mut_d_info.d_uuid != domain_uuid {
|
||||||
|
@ -2020,6 +2038,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
mut_d_info.d_name = domain_name;
|
mut_d_info.d_name = domain_name;
|
||||||
}
|
}
|
||||||
mut_d_info.d_display = display_name;
|
mut_d_info.d_display = display_name;
|
||||||
|
mut_d_info.d_image = domain_image;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,7 @@ pub struct ValueSetImage {
|
||||||
|
|
||||||
pub(crate) const MAX_IMAGE_HEIGHT: u32 = 1024;
|
pub(crate) const MAX_IMAGE_HEIGHT: u32 = 1024;
|
||||||
pub(crate) const MAX_IMAGE_WIDTH: u32 = 1024;
|
pub(crate) const MAX_IMAGE_WIDTH: u32 = 1024;
|
||||||
/// 128kb should be enough for anyone... right? :D
|
pub(crate) const MAX_FILE_SIZE: u32 = 1024 * 256;
|
||||||
pub(crate) const MAX_FILE_SIZE: u32 = 1024 * 128;
|
|
||||||
|
|
||||||
const WEBP_MAGIC: &[u8; 4] = b"RIFF";
|
const WEBP_MAGIC: &[u8; 4] = b"RIFF";
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
use crate::common::OpType;
|
use crate::common::OpType;
|
||||||
use crate::{handle_client_error, DomainOpt};
|
use crate::{handle_client_error, DomainOpt};
|
||||||
|
use anyhow::{Context, Error};
|
||||||
|
use kanidm_proto::internal::ImageValue;
|
||||||
|
use std::fs::read;
|
||||||
|
|
||||||
impl DomainOpt {
|
impl DomainOpt {
|
||||||
pub fn debug(&self) -> bool {
|
pub fn debug(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
DomainOpt::SetDisplayName(copt) => copt.copt.debug,
|
DomainOpt::SetDisplayName(copt) => copt.copt.debug,
|
||||||
DomainOpt::SetLdapBasedn { copt, .. }
|
DomainOpt::SetLdapBasedn { copt, .. }
|
||||||
|
| DomainOpt::SetImage { copt, .. }
|
||||||
|
| DomainOpt::RemoveImage { copt }
|
||||||
| DomainOpt::SetLdapAllowUnixPasswordBind { copt, .. }
|
| DomainOpt::SetLdapAllowUnixPasswordBind { copt, .. }
|
||||||
| DomainOpt::RevokeKey { copt, .. }
|
| DomainOpt::RevokeKey { copt, .. }
|
||||||
| DomainOpt::Show(copt) => copt.debug,
|
| DomainOpt::Show(copt) => copt.debug,
|
||||||
|
@ -60,6 +65,58 @@ impl DomainOpt {
|
||||||
Err(e) => handle_client_error(e, copt.output_mode),
|
Err(e) => handle_client_error(e, copt.output_mode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DomainOpt::SetImage {
|
||||||
|
copt,
|
||||||
|
path,
|
||||||
|
image_type,
|
||||||
|
} => {
|
||||||
|
let client = copt.to_client(OpType::Write).await;
|
||||||
|
let img_res: Result<ImageValue, Error> = (move || {
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.context("Please pass a file")?
|
||||||
|
.to_str()
|
||||||
|
.context("Path contains non utf-8")?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let image_type = if let Some(image_type) = image_type {
|
||||||
|
image_type.as_str().try_into().map_err(Error::msg)?
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
.extension().context("Path has no extension so we can't infer the imageType, or you could pass the optional imageType argument yourself.")?
|
||||||
|
.to_str().context("Path contains invalid utf-8")?
|
||||||
|
.try_into()
|
||||||
|
.map_err(Error::msg)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let read_res = read(path);
|
||||||
|
match read_res {
|
||||||
|
Ok(data) => Ok(ImageValue::new(file_name, image_type, data)),
|
||||||
|
Err(err) => Err(err).context("Reading error"),
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
let img = match img_res {
|
||||||
|
Ok(img) => img,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match client.idm_domain_update_image(img).await {
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => handle_client_error(e, copt.output_mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DomainOpt::RemoveImage { copt } => {
|
||||||
|
let client = copt.to_client(OpType::Write).await;
|
||||||
|
|
||||||
|
match client.idm_domain_delete_image().await {
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => handle_client_error(e, copt.output_mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1274,6 +1274,22 @@ pub enum DomainOpt {
|
||||||
copt: CommonOpt,
|
copt: CommonOpt,
|
||||||
key_id: String,
|
key_id: String,
|
||||||
},
|
},
|
||||||
|
/// The image presented as the instance logo
|
||||||
|
#[clap(name="set-image")]
|
||||||
|
SetImage {
|
||||||
|
#[clap(flatten)]
|
||||||
|
copt: CommonOpt,
|
||||||
|
#[clap(name = "file-path")]
|
||||||
|
path: PathBuf,
|
||||||
|
#[clap(name = "image-type")]
|
||||||
|
image_type: Option<String>,
|
||||||
|
},
|
||||||
|
/// The remove the current instance logo, reverting to the default.
|
||||||
|
#[clap(name="remove-image")]
|
||||||
|
RemoveImage {
|
||||||
|
#[clap(flatten)]
|
||||||
|
copt: CommonOpt,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
|
|
Loading…
Reference in a new issue