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