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)]
34pub 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 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)]
75pub 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)]
97pub 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
257async 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), 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 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 .route("/scim/v1/Entry/:id", get(scim_entry_id_get))
537 .route("/scim/v1/Person/:id", get(scim_person_id_get))
541 .route("/scim/v1/Application", post(scim_application_post))
553 .route(
556 "/scim/v1/Application/:id",
557 delete(scim_application_id_delete),
558 )
559 .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)) }