1use 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
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73pub enum UnconfiguredPolicy {
74 #[default]
75 Open,
76 Closed,
77}
78
79impl UnconfiguredPolicy {
80 pub fn from_str_or_err(s: &str) -> Result<Self, String> {
82 match s {
83 "open" => Ok(UnconfiguredPolicy::Open),
84 "closed" => Ok(UnconfiguredPolicy::Closed),
85 other => Err(format!("未知 unconfigured_policy: {other}(仅 open / closed)")),
86 }
87 }
88 pub fn as_str(&self) -> &'static str {
89 match self {
90 UnconfiguredPolicy::Open => "open",
91 UnconfiguredPolicy::Closed => "closed",
92 }
93 }
94}
95
96#[derive(Debug, Clone, Default)]
99pub struct RoleRules {
100 pub exempt_roles: Vec<String>,
102 pub deny_write_roles: Vec<String>,
104 pub unconfigured: UnconfiguredPolicy,
106}
107
108pub fn evaluate(
118 rules: &RoleRules,
119 ctx: Option<&Context>,
120 role_list: Option<&[String]>,
121 doc: Doc,
122) -> bool {
123 let Some(c) = ctx else { return true };
124 if c.internal {
125 return true;
126 }
127 if has_any(c, &rules.exempt_roles) {
129 return true;
130 }
131 let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
132 if empty {
133 return rules.unconfigured == UnconfiguredPolicy::Open;
135 }
136
137 let effective = effective(c);
139 let Some(list) = role_list else { return true };
141 if list.iter().any(|r| effective.iter().any(|x| x == r)) {
142 return true;
143 }
144
145 match_creator(c, doc, list)
147}
148
149fn match_creator(ctx: &Context, doc: Doc, role_list: &[String]) -> bool {
150 if !role_list.iter().any(|r| r == "creator") {
151 return false;
152 }
153 match doc {
154 Doc::Missing => true,
155 Doc::NoResult => false,
156 Doc::Doc(d) => {
157 let uid = ctx.user_id.clone().unwrap_or_default();
158 if uid.is_empty() {
159 return false;
160 }
161 d.get("createdBy")
162 .and_then(|v| v.as_str())
163 .map(|cb| cb == uid)
164 .unwrap_or(false)
165 }
166 }
167}
168
169pub fn get_readable_fields(
173 rules: &RoleRules,
174 schema: &Schema,
175 ctx: Option<&Context>,
176) -> Option<HashSet<String>> {
177 let c = ctx?;
178 let mut allowed = HashSet::new();
179 for (key, field) in &schema.fields {
180 match &field.read {
181 Some(rl) => {
182 if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
183 allowed.insert(key.clone());
184 }
185 }
186 None => {
187 allowed.insert(key.clone());
188 }
189 }
190 }
191 Some(allowed)
192}
193
194pub fn get_readable_computes(
195 rules: &RoleRules,
196 schema: &Schema,
197 ctx: Option<&Context>,
198) -> Option<HashSet<String>> {
199 let c = ctx?;
200 let mut allowed = HashSet::new();
201 for (key, comp) in &schema.computes {
202 match &comp.read {
203 Some(rl) => {
204 if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
205 allowed.insert(key.clone());
206 }
207 }
208 None => {
209 allowed.insert(key.clone());
210 }
211 }
212 }
213 Some(allowed)
214}
215
216pub fn get_readable_relations(
217 rules: &RoleRules,
218 schema: &Schema,
219 ctx: Option<&Context>,
220) -> Option<HashSet<String>> {
221 let c = ctx?;
222 let mut allowed = HashSet::new();
223 for (key, rel) in &schema.relations {
224 match &rel.read {
225 Some(rl) => {
226 if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
227 allowed.insert(key.clone());
228 }
229 }
230 None => {
231 allowed.insert(key.clone());
232 }
233 }
234 }
235 Some(allowed)
236}
237
238pub fn is_field_readable(
244 rules: &RoleRules,
245 schema: &Schema,
246 ctx: Option<&Context>,
247 field: &str,
248) -> bool {
249 let root = field.split('.').next().unwrap_or(field);
250 if !schema.fields.contains_key(root) {
251 return true;
252 }
253 match get_readable_fields(rules, schema, ctx) {
254 None => true,
255 Some(allowed) => allowed.contains(root),
256 }
257}
258
259pub fn is_relation_readable(
261 rules: &RoleRules,
262 schema: &Schema,
263 ctx: Option<&Context>,
264 rel: &str,
265) -> bool {
266 match get_readable_relations(rules, schema, ctx) {
267 None => true,
268 Some(allowed) => allowed.contains(rel),
269 }
270}
271
272pub fn can_read_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
275 evaluate(rules, ctx, schema.read.as_deref(), Doc::Missing)
276}
277
278pub fn can_write_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
282 if let Some(c) = ctx {
283 if has_any(c, &rules.deny_write_roles) {
284 return false;
285 }
286 if !c.internal && matches!(schema.write, Some(ref w) if w.is_empty()) {
287 if !has_any(c, &rules.exempt_roles) {
288 return false;
289 }
290 }
291 }
292 evaluate(rules, ctx, schema.write.as_deref(), Doc::Missing)
293}
294
295fn has_role(ctx: &Context, role: &str) -> bool {
296 ctx.roles
297 .as_ref()
298 .map(|r| r.iter().any(|x| x == role))
299 .unwrap_or(false)
300}
301
302fn effective(c: &Context) -> Vec<String> {
306 match &c.roles {
307 Some(r) if !r.is_empty() => r.clone(),
308 _ => vec![c.role.clone().unwrap_or_default()],
309 }
310}
311
312fn has_any(c: &Context, list: &[String]) -> bool {
314 effective(c).iter().any(|r| list.iter().any(|x| x == r))
315}
316
317pub(crate) fn has_any_role(c: &Context, list: &[String]) -> bool {
319 has_any(c, list)
320}
321
322pub fn should_inject_owner_condition(
325 rules: &RoleRules,
326 schema: &Schema,
327 ctx: Option<&Context>,
328) -> bool {
329 let Some(c) = ctx else { return false };
330 if c.user_id.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
332 return false;
333 }
334 if c.internal {
335 return false;
336 }
337 if has_any(c, &rules.exempt_roles) {
338 return false;
339 }
340 let effective = effective(c);
341 let read = schema.read.clone().unwrap_or_default();
342 let real_roles: Vec<&String> = read.iter().filter(|r| *r != "creator").collect();
343 if real_roles.iter().any(|r| effective.iter().any(|x| x == *r)) {
344 return false;
345 }
346 read.iter().any(|r| r == "creator")
347}
348
349pub fn merge_owner_condition(
351 rules: &RoleRules,
352 schema: &Schema,
353 ctx: Option<&Context>,
354 condition: Option<Value>,
355) -> Option<Value> {
356 if !should_inject_owner_condition(rules, schema, ctx) {
357 return condition;
358 }
359 let owner = json!({ "createdBy": ctx?.user_id });
360 match condition.filter(is_truthy) {
361 None => Some(owner),
362 Some(c) => Some(json!({ "$and": [c, owner] })),
363 }
364}
365
366pub fn get_writable_fields(
369 rules: &RoleRules,
370 schema: &Schema,
371 ctx: Option<&Context>,
372) -> Option<HashSet<String>> {
373 let c = ctx?;
374 let schema_writable = can_write_schema(rules, schema, ctx);
375 let mut allowed = HashSet::new();
376 for (key, field) in &schema.fields {
377 match &field.write {
378 Some(wl) => {
379 if evaluate(rules, Some(c), Some(wl), Doc::Missing) {
380 allowed.insert(key.clone());
381 }
382 }
383 None => {
384 if schema_writable {
385 allowed.insert(key.clone());
386 }
387 }
388 }
389 }
390 Some(allowed)
391}
392
393pub fn filter_writable_data(
395 rules: &RoleRules,
396 schema: &Schema,
397 ctx: Option<&Context>,
398 data: &Value,
399) -> Value {
400 let Some(writable) = get_writable_fields(rules, schema, ctx) else {
401 return data.clone();
402 };
403 let Some(obj) = data.as_object() else {
404 return data.clone();
405 };
406 let mut result = Map::new();
407 for (key, val) in obj {
408 let root = key.split('.').next().unwrap_or(key);
409 if key == "_id" || writable.contains(root) {
412 result.insert(key.clone(), val.clone());
413 }
414 }
415 Value::Object(result)
416}