1use std::collections::{HashMap, HashSet};
16
17use serde_json::{json, Map, Value};
18
19use crate::schema::Schema;
20use crate::types::{is_truthy, str_list};
21
22const CTX_IDENTITY_KEYS: [&str; 4] = ["userId", "roles", "role", "internal"];
24
25#[derive(Debug, Clone, Default)]
26pub struct Context {
27 pub user_id: Option<String>,
28 pub roles: Option<Vec<String>>,
29 pub role: Option<String>,
30 pub internal: bool,
31 pub access_lists: HashMap<String, Value>,
34 pub access_list_errors: HashMap<String, String>,
36}
37
38impl Context {
39 pub fn system() -> Self {
44 Self {
45 internal: true,
46 ..Default::default()
47 }
48 }
49}
50
51pub fn context_from_value(v: &Value) -> Option<Context> {
53 if v.is_null() {
54 return None;
55 }
56 let o = v.as_object()?;
57 let mut access_lists: HashMap<String, Value> = HashMap::new();
60 let mut access_list_errors: HashMap<String, String> = HashMap::new();
61 for (k, val) in o {
62 if CTX_IDENTITY_KEYS.contains(&k.as_str()) || val.is_null() {
63 continue;
64 }
65 match val {
66 Value::Array(items) if items.iter().all(|x| x.is_string() || x.is_number()) => {
67 access_lists.insert(k.clone(), val.clone());
68 }
69 Value::Array(_) => {
70 access_list_errors.insert(k.clone(), "清单数组元素必须为字符串或数字".to_string());
71 }
72 _ => {
73 access_list_errors.insert(k.clone(), "清单必须为数组".to_string());
74 }
75 }
76 }
77 Some(Context {
78 user_id: o.get("userId").and_then(|x| x.as_str()).map(String::from),
79 roles: str_list(o.get("roles")),
80 role: o.get("role").and_then(|x| x.as_str()).map(String::from),
81 internal: o.get("internal").map(is_truthy).unwrap_or(false),
82 access_lists,
83 access_list_errors,
84 })
85}
86
87#[derive(Debug, Clone, Copy)]
92pub enum Doc<'a> {
93 Missing,
94 NoResult,
95 Doc(&'a Value),
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101pub enum UnconfiguredPolicy {
102 #[default]
103 Open,
104 Closed,
105}
106
107impl UnconfiguredPolicy {
108 pub fn from_str_or_err(s: &str) -> Result<Self, String> {
110 match s {
111 "open" => Ok(UnconfiguredPolicy::Open),
112 "closed" => Ok(UnconfiguredPolicy::Closed),
113 other => Err(format!(
114 "未知 unconfigured_policy: {other}(仅 open / closed)"
115 )),
116 }
117 }
118 pub fn as_str(&self) -> &'static str {
119 match self {
120 UnconfiguredPolicy::Open => "open",
121 UnconfiguredPolicy::Closed => "closed",
122 }
123 }
124}
125
126#[derive(Debug, Clone, Default)]
129pub struct RoleRules {
130 pub exempt_roles: Vec<String>,
132 pub deny_write_roles: Vec<String>,
134 pub unconfigured: UnconfiguredPolicy,
136}
137
138#[derive(Debug, Clone, Default)]
143pub struct MetaPolicy {
144 pub closed: bool,
146 pub roles: Vec<String>,
148}
149
150pub fn can_register(policy: &MetaPolicy, ctx: Option<&Context>) -> bool {
155 if !policy.closed {
156 return true;
157 }
158 match ctx {
159 None => false,
160 Some(c) => c.internal || has_any(c, &policy.roles),
161 }
162}
163
164pub fn evaluate(
174 rules: &RoleRules,
175 ctx: Option<&Context>,
176 role_list: Option<&[String]>,
177 doc: Doc,
178 owner_key: &str,
179) -> bool {
180 let Some(c) = ctx else { return true };
181 if c.internal {
182 return true;
183 }
184 if has_any(c, &rules.exempt_roles) {
186 return true;
187 }
188 let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
189 if empty {
190 return rules.unconfigured == UnconfiguredPolicy::Open;
192 }
193
194 let effective = effective(c);
196 let Some(list) = role_list else { return true };
198 if list.iter().any(|r| effective.iter().any(|x| x == r)) {
199 return true;
200 }
201
202 match_creator(c, doc, list, owner_key)
204}
205
206fn match_creator(ctx: &Context, doc: Doc, role_list: &[String], owner_key: &str) -> bool {
207 if !role_list.iter().any(|r| r == "creator") {
208 return false;
209 }
210 match doc {
211 Doc::Missing => true,
212 Doc::NoResult => false,
213 Doc::Doc(d) => {
214 let uid = ctx.user_id.clone().unwrap_or_default();
215 if uid.is_empty() {
216 return false;
217 }
218 d.get(owner_key)
219 .and_then(|v| v.as_str())
220 .map(|cb| cb == uid)
221 .unwrap_or(false)
222 }
223 }
224}
225
226pub fn get_readable_fields(
230 rules: &RoleRules,
231 schema: &Schema,
232 ctx: Option<&Context>,
233) -> Option<HashSet<String>> {
234 let c = ctx?;
235 let mut allowed = HashSet::new();
236 for (key, field) in &schema.fields {
237 match &field.read {
238 Some(rl) => {
239 if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
240 allowed.insert(key.clone());
241 }
242 }
243 None => {
244 allowed.insert(key.clone());
245 }
246 }
247 }
248 Some(allowed)
249}
250
251pub fn get_readable_computes(
252 rules: &RoleRules,
253 schema: &Schema,
254 ctx: Option<&Context>,
255) -> Option<HashSet<String>> {
256 let c = ctx?;
257 let mut allowed = HashSet::new();
258 for (key, comp) in &schema.computes {
259 match &comp.read {
260 Some(rl) => {
261 if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
262 allowed.insert(key.clone());
263 }
264 }
265 None => {
266 allowed.insert(key.clone());
267 }
268 }
269 }
270 Some(allowed)
271}
272
273pub fn get_readable_relations(
274 rules: &RoleRules,
275 schema: &Schema,
276 ctx: Option<&Context>,
277) -> Option<HashSet<String>> {
278 let c = ctx?;
279 let mut allowed = HashSet::new();
280 for (key, rel) in &schema.relations {
281 match &rel.read {
282 Some(rl) => {
283 if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
284 allowed.insert(key.clone());
285 }
286 }
287 None => {
288 allowed.insert(key.clone());
289 }
290 }
291 }
292 Some(allowed)
293}
294
295pub fn is_field_readable(
301 rules: &RoleRules,
302 schema: &Schema,
303 ctx: Option<&Context>,
304 field: &str,
305) -> bool {
306 let root = field.split('.').next().unwrap_or(field);
307 if !schema.fields.contains_key(root) {
308 return true;
309 }
310 match get_readable_fields(rules, schema, ctx) {
311 None => true,
312 Some(allowed) => allowed.contains(root),
313 }
314}
315
316pub fn is_relation_readable(
318 rules: &RoleRules,
319 schema: &Schema,
320 ctx: Option<&Context>,
321 rel: &str,
322) -> bool {
323 match get_readable_relations(rules, schema, ctx) {
324 None => true,
325 Some(allowed) => allowed.contains(rel),
326 }
327}
328
329pub fn can_read_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
332 evaluate(
333 rules,
334 ctx,
335 schema.read.as_deref(),
336 Doc::Missing,
337 schema.owner_key(),
338 )
339}
340
341pub fn can_write_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
345 if let Some(c) = ctx {
346 if has_any(c, &rules.deny_write_roles) {
347 return false;
348 }
349 if !c.internal
350 && matches!(schema.write, Some(ref w) if w.is_empty())
351 && !has_any(c, &rules.exempt_roles)
352 {
353 return false;
354 }
355 }
356 evaluate(
357 rules,
358 ctx,
359 schema.write.as_deref(),
360 Doc::Missing,
361 schema.owner_key(),
362 )
363}
364
365fn effective(c: &Context) -> Vec<String> {
369 match &c.roles {
370 Some(r) if !r.is_empty() => r.clone(),
371 _ => vec![c.role.clone().unwrap_or_default()],
372 }
373}
374
375fn has_any(c: &Context, list: &[String]) -> bool {
377 effective(c).iter().any(|r| list.iter().any(|x| x == r))
378}
379
380pub(crate) fn has_any_role(c: &Context, list: &[String]) -> bool {
382 has_any(c, list)
383}
384
385pub fn should_inject_owner_condition(
388 rules: &RoleRules,
389 schema: &Schema,
390 ctx: Option<&Context>,
391) -> bool {
392 let Some(c) = ctx else { return false };
393 if c.user_id.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
395 return false;
396 }
397 if c.internal {
398 return false;
399 }
400 if has_any(c, &rules.exempt_roles) {
401 return false;
402 }
403 let effective = effective(c);
404 let read = schema.read.clone().unwrap_or_default();
405 let real_roles: Vec<&String> = read.iter().filter(|r| *r != "creator").collect();
406 if real_roles.iter().any(|r| effective.iter().any(|x| x == *r)) {
407 return false;
408 }
409 read.iter().any(|r| r == "creator")
410}
411
412pub fn merge_owner_condition(
415 rules: &RoleRules,
416 schema: &Schema,
417 ctx: Option<&Context>,
418 condition: Option<Value>,
419) -> Option<Value> {
420 if !should_inject_owner_condition(rules, schema, ctx) {
421 return condition;
422 }
423 let mut owner = Map::new();
424 owner.insert(schema.owner_key().to_string(), json!(ctx?.user_id));
425 let owner = Value::Object(owner);
426 match condition.filter(is_truthy) {
427 None => Some(owner),
428 Some(c) => Some(json!({ "$and": [c, owner] })),
429 }
430}
431
432pub fn get_writable_fields(
435 rules: &RoleRules,
436 schema: &Schema,
437 ctx: Option<&Context>,
438) -> Option<HashSet<String>> {
439 let c = ctx?;
440 let schema_writable = can_write_schema(rules, schema, ctx);
441 let mut allowed = HashSet::new();
442 for (key, field) in &schema.fields {
443 match &field.write {
444 Some(wl) => {
445 if evaluate(rules, Some(c), Some(wl), Doc::Missing, schema.owner_key()) {
446 allowed.insert(key.clone());
447 }
448 }
449 None => {
450 if schema_writable {
451 allowed.insert(key.clone());
452 }
453 }
454 }
455 }
456 Some(allowed)
457}
458
459pub fn filter_writable_data(
461 rules: &RoleRules,
462 schema: &Schema,
463 ctx: Option<&Context>,
464 data: &Value,
465) -> Value {
466 let Some(writable) = get_writable_fields(rules, schema, ctx) else {
467 return data.clone();
468 };
469 let Some(obj) = data.as_object() else {
470 return data.clone();
471 };
472 let mut result = Map::new();
473 for (key, val) in obj {
474 let root = key.split('.').next().unwrap_or(key);
475 if key == "_id" || writable.contains(root) {
478 result.insert(key.clone(), val.clone());
479 }
480 }
481 Value::Object(result)
482}