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)]
33pub 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 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)]
74pub 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)]
96pub 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
256async 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), 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 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 .route("/scim/v1/Entry/:id", get(scim_entry_id_get))
477 .route("/scim/v1/Person/:id", get(scim_person_id_get))
481 .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)) }