1use crate::auth::Principal;
4use crate::memtable::{Row, Value};
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
9pub struct SecurityCatalog {
10 #[serde(default)]
11 pub rls_tables: Vec<String>,
12 #[serde(default)]
13 pub policies: Vec<RowPolicy>,
14 #[serde(default)]
15 pub masks: Vec<ColumnMask>,
16}
17
18pub struct CandidateAuthorization<'a> {
20 pub table: &'a str,
21 pub security: &'a SecurityCatalog,
22 pub principal: &'a Principal,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub struct RowPolicy {
27 pub name: String,
28 pub table: String,
29 pub command: PolicyCommand,
30 #[serde(default)]
31 pub subjects: Vec<String>,
32 #[serde(default = "default_true")]
33 pub permissive: bool,
34 pub using: Option<SecurityExpr>,
35 pub with_check: Option<SecurityExpr>,
36}
37
38#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "snake_case")]
40pub enum PolicyCommand {
41 All,
42 Select,
43 Insert,
44 Update,
45 Delete,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49#[serde(tag = "kind", rename_all = "snake_case")]
50pub enum SecurityExpr {
51 True,
52 ColumnEqCurrentUser {
53 column: u16,
54 },
55 ColumnEqValue {
56 column: u16,
57 value: Value,
58 },
59 And {
60 left: Box<SecurityExpr>,
61 right: Box<SecurityExpr>,
62 },
63 Or {
64 left: Box<SecurityExpr>,
65 right: Box<SecurityExpr>,
66 },
67 Not {
68 expression: Box<SecurityExpr>,
69 },
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73pub struct ColumnMask {
74 pub name: String,
75 pub table: String,
76 pub column: u16,
77 pub strategy: MaskStrategy,
78 #[serde(default)]
79 pub exempt_subjects: Vec<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83#[serde(tag = "kind", rename_all = "snake_case")]
84pub enum MaskStrategy {
85 Null,
86 Redact { replacement: String },
87 Sha256,
88}
89
90impl SecurityCatalog {
91 pub fn table_has_security(&self, table: &str) -> bool {
92 self.rls_tables.iter().any(|name| name == table)
93 || self.masks.iter().any(|mask| mask.table == table)
94 }
95
96 pub fn table_has_objects(&self, table: &str) -> bool {
97 self.table_has_security(table) || self.policies.iter().any(|policy| policy.table == table)
98 }
99
100 pub fn rls_enabled(&self, table: &str) -> bool {
101 self.rls_tables.iter().any(|name| name == table)
102 }
103
104 pub fn row_allowed(
105 &self,
106 table: &str,
107 command: PolicyCommand,
108 row: &Row,
109 principal: &Principal,
110 check_new: bool,
111 ) -> bool {
112 if principal.is_admin || !self.rls_enabled(table) {
113 return true;
114 }
115 let applicable = self.policies.iter().filter(|policy| {
116 policy.table == table
117 && (matches!(policy.command, PolicyCommand::All) || policy.command == command)
118 });
119 let mut has_permissive = false;
120 let mut permissive_allowed = false;
121 let mut restrictive_allowed = true;
122 for policy in applicable.filter(|policy| subject_matches(&policy.subjects, principal)) {
123 let expression = if check_new {
124 policy.with_check.as_ref().or(policy.using.as_ref())
125 } else {
126 policy.using.as_ref()
127 };
128 let allowed = expression.is_some_and(|expression| expression.eval(row, principal));
129 if policy.permissive {
130 has_permissive = true;
131 permissive_allowed |= allowed;
132 } else {
133 restrictive_allowed &= allowed;
134 }
135 }
136 has_permissive && permissive_allowed && restrictive_allowed
137 }
138
139 pub fn apply_masks(&self, table: &str, row: &mut Row, principal: &Principal) {
140 if principal.is_admin {
141 return;
142 }
143 for mask in self.masks.iter().filter(|mask| mask.table == table) {
144 if !mask.exempt_subjects.is_empty() && subject_matches(&mask.exempt_subjects, principal)
145 {
146 continue;
147 }
148 let Some(value) = row.columns.get_mut(&mask.column) else {
149 continue;
150 };
151 *value = mask.strategy.apply(value);
152 }
153 }
154
155 pub fn apply_masks_to_cells(
156 &self,
157 table: &str,
158 cells: &mut [(u16, Value)],
159 principal: &Principal,
160 ) {
161 if principal.is_admin {
162 return;
163 }
164 for mask in self.masks.iter().filter(|mask| mask.table == table) {
165 if !mask.exempt_subjects.is_empty() && subject_matches(&mask.exempt_subjects, principal)
166 {
167 continue;
168 }
169 if let Some((_, value)) = cells
170 .iter_mut()
171 .find(|(column_id, _)| *column_id == mask.column)
172 {
173 *value = mask.strategy.apply(value);
174 }
175 }
176 }
177
178 pub fn select_policy_columns(&self, table: &str, principal: &Principal) -> Vec<u16> {
180 let mut columns = Vec::new();
181 for policy in self.policies.iter().filter(|policy| {
182 policy.table == table
183 && (matches!(policy.command, PolicyCommand::All)
184 || policy.command == PolicyCommand::Select)
185 && subject_matches(&policy.subjects, principal)
186 }) {
187 if let Some(expression) = &policy.using {
188 expression.collect_columns(&mut columns);
189 }
190 }
191 columns.sort_unstable();
192 columns.dedup();
193 columns
194 }
195}
196
197impl SecurityExpr {
198 pub fn eval(&self, row: &Row, principal: &Principal) -> bool {
199 match self {
200 SecurityExpr::True => true,
201 SecurityExpr::ColumnEqCurrentUser { column } => {
202 row.columns.get(column).is_some_and(|value| {
203 value == &Value::Bytes(principal.username.as_bytes().to_vec())
204 })
205 }
206 SecurityExpr::ColumnEqValue { column, value } => row
207 .columns
208 .get(column)
209 .is_some_and(|current| current == value),
210 SecurityExpr::And { left, right } => {
211 left.eval(row, principal) && right.eval(row, principal)
212 }
213 SecurityExpr::Or { left, right } => {
214 left.eval(row, principal) || right.eval(row, principal)
215 }
216 SecurityExpr::Not { expression } => !expression.eval(row, principal),
217 }
218 }
219
220 fn collect_columns(&self, columns: &mut Vec<u16>) {
221 match self {
222 SecurityExpr::True => {}
223 SecurityExpr::ColumnEqCurrentUser { column }
224 | SecurityExpr::ColumnEqValue { column, .. } => columns.push(*column),
225 SecurityExpr::And { left, right } | SecurityExpr::Or { left, right } => {
226 left.collect_columns(columns);
227 right.collect_columns(columns);
228 }
229 SecurityExpr::Not { expression } => expression.collect_columns(columns),
230 }
231 }
232}
233
234impl MaskStrategy {
235 fn apply(&self, value: &Value) -> Value {
236 match self {
237 MaskStrategy::Null => Value::Null,
238 MaskStrategy::Redact { replacement } => match value {
239 Value::Null => Value::Null,
240 Value::Bytes(_) => Value::Bytes(replacement.as_bytes().to_vec()),
241 _ => Value::Null,
242 },
243 MaskStrategy::Sha256 => match value {
244 Value::Null => Value::Null,
245 Value::Bytes(bytes) => Value::Bytes(
246 Sha256::digest(bytes)
247 .iter()
248 .map(|byte| format!("{byte:02x}"))
249 .collect::<String>()
250 .into_bytes(),
251 ),
252 _ => Value::Null,
253 },
254 }
255 }
256}
257
258fn subject_matches(subjects: &[String], principal: &Principal) -> bool {
259 subjects.is_empty()
260 || subjects.iter().any(|subject| {
261 subject.eq_ignore_ascii_case("public")
262 || subject == &principal.username
263 || principal.roles.contains(subject)
264 })
265}
266
267fn default_true() -> bool {
268 true
269}