kanidmd_core/https/
v1_oauth2.rs

1use super::apidocs::response_schema::{ApiResponseWithout200, DefaultApiResponse};
2use super::errors::WebError;
3use super::middleware::KOpId;
4use super::oauth2::oauth2_id;
5use super::v1::{
6    json_rest_event_delete_id_attr, json_rest_event_get, json_rest_event_post,
7    json_rest_event_post_id_attr,
8};
9use super::ServerState;
10
11use crate::https::extractors::VerifiedClientInformation;
12use axum::extract::{Path, State};
13use axum::{Extension, Json};
14use kanidm_proto::internal::{ImageType, ImageValue, Oauth2ClaimMapJoin};
15use kanidm_proto::v1::Entry as ProtoEntry;
16use kanidmd_lib::prelude::*;
17use kanidmd_lib::valueset::image::ImageValueThings;
18use sketching::admin_error;
19
20#[utoipa::path(
21    get,
22    path = "/v1/oauth2",
23    responses(
24        (status = 200,content_type="application/json", body=Vec<ProtoEntry>),
25        ApiResponseWithout200,
26    ),
27    security(("token_jwt" = [])),
28    tag = "v1/oauth2",
29    operation_id = "oauth2_get"
30)]
31/// Lists all the OAuth2 Resource Servers
32pub(crate) async fn oauth2_get(
33    State(state): State<ServerState>,
34    Extension(kopid): Extension<KOpId>,
35    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
36) -> Result<Json<Vec<ProtoEntry>>, WebError> {
37    let filter = filter_all!(f_eq(
38        Attribute::Class,
39        EntryClass::OAuth2ResourceServer.into()
40    ));
41    json_rest_event_get(state, None, filter, kopid, client_auth_info).await
42}
43
44#[utoipa::path(
45    post,
46    path = "/v1/oauth2/_basic",
47    request_body=ProtoEntry,
48    responses(
49        DefaultApiResponse,
50    ),
51    security(("token_jwt" = [])),
52    tag = "v1/oauth2",
53    operation_id = "oauth2_basic_post"
54)]
55/// Create a new Confidential OAuth2 client that authenticates with Http Basic.
56pub(crate) async fn oauth2_basic_post(
57    State(state): State<ServerState>,
58    Extension(kopid): Extension<KOpId>,
59    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
60    Json(obj): Json<ProtoEntry>,
61) -> Result<Json<()>, WebError> {
62    let classes = vec![
63        EntryClass::OAuth2ResourceServer.to_string(),
64        EntryClass::OAuth2ResourceServerBasic.to_string(),
65        EntryClass::Account.to_string(),
66        EntryClass::Object.to_string(),
67    ];
68    json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
69}
70
71#[utoipa::path(
72    post,
73    path = "/v1/oauth2/_public",
74    request_body=ProtoEntry,
75    responses(
76        DefaultApiResponse,
77    ),
78    security(("token_jwt" = [])),
79    tag = "v1/oauth2",
80    operation_id = "oauth2_public_post"
81)]
82/// Create a new Public OAuth2 client
83pub(crate) async fn oauth2_public_post(
84    State(state): State<ServerState>,
85    Extension(kopid): Extension<KOpId>,
86    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
87    Json(obj): Json<ProtoEntry>,
88) -> Result<Json<()>, WebError> {
89    let classes = vec![
90        EntryClass::OAuth2ResourceServer.to_string(),
91        EntryClass::OAuth2ResourceServerPublic.to_string(),
92        EntryClass::Account.to_string(),
93        EntryClass::Object.to_string(),
94    ];
95    json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
96}
97
98#[utoipa::path(
99    get,
100    path = "/v1/oauth2/{rs_name}",
101    responses(
102        (status = 200, body=Option<ProtoEntry>, content_type="application/json"),
103        ApiResponseWithout200,
104    ),
105    security(("token_jwt" = [])),
106    tag = "v1/oauth2",
107    operation_id = "oauth2_id_get"
108)]
109/// Get the details of a given OAuth2 Resource Server.
110pub(crate) async fn oauth2_id_get(
111    State(state): State<ServerState>,
112    Path(rs_name): Path<String>,
113    Extension(kopid): Extension<KOpId>,
114    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
115) -> Result<Json<Option<ProtoEntry>>, WebError> {
116    let filter = oauth2_id(&rs_name);
117    state
118        .qe_r_ref
119        .handle_internalsearch(client_auth_info, filter, None, kopid.eventid)
120        .await
121        .map(|mut r| r.pop())
122        .map(Json::from)
123        .map_err(WebError::from)
124}
125
126#[utoipa::path(
127    get,
128    path = "/v1/oauth2/{rs_name}/_basic_secret",
129    responses(
130        (status = 200,content_type="application/json", body=Option<String>),
131        ApiResponseWithout200,
132    ),
133    security(("token_jwt" = [])),
134    tag = "v1/oauth2",
135    operation_id = "oauth2_id_get_basic_secret"
136)]
137/// Get the basic secret for a given OAuth2 Resource Server. This is used for authentication.
138#[instrument(level = "info", skip(state))]
139pub(crate) async fn oauth2_id_get_basic_secret(
140    State(state): State<ServerState>,
141    Extension(kopid): Extension<KOpId>,
142    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
143    Path(rs_name): Path<String>,
144) -> Result<Json<Option<String>>, WebError> {
145    let filter = oauth2_id(&rs_name);
146    state
147        .qe_r_ref
148        .handle_oauth2_basic_secret_read(client_auth_info, filter, kopid.eventid)
149        .await
150        .map(Json::from)
151        .map_err(WebError::from)
152}
153
154#[utoipa::path(
155    patch,
156    path = "/v1/oauth2/{rs_name}",
157    request_body=ProtoEntry,
158    responses(
159        DefaultApiResponse,
160    ),
161    security(("token_jwt" = [])),
162    tag = "v1/oauth2",
163    operation_id = "oauth2_id_patch"
164)]
165/// Modify an OAuth2 Resource Server
166pub(crate) async fn oauth2_id_patch(
167    State(state): State<ServerState>,
168    Path(rs_name): Path<String>,
169    Extension(kopid): Extension<KOpId>,
170    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
171    Json(obj): Json<ProtoEntry>,
172) -> Result<Json<()>, WebError> {
173    let filter = oauth2_id(&rs_name);
174
175    state
176        .qe_w_ref
177        .handle_internalpatch(client_auth_info, filter, obj, kopid.eventid)
178        .await
179        .map(Json::from)
180        .map_err(WebError::from)
181}
182
183#[utoipa::path(
184    post,
185    path = "/v1/oauth2/{rs_name}/_scopemap/{group}",
186    request_body=Vec<String>,
187    responses(
188        DefaultApiResponse,
189    ),
190    security(("token_jwt" = [])),
191    tag = "v1/oauth2",
192    operation_id = "oauth2_id_scopemap_post"
193)]
194/// Modify the scope map for a given OAuth2 Resource Server
195pub(crate) async fn oauth2_id_scopemap_post(
196    State(state): State<ServerState>,
197    Extension(kopid): Extension<KOpId>,
198    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
199    Path((rs_name, group)): Path<(String, String)>,
200    Json(scopes): Json<Vec<String>>,
201) -> Result<Json<()>, WebError> {
202    let filter = oauth2_id(&rs_name);
203
204    state
205        .qe_w_ref
206        .handle_oauth2_scopemap_update(client_auth_info, group, scopes, filter, kopid.eventid)
207        .await
208        .map(Json::from)
209        .map_err(WebError::from)
210}
211
212#[utoipa::path(
213    post,
214    path = "/v1/oauth2/{rs_name}/_attr/{attr}",
215    request_body=Vec<String>,
216    responses(
217        DefaultApiResponse,
218    ),
219    security(("token_jwt" = [])),
220    tag = "v1/oauth2/attr",
221    operation_id = "oauth2_id_attr_post",
222)]
223pub async fn oauth2_id_attr_post(
224    Path((id, attr)): Path<(String, String)>,
225    State(state): State<ServerState>,
226    Extension(kopid): Extension<KOpId>,
227    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
228    Json(values): Json<Vec<String>>,
229) -> Result<Json<()>, WebError> {
230    let filter = filter_all!(f_eq(
231        Attribute::Class,
232        EntryClass::OAuth2ResourceServer.into()
233    ));
234    json_rest_event_post_id_attr(state, id, attr, filter, values, kopid, client_auth_info).await
235}
236
237#[utoipa::path(
238    delete,
239    path = "/v1/oauth2/{rs_name}/_attr/{attr}",
240    request_body=Option<Vec<String>>,
241    responses(
242        DefaultApiResponse,
243    ),
244    security(("token_jwt" = [])),
245    tag = "v1/oauth2/attr",
246    operation_id = "oauth2_id_attr_delete",
247)]
248pub async fn oauth2_id_attr_delete(
249    Path((id, attr)): Path<(String, String)>,
250    State(state): State<ServerState>,
251    Extension(kopid): Extension<KOpId>,
252    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
253    values: Option<Json<Vec<String>>>,
254) -> Result<Json<()>, WebError> {
255    let filter = filter_all!(f_eq(
256        Attribute::Class,
257        EntryClass::OAuth2ResourceServer.into()
258    ));
259    let values = values.map(|v| v.0);
260    json_rest_event_delete_id_attr(state, id, attr, filter, values, kopid, client_auth_info).await
261}
262
263#[utoipa::path(
264    delete,
265    path = "/v1/oauth2/{rs_name}/_scopemap/{group}",
266    responses(
267        DefaultApiResponse,
268    ),
269    security(("token_jwt" = [])),
270    tag = "v1/oauth2",
271    operation_id = "oauth2_id_scopemap_delete"
272)]
273// Delete a scope map for a given OAuth2 Resource Server
274pub(crate) async fn oauth2_id_scopemap_delete(
275    State(state): State<ServerState>,
276    Extension(kopid): Extension<KOpId>,
277    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
278    Path((rs_name, group)): Path<(String, String)>,
279) -> Result<Json<()>, WebError> {
280    let filter = oauth2_id(&rs_name);
281    state
282        .qe_w_ref
283        .handle_oauth2_scopemap_delete(client_auth_info, group, filter, kopid.eventid)
284        .await
285        .map(Json::from)
286        .map_err(WebError::from)
287}
288
289#[utoipa::path(
290    post,
291    path = "/v1/oauth2/{rs_name}/_claimmap/{claim_name}/{group}",
292    request_body=Vec<String>,
293    responses(
294        DefaultApiResponse,
295    ),
296    security(("token_jwt" = [])),
297    tag = "v1/oauth2",
298    operation_id = "oauth2_id_claimmap_post"
299)]
300/// Modify the claim map for a given OAuth2 Resource Server
301pub(crate) async fn oauth2_id_claimmap_post(
302    State(state): State<ServerState>,
303    Extension(kopid): Extension<KOpId>,
304    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
305    Path((rs_name, claim_name, group)): Path<(String, String, String)>,
306    Json(claims): Json<Vec<String>>,
307) -> Result<Json<()>, WebError> {
308    let filter = oauth2_id(&rs_name);
309    state
310        .qe_w_ref
311        .handle_oauth2_claimmap_update(
312            client_auth_info,
313            claim_name,
314            group,
315            claims,
316            filter,
317            kopid.eventid,
318        )
319        .await
320        .map(Json::from)
321        .map_err(WebError::from)
322}
323
324#[utoipa::path(
325    post,
326    path = "/v1/oauth2/{rs_name}/_claimmap/{claim_name}",
327    request_body=Oauth2ClaimMapJoin,
328    responses(
329        DefaultApiResponse,
330    ),
331    security(("token_jwt" = [])),
332    tag = "v1/oauth2",
333    operation_id = "oauth2_id_claimmap_join_post"
334)]
335/// Modify the claim map join strategy for a given OAuth2 Resource Server
336pub(crate) async fn oauth2_id_claimmap_join_post(
337    State(state): State<ServerState>,
338    Extension(kopid): Extension<KOpId>,
339    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
340    Path((rs_name, claim_name)): Path<(String, String)>,
341    Json(join): Json<Oauth2ClaimMapJoin>,
342) -> Result<Json<()>, WebError> {
343    let filter = oauth2_id(&rs_name);
344    state
345        .qe_w_ref
346        .handle_oauth2_claimmap_join_update(
347            client_auth_info,
348            claim_name,
349            join,
350            filter,
351            kopid.eventid,
352        )
353        .await
354        .map(Json::from)
355        .map_err(WebError::from)
356}
357
358#[utoipa::path(
359    delete,
360    path = "/v1/oauth2/{rs_name}/_claimmap/{claim_name}/{group}",
361    responses(
362        DefaultApiResponse,
363    ),
364    security(("token_jwt" = [])),
365    tag = "v1/oauth2",
366    operation_id = "oauth2_id_claimmap_delete"
367)]
368// Delete a claim map for a given OAuth2 Resource Server
369pub(crate) async fn oauth2_id_claimmap_delete(
370    State(state): State<ServerState>,
371    Extension(kopid): Extension<KOpId>,
372    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
373    Path((rs_name, claim_name, group)): Path<(String, String, String)>,
374) -> Result<Json<()>, WebError> {
375    let filter = oauth2_id(&rs_name);
376    state
377        .qe_w_ref
378        .handle_oauth2_claimmap_delete(client_auth_info, claim_name, group, filter, kopid.eventid)
379        .await
380        .map(Json::from)
381        .map_err(WebError::from)
382}
383
384#[utoipa::path(
385    post,
386    path = "/v1/oauth2/{rs_name}/_sup_scopemap/{group}",
387    responses(
388        DefaultApiResponse,
389    ),
390    security(("token_jwt" = [])),
391    tag = "v1/oauth2",
392    operation_id = "oauth2_id_sup_scopemap_post"
393)]
394/// Create a supplemental scope map for a given OAuth2 Resource Server
395pub(crate) async fn oauth2_id_sup_scopemap_post(
396    State(state): State<ServerState>,
397    Extension(kopid): Extension<KOpId>,
398    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
399    Path((rs_name, group)): Path<(String, String)>,
400    Json(scopes): Json<Vec<String>>,
401) -> Result<Json<()>, WebError> {
402    let filter = oauth2_id(&rs_name);
403    state
404        .qe_w_ref
405        .handle_oauth2_sup_scopemap_update(client_auth_info, group, scopes, filter, kopid.eventid)
406        .await
407        .map(Json::from)
408        .map_err(WebError::from)
409}
410
411#[utoipa::path(
412    delete,
413    path = "/v1/oauth2/{rs_name}/_sup_scopemap/{group}",
414    responses(
415        DefaultApiResponse,
416    ),
417    security(("token_jwt" = [])),
418    tag = "v1/oauth2",
419    operation_id = "oauth2_id_sup_scopemap_delete"
420)]
421// Delete a supplemental scope map configuration.
422pub(crate) async fn oauth2_id_sup_scopemap_delete(
423    State(state): State<ServerState>,
424    Extension(kopid): Extension<KOpId>,
425    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
426    Path((rs_name, group)): Path<(String, String)>,
427) -> Result<Json<()>, WebError> {
428    let filter = oauth2_id(&rs_name);
429    state
430        .qe_w_ref
431        .handle_oauth2_sup_scopemap_delete(client_auth_info, group, filter, kopid.eventid)
432        .await
433        .map(Json::from)
434        .map_err(WebError::from)
435}
436
437#[utoipa::path(
438    delete,
439    path = "/v1/oauth2/{rs_name}",
440    responses(
441        DefaultApiResponse,
442        (status = 404),
443    ),
444    security(("token_jwt" = [])),
445    tag = "v1/oauth2",
446    operation_id = "oauth2_id_delete"
447)]
448/// Delete an OAuth2 Resource Server
449pub(crate) async fn oauth2_id_delete(
450    State(state): State<ServerState>,
451    Extension(kopid): Extension<KOpId>,
452    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
453    Path(rs_name): Path<String>,
454) -> Result<Json<()>, WebError> {
455    let filter = oauth2_id(&rs_name);
456    state
457        .qe_w_ref
458        .handle_internaldelete(client_auth_info, filter, kopid.eventid)
459        .await
460        .map(Json::from)
461        .map_err(WebError::from)
462}
463
464#[utoipa::path(
465    delete,
466    path = "/v1/oauth2/{rs_name}/_image",
467    responses(
468        DefaultApiResponse,
469    ),
470    security(("token_jwt" = [])),
471    tag = "v1/oauth2",
472    operation_id = "oauth2_id_image_delete"
473)]
474// API endpoint for deleting the image associated with an OAuth2 Resource Server.
475pub(crate) async fn oauth2_id_image_delete(
476    State(state): State<ServerState>,
477    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
478    Path(rs_name): Path<String>,
479) -> Result<Json<()>, WebError> {
480    state
481        .qe_w_ref
482        .handle_image_update(client_auth_info, oauth2_id(&rs_name), None)
483        .await
484        .map(Json::from)
485        .map_err(WebError::from)
486}
487
488#[utoipa::path(
489    post,
490    path = "/v1/oauth2/{rs_name}/_image",
491    responses(
492        DefaultApiResponse,
493    ),
494    security(("token_jwt" = [])),
495    tag = "v1/oauth2",
496    operation_id = "oauth2_id_image_post"
497)]
498/// API endpoint for creating/replacing the image associated with an OAuth2 Resource Server.
499///
500/// It requires a multipart form with the image file, and the content type must be one of the
501/// [VALID_IMAGE_UPLOAD_CONTENT_TYPES].
502pub(crate) async fn oauth2_id_image_post(
503    State(state): State<ServerState>,
504    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
505    Path(rs_name): Path<String>,
506    mut multipart: axum::extract::Multipart,
507) -> Result<Json<()>, WebError> {
508    // because we might not get an image
509    let mut image: Option<ImageValue> = None;
510
511    while let Some(field) = multipart.next_field().await.unwrap_or(None) {
512        let filename = field.file_name().map(|f| f.to_string()).clone();
513        if let Some(filename) = filename {
514            let content_type = field.content_type().map(|f| f.to_string()).clone();
515
516            let content_type = match content_type {
517                Some(val) => {
518                    if VALID_IMAGE_UPLOAD_CONTENT_TYPES.contains(&val.as_str()) {
519                        val
520                    } else {
521                        debug!("Invalid content type: {}", val);
522                        return Err(OperationError::InvalidRequestState.into());
523                    }
524                }
525                None => {
526                    debug!("No content type header provided");
527                    return Err(OperationError::InvalidRequestState.into());
528                }
529            };
530            let data = match field.bytes().await {
531                Ok(val) => val,
532                Err(_e) => return Err(OperationError::InvalidRequestState.into()),
533            };
534
535            let filetype = match ImageType::try_from_content_type(&content_type) {
536                Ok(val) => val,
537                Err(_err) => return Err(OperationError::InvalidRequestState.into()),
538            };
539
540            image = Some(ImageValue {
541                filetype,
542                filename: filename.to_string(),
543                contents: data.to_vec(),
544            });
545        };
546    }
547
548    match image {
549        Some(image) => {
550            let image_validation_result = image.validate_image();
551            match image_validation_result {
552                Err(err) => {
553                    admin_error!("Invalid image uploaded: {:?}", err);
554                    Err(WebError::from(OperationError::InvalidRequestState))
555                }
556                Ok(_) => {
557                    let rs_filter = oauth2_id(&rs_name);
558                    state
559                        .qe_w_ref
560                        .handle_image_update(client_auth_info, rs_filter, Some(image))
561                        .await
562                        .map(Json::from)
563                        .map_err(WebError::from)
564                }
565            }
566        }
567        None => Err(WebError::from(OperationError::InvalidAttribute(
568            "No image included, did you mean to use the DELETE method?".to_string(),
569        ))),
570    }
571}