rust_store_core/
permission.rs1use std::collections::HashSet;
16
17use serde_json::{json, Map, Value};
18
19use crate::schema::Schema;
20use crate::types::{is_truthy, str_list};
21
22#[derive(Debug, Clone, Default)]
23pub struct Context {
24 pub user_id: Option<String>,
25 pub roles: Option<Vec<String>>,
26 pub role: Option<String>,
27 pub internal: bool,
28}
29
30impl Context {
31 pub fn system() -> Self {
36 Self {
37 user_id: None,
38 roles: None,
39 role: None,
40 internal: true,
41 }
42 }
43}
44
45pub fn context_from_value(v: &Value) -> Option<Context> {
47 if v.is_null() {
48 return None;
49 }
50 let o = v.as_object()?;
51 Some(Context {
52 user_id: o.get("userId").and_then(|x| x.as_str()).map(String::from),
53 roles: str_list(o.get("roles")),
54 role: o.get("role").and_then(|x| x.as_str()).map(String::from),
55 internal: o.get("internal").map(is_truthy).unwrap_or(false),
56 })
57}
58
59#[derive(Debug, Clone, Copy)]
64pub enum Doc<'a> {
65 Missing,
66 NoResult,
67 Doc(&'a Value),
68}
69
70pub fn evaluate(ctx: Option<&Context>, role_list: Option<&[String]>, doc: Doc) -> bool {
72 let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
73 if empty {
74 return match ctx {
76 None => true,
77 Some(c) => {
78 if c.internal {
79 true
80 } else {
81 !c.roles
82 .clone()
83 .unwrap_or_default()
84 .iter()
85 .any(|r| r == "guest")
86 }
87 }
88 };
89 }
90
91 let Some(c) = ctx else { return true };
92 if c.internal {
93 return true;
94 }
95
96 let roles = c.roles.clone().unwrap_or_default();
97 if roles.iter().any(|r| r == "super_admin" || r == "admin") {
99 return true;
100 }
101
102 let effective = if roles.is_empty() {
104 vec![c.role.clone().unwrap_or_default()]
105 } else {
106 roles
107 };
108 let Some(list) = role_list else { return true };
110 if list.iter().any(|r| effective.iter().any(|x| x == r)) {
111 return true;
112 }
113
114 match_creator(c, doc, list)
116}
117
118fn match_creator(ctx: &Context, doc: Doc, role_list: &[String]) -> bool {
119 if !role_list.iter().any(|r| r == "creator") {
120 return false;
121 }
122 match doc {
123 Doc::Missing => true,
124 Doc::NoResult => false,
125 Doc::Doc(d) => {
126 let uid = ctx.user_id.clone().unwrap_or_default();
127 if uid.is_empty() {
128 return false;
129 }
130 d.get("createdBy")
131 .and_then(|v| v.as_str())
132 .map(|cb| cb == uid)
133 .unwrap_or(false)
134 }
135 }
136}
137
138pub fn get_readable_fields(schema: &Schema, ctx: Option<&Context>) -> Option<HashSet<String>> {
142 let c = ctx?;
143 let mut allowed = HashSet::new();
144 for (key, field) in &schema.fields {
145 match &field.read {
146 Some(rl) => {
147 if evaluate(Some(c), Some(rl), Doc::Missing) {
148 allowed.insert(key.clone());
149 }
150 }
151 None => {
152 allowed.insert(key.clone());
153 }
154 }
155 }
156 Some(allowed)
157}
158
159pub fn get_readable_computes(schema: &Schema, ctx: Option<&Context>) -> Option<HashSet<String>> {
160 let c = ctx?;
161 let mut allowed = HashSet::new();
162 for (key, comp) in &schema.computes {
163 match &comp.read {
164 Some(rl) => {
165 if evaluate(Some(c), Some(rl), Doc::Missing) {
166 allowed.insert(key.clone());
167 }
168 }
169 None => {
170 allowed.insert(key.clone());
171 }
172 }
173 }
174 Some(allowed)
175}
176
177pub fn get_readable_relations(schema: &Schema, ctx: Option<&Context>) -> Option<HashSet<String>> {
178 let c = ctx?;
179 let mut allowed = HashSet::new();
180 for (key, rel) in &schema.relations {
181 match &rel.read {
182 Some(rl) => {
183 if evaluate(Some(c), Some(rl), Doc::Missing) {
184 allowed.insert(key.clone());
185 }
186 }
187 None => {
188 allowed.insert(key.clone());
189 }
190 }
191 }
192 Some(allowed)
193}
194
195pub fn is_field_readable(schema: &Schema, ctx: Option<&Context>, field: &str) -> bool {
201 let root = field.split('.').next().unwrap_or(field);
202 if !schema.fields.contains_key(root) {
203 return true;
204 }
205 match get_readable_fields(schema, ctx) {
206 None => true,
207 Some(allowed) => allowed.contains(root),
208 }
209}
210
211pub fn is_relation_readable(schema: &Schema, ctx: Option<&Context>, rel: &str) -> bool {
213 match get_readable_relations(schema, ctx) {
214 None => true,
215 Some(allowed) => allowed.contains(rel),
216 }
217}
218
219pub fn can_read_schema(schema: &Schema, ctx: Option<&Context>) -> bool {
222 evaluate(ctx, schema.read.as_deref(), Doc::Missing)
223}
224
225pub fn can_write_schema(schema: &Schema, ctx: Option<&Context>) -> bool {
227 if let Some(c) = ctx {
228 if has_role(c, "guest") {
229 return false;
230 }
231 if !c.internal && matches!(schema.write, Some(ref w) if w.is_empty()) {
234 let roles = c.roles.clone().unwrap_or_default();
235 if !roles.iter().any(|r| r == "super_admin" || r == "admin") {
236 return false;
237 }
238 }
239 }
240 evaluate(ctx, schema.write.as_deref(), Doc::Missing)
241}
242
243fn has_role(ctx: &Context, role: &str) -> bool {
244 ctx.roles
245 .as_ref()
246 .map(|r| r.iter().any(|x| x == role))
247 .unwrap_or(false)
248}
249
250pub fn should_inject_owner_condition(schema: &Schema, ctx: Option<&Context>) -> bool {
253 let Some(c) = ctx else { return false };
254 if c.user_id.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
256 return false;
257 }
258 if c.internal {
259 return false;
260 }
261 let roles = c.roles.clone().unwrap_or_default();
262 if roles.iter().any(|r| r == "super_admin" || r == "admin") {
263 return false;
264 }
265 let effective = if roles.is_empty() {
266 vec![c.role.clone().unwrap_or_default()]
267 } else {
268 roles
269 };
270 let read = schema.read.clone().unwrap_or_default();
271 let real_roles: Vec<&String> = read.iter().filter(|r| *r != "creator").collect();
272 if real_roles.iter().any(|r| effective.iter().any(|x| x == *r)) {
273 return false;
274 }
275 read.iter().any(|r| r == "creator")
276}
277
278pub fn merge_owner_condition(
280 schema: &Schema,
281 ctx: Option<&Context>,
282 condition: Option<Value>,
283) -> Option<Value> {
284 if !should_inject_owner_condition(schema, ctx) {
285 return condition;
286 }
287 let owner = json!({ "createdBy": ctx?.user_id });
288 match condition.filter(is_truthy) {
289 None => Some(owner),
290 Some(c) => Some(json!({ "$and": [c, owner] })),
291 }
292}
293
294pub fn get_writable_fields(schema: &Schema, ctx: Option<&Context>) -> Option<HashSet<String>> {
297 let c = ctx?;
298 let schema_writable = can_write_schema(schema, ctx);
299 let mut allowed = HashSet::new();
300 for (key, field) in &schema.fields {
301 match &field.write {
302 Some(wl) => {
303 if evaluate(Some(c), Some(wl), Doc::Missing) {
304 allowed.insert(key.clone());
305 }
306 }
307 None => {
308 if schema_writable {
309 allowed.insert(key.clone());
310 }
311 }
312 }
313 }
314 Some(allowed)
315}
316
317pub fn filter_writable_data(schema: &Schema, ctx: Option<&Context>, data: &Value) -> Value {
319 let Some(writable) = get_writable_fields(schema, ctx) else {
320 return data.clone();
321 };
322 let Some(obj) = data.as_object() else {
323 return data.clone();
324 };
325 let mut result = Map::new();
326 for (key, val) in obj {
327 let root = key.split('.').next().unwrap_or(key);
328 if key == "_id" || writable.contains(root) {
331 result.insert(key.clone(), val.clone());
332 }
333 }
334 Value::Object(result)
335}