kanidm_client/
oauth.rs

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    // ==== Oauth2 resource server configuration
20    #[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    // TODO: the "id" here is actually the *name* not the uuid of the entry...
76    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    /// Want to delete the image associated with a resource server? Here's your thing!
207    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    /// Want to add/update the image associated with a resource server? Here's your thing!
213    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        // send it
237        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        // TODO: should we normalise loopback origins, so when a user specifies `http://localhost/foo` we store it as `http://[::1]/foo`?
452
453        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}