Skip to main content

rustlavel_oauth/
scope.rs

1//! Scopes: what a token is allowed to do.
2
3use std::collections::BTreeSet;
4
5/// A set of scopes, kept sorted so one set always renders identically.
6///
7/// OAuth writes scopes as a space-delimited string, which is easy to get wrong
8/// by hand — an empty scope, a double space, or a duplicate all produce a
9/// string that looks fine and compares unequal.
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
11pub struct Scopes {
12    values: BTreeSet<String>,
13}
14
15impl Scopes {
16    pub fn new() -> Self {
17        Scopes::default()
18    }
19
20    /// Parse the space-delimited form OAuth sends on the wire.
21    pub fn parse(raw: &str) -> Scopes {
22        Scopes {
23            values: raw
24                .split_whitespace()
25                .filter(|scope| !scope.is_empty())
26                .map(str::to_string)
27                .collect(),
28        }
29    }
30
31    pub fn of<I, S>(scopes: I) -> Scopes
32    where
33        I: IntoIterator<Item = S>,
34        S: Into<String>,
35    {
36        Scopes { values: scopes.into_iter().map(Into::into).collect() }
37    }
38
39    pub fn add(&mut self, scope: impl Into<String>) {
40        self.values.insert(scope.into());
41    }
42
43    pub fn with(mut self, scope: impl Into<String>) -> Self {
44        self.add(scope);
45        self
46    }
47
48    pub fn contains(&self, scope: &str) -> bool {
49        self.values.contains(scope)
50    }
51
52    /// Whether this set covers everything `required` asks for.
53    ///
54    /// The question an authorisation check actually asks: not "are these equal"
55    /// but "is what I was granted enough".
56    pub fn covers(&self, required: &Scopes) -> bool {
57        required.values.is_subset(&self.values)
58    }
59
60    /// The scopes in this set that are also in `allowed`.
61    ///
62    /// A client asking for more than it is registered for gets the intersection
63    /// rather than a refusal, which is what most providers do — but the caller
64    /// decides, because silently granting less than was asked for is worth
65    /// knowing about.
66    pub fn intersect(&self, allowed: &Scopes) -> Scopes {
67        Scopes { values: self.values.intersection(&allowed.values).cloned().collect() }
68    }
69
70    /// Anything in this set that `allowed` does not contain.
71    pub fn beyond(&self, allowed: &Scopes) -> Scopes {
72        Scopes { values: self.values.difference(&allowed.values).cloned().collect() }
73    }
74
75    pub fn is_empty(&self) -> bool {
76        self.values.is_empty()
77    }
78
79    pub fn len(&self) -> usize {
80        self.values.len()
81    }
82
83    pub fn iter(&self) -> impl Iterator<Item = &str> {
84        self.values.iter().map(String::as_str)
85    }
86}
87
88impl std::fmt::Display for Scopes {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        let joined: Vec<&str> = self.values.iter().map(String::as_str).collect();
91        f.write_str(&joined.join(" "))
92    }
93}
94
95impl From<&str> for Scopes {
96    fn from(raw: &str) -> Scopes {
97        Scopes::parse(raw)
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn parses_the_wire_form_forgivingly() {
107        let scopes = Scopes::parse("  read   write  read ");
108
109        assert_eq!(scopes.len(), 2, "duplicates and blanks are dropped");
110        assert!(scopes.contains("read"));
111        assert!(scopes.contains("write"));
112    }
113
114    #[test]
115    fn renders_in_a_stable_order() {
116        // Two sets built differently must produce the same string, or a
117        // signature over them would differ for no reason.
118        assert_eq!(Scopes::of(["write", "read"]).to_string(), "read write");
119        assert_eq!(Scopes::parse("write read").to_string(), "read write");
120    }
121
122    #[test]
123    fn an_empty_set_is_an_empty_string() {
124        assert_eq!(Scopes::new().to_string(), "");
125        assert!(Scopes::parse("   ").is_empty());
126    }
127
128    #[test]
129    fn coverage_asks_whether_what_was_granted_is_enough() {
130        let granted = Scopes::of(["read", "write", "delete"]);
131
132        assert!(granted.covers(&Scopes::of(["read"])));
133        assert!(granted.covers(&Scopes::of(["read", "write"])));
134        assert!(granted.covers(&Scopes::new()), "asking for nothing is always covered");
135        assert!(!granted.covers(&Scopes::of(["admin"])));
136    }
137
138    #[test]
139    fn a_request_can_be_narrowed_to_what_a_client_may_have() {
140        let asked = Scopes::of(["read", "write", "admin"]);
141        let allowed = Scopes::of(["read", "write"]);
142
143        assert_eq!(asked.intersect(&allowed), Scopes::of(["read", "write"]));
144        assert_eq!(asked.beyond(&allowed), Scopes::of(["admin"]));
145    }
146
147    #[test]
148    fn round_trips_through_the_wire_form() {
149        let original = Scopes::of(["openid", "profile", "email"]);
150        assert_eq!(Scopes::parse(&original.to_string()), original);
151    }
152}