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, ScimEntryGetQuery, ScimSyncRequest,
16    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    post,
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_post"
397)]
398async fn scim_application_post(
399    State(state): State<ServerState>,
400    Extension(kopid): Extension<KOpId>,
401    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
402    Json(entry_post): Json<ScimEntryPostGeneric>,
403) -> Result<Json<ScimEntryKanidm>, WebError> {
404    state
405        .qe_w_ref
406        .scim_entry_create(
407            client_auth_info,
408            kopid.eventid,
409            &[
410                EntryClass::Account,
411                EntryClass::ServiceAccount,
412                EntryClass::Application,
413            ],
414            entry_post,
415        )
416        .await
417        .map(Json::from)
418        .map_err(WebError::from)
419}
420
421#[utoipa::path(
422    delete,
423    path = "/scim/v1/Application/{id}",
424    responses(
425        (status = 200, content_type="application/json"),
426        ApiResponseWithout200,
427    ),
428    security(("token_jwt" = [])),
429    tag = "scim",
430    operation_id = "scim_application_id_delete"
431)]
432async fn scim_application_id_delete(
433    State(state): State<ServerState>,
434    Path(id): Path<String>,
435    Extension(kopid): Extension<KOpId>,
436    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
437) -> Result<Json<()>, WebError> {
438    state
439        .qe_w_ref
440        .scim_entry_id_delete(client_auth_info, kopid.eventid, id, EntryClass::Application)
441        .await
442        .map(Json::from)
443        .map_err(WebError::from)
444}
445
446pub fn route_setup() -> Router<ServerState> {
447    Router::new()
448        .route(
449            "/v1/sync_account",
450            get(sync_account_get).post(sync_account_post),
451        )
452        .route(
453            "/v1/sync_account/:id",
454            get(sync_account_id_get).patch(sync_account_id_patch),
455        )
456        .route(
457            "/v1/sync_account/:id/_attr/:attr",
458            get(sync_account_id_attr_get).put(sync_account_id_attr_put),
459        )
460        .route(
461            "/v1/sync_account/:id/_finalise",
462            get(sync_account_id_finalise_get),
463        )
464        .route(
465            "/v1/sync_account/:id/_terminate",
466            get(sync_account_id_terminate_get),
467        )
468        .route(
469            "/v1/sync_account/:id/_sync_token",
470            post(sync_account_token_post).delete(sync_account_token_delete),
471        )
472        // https://datatracker.ietf.org/doc/html/rfc7644#section-3.2
473        //
474        //  HTTP   SCIM Usage
475        //  Method
476        //  ------ --------------------------------------------------------------
477        //  GET    Retrieves one or more complete or partial resources.
478        //
479        //  POST   Depending on the endpoint, creates new resources, creates a
480        //         search request, or MAY be used to bulk-modify resources.
481        //
482        //  PUT    Modifies a resource by replacing existing attributes with a
483        //         specified set of replacement attributes (replace).  PUT
484        //         MUST NOT be used to create new resources.
485        //
486        //  PATCH  Modifies a resource with a set of client-specified changes
487        //         (partial update).
488        //
489        //  DELETE Deletes a resource.
490        //
491        //  Resource Endpoint         Operations             Description
492        //  -------- ---------------- ---------------------- --------------------
493        //  User     /Users           GET (Section 3.4.1),   Retrieve, add,
494        //                            POST (Section 3.3),    modify Users.
495        //                            PUT (Section 3.5.1),
496        //                            PATCH (Section 3.5.2),
497        //                            DELETE (Section 3.6)
498        //
499        //  Group    /Groups          GET (Section 3.4.1),   Retrieve, add,
500        //                            POST (Section 3.3),    modify Groups.
501        //                            PUT (Section 3.5.1),
502        //                            PATCH (Section 3.5.2),
503        //                            DELETE (Section 3.6)
504        //
505        //  Self     /Me              GET, POST, PUT, PATCH, Alias for operations
506        //                            DELETE (Section 3.11)  against a resource
507        //                                                   mapped to an
508        //                                                   authenticated
509        //                                                   subject (e.g.,
510        //                                                   User).
511        //
512        //  Service  /ServiceProvider GET (Section 4)        Retrieve service
513        //  provider Config                                  provider's
514        //  config.                                          configuration.
515        //
516        //  Resource /ResourceTypes   GET (Section 4)        Retrieve supported
517        //  type                                             resource types.
518        //
519        //  Schema   /Schemas         GET (Section 4)        Retrieve one or more
520        //                                                   supported schemas.
521        //
522        //  Bulk     /Bulk            POST (Section 3.7)     Bulk updates to one
523        //                                                   or more resources.
524        //
525        //  Search   [prefix]/.search POST (Section 3.4.3)   Search from system
526        //                                                   root or within a
527        //                                                   resource endpoint
528        //                                                   for one or more
529        //                                                   resource types using
530        //                                                   POST.
531        //  -- Kanidm Resources
532        //
533        //  Entry    /Entry/{id}      GET                    Retrieve a generic entry
534        //                                                   of any kind from the database.
535        //                                                   {id} is any unique id.
536        .route("/scim/v1/Entry/:id", get(scim_entry_id_get))
537        //  Person   /Person/{id}     GET                    Retrieve a a person from the
538        //                                                   database.
539        //                                                   {id} is any unique id.
540        .route("/scim/v1/Person/:id", get(scim_person_id_get))
541        //
542        //  Sync     /Sync            GET                    Retrieve the current
543        //                                                   sync state associated
544        //                                                   with the authenticated
545        //                                                   session
546        //
547        //                            POST                   Send a sync update
548        //
549        //
550        //  Application   /Application     Post              Create a new application
551        //
552        .route("/scim/v1/Application", post(scim_application_post))
553        //  Application   /Application/{id}     Delete      Delete the application identified by id
554        //
555        .route(
556            "/scim/v1/Application/:id",
557            delete(scim_application_id_delete),
558        )
559        // Synchronisation routes.
560        .route(
561            "/scim/v1/Sync",
562            post(scim_sync_post)
563                .layer(DefaultBodyLimit::max(DEFAULT_SCIM_SYNC_BYTES))
564                .get(scim_sync_get),
565        )
566        .route("/scim/v1/Sink", get(scim_sink_get)) // skip_route_check
567}