Skip to main content

alibi_plugins/
access.rs

1//! Public access-control utilities matching Better Auth's resource and action rules.
2//!
3//! These operators apply to application-side authorization. Organization HTTP
4//! permission requests continue to accept action arrays and use AND semantics.
5
6use indexmap::IndexMap;
7
8/// Literal resource names mapped to literal allowed actions.
9pub type Statements = IndexMap<String, Vec<String>>;
10
11/// How to combine resources, or the actions within one resource.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13pub enum Connector {
14    #[default]
15    And,
16    Or,
17}
18
19/// An action array uses AND; a resource rule can select its own connector.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ActionRequest {
22    Actions(Vec<String>),
23    Rule {
24        actions: Vec<String>,
25        connector: Connector,
26    },
27}
28
29impl From<Vec<String>> for ActionRequest {
30    fn from(actions: Vec<String>) -> Self {
31        Self::Actions(actions)
32    }
33}
34
35/// Resource requests. Numeric index names are visited first, in ascending
36/// order; other names retain insertion order, matching Source object entries.
37pub type AuthorizeRequest = IndexMap<String, ActionRequest>;
38
39/// A successful authorization has no error; a rejection explains the first failure.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum AuthorizeResponse {
42    Success,
43    Denied(String),
44}
45
46impl AuthorizeResponse {
47    #[must_use]
48    pub fn success(&self) -> bool {
49        matches!(self, Self::Success)
50    }
51}
52
53/// A role's grants; authorization never combines different roles' statements.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Role {
56    pub statements: Statements,
57}
58
59/// Construct a role directly from literal grants.
60#[must_use]
61pub fn role(statements: Statements) -> Role {
62    Role { statements }
63}
64
65impl Role {
66    /// Authorize all requested resources, with AND for plain action arrays.
67    #[must_use]
68    pub fn authorize(&self, request: &AuthorizeRequest) -> AuthorizeResponse {
69        self.authorize_with_connector(request, Connector::And)
70    }
71
72    /// Authorize with an explicit resource connector. Empty requests and empty
73    /// action lists deny. OR skips missing resources and succeeds on any grant.
74    #[must_use]
75    pub fn authorize_with_connector(
76        &self,
77        request: &AuthorizeRequest,
78        connector: Connector,
79    ) -> AuthorizeResponse {
80        let mut entries: Vec<_> = request.iter().collect();
81        entries.sort_by_key(|(resource, _)| {
82            resource
83                .parse::<u32>()
84                .ok()
85                .filter(|index| *index != u32::MAX && index.to_string() == resource.as_str())
86                .unwrap_or(u32::MAX)
87        });
88        authorize(
89            |resource| self.statements.get(resource).map(Vec::as_slice),
90            entries.into_iter().map(|(resource, rule)| {
91                let (actions, connector) = match rule {
92                    ActionRequest::Actions(actions) => (actions, Connector::And),
93                    ActionRequest::Rule { actions, connector } => (actions, *connector),
94                };
95                (resource.as_str(), actions.as_slice(), connector)
96            }),
97            connector,
98        )
99    }
100}
101
102/// Declared access-control statements and a constructor for roles. As in the
103/// Source runtime, constructing a role does not validate its grants against
104/// these declarations. Rust callers supply typed string action lists.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct AccessControl {
107    pub statements: Statements,
108}
109
110#[must_use]
111pub fn create_access_control(statements: Statements) -> AccessControl {
112    AccessControl { statements }
113}
114
115impl AccessControl {
116    #[must_use]
117    pub fn new_role(&self, statements: Statements) -> Role {
118        role(statements)
119    }
120}
121
122pub(super) fn authorize<'a, 'b>(
123    grants: impl Fn(&str) -> Option<&'b [String]>,
124    request: impl IntoIterator<Item = (&'a str, &'a [String], Connector)>,
125    connector: Connector,
126) -> AuthorizeResponse {
127    let mut authorized = false;
128    for (resource, actions, action_connector) in request {
129        let Some(allowed) = grants(resource) else {
130            if connector == Connector::And {
131                return AuthorizeResponse::Denied(format!(
132                    "You are not allowed to access resource: {resource}"
133                ));
134            }
135            continue;
136        };
137        let granted = !actions.is_empty()
138            && match action_connector {
139                Connector::And => actions.iter().all(|action| allowed.contains(action)),
140                Connector::Or => actions.iter().any(|action| allowed.contains(action)),
141            };
142        if granted {
143            authorized = true;
144            if connector == Connector::Or {
145                return AuthorizeResponse::Success;
146            }
147        } else if connector == Connector::And {
148            return AuthorizeResponse::Denied(format!(
149                "unauthorized to access resource \"{resource}\""
150            ));
151        }
152    }
153    if authorized {
154        AuthorizeResponse::Success
155    } else {
156        AuthorizeResponse::Denied("Not authorized".into())
157    }
158}