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
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct RowPolicy {
20 pub name: String,
21 pub table: String,
22 pub command: PolicyCommand,
23 #[serde(default)]
24 pub subjects: Vec<String>,
25 #[serde(default = "default_true")]
26 pub permissive: bool,
27 pub using: Option<SecurityExpr>,
28 pub with_check: Option<SecurityExpr>,
29}
30
31#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
32#[serde(rename_all = "snake_case")]
33pub enum PolicyCommand {
34 All,
35 Select,
36 Insert,
37 Update,
38 Delete,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42#[serde(tag = "kind", rename_all = "snake_case")]
43pub enum SecurityExpr {
44 True,
45 ColumnEqCurrentUser {
46 column: u16,
47 },
48 ColumnEqValue {
49 column: u16,
50 value: Value,
51 },
52 And {
53 left: Box<SecurityExpr>,
54 right: Box<SecurityExpr>,
55 },
56 Or {
57 left: Box<SecurityExpr>,
58 right: Box<SecurityExpr>,
59 },
60 Not {
61 expression: Box<SecurityExpr>,
62 },
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66pub struct ColumnMask {
67 pub name: String,
68 pub table: String,
69 pub column: u16,
70 pub strategy: MaskStrategy,
71 #[serde(default)]
72 pub exempt_subjects: Vec<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76#[serde(tag = "kind", rename_all = "snake_case")]
77pub enum MaskStrategy {
78 Null,
79 Redact { replacement: String },
80 Sha256,
81}
82
83impl SecurityCatalog {
84 pub fn table_has_security(&self, table: &str) -> bool {
85 self.rls_tables.iter().any(|name| name == table)
86 || self.masks.iter().any(|mask| mask.table == table)
87 }
88
89 pub fn table_has_objects(&self, table: &str) -> bool {
90 self.table_has_security(table) || self.policies.iter().any(|policy| policy.table == table)
91 }
92
93 pub fn rls_enabled(&self, table: &str) -> bool {
94 self.rls_tables.iter().any(|name| name == table)
95 }
96
97 pub fn row_allowed(
98 &self,
99 table: &str,
100 command: PolicyCommand,
101 row: &Row,
102 principal: &Principal,
103 check_new: bool,
104 ) -> bool {
105 if principal.is_admin || !self.rls_enabled(table) {
106 return true;
107 }
108 let applicable = self.policies.iter().filter(|policy| {
109 policy.table == table
110 && (matches!(policy.command, PolicyCommand::All) || policy.command == command)
111 });
112 let mut has_permissive = false;
113 let mut permissive_allowed = false;
114 let mut restrictive_allowed = true;
115 for policy in applicable.filter(|policy| subject_matches(&policy.subjects, principal)) {
116 let expression = if check_new {
117 policy.with_check.as_ref().or(policy.using.as_ref())
118 } else {
119 policy.using.as_ref()
120 };
121 let allowed = expression.is_some_and(|expression| expression.eval(row, principal));
122 if policy.permissive {
123 has_permissive = true;
124 permissive_allowed |= allowed;
125 } else {
126 restrictive_allowed &= allowed;
127 }
128 }
129 has_permissive && permissive_allowed && restrictive_allowed
130 }
131
132 pub fn apply_masks(&self, table: &str, row: &mut Row, principal: &Principal) {
133 if principal.is_admin {
134 return;
135 }
136 for mask in self.masks.iter().filter(|mask| mask.table == table) {
137 if !mask.exempt_subjects.is_empty() && subject_matches(&mask.exempt_subjects, principal)
138 {
139 continue;
140 }
141 let Some(value) = row.columns.get_mut(&mask.column) else {
142 continue;
143 };
144 *value = mask.strategy.apply(value);
145 }
146 }
147}
148
149impl SecurityExpr {
150 pub fn eval(&self, row: &Row, principal: &Principal) -> bool {
151 match self {
152 SecurityExpr::True => true,
153 SecurityExpr::ColumnEqCurrentUser { column } => {
154 row.columns.get(column).is_some_and(|value| {
155 value == &Value::Bytes(principal.username.as_bytes().to_vec())
156 })
157 }
158 SecurityExpr::ColumnEqValue { column, value } => row
159 .columns
160 .get(column)
161 .is_some_and(|current| current == value),
162 SecurityExpr::And { left, right } => {
163 left.eval(row, principal) && right.eval(row, principal)
164 }
165 SecurityExpr::Or { left, right } => {
166 left.eval(row, principal) || right.eval(row, principal)
167 }
168 SecurityExpr::Not { expression } => !expression.eval(row, principal),
169 }
170 }
171}
172
173impl MaskStrategy {
174 fn apply(&self, value: &Value) -> Value {
175 match self {
176 MaskStrategy::Null => Value::Null,
177 MaskStrategy::Redact { replacement } => match value {
178 Value::Null => Value::Null,
179 Value::Bytes(_) => Value::Bytes(replacement.as_bytes().to_vec()),
180 _ => Value::Null,
181 },
182 MaskStrategy::Sha256 => match value {
183 Value::Null => Value::Null,
184 Value::Bytes(bytes) => Value::Bytes(
185 Sha256::digest(bytes)
186 .iter()
187 .map(|byte| format!("{byte:02x}"))
188 .collect::<String>()
189 .into_bytes(),
190 ),
191 _ => Value::Null,
192 },
193 }
194 }
195}
196
197fn subject_matches(subjects: &[String], principal: &Principal) -> bool {
198 subjects.is_empty()
199 || subjects.iter().any(|subject| {
200 subject.eq_ignore_ascii_case("public")
201 || subject == &principal.username
202 || principal.roles.contains(subject)
203 })
204}
205
206fn default_true() -> bool {
207 true
208}