1use std::collections::BTreeSet;
2
3use crate::ast::Action;
4use crate::rls::SuperAdminToken;
5
6use super::ident::normalize_column_name;
7
8#[derive(
10 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
11)]
12#[serde(rename_all = "lowercase")]
13pub enum AccessOperation {
14 Read,
16 Create,
18 Update,
20 Delete,
22}
23
24impl AccessOperation {
25 pub fn required_for_action(action: Action) -> Option<&'static [Self]> {
27 match action {
28 Action::Get
29 | Action::Cnt
30 | Action::Export
31 | Action::With
32 | Action::Search
33 | Action::Scroll => Some(&[Self::Read]),
34 Action::Add => Some(&[Self::Create]),
35 Action::Set | Action::Put | Action::Over => Some(&[Self::Update]),
36 Action::Upsert => Some(&[Self::Create, Self::Update]),
37 Action::Del => Some(&[Self::Delete]),
38 _ => None,
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct AccessContext {
46 pub subject_id: Option<String>,
48 pub tenant_id: Option<String>,
50 pub roles: BTreeSet<String>,
52 pub scopes: BTreeSet<String>,
54 bypass: bool,
55}
56
57impl AccessContext {
58 pub fn anonymous() -> Self {
60 Self {
61 subject_id: None,
62 tenant_id: None,
63 roles: BTreeSet::new(),
64 scopes: BTreeSet::new(),
65 bypass: false,
66 }
67 }
68
69 pub fn subject(subject_id: impl Into<String>) -> Self {
71 Self {
72 subject_id: Some(subject_id.into()),
73 ..Self::anonymous()
74 }
75 }
76
77 pub fn super_admin(_token: SuperAdminToken) -> Self {
79 Self {
80 bypass: true,
81 ..Self::anonymous()
82 }
83 }
84
85 pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
87 self.tenant_id = Some(tenant_id.into());
88 self
89 }
90
91 pub fn with_role(mut self, role: impl Into<String>) -> Self {
93 self.roles.insert(role.into());
94 self
95 }
96
97 pub fn with_roles<I, S>(mut self, roles: I) -> Self
99 where
100 I: IntoIterator<Item = S>,
101 S: Into<String>,
102 {
103 self.roles.extend(roles.into_iter().map(Into::into));
104 self
105 }
106
107 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
109 self.scopes.insert(scope.into());
110 self
111 }
112
113 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
115 where
116 I: IntoIterator<Item = S>,
117 S: Into<String>,
118 {
119 self.scopes.extend(scopes.into_iter().map(Into::into));
120 self
121 }
122
123 pub fn bypasses_access(&self) -> bool {
125 self.bypass
126 }
127
128 pub(super) fn has_any_role(&self, required: &BTreeSet<String>) -> bool {
129 required.is_empty() || required.iter().any(|role| self.roles.contains(role))
130 }
131
132 pub(super) fn has_all_scopes(&self, required: &BTreeSet<String>) -> bool {
133 required.is_subset(&self.scopes)
134 }
135}
136
137impl Default for AccessContext {
138 fn default() -> Self {
139 Self::anonymous()
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
145#[serde(rename_all = "lowercase")]
146pub enum AccessDecision {
147 Allow,
149 Deny,
151}
152
153#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
155#[serde(rename_all = "snake_case")]
156pub enum ColumnRule {
157 #[default]
159 Any,
160 DenyAll,
162 Only(BTreeSet<String>),
164 Except(BTreeSet<String>),
166}
167
168impl ColumnRule {
169 pub fn only<I, S>(columns: I) -> Self
171 where
172 I: IntoIterator<Item = S>,
173 S: Into<String>,
174 {
175 Self::Only(columns.into_iter().map(normalize_column_name).collect())
176 }
177
178 pub fn except<I, S>(columns: I) -> Self
180 where
181 I: IntoIterator<Item = S>,
182 S: Into<String>,
183 {
184 Self::Except(columns.into_iter().map(normalize_column_name).collect())
185 }
186
187 pub fn is_restrictive(&self) -> bool {
189 !matches!(self, Self::Any)
190 }
191
192 pub fn allows(&self, column: &str) -> bool {
194 let normalized = normalize_column_name(column);
195 match self {
196 Self::Any => true,
197 Self::DenyAll => false,
198 Self::Only(columns) => columns.contains(&normalized),
199 Self::Except(columns) => !columns.contains(&normalized),
200 }
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
206pub struct TableAccessPolicy {
207 #[serde(default)]
209 pub operations: BTreeSet<AccessOperation>,
210 #[serde(default)]
212 pub denied_operations: BTreeSet<AccessOperation>,
213 #[serde(default)]
215 pub read_columns: ColumnRule,
216 #[serde(default)]
218 pub write_columns: ColumnRule,
219 #[serde(default)]
221 pub returning_columns: ColumnRule,
222 #[serde(default)]
224 pub require_any_role: BTreeSet<String>,
225 #[serde(default)]
227 pub require_scopes: BTreeSet<String>,
228}
229
230impl TableAccessPolicy {
231 pub fn new() -> Self {
233 Self::default()
234 }
235
236 pub fn allow_operations<I>(mut self, operations: I) -> Self
238 where
239 I: IntoIterator<Item = AccessOperation>,
240 {
241 self.operations.extend(operations);
242 self
243 }
244
245 pub fn deny_operations<I>(mut self, operations: I) -> Self
247 where
248 I: IntoIterator<Item = AccessOperation>,
249 {
250 self.denied_operations.extend(operations);
251 self
252 }
253
254 pub fn read_columns(mut self, rule: ColumnRule) -> Self {
256 self.read_columns = rule;
257 self
258 }
259
260 pub fn write_columns(mut self, rule: ColumnRule) -> Self {
262 self.write_columns = rule;
263 self
264 }
265
266 pub fn returning_columns(mut self, rule: ColumnRule) -> Self {
268 self.returning_columns = rule;
269 self
270 }
271
272 pub fn require_any_role<I, S>(mut self, roles: I) -> Self
274 where
275 I: IntoIterator<Item = S>,
276 S: Into<String>,
277 {
278 self.require_any_role
279 .extend(roles.into_iter().map(Into::into));
280 self
281 }
282
283 pub fn require_scopes<I, S>(mut self, scopes: I) -> Self
285 where
286 I: IntoIterator<Item = S>,
287 S: Into<String>,
288 {
289 self.require_scopes
290 .extend(scopes.into_iter().map(Into::into));
291 self
292 }
293
294 pub(super) fn allows_operation(&self, operation: AccessOperation) -> bool {
295 self.operations.contains(&operation) && !self.denied_operations.contains(&operation)
296 }
297}
298
299impl Default for TableAccessPolicy {
300 fn default() -> Self {
301 Self {
302 operations: BTreeSet::new(),
303 denied_operations: BTreeSet::new(),
304 read_columns: ColumnRule::Any,
305 write_columns: ColumnRule::Any,
306 returning_columns: ColumnRule::Any,
307 require_any_role: BTreeSet::new(),
308 require_scopes: BTreeSet::new(),
309 }
310 }
311}