1use super::{AuthError, AuthResult};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum Scope {
10 Read,
12 Deploy,
14 Admin,
16 Tools(String),
18}
19
20impl Scope {
21 pub fn parse(s: &str) -> AuthResult<Scope> {
22 let s = s.trim();
23 match s {
24 "read" => Ok(Scope::Read),
25 "deploy" => Ok(Scope::Deploy),
26 "admin" => Ok(Scope::Admin),
27 _ => match s.strip_prefix("tool:") {
28 Some(g)
29 if !g.is_empty()
30 && g.len() <= 128
31 && g.bytes()
32 .all(|b| b.is_ascii_alphanumeric() || b"_.-*?[]!^".contains(&b)) =>
33 {
34 Ok(Scope::Tools(g.to_string()))
35 }
36 _ => Err(AuthError::Invalid(format!(
37 "scope {s:?}: read, deploy, admin or tool:GLOB"
38 ))),
39 },
40 }
41 }
42
43 pub fn as_string(&self) -> String {
44 match self {
45 Scope::Read => "read".into(),
46 Scope::Deploy => "deploy".into(),
47 Scope::Admin => "admin".into(),
48 Scope::Tools(g) => format!("tool:{g}"),
49 }
50 }
51
52 pub fn normalize(v: &[String]) -> AuthResult<Vec<String>> {
54 if v.len() > 32 {
55 return Err(AuthError::Invalid("at most 32 scopes".into()));
56 }
57 let mut out: Vec<String> = Vec::new();
58 for s in v {
59 let s = Scope::parse(s)?.as_string();
60 if !out.contains(&s) {
61 out.push(s);
62 }
63 }
64 Ok(out)
65 }
66}