1use 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum ScopeToken {
27 Openid,
31
32 Profile,
36
37 Email,
41
42 Address,
46
47 Phone,
51
52 OfflineAccess,
58
59 MatrixApi(MatrixApiScopeToken),
63
64 MatrixDevice(PrivString),
70
71 Custom(PrivString),
75}
76
77impl ScopeToken {
78 pub fn try_with_matrix_device(device_id: String) -> Result<Self, InvalidScope> {
85 StrScopeToken::from_str(&device_id)?;
87
88 Ok(Self::MatrixDevice(PrivString(device_id)))
89 }
90
91 #[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
170pub trait ScopeExt {
172 fn insert_token(&mut self, token: ScopeToken) -> bool;
174
175 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
197pub enum MatrixApiScopeToken {
198 Full,
202
203 Guest,
207
208 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 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}