kanidmd_core/https/
v1_domain.rs

1use super::apidocs::response_schema::DefaultApiResponse;
2use super::errors::WebError;
3use super::ServerState;
4use crate::https::extractors::DomainInfo;
5use crate::https::extractors::VerifiedClientInformation;
6use axum::extract::State;
7use axum::Json;
8use axum::{
9    http::header::CONTENT_TYPE,
10    http::StatusCode,
11    response::{IntoResponse, Response},
12};
13use kanidm_proto::internal::{ImageType, ImageValue};
14use kanidmd_lib::prelude::*;
15use kanidmd_lib::valueset::image::ImageValueThings;
16use sketching::admin_error;
17
18pub(crate) async fn image_get(DomainInfo(domain_info): DomainInfo) -> Response {
19    match domain_info.image() {
20        Some(image) => (
21            StatusCode::OK,
22            [(CONTENT_TYPE, image.filetype.as_content_type_str())],
23            image.contents.clone(),
24        )
25            .into_response(),
26        None => {
27            warn!("No image set for domain");
28            (StatusCode::NOT_FOUND, "").into_response()
29        }
30    }
31}
32
33#[utoipa::path(
34    delete,
35    path = "/v1/domain/_image",
36    responses(
37        DefaultApiResponse,
38    ),
39    security(("token_jwt" = [])),
40    tag = "v1/domain",
41    operation_id = "domain_image_delete"
42)]
43pub(crate) async fn image_delete(
44    State(state): State<ServerState>,
45    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
46) -> Result<Json<()>, WebError> {
47    let f_uuid = filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO)));
48
49    state
50        .qe_w_ref
51        .handle_image_update(client_auth_info, f_uuid, None)
52        .await
53        .map(Json::from)
54        .map_err(WebError::from)
55}
56
57#[utoipa::path(
58    post,
59    path = "/v1/domain/_image",
60    responses(
61        DefaultApiResponse,
62    ),
63    security(("token_jwt" = [])),
64    tag = "v1/domain",
65    operation_id = "domain_image_post"
66)]
67/// API endpoint for creating/replacing the image associated with an OAuth2 Resource Server.
68///
69/// It requires a multipart form with the image file, and the content type must be one of the
70/// [VALID_IMAGE_UPLOAD_CONTENT_TYPES].
71pub(crate) async fn image_post(
72    State(state): State<ServerState>,
73    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
74    mut multipart: axum::extract::Multipart,
75) -> Result<Json<()>, WebError> {
76    // because we might not get an image
77    let mut image: Option<ImageValue> = None;
78
79    while let Some(field) = multipart.next_field().await.unwrap_or(None) {
80        let filename = field.file_name().map(|f| f.to_string()).clone();
81        if let Some(filename) = filename {
82            let content_type = field.content_type().map(|f| f.to_string()).clone();
83
84            let content_type = match content_type {
85                Some(val) => {
86                    if VALID_IMAGE_UPLOAD_CONTENT_TYPES.contains(&val.as_str()) {
87                        val
88                    } else {
89                        debug!("Invalid content type: {}", val);
90                        return Err(OperationError::InvalidRequestState.into());
91                    }
92                }
93                None => {
94                    debug!("No content type header provided");
95                    return Err(OperationError::InvalidRequestState.into());
96                }
97            };
98            let data = match field.bytes().await {
99                Ok(val) => val,
100                Err(_e) => return Err(OperationError::InvalidRequestState.into()),
101            };
102
103            let filetype = match ImageType::try_from_content_type(&content_type) {
104                Ok(val) => val,
105                Err(_err) => return Err(OperationError::InvalidRequestState.into()),
106            };
107
108            image = Some(ImageValue {
109                filetype,
110                filename: filename.to_string(),
111                contents: data.to_vec(),
112            });
113        };
114    }
115
116    match image {
117        Some(image) => {
118            let image_validation_result = image.validate_image();
119            match image_validation_result {
120                Err(err) => {
121                    admin_error!("Invalid image uploaded: {:?}", err);
122                    Err(WebError::from(OperationError::InvalidRequestState))
123                }
124                Ok(_) => {
125                    let f_uuid =
126                        filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO)));
127                    state
128                        .qe_w_ref
129                        .handle_image_update(client_auth_info, f_uuid, Some(image))
130                        .await
131                        .map(Json::from)
132                        .map_err(WebError::from)
133                }
134            }
135        }
136        None => Err(WebError::from(OperationError::InvalidAttribute(
137            "No image included, did you mean to use the DELETE method?".to_string(),
138        ))),
139    }
140}