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