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    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    /// Want to delete the image associated with a resource server? Here's your thing!
212    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    /// Want to add/update the image associated with a resource server? Here's your thing!
218    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        // send it
242        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        // TODO: should we normalise loopback origins, so when a user specifies `http://localhost/foo` we store it as `http://[::1]/foo`?
457
458        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}