Skip to main content

oauth_toolkit/
scope.rs

1//! OAuth2 scope parsing and validation.
2
3/// Parse a space- or comma-separated scope string into a sorted, deduplicated vec.
4pub fn parse_scopes(input: &str) -> Vec<String> {
5    let mut scopes: Vec<String> = input
6        .split(|c: char| c.is_whitespace() || c == ',')
7        .filter(|s| !s.is_empty())
8        .map(|s| s.trim().to_lowercase())
9        .collect();
10    scopes.sort();
11    scopes.dedup();
12    scopes
13}
14
15/// Validate that every requested scope is in the allowed set.
16///
17/// Returns the normalized scope list on success, or the first unknown scope as an error.
18pub fn validate_scopes(requested: &[String], allowed: &[&str]) -> Result<Vec<String>, ScopeError> {
19    for scope in requested {
20        if !allowed.contains(&scope.as_str()) {
21            return Err(ScopeError::UnknownScope(scope.clone()));
22        }
23    }
24    Ok(requested.to_vec())
25}
26
27/// Check that `requested` scopes are a subset of `allowed` scopes.
28pub fn scopes_subset(requested: &[String], allowed: &[String]) -> bool {
29    requested.iter().all(|r| allowed.contains(r))
30}
31
32/// Errors for scope operations.
33#[derive(Debug, thiserror::Error)]
34pub enum ScopeError {
35    /// An unknown scope was requested.
36    #[error("unknown scope: {0}")]
37    UnknownScope(String),
38}
39
40#[cfg(test)]
41// Test code: unwrap/expect are the idiomatic way to assert setup success.
42#[allow(clippy::unwrap_used, clippy::expect_used)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn parse_scopes_whitespace() {
48        assert_eq!(
49            parse_scopes("read write admin"),
50            vec!["admin", "read", "write"]
51        );
52    }
53
54    #[test]
55    fn parse_scopes_comma() {
56        assert_eq!(
57            parse_scopes("read,write,admin"),
58            vec!["admin", "read", "write"]
59        );
60    }
61
62    #[test]
63    fn parse_scopes_dedup() {
64        assert_eq!(parse_scopes("read read"), vec!["read"]);
65    }
66
67    #[test]
68    fn validate_scopes_ok() {
69        let requested = vec!["read".into(), "write".into()];
70        let allowed = &["read", "write", "admin"];
71        assert!(validate_scopes(&requested, allowed).is_ok());
72    }
73
74    #[test]
75    fn validate_scopes_unknown() {
76        let requested = vec!["read".into(), "dangerous".into()];
77        let allowed = &["read", "write"];
78        assert!(validate_scopes(&requested, allowed).is_err());
79    }
80
81    #[test]
82    fn scopes_subset_true() {
83        assert!(scopes_subset(&["a".into()], &["a".into(), "b".into()]));
84    }
85
86    #[test]
87    fn scopes_subset_false() {
88        assert!(!scopes_subset(
89            &["a".into(), "c".into()],
90            &["a".into(), "b".into()]
91        ));
92    }
93}