kanidmd_core/https/
v1_scim.rs

1use super::apidocs::response_schema::{ApiResponseWithout200, DefaultApiResponse};
2use super::errors::WebError;
3use super::middleware::KOpId;
4use super::v1::{
5    json_rest_event_get, json_rest_event_get_id, json_rest_event_get_id_attr, json_rest_event_post,
6    json_rest_event_put_attr,
7};
8use super::ServerState;
9use crate::https::extractors::VerifiedClientInformation;
10use axum::extract::{rejection::JsonRejection, DefaultBodyLimit, Path, Query, State};
11use axum::response::{Html, IntoResponse, Response};
12use axum::routing::{delete, get, post};
13use axum::{Extension, Json, Router};
14use kanidm_proto::scim_v1::{
15    client::ScimEntryPostGeneric, server::ScimEntryKanidm, server::ScimListResponse,
16    ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
17};
18use kanidm_proto::v1::Entry as ProtoEntry;
19use kanidmd_lib::prelude::*;
20
21const DEFAULT_SCIM_SYNC_BYTES: usize = 1024 * 1024 * 32;
22
23#[utoipa::path(
24    get,
25    path = "/v1/sync_account",
26    responses(
27        (status = 200,content_type="application/json", body=Vec<ProtoEntry>),
28        ApiResponseWithout200,
29    ),
30    security(("token_jwt" = [])),
31    tag = "v1/sync_account",
32    operation_id = "sync_account_get"
33)]
34/// Get all? the sync accounts.
35pub async fn sync_account_get(
36    State(state): State<ServerState>,
37    Extension(kopid): Extension<KOpId>,
38    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
39) -> Result<Json<Vec<ProtoEntry>>, WebError> {
40    let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
41    json_rest_event_get(state, None, filter, kopid, client_auth_info).await
42}
43
44#[utoipa::path(
45    post,
46    path = "/v1/sync_account",
47    // request_body=ProtoEntry,
48    responses(
49        DefaultApiResponse,
50    ),
51    security(("token_jwt" = [])),
52    tag = "v1/sync_account",
53    operation_id = "sync_account_post"
54)]
55pub async fn sync_account_post(
56    State(state): State<ServerState>,
57    Extension(kopid): Extension<KOpId>,
58    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
59    Json(obj): Json<ProtoEntry>,
60) -> Result<Json<()>, WebError> {
61    let classes: Vec<String> = vec![EntryClass::SyncAccount.into(), EntryClass::Object.into()];
62    json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
63}
64
65#[utoipa::path(
66    get,
67    path = "/v1/sync_account/{id}",
68    responses(
69        (status = 200,content_type="application/json", body=Option<ProtoEntry>),
70        ApiResponseWithout200,
71    ),
72    security(("token_jwt" = [])),
73    tag = "v1/sync_account",
74)]
75/// Get the details of a sync account
76pub async fn sync_account_id_get(
77    State(state): State<ServerState>,
78    Path(id): Path<String>,
79    Extension(kopid): Extension<KOpId>,
80    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
81) -> Result<Json<Option<ProtoEntry>>, WebError> {
82    let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
83    json_rest_event_get_id(state, id, filter, None, kopid, client_auth_info).await
84}
85
86#[utoipa::path(
87    patch,
88    path = "/v1/sync_account/{id}",
89    request_body=ProtoEntry,
90    responses(
91        DefaultApiResponse,
92    ),
93    security(("token_jwt" = [])),
94    tag = "v1/sync_account",
95    operation_id = "sync_account_id_patch"
96)]
97/// Modify a sync account in-place
98pub async fn sync_account_id_patch(
99    State(state): State<ServerState>,
100    Path(id): Path<String>,
101    Extension(kopid): Extension<KOpId>,
102    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
103    Json(obj): Json<ProtoEntry>,
104) -> Result<Json<()>, WebError> {
105    let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
106    let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str())));
107
108    state
109        .qe_w_ref
110        .handle_internalpatch(client_auth_info, filter, obj, kopid.eventid)
111        .await
112        .map(Json::from)
113        .map_err(WebError::from)
114}
115
116#[utoipa::path(
117    get,
118    path = "/v1/sync_account/{id}/_finalise",
119    responses(
120        DefaultApiResponse,
121    ),
122    security(("token_jwt" = [])),
123    tag = "v1/sync_account",
124    operation_id = "sync_account_id_finalise_get"
125)]
126pub async fn sync_account_id_finalise_get(
127    State(state): State<ServerState>,
128    Path(id): Path<String>,
129    Extension(kopid): Extension<KOpId>,
130    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
131) -> Result<Json<()>, WebError> {
132    state
133        .qe_w_ref
134        .handle_sync_account_finalise(client_auth_info, id, kopid.eventid)
135        .await
136        .map(Json::from)
137        .map_err(WebError::from)
138}
139
140#[utoipa::path(
141    get,
142    path = "/v1/sync_account/{id}/_terminate",
143    responses(
144        DefaultApiResponse,
145    ),
146    security(("token_jwt" = [])),
147    tag = "v1/sync_account",
148    operation_id = "sync_account_id_terminate_get"
149)]
150pub async fn sync_account_id_terminate_get(
151    State(state): State<ServerState>,
152    Path(id): Path<String>,
153    Extension(kopid): Extension<KOpId>,
154    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
155) -> Result<Json<()>, WebError> {
156    state
157        .qe_w_ref
158        .handle_sync_account_terminate(client_auth_info, id, kopid.eventid)
159        .await
160        .map(Json::from)
161        .map_err(WebError::from)
162}
163
164#[utoipa::path(
165    post,
166    path = "/v1/sync_account/{id}/_sync_token",
167    responses(
168        (status = 200, body=String, content_type="application/json"),
169        ApiResponseWithout200,
170    ),
171    security(("token_jwt" = [])),
172    tag = "v1/sync_account",
173    operation_id = "sync_account_token_post"
174)]
175pub async fn sync_account_token_post(
176    State(state): State<ServerState>,
177    Path(id): Path<String>,
178    Extension(kopid): Extension<KOpId>,
179    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
180    Json(label): Json<String>,
181) -> Result<Json<String>, WebError> {
182    state
183        .qe_w_ref
184        .handle_sync_account_token_generate(client_auth_info, id, label, kopid.eventid)
185        .await
186        .map(Json::from)
187        .map_err(WebError::from)
188}
189
190#[utoipa::path(
191    delete,
192    path = "/v1/sync_account/{id}/_sync_token",
193    responses(
194        DefaultApiResponse,
195    ),
196    security(("token_jwt" = [])),
197    tag = "v1/sync_account",
198    operation_id = "sync_account_token_delete"
199)]
200pub async fn sync_account_token_delete(
201    State(state): State<ServerState>,
202    Path(id): Path<String>,
203    Extension(kopid): Extension<KOpId>,
204    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
205) -> Result<Json<()>, WebError> {
206    state
207        .qe_w_ref
208        .handle_sync_account_token_destroy(client_auth_info, id, kopid.eventid)
209        .await
210        .map(Json::from)
211        .map_err(WebError::from)
212}
213
214#[utoipa::path(
215    get,
216    path = "/v1/sync_account/{id}/_attr/{attr}",
217    responses(
218        (status = 200, body=Option<Vec<String>>, content_type="application/json"),
219        ApiResponseWithout200,
220    ),
221    security(("token_jwt" = [])),
222    tag = "v1/sync_account",
223    operation_id = "sync_account_id_attr_get"
224)]
225pub async fn sync_account_id_attr_get(
226    State(state): State<ServerState>,
227    Extension(kopid): Extension<KOpId>,
228    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
229    Path((id, attr)): Path<(String, String)>,
230) -> Result<Json<Option<Vec<String>>>, WebError> {
231    let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
232    json_rest_event_get_id_attr(state, id, attr, filter, kopid, client_auth_info).await
233}
234
235#[utoipa::path(
236    post,
237    path = "/v1/sync_account/{id}/_attr/{attr}",
238    request_body=Vec<String>,
239    responses(
240        DefaultApiResponse,
241    ),
242    security(("token_jwt" = [])),
243    tag = "v1/sync_account",
244    operation_id = "sync_account_id_attr_put"
245)]
246pub async fn sync_account_id_attr_put(
247    State(state): State<ServerState>,
248    Extension(kopid): Extension<KOpId>,
249    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
250    Path((id, attr)): Path<(String, String)>,
251    Json(values): Json<Vec<String>>,
252) -> Result<Json<()>, WebError> {
253    let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
254    json_rest_event_put_attr(state, id, attr, filter, values, kopid, client_auth_info).await
255}
256
257/// When you want the kitchen Sink
258async fn scim_sink_get() -> Html<&'static str> {
259    Html::from(include_str!("scim/sink.html"))
260}
261
262#[utoipa::path(
263    post,
264    path = "/scim/v1/Sync",
265    request_body = ScimSyncRequest,
266    responses(
267        DefaultApiResponse,
268    ),
269    security(("token_jwt" = [])),
270    tag = "scim",
271    operation_id = "scim_sync_post"
272)]
273async fn scim_sync_post(
274    State(state): State<ServerState>,
275    Extension(kopid): Extension<KOpId>,
276    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
277    payload: Result<Json<ScimSyncRequest>, JsonRejection>,
278) -> Response {
279    match payload {
280        Ok(Json(changes)) => {
281            let res = state
282                .qe_w_ref
283                .handle_scim_sync_apply(client_auth_info, changes, kopid.eventid)
284                .await;
285
286            match res {
287                Ok(data) => Json::from(data).into_response(),
288                Err(err) => WebError::from(err).into_response(),
289            }
290        }
291        Err(rejection) => {
292            error!(?rejection, "Unable to process JSON");
293            rejection.into_response()
294        }
295    }
296}
297
298#[utoipa::path(
299    get,
300    path = "/scim/v1/Sync",
301    responses(
302        (status = 200, content_type="application/json", body=ScimSyncState), // TODO: response content
303        ApiResponseWithout200,
304    ),
305    security(("token_jwt" = [])),
306    tag = "scim",
307    operation_id = "scim_sync_get"
308)]
309async fn scim_sync_get(
310    State(state): State<ServerState>,
311    Extension(kopid): Extension<KOpId>,
312    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
313) -> Result<Json<ScimSyncState>, WebError> {
314    // Given the token, what is it's connected sync state?
315    state
316        .qe_r_ref
317        .handle_scim_sync_status(client_auth_info, kopid.eventid)
318        .await
319        .map(Json::from)
320        .map_err(WebError::from)
321}
322
323#[utoipa::path(
324    get,
325    path = "/scim/v1/Entry/{id}",
326    responses(
327        (status = 200, content_type="application/json", body=ScimEntry),
328        ApiResponseWithout200,
329    ),
330    security(("token_jwt" = [])),
331    tag = "scim",
332    operation_id = "scim_entry_id_get"
333)]
334async fn scim_entry_id_get(
335    State(state): State<ServerState>,
336    Path(id): Path<String>,
337    Extension(kopid): Extension<KOpId>,
338    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
339    Query(scim_entry_get_query): Query<ScimEntryGetQuery>,
340) -> Result<Json<ScimEntryKanidm>, WebError> {
341    state
342        .qe_r_ref
343        .scim_entry_id_get(
344            client_auth_info,
345            kopid.eventid,
346            id,
347            EntryClass::Object,
348            scim_entry_get_query,
349        )
350        .await
351        .map(Json::from)
352        .map_err(WebError::from)
353}
354
355#[utoipa::path(
356    get,
357    path = "/scim/v1/Person/{id}",
358    responses(
359        (status = 200, content_type="application/json", body=ScimEntry),
360        ApiResponseWithout200,
361    ),
362    security(("token_jwt" = [])),
363    tag = "scim",
364    operation_id = "scim_person_id_get"
365)]
366async fn scim_person_id_get(
367    State(state): State<ServerState>,
368    Path(id): Path<String>,
369    Extension(kopid): Extension<KOpId>,
370    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
371    Query(scim_entry_get_query): Query<ScimEntryGetQuery>,
372) -> Result<Json<ScimEntryKanidm>, WebError> {
373    state
374        .qe_r_ref
375        .scim_entry_id_get(
376            client_auth_info,
377            kopid.eventid,
378            id,
379            EntryClass::Person,
380            scim_entry_get_query,
381        )
382        .await
383        .map(Json::from)
384        .map_err(WebError::from)
385}
386
387#[utoipa::path(
388    get,
389    path = "/scim/v1/Application",
390    responses(
391        (status = 200, content_type="application/json", body=ScimEntry),
392        ApiResponseWithout200,
393    ),
394    security(("token_jwt" = [])),
395    tag = "scim",
396    operation_id = "scim_application_get"
397)]
398async fn scim_application_get(
399    State(state): State<ServerState>,
400    Extension(kopid): Extension<KOpId>,
401    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
402    Query(scim_entry_get_query): Query<ScimEntryGetQuery>,
403) -> Result<Json<ScimListResponse>, WebError> {
404    state
405        .qe_r_ref
406        .scim_entry_search(
407            client_auth_info,
408            kopid.eventid,
409            EntryClass::Application.into(),
410            scim_entry_get_query,
411        )
412        .await
413        .map(Json::from)
414        .map_err(WebError::from)
415}
416
417#[utoipa::path(
418    post,
419    path = "/scim/v1/Application",
420    request_body = ScimEntryPostGeneric,
421    responses(
422        (status = 200, content_type="application/json", body=ScimEntry),
423        ApiResponseWithout200,
424    ),
425    security(("token_jwt" = [])),
426    tag = "scim",
427    operation_id = "scim_application_post"
428)]
429async fn scim_application_post(
430    State(state): State<ServerState>,
431    Extension(kopid): Extension<KOpId>,
432    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
433    Json(entry_post): Json<ScimEntryPostGeneric>,
434) -> Result<Json<ScimEntryKanidm>, WebError> {
435    state
436        .qe_w_ref
437        .scim_entry_create(
438            client_auth_info,
439            kopid.eventid,
440            &[
441                EntryClass::Account,
442                EntryClass::ServiceAccount,
443                EntryClass::Application,
444            ],
445            entry_post,
446        )
447        .await
448        .map(Json::from)
449        .map_err(WebError::from)
450}
451
452#[utoipa::path(
453    delete,
454    path = "/scim/v1/Application/{id}",
455    responses(
456        (status = 200, content_type="application/json"),
457        ApiResponseWithout200,
458    ),
459    security(("token_jwt" = [])),
460    tag = "scim",
461    operation_id = "scim_application_id_delete"
462)]
463async fn scim_application_id_delete(
464    State(state): State<ServerState>,
465    Path(id): Path<String>,
466    Extension(kopid): Extension<KOpId>,
467    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
468) -> Result<Json<()>, WebError> {
469    state
470        .qe_w_ref
471        .scim_entry_id_delete(client_auth_info, kopid.eventid, id, EntryClass::Application)
472        .await
473        .map(Json::from)
474        .map_err(WebError::from)
475}
476
477pub fn route_setup() -> Router<ServerState> {
478    Router::new()
479        .route(
480            "/v1/sync_account",
481            get(sync_account_get).post(sync_account_post),
482        )
483        .route(
484            "/v1/sync_account/:id",
485            get(sync_account_id_get).patch(sync_account_id_patch),
486        )
487        .route(
488            "/v1/sync_account/:id/_attr/:attr",
489            get(sync_account_id_attr_get).put(sync_account_id_attr_put),
490        )
491        .route(
492            "/v1/sync_account/:id/_finalise",
493            get(sync_account_id_finalise_get),
494        )
495        .route(
496            "/v1/sync_account/:id/_terminate",
497            get(sync_account_id_terminate_get),
498        )
499        .route(
500            "/v1/sync_account/:id/_sync_token",
501            post(sync_account_token_post).delete(sync_account_token_delete),
502        )
503        // https://datatracker.ietf.org/doc/html/rfc7644#section-3.2
504        //
505        //  HTTP   SCIM Usage
506        //  Method
507        //  ------ --------------------------------------------------------------
508        //  GET    Retrieves one or more complete or partial resources.
509        //
510        //  POST   Depending on the endpoint, creates new resources, creates a
511        //         search request, or MAY be used to bulk-modify resources.
512        //
513        //  PUT    Modifies a resource by replacing existing attributes with a
514        //         specified set of replacement attributes (replace).  PUT
515        //         MUST NOT be used to create new resources.
516        //
517        //  PATCH  Modifies a resource with a set of client-specified changes
518        //         (partial update).
519        //
520        //  DELETE Deletes a resource.
521        //
522        //  Resource Endpoint         Operations             Description
523        //  -------- ---------------- ---------------------- --------------------
524        //  User     /Users           GET (Section 3.4.1),   Retrieve, add,
525        //                            POST (Section 3.3),    modify Users.
526        //                            PUT (Section 3.5.1),
527        //                            PATCH (Section 3.5.2),
528        //                            DELETE (Section 3.6)
529        //
530        //  Group    /Groups          GET (Section 3.4.1),   Retrieve, add,
531        //                            POST (Section 3.3),    modify Groups.
532        //                            PUT (Section 3.5.1),
533        //                            PATCH (Section 3.5.2),
534        //                            DELETE (Section 3.6)
535        //
536        //  Self     /Me              GET, POST, PUT, PATCH, Alias for operations
537        //                            DELETE (Section 3.11)  against a resource
538        //                                                   mapped to an
539        //                                                   authenticated
540        //                                                   subject (e.g.,
541        //                                                   User).
542        //
543        //  Service  /ServiceProvider GET (Section 4)        Retrieve service
544        //  provider Config                                  provider's
545        //  config.                                          configuration.
546        //
547        //  Resource /ResourceTypes   GET (Section 4)        Retrieve supported
548        //  type                                             resource types.
549        //
550        //  Schema   /Schemas         GET (Section 4)        Retrieve one or more
551        //                                                   supported schemas.
552        //
553        //  Bulk     /Bulk            POST (Section 3.7)     Bulk updates to one
554        //                                                   or more resources.
555        //
556        //  Search   [prefix]/.search POST (Section 3.4.3)   Search from system
557        //                                                   root or within a
558        //                                                   resource endpoint
559        //                                                   for one or more
560        //                                                   resource types using
561        //                                                   POST.
562        //  -- Kanidm Resources
563        //
564        //  Entry    /Entry/{id}      GET                    Retrieve a generic entry
565        //                                                   of any kind from the database.
566        //                                                   {id} is any unique id.
567        .route("/scim/v1/Entry/:id", get(scim_entry_id_get))
568        //  Person   /Person/{id}     GET                    Retrieve a a person from the
569        //                                                   database.
570        //                                                   {id} is any unique id.
571        .route("/scim/v1/Person/:id", get(scim_person_id_get))
572        //
573        //  Sync     /Sync            GET                    Retrieve the current
574        //                                                   sync state associated
575        //                                                   with the authenticated
576        //                                                   session
577        //
578        //                            POST                   Send a sync update
579        //
580        //
581        //  Application   /Application     Post              Create a new application
582        //
583        .route(
584            "/scim/v1/Application",
585            get(scim_application_get).post(scim_application_post),
586        )
587        //  Application   /Application/{id}     Delete      Delete the application identified by id
588        //
589        .route(
590            "/scim/v1/Application/:id",
591            delete(scim_application_id_delete),
592        )
593        // Synchronisation routes.
594        .route(
595            "/scim/v1/Sync",
596            post(scim_sync_post)
597                .layer(DefaultBodyLimit::max(DEFAULT_SCIM_SYNC_BYTES))
598                .get(scim_sync_get),
599        )
600        .route("/scim/v1/Sink", get(scim_sink_get)) // skip_route_check
601}