1use indexmap::IndexMap;
7
8pub type Statements = IndexMap<String, Vec<String>>;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13pub enum Connector {
14 #[default]
15 And,
16 Or,
17}
18
19#[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
35pub type AuthorizeRequest = IndexMap<String, ActionRequest>;
38
39#[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#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Role {
56 pub statements: Statements,
57}
58
59#[must_use]
61pub fn role(statements: Statements) -> Role {
62 Role { statements }
63}
64
65impl Role {
66 #[must_use]
68 pub fn authorize(&self, request: &AuthorizeRequest) -> AuthorizeResponse {
69 self.authorize_with_connector(request, Connector::And)
70 }
71
72 #[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#[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}