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