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        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        // TODO: should we normalise loopback origins, so when a user specifies `http://localhost/foo` we store it as `http://[::1]/foo`?
442
443        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}