Skip to main content

mas_oidc_client/types/
scope.rs

1// Copyright 2022 Kévin Commaille.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Helpers types to use scopes.
16
17use std::{fmt, str::FromStr};
18
19use oauth2_types::scope::ScopeToken as StrScopeToken;
20pub use oauth2_types::scope::{InvalidScope, Scope};
21
22use crate::PrivString;
23
24/// Tokens to define the scope of an access token or to request specific claims.
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum ScopeToken {
27    /// `openid`
28    ///
29    /// Required for OpenID Connect requests.
30    Openid,
31
32    /// `profile`
33    ///
34    /// Requests access to the end-user's profile.
35    Profile,
36
37    /// `email`
38    ///
39    /// Requests access to the end-user's email address.
40    Email,
41
42    /// `address`
43    ///
44    /// Requests access to the end-user's address.
45    Address,
46
47    /// `phone`
48    ///
49    /// Requests access to the end-user's phone number.
50    Phone,
51
52    /// `offline_access`
53    ///
54    /// Requests that an OAuth 2.0 refresh token be issued that can be used to
55    /// obtain an access token that grants access to the end-user's `UserInfo`
56    /// Endpoint even when the end-user is not present (not logged in).
57    OfflineAccess,
58
59    /// `urn:matrix:org.matrix.msc2967.client:api:{token}`
60    ///
61    /// Requests access to the Matrix Client-Server API.
62    MatrixApi(MatrixApiScopeToken),
63
64    /// `urn:matrix:org.matrix.msc2967.client:device:{device_id}`
65    ///
66    /// Requests access to the Matrix device with the given `device_id`.
67    ///
68    /// To access the device ID, use [`ScopeToken::matrix_device_id`].
69    MatrixDevice(PrivString),
70
71    /// Another scope token.
72    ///
73    /// To access it's value use this type's `Display` implementation.
74    Custom(PrivString),
75}
76
77impl ScopeToken {
78    /// Creates a Matrix device scope token with the given device ID.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if the device ID string is not compatible with the
83    /// scope syntax.
84    pub fn try_with_matrix_device(device_id: String) -> Result<Self, InvalidScope> {
85        // Check that the device ID is compatible with the scope format.
86        StrScopeToken::from_str(&device_id)?;
87
88        Ok(Self::MatrixDevice(PrivString(device_id)))
89    }
90
91    /// Get the device ID of this scope token, if it is a
92    /// [`ScopeToken::MatrixDevice`].
93    #[must_use]
94    pub fn matrix_device_id(&self) -> Option<&str> {
95        match &self {
96            Self::MatrixDevice(id) => Some(&id.0),
97            _ => None,
98        }
99    }
100}
101
102impl fmt::Display for ScopeToken {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match self {
105            ScopeToken::Openid => write!(f, "openid"),
106            ScopeToken::Profile => write!(f, "profile"),
107            ScopeToken::Email => write!(f, "email"),
108            ScopeToken::Address => write!(f, "address"),
109            ScopeToken::Phone => write!(f, "phone"),
110            ScopeToken::OfflineAccess => write!(f, "offline_access"),
111            ScopeToken::MatrixApi(scope) => {
112                write!(f, "urn:matrix:org.matrix.msc2967.client:api:{scope}")
113            }
114            ScopeToken::MatrixDevice(s) => {
115                write!(f, "urn:matrix:org.matrix.msc2967.client:device:{}", s.0)
116            }
117            ScopeToken::Custom(s) => f.write_str(&s.0),
118        }
119    }
120}
121
122impl From<StrScopeToken> for ScopeToken {
123    fn from(t: StrScopeToken) -> Self {
124        match &*t {
125            "openid" => Self::Openid,
126            "profile" => Self::Profile,
127            "email" => Self::Email,
128            "address" => Self::Address,
129            "phone" => Self::Phone,
130            "offline_access" => Self::OfflineAccess,
131            s => {
132                if let Some(matrix_scope) =
133                    s.strip_prefix("urn:matrix:org.matrix.msc2967.client:api:")
134                {
135                    Self::MatrixApi(
136                        MatrixApiScopeToken::from_str(matrix_scope)
137                            .expect("If the whole string is a valid scope, a substring is too"),
138                    )
139                } else if let Some(device_id) =
140                    s.strip_prefix("urn:matrix:org.matrix.msc2967.client:device:")
141                {
142                    Self::MatrixDevice(PrivString(device_id.to_owned()))
143                } else {
144                    Self::Custom(PrivString(s.to_owned()))
145                }
146            }
147        }
148    }
149}
150
151impl From<ScopeToken> for StrScopeToken {
152    fn from(t: ScopeToken) -> Self {
153        let s = t.to_string();
154        match StrScopeToken::from_str(&s) {
155            Ok(t) => t,
156            Err(_) => unreachable!(),
157        }
158    }
159}
160
161impl FromStr for ScopeToken {
162    type Err = InvalidScope;
163
164    fn from_str(s: &str) -> Result<Self, Self::Err> {
165        let t = StrScopeToken::from_str(s)?;
166        Ok(t.into())
167    }
168}
169
170/// Helpers for [`Scope`] to work with [`ScopeToken`].
171pub trait ScopeExt {
172    /// Insert the given `ScopeToken` into this `Scope`.
173    fn insert_token(&mut self, token: ScopeToken) -> bool;
174
175    /// Whether this `Scope` contains the given `ScopeToken`.
176    fn contains_token(&self, token: &ScopeToken) -> bool;
177}
178
179impl ScopeExt for Scope {
180    fn insert_token(&mut self, token: ScopeToken) -> bool {
181        self.insert(token.into())
182    }
183
184    fn contains_token(&self, token: &ScopeToken) -> bool {
185        self.contains(&token.to_string())
186    }
187}
188
189impl FromIterator<ScopeToken> for Scope {
190    fn from_iter<T: IntoIterator<Item = ScopeToken>>(iter: T) -> Self {
191        iter.into_iter().map(Into::<StrScopeToken>::into).collect()
192    }
193}
194
195/// Tokens to define the scope of an access to the Matrix Client-Server API.
196#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
197pub enum MatrixApiScopeToken {
198    /// `*`
199    ///
200    /// Access the full Client-Server API.
201    Full,
202
203    /// `guest`
204    ///
205    /// Access the Client-Server API as a guest.
206    Guest,
207
208    /// Another scope token.
209    ///
210    /// To access it's value use this type's `Display` implementation.
211    Custom(PrivString),
212}
213
214impl fmt::Display for MatrixApiScopeToken {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        match self {
217            Self::Full => write!(f, "*"),
218            Self::Guest => write!(f, "guest"),
219            Self::Custom(s) => f.write_str(&s.0),
220        }
221    }
222}
223
224impl FromStr for MatrixApiScopeToken {
225    type Err = InvalidScope;
226
227    fn from_str(s: &str) -> Result<Self, Self::Err> {
228        // Check that it's a valid scope string.
229        StrScopeToken::from_str(s)?;
230
231        let t = match s {
232            "*" => Self::Full,
233            "guest" => Self::Guest,
234            _ => Self::Custom(PrivString(s.to_owned())),
235        };
236        Ok(t)
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use assert_matches::assert_matches;
243
244    use super::*;
245
246    #[test]
247    fn parse_scope_token() {
248        assert_eq!(ScopeToken::from_str("openid"), Ok(ScopeToken::Openid));
249
250        let scope =
251            ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:device:ABCDEFGHIJKL")
252                .unwrap();
253        assert_matches!(scope, ScopeToken::MatrixDevice(_));
254        assert_eq!(scope.matrix_device_id(), Some("ABCDEFGHIJKL"));
255
256        let scope = ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:api:*").unwrap();
257        assert_eq!(scope, ScopeToken::MatrixApi(MatrixApiScopeToken::Full));
258
259        let scope = ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:api:guest").unwrap();
260        assert_eq!(scope, ScopeToken::MatrixApi(MatrixApiScopeToken::Guest));
261
262        let scope =
263            ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:api:my.custom.scope")
264                .unwrap();
265        let api_scope = assert_matches!(scope, ScopeToken::MatrixApi(s) => s);
266        assert_matches!(api_scope, MatrixApiScopeToken::Custom(_));
267        assert_eq!(api_scope.to_string(), "my.custom.scope");
268
269        assert_eq!(ScopeToken::from_str("invalid\\scope"), Err(InvalidScope));
270        assert_eq!(
271            MatrixApiScopeToken::from_str("invalid\\scope"),
272            Err(InvalidScope)
273        );
274    }
275
276    #[test]
277    fn display_scope_token() {
278        let scope = ScopeToken::MatrixApi(MatrixApiScopeToken::Full);
279        assert_eq!(
280            scope.to_string(),
281            "urn:matrix:org.matrix.msc2967.client:api:*"
282        );
283
284        let scope = ScopeToken::MatrixApi(MatrixApiScopeToken::Guest);
285        assert_eq!(
286            scope.to_string(),
287            "urn:matrix:org.matrix.msc2967.client:api:guest"
288        );
289
290        let api_scope = MatrixApiScopeToken::from_str("my.custom.scope").unwrap();
291        let scope = ScopeToken::MatrixApi(api_scope);
292        assert_eq!(
293            scope.to_string(),
294            "urn:matrix:org.matrix.msc2967.client:api:my.custom.scope"
295        );
296    }
297
298    #[test]
299    fn parse_scope() {
300        let scope = Scope::from_str("openid profile address").unwrap();
301        assert_eq!(scope.len(), 3);
302        assert!(scope.contains_token(&ScopeToken::Openid));
303        assert!(scope.contains_token(&ScopeToken::Profile));
304        assert!(scope.contains_token(&ScopeToken::Address));
305        assert!(!scope.contains_token(&ScopeToken::OfflineAccess));
306    }
307
308    #[test]
309    fn display_scope() {
310        let mut scope: Scope = [ScopeToken::Profile].into_iter().collect();
311        assert_eq!(scope.to_string(), "profile");
312
313        scope.insert_token(ScopeToken::MatrixApi(MatrixApiScopeToken::Full));
314        assert_eq!(
315            scope.to_string(),
316            "profile urn:matrix:org.matrix.msc2967.client:api:*"
317        );
318    }
319}