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 self.ok_or_clienterror(&opid, response)
258 .await?
259 .json()
260 .await
261 .map_err(|e| ClientError::JsonDecode(e, opid))
262 }
263
264 pub async fn idm_oauth2_rs_enable_pkce(&self, id: &str) -> Result<(), ClientError> {
265 let mut update_oauth2_rs = Entry {
266 attrs: BTreeMap::new(),
267 };
268 update_oauth2_rs.attrs.insert(
269 ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE.to_string(),
270 Vec::new(),
271 );
272 self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
273 .await
274 }
275
276 pub async fn idm_oauth2_rs_disable_pkce(&self, id: &str) -> Result<(), ClientError> {
277 let mut update_oauth2_rs = Entry {
278 attrs: BTreeMap::new(),
279 };
280 update_oauth2_rs.attrs.insert(
281 ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE.to_string(),
282 vec!["true".to_string()],
283 );
284 self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
285 .await
286 }
287
288 pub async fn idm_oauth2_rs_enable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> {
289 let mut update_oauth2_rs = Entry {
290 attrs: BTreeMap::new(),
291 };
292 update_oauth2_rs.attrs.insert(
293 ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE.to_string(),
294 vec!["true".to_string()],
295 );
296 self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
297 .await
298 }
299
300 pub async fn idm_oauth2_rs_disable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> {
301 let mut update_oauth2_rs = Entry {
302 attrs: BTreeMap::new(),
303 };
304 update_oauth2_rs.attrs.insert(
305 ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE.to_string(),
306 vec!["false".to_string()],
307 );
308 self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
309 .await
310 }
311
312 pub async fn idm_oauth2_rs_prefer_short_username(&self, id: &str) -> Result<(), ClientError> {
313 let mut update_oauth2_rs = Entry {
314 attrs: BTreeMap::new(),
315 };
316 update_oauth2_rs.attrs.insert(
317 ATTR_OAUTH2_PREFER_SHORT_USERNAME.to_string(),
318 vec!["true".to_string()],
319 );
320 self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
321 .await
322 }
323
324 pub async fn idm_oauth2_rs_prefer_spn_username(&self, id: &str) -> Result<(), ClientError> {
325 let mut update_oauth2_rs = Entry {
326 attrs: BTreeMap::new(),
327 };
328 update_oauth2_rs.attrs.insert(
329 ATTR_OAUTH2_PREFER_SHORT_USERNAME.to_string(),
330 vec!["false".to_string()],
331 );
332 self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
333 .await
334 }
335
336 pub async fn idm_oauth2_rs_enable_public_localhost_redirect(
337 &self,
338 id: &str,
339 ) -> Result<(), ClientError> {
340 let mut update_oauth2_rs = Entry {
341 attrs: BTreeMap::new(),
342 };
343 update_oauth2_rs.attrs.insert(
344 ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT.to_string(),
345 vec!["true".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_disable_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!["false".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_enable_strict_redirect_uri(
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_STRICT_REDIRECT_URI.to_string(),
375 vec!["true".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_disable_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!["false".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_update_claim_map(
397 &self,
398 id: &str,
399 claim_name: &str,
400 group_id: &str,
401 values: &[String],
402 ) -> Result<(), ClientError> {
403 let values: Vec<String> = values.to_vec();
404 self.perform_post_request(
405 format!("/v1/oauth2/{id}/_claimmap/{claim_name}/{group_id}").as_str(),
406 values,
407 )
408 .await
409 }
410
411 pub async fn idm_oauth2_rs_update_claim_map_join(
412 &self,
413 id: &str,
414 claim_name: &str,
415 join: Oauth2ClaimMapJoin,
416 ) -> Result<(), ClientError> {
417 self.perform_post_request(
418 format!("/v1/oauth2/{id}/_claimmap/{claim_name}").as_str(),
419 join,
420 )
421 .await
422 }
423
424 pub async fn idm_oauth2_rs_delete_claim_map(
425 &self,
426 id: &str,
427 claim_name: &str,
428 group_id: &str,
429 ) -> Result<(), ClientError> {
430 self.perform_delete_request(
431 format!("/v1/oauth2/{id}/_claimmap/{claim_name}/{group_id}").as_str(),
432 )
433 .await
434 }
435
436 pub async fn idm_oauth2_client_add_origin(
437 &self,
438 id: &str,
439 origin: &Url,
440 ) -> Result<(), ClientError> {
441 let url_to_add = &[origin.as_str()];
444 self.perform_post_request(
445 format!("/v1/oauth2/{id}/_attr/{ATTR_OAUTH2_RS_ORIGIN}").as_str(),
446 url_to_add,
447 )
448 .await
449 }
450
451 pub async fn idm_oauth2_client_remove_origin(
452 &self,
453 id: &str,
454 origin: &Url,
455 ) -> Result<(), ClientError> {
456 let url_to_remove = &[origin.as_str()];
457 self.perform_delete_request_with_body(
458 format!("/v1/oauth2/{id}/_attr/{ATTR_OAUTH2_RS_ORIGIN}").as_str(),
459 url_to_remove,
460 )
461 .await
462 }
463
464 pub async fn idm_oauth2_client_device_flow_update(
465 &self,
466 id: &str,
467 value: bool,
468 ) -> Result<(), ClientError> {
469 match value {
470 true => {
471 let mut update_oauth2_rs = Entry {
472 attrs: BTreeMap::new(),
473 };
474 update_oauth2_rs.attrs.insert(
475 Attribute::OAuth2DeviceFlowEnable.into(),
476 vec![value.to_string()],
477 );
478 self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
479 .await
480 }
481 false => {
482 self.perform_delete_request(&format!(
483 "/v1/oauth2/{}/_attr/{}",
484 id,
485 Attribute::OAuth2DeviceFlowEnable.as_str()
486 ))
487 .await
488 }
489 }
490 }
491}