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