1use crate::{ClientError, KanidmClient};
2use kanidm_proto::attribute::Attribute;
3use kanidm_proto::constants::{
4 ATTR_DISPLAYNAME, ATTR_KEY_ACTION_REVOKE, ATTR_KEY_ACTION_ROTATE, ATTR_NAME,
5 ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE, ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
6 ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, ATTR_OAUTH2_PREFER_SHORT_USERNAME,
7 ATTR_OAUTH2_RS_BASIC_SECRET, ATTR_OAUTH2_RS_ORIGIN, ATTR_OAUTH2_RS_ORIGIN_LANDING,
8 ATTR_OAUTH2_STRICT_REDIRECT_URI,
9};
10use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin};
11use kanidm_proto::v1::Entry;
12use reqwest::multipart;
13use std::collections::BTreeMap;
14use time::format_description::well_known::Rfc3339;
15use time::OffsetDateTime;
16use url::Url;
17
18impl KanidmClient {
19 #[instrument(level = "debug")]
21 pub async fn idm_oauth2_rs_list(&self) -> Result<Vec<Entry>, ClientError> {
22 self.perform_get_request("/v1/oauth2").await
23 }
24
25 pub async fn idm_oauth2_rs_basic_create(
26 &self,
27 name: &str,
28 displayname: &str,
29 origin: &str,
30 ) -> Result<(), ClientError> {
31 let mut new_oauth2_rs = Entry::default();
32 new_oauth2_rs
33 .attrs
34 .insert(ATTR_NAME.to_string(), vec![name.to_string()]);
35 new_oauth2_rs
36 .attrs
37 .insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
38 new_oauth2_rs.attrs.insert(
39 ATTR_OAUTH2_RS_ORIGIN_LANDING.to_string(),
40 vec![origin.to_string()],
41 );
42 new_oauth2_rs.attrs.insert(
43 ATTR_OAUTH2_STRICT_REDIRECT_URI.to_string(),
44 vec!["true".to_string()],
45 );
46 self.perform_post_request("/v1/oauth2/_basic", new_oauth2_rs)
47 .await
48 }
49
50 pub async fn idm_oauth2_rs_public_create(
51 &self,
52 name: &str,
53 displayname: &str,
54 origin: &str,
55 ) -> Result<(), ClientError> {
56 let mut new_oauth2_rs = Entry::default();
57 new_oauth2_rs
58 .attrs
59 .insert(ATTR_NAME.to_string(), vec![name.to_string()]);
60 new_oauth2_rs
61 .attrs
62 .insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
63 new_oauth2_rs.attrs.insert(
64 ATTR_OAUTH2_RS_ORIGIN_LANDING.to_string(),
65 vec![origin.to_string()],
66 );
67 new_oauth2_rs.attrs.insert(
68 ATTR_OAUTH2_STRICT_REDIRECT_URI.to_string(),
69 vec!["true".to_string()],
70 );
71 self.perform_post_request("/v1/oauth2/_public", new_oauth2_rs)
72 .await
73 }
74
75 pub async fn idm_oauth2_rs_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
77 self.perform_get_request(format!("/v1/oauth2/{}", id).as_str())
78 .await
79 }
80
81 pub async fn idm_oauth2_rs_get_basic_secret(
82 &self,
83 id: &str,
84 ) -> Result<Option<String>, ClientError> {
85 self.perform_get_request(format!("/v1/oauth2/{}/_basic_secret", id).as_str())
86 .await
87 }
88
89 pub async fn idm_oauth2_rs_revoke_key(
90 &self,
91 id: &str,
92 key_id: &str,
93 ) -> Result<(), ClientError> {
94 self.perform_post_request(
95 &format!("/v1/oauth2/{}/_attr/{}", id, ATTR_KEY_ACTION_REVOKE),
96 vec![key_id.to_string()],
97 )
98 .await
99 }
100
101 pub async fn idm_oauth2_rs_rotate_keys(
102 &self,
103 id: &str,
104 rotate_at_time: OffsetDateTime,
105 ) -> Result<(), ClientError> {
106 let rfc_3339_str = rotate_at_time.format(&Rfc3339).map_err(|_| {
107 ClientError::InvalidRequest("Unable to format rfc 3339 datetime".into())
108 })?;
109
110 self.perform_post_request(
111 &format!("/v1/oauth2/{}/_attr/{}", id, ATTR_KEY_ACTION_ROTATE),
112 vec![rfc_3339_str],
113 )
114 .await
115 }
116
117 pub async fn idm_oauth2_rs_update(
118 &self,
119 id: &str,
120 name: Option<&str>,
121 displayname: Option<&str>,
122 landing: Option<&str>,
123 reset_secret: bool,
124 ) -> Result<(), ClientError> {
125 let mut update_oauth2_rs = Entry {
126 attrs: BTreeMap::new(),
127 };
128
129 if let Some(newname) = name {
130 update_oauth2_rs
131 .attrs
132 .insert(ATTR_NAME.to_string(), vec![newname.to_string()]);
133 }
134 if let Some(newdisplayname) = displayname {
135 update_oauth2_rs.attrs.insert(
136 ATTR_DISPLAYNAME.to_string(),
137 vec![newdisplayname.to_string()],
138 );
139 }
140 if let Some(newlanding) = landing {
141 update_oauth2_rs.attrs.insert(
142 ATTR_OAUTH2_RS_ORIGIN_LANDING.to_string(),
143 vec![newlanding.to_string()],
144 );
145 }
146 if reset_secret {
147 update_oauth2_rs
148 .attrs
149 .insert(ATTR_OAUTH2_RS_BASIC_SECRET.to_string(), Vec::new());
150 }
151 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
152 .await
153 }
154
155 pub async fn idm_oauth2_rs_update_scope_map(
156 &self,
157 id: &str,
158 group: &str,
159 scopes: Vec<&str>,
160 ) -> Result<(), ClientError> {
161 let scopes: Vec<String> = scopes.into_iter().map(str::to_string).collect();
162 self.perform_post_request(
163 format!("/v1/oauth2/{}/_scopemap/{}", id, group).as_str(),
164 scopes,
165 )
166 .await
167 }
168
169 pub async fn idm_oauth2_rs_delete_scope_map(
170 &self,
171 id: &str,
172 group: &str,
173 ) -> Result<(), ClientError> {
174 self.perform_delete_request(format!("/v1/oauth2/{}/_scopemap/{}", id, group).as_str())
175 .await
176 }
177
178 pub async fn idm_oauth2_rs_update_sup_scope_map(
179 &self,
180 id: &str,
181 group: &str,
182 scopes: Vec<&str>,
183 ) -> Result<(), ClientError> {
184 let scopes: Vec<String> = scopes.into_iter().map(str::to_string).collect();
185 self.perform_post_request(
186 format!("/v1/oauth2/{}/_sup_scopemap/{}", id, group).as_str(),
187 scopes,
188 )
189 .await
190 }
191
192 pub async fn idm_oauth2_rs_delete_sup_scope_map(
193 &self,
194 id: &str,
195 group: &str,
196 ) -> Result<(), ClientError> {
197 self.perform_delete_request(format!("/v1/oauth2/{}/_sup_scopemap/{}", id, group).as_str())
198 .await
199 }
200
201 pub async fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
202 self.perform_delete_request(["/v1/oauth2/", id].concat().as_str())
203 .await
204 }
205
206 pub async fn idm_oauth2_rs_delete_image(&self, id: &str) -> Result<(), ClientError> {
208 self.perform_delete_request(format!("/v1/oauth2/{}/_image", id).as_str())
209 .await
210 }
211
212 pub async fn idm_oauth2_rs_update_image(
214 &self,
215 id: &str,
216 image: ImageValue,
217 ) -> Result<(), ClientError> {
218 let file_content_type = image.filetype.as_content_type_str();
219
220 let file_data = match multipart::Part::bytes(image.contents.clone())
221 .file_name(image.filename)
222 .mime_str(file_content_type)
223 {
224 Ok(part) => part,
225 Err(err) => {
226 error!(
227 "Failed to generate multipart body from image data: {:}",
228 err
229 );
230 return Err(ClientError::SystemError);
231 }
232 };
233
234 let form = multipart::Form::new().part("image", file_data);
235
236 let response = self
238 .client
239 .post(self.make_url(&format!("/v1/oauth2/{}/_image", id)))
240 .multipart(form);
241
242 let response = {
243 let tguard = self.bearer_token.read().await;
244 if let Some(token) = &(*tguard) {
245 response.bearer_auth(token)
246 } else {
247 response
248 }
249 };
250 let response = response
251 .send()
252 .await
253 .map_err(|err| self.handle_response_error(err))?;
254 self.expect_version(&response).await;
255
256 let opid = self.get_kopid_from_response(&response);
257
258 match response.status() {
259 reqwest::StatusCode::OK => {}
260 unexpect => {
261 return Err(ClientError::Http(
262 unexpect,
263 response.json().await.ok(),
264 opid,
265 ))
266 }
267 }
268 response
269 .json()
270 .await
271 .map_err(|e| ClientError::JsonDecode(e, opid))
272 }
273
274 pub async fn idm_oauth2_rs_enable_pkce(&self, id: &str) -> Result<(), ClientError> {
275 let mut update_oauth2_rs = Entry {
276 attrs: BTreeMap::new(),
277 };
278 update_oauth2_rs.attrs.insert(
279 ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE.to_string(),
280 Vec::new(),
281 );
282 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
283 .await
284 }
285
286 pub async fn idm_oauth2_rs_disable_pkce(&self, id: &str) -> Result<(), ClientError> {
287 let mut update_oauth2_rs = Entry {
288 attrs: BTreeMap::new(),
289 };
290 update_oauth2_rs.attrs.insert(
291 ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE.to_string(),
292 vec!["true".to_string()],
293 );
294 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
295 .await
296 }
297
298 pub async fn idm_oauth2_rs_enable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> {
299 let mut update_oauth2_rs = Entry {
300 attrs: BTreeMap::new(),
301 };
302 update_oauth2_rs.attrs.insert(
303 ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE.to_string(),
304 vec!["true".to_string()],
305 );
306 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
307 .await
308 }
309
310 pub async fn idm_oauth2_rs_disable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> {
311 let mut update_oauth2_rs = Entry {
312 attrs: BTreeMap::new(),
313 };
314 update_oauth2_rs.attrs.insert(
315 ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE.to_string(),
316 vec!["false".to_string()],
317 );
318 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
319 .await
320 }
321
322 pub async fn idm_oauth2_rs_prefer_short_username(&self, id: &str) -> Result<(), ClientError> {
323 let mut update_oauth2_rs = Entry {
324 attrs: BTreeMap::new(),
325 };
326 update_oauth2_rs.attrs.insert(
327 ATTR_OAUTH2_PREFER_SHORT_USERNAME.to_string(),
328 vec!["true".to_string()],
329 );
330 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
331 .await
332 }
333
334 pub async fn idm_oauth2_rs_prefer_spn_username(&self, id: &str) -> Result<(), ClientError> {
335 let mut update_oauth2_rs = Entry {
336 attrs: BTreeMap::new(),
337 };
338 update_oauth2_rs.attrs.insert(
339 ATTR_OAUTH2_PREFER_SHORT_USERNAME.to_string(),
340 vec!["false".to_string()],
341 );
342 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
343 .await
344 }
345
346 pub async fn idm_oauth2_rs_enable_public_localhost_redirect(
347 &self,
348 id: &str,
349 ) -> Result<(), ClientError> {
350 let mut update_oauth2_rs = Entry {
351 attrs: BTreeMap::new(),
352 };
353 update_oauth2_rs.attrs.insert(
354 ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT.to_string(),
355 vec!["true".to_string()],
356 );
357 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
358 .await
359 }
360
361 pub async fn idm_oauth2_rs_disable_public_localhost_redirect(
362 &self,
363 id: &str,
364 ) -> Result<(), ClientError> {
365 let mut update_oauth2_rs = Entry {
366 attrs: BTreeMap::new(),
367 };
368 update_oauth2_rs.attrs.insert(
369 ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT.to_string(),
370 vec!["false".to_string()],
371 );
372 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
373 .await
374 }
375
376 pub async fn idm_oauth2_rs_enable_strict_redirect_uri(
377 &self,
378 id: &str,
379 ) -> Result<(), ClientError> {
380 let mut update_oauth2_rs = Entry {
381 attrs: BTreeMap::new(),
382 };
383 update_oauth2_rs.attrs.insert(
384 ATTR_OAUTH2_STRICT_REDIRECT_URI.to_string(),
385 vec!["true".to_string()],
386 );
387 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
388 .await
389 }
390
391 pub async fn idm_oauth2_rs_disable_strict_redirect_uri(
392 &self,
393 id: &str,
394 ) -> Result<(), ClientError> {
395 let mut update_oauth2_rs = Entry {
396 attrs: BTreeMap::new(),
397 };
398 update_oauth2_rs.attrs.insert(
399 ATTR_OAUTH2_STRICT_REDIRECT_URI.to_string(),
400 vec!["false".to_string()],
401 );
402 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
403 .await
404 }
405
406 pub async fn idm_oauth2_rs_update_claim_map(
407 &self,
408 id: &str,
409 claim_name: &str,
410 group_id: &str,
411 values: &[String],
412 ) -> Result<(), ClientError> {
413 let values: Vec<String> = values.to_vec();
414 self.perform_post_request(
415 format!("/v1/oauth2/{}/_claimmap/{}/{}", id, claim_name, group_id).as_str(),
416 values,
417 )
418 .await
419 }
420
421 pub async fn idm_oauth2_rs_update_claim_map_join(
422 &self,
423 id: &str,
424 claim_name: &str,
425 join: Oauth2ClaimMapJoin,
426 ) -> Result<(), ClientError> {
427 self.perform_post_request(
428 format!("/v1/oauth2/{}/_claimmap/{}", id, claim_name).as_str(),
429 join,
430 )
431 .await
432 }
433
434 pub async fn idm_oauth2_rs_delete_claim_map(
435 &self,
436 id: &str,
437 claim_name: &str,
438 group_id: &str,
439 ) -> Result<(), ClientError> {
440 self.perform_delete_request(
441 format!("/v1/oauth2/{}/_claimmap/{}/{}", id, claim_name, group_id).as_str(),
442 )
443 .await
444 }
445
446 pub async fn idm_oauth2_client_add_origin(
447 &self,
448 id: &str,
449 origin: &Url,
450 ) -> Result<(), ClientError> {
451 let url_to_add = &[origin.as_str()];
454 self.perform_post_request(
455 format!("/v1/oauth2/{}/_attr/{}", id, ATTR_OAUTH2_RS_ORIGIN).as_str(),
456 url_to_add,
457 )
458 .await
459 }
460
461 pub async fn idm_oauth2_client_remove_origin(
462 &self,
463 id: &str,
464 origin: &Url,
465 ) -> Result<(), ClientError> {
466 let url_to_remove = &[origin.as_str()];
467 self.perform_delete_request_with_body(
468 format!("/v1/oauth2/{}/_attr/{}", id, ATTR_OAUTH2_RS_ORIGIN).as_str(),
469 url_to_remove,
470 )
471 .await
472 }
473
474 pub async fn idm_oauth2_client_device_flow_update(
475 &self,
476 id: &str,
477 value: bool,
478 ) -> Result<(), ClientError> {
479 match value {
480 true => {
481 let mut update_oauth2_rs = Entry {
482 attrs: BTreeMap::new(),
483 };
484 update_oauth2_rs.attrs.insert(
485 Attribute::OAuth2DeviceFlowEnable.into(),
486 vec![value.to_string()],
487 );
488 self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
489 .await
490 }
491 false => {
492 self.perform_delete_request(&format!(
493 "/v1/oauth2/{}/_attr/{}",
494 id,
495 Attribute::OAuth2DeviceFlowEnable.as_str()
496 ))
497 .await
498 }
499 }
500 }
501}