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)
67 if items.iter().all(|x| x.is_string() || x.is_number()) =>
68 {
69 access_lists.insert(k.clone(), val.clone());
70 }
71 Value::Array(_) => {
72 access_list_errors.insert(
73 k.clone(),
74 "清单数组元素必须为字符串或数字".to_string(),
75 );
76 }
77 _ => {
78 access_list_errors
79 .insert(k.clone(), "清单必须为数组".to_string());
80 }
81 }
82 }
83 Some(Context {
84 user_id: o.get("userId").and_then(|x| x.as_str()).map(String::from),
85 roles: str_list(o.get("roles")),
86 role: o.get("role").and_then(|x| x.as_str()).map(String::from),
87 internal: o.get("internal").map(is_truthy).unwrap_or(false),
88 access_lists,
89 access_list_errors,
90 })
91}
92
93#[derive(Debug, Clone, Copy)]
98pub enum Doc<'a> {
99 Missing,
100 NoResult,
101 Doc(&'a Value),
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub enum UnconfiguredPolicy {
108 #[default]
109 Open,
110 Closed,
111}
112
113impl UnconfiguredPolicy {
114 pub fn from_str_or_err(s: &str) -> Result<Self, String> {
116 match s {
117 "open" => Ok(UnconfiguredPolicy::Open),
118 "closed" => Ok(UnconfiguredPolicy::Closed),
119 other => Err(format!(
120 "未知 unconfigured_policy: {other}(仅 open / closed)"
121 )),
122 }
123 }
124 pub fn as_str(&self) -> &'static str {
125 match self {
126 UnconfiguredPolicy::Open => "open",
127 UnconfiguredPolicy::Closed => "closed",
128 }
129 }
130}
131
132#[derive(Debug, Clone, Default)]
135pub struct RoleRules {
136 pub exempt_roles: Vec<String>,
138 pub deny_write_roles: Vec<String>,
140 pub unconfigured: UnconfiguredPolicy,
142}
143
144#[derive(Debug, Clone, Default)]
149pub struct MetaPolicy {
150 pub closed: bool,
152 pub roles: Vec<String>,
154}
155
156pub fn can_register(policy: &MetaPolicy, ctx: Option<&Context>) -> bool {
161 if !policy.closed {
162 return true;
163 }
164 match ctx {
165 None => false,
166 Some(c) => c.internal || has_any(c, &policy.roles),
167 }
168}
169
170pub fn evaluate(
180 rules: &RoleRules,
181 ctx: Option<&Context>,
182 role_list: Option<&[String]>,
183 doc: Doc,
184 owner_key: &str,
185) -> bool {
186 let Some(c) = ctx else { return true };
187 if c.internal {
188 return true;
189 }
190 if has_any(c, &rules.exempt_roles) {
192 return true;
193 }
194 let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
195 if empty {
196 return rules.unconfigured == UnconfiguredPolicy::Open;
198 }
199
200 let effective = effective(c);
202 let Some(list) = role_list else { return true };
204 if list.iter().any(|r| effective.iter().any(|x| x == r)) {
205 return true;
206 }
207
208 match_creator(c, doc, list, owner_key)
210}
211
212fn match_creator(ctx: &Context, doc: Doc, role_list: &[String], owner_key: &str) -> bool {
213 if !role_list.iter().any(|r| r == "creator") {
214 return false;
215 }
216 match doc {
217 Doc::Missing => true,
218 Doc::NoResult => false,
219 Doc::Doc(d) => {
220 let uid = ctx.user_id.clone().unwrap_or_default();
221 if uid.is_empty() {
222 return false;
223 }
224 d.get(owner_key)
225 .and_then(|v| v.as_str())
226 .map(|cb| cb == uid)
227 .unwrap_or(false)
228 }
229 }
230}
231
232pub fn get_readable_fields(
236 rules: &RoleRules,
237 schema: &Schema,
238 ctx: Option<&Context>,
239) -> Option<HashSet<String>> {
240 let c = ctx?;
241 let mut allowed = HashSet::new();
242 for (key, field) in &schema.fields {
243 match &field.read {
244 Some(rl) => {
245 if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
246 allowed.insert(key.clone());
247 }
248 }
249 None => {
250 allowed.insert(key.clone());
251 }
252 }
253 }
254 Some(allowed)
255}
256
257pub fn get_readable_computes(
258 rules: &RoleRules,
259 schema: &Schema,
260 ctx: Option<&Context>,
261) -> Option<HashSet<String>> {
262 let c = ctx?;
263 let mut allowed = HashSet::new();
264 for (key, comp) in &schema.computes {
265 match &comp.read {
266 Some(rl) => {
267 if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
268 allowed.insert(key.clone());
269 }
270 }
271 None => {
272 allowed.insert(key.clone());
273 }
274 }
275 }
276 Some(allowed)
277}
278
279pub fn get_readable_relations(
280 rules: &RoleRules,
281 schema: &Schema,
282 ctx: Option<&Context>,
283) -> Option<HashSet<String>> {
284 let c = ctx?;
285 let mut allowed = HashSet::new();
286 for (key, rel) in &schema.relations {
287 match &rel.read {
288 Some(rl) => {
289 if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
290 allowed.insert(key.clone());
291 }
292 }
293 None => {
294 allowed.insert(key.clone());
295 }
296 }
297 }
298 Some(allowed)
299}
300
301pub fn is_field_readable(
307 rules: &RoleRules,
308 schema: &Schema,
309 ctx: Option<&Context>,
310 field: &str,
311) -> bool {
312 let root = field.split('.').next().unwrap_or(field);
313 if !schema.fields.contains_key(root) {
314 return true;
315 }
316 match get_readable_fields(rules, schema, ctx) {
317 None => true,
318 Some(allowed) => allowed.contains(root),
319 }
320}
321
322pub fn is_relation_readable(
324 rules: &RoleRules,
325 schema: &Schema,
326 ctx: Option<&Context>,
327 rel: &str,
328) -> bool {
329 match get_readable_relations(rules, schema, ctx) {
330 None => true,
331 Some(allowed) => allowed.contains(rel),
332 }
333}
334
335pub fn can_read_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
338 evaluate(
339 rules,
340 ctx,
341 schema.read.as_deref(),
342 Doc::Missing,
343 schema.owner_key(),
344 )
345}
346
347pub fn can_write_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
351 if let Some(c) = ctx {
352 if has_any(c, &rules.deny_write_roles) {
353 return false;
354 }
355 if !c.internal
356 && matches!(schema.write, Some(ref w) if w.is_empty())
357 && !has_any(c, &rules.exempt_roles)
358 {
359 return false;
360 }
361 }
362 evaluate(
363 rules,
364 ctx,
365 schema.write.as_deref(),
366 Doc::Missing,
367 schema.owner_key(),
368 )
369}
370
371fn effective(c: &Context) -> Vec<String> {
375 match &c.roles {
376 Some(r) if !r.is_empty() => r.clone(),
377 _ => vec![c.role.clone().unwrap_or_default()],
378 }
379}
380
381fn has_any(c: &Context, list: &[String]) -> bool {
383 effective(c).iter().any(|r| list.iter().any(|x| x == r))
384}
385
386pub(crate) fn has_any_role(c: &Context, list: &[String]) -> bool {
388 has_any(c, list)
389}
390
391pub fn should_inject_owner_condition(
394 rules: &RoleRules,
395 schema: &Schema,
396 ctx: Option<&Context>,
397) -> bool {
398 let Some(c) = ctx else { return false };
399 if c.user_id.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
401 return false;
402 }
403 if c.internal {
404 return false;
405 }
406 if has_any(c, &rules.exempt_roles) {
407 return false;
408 }
409 let effective = effective(c);
410 let read = schema.read.clone().unwrap_or_default();
411 let real_roles: Vec<&String> = read.iter().filter(|r| *r != "creator").collect();
412 if real_roles.iter().any(|r| effective.iter().any(|x| x == *r)) {
413 return false;
414 }
415 read.iter().any(|r| r == "creator")
416}
417
418pub fn merge_owner_condition(
421 rules: &RoleRules,
422 schema: &Schema,
423 ctx: Option<&Context>,
424 condition: Option<Value>,
425) -> Option<Value> {
426 if !should_inject_owner_condition(rules, schema, ctx) {
427 return condition;
428 }
429 let mut owner = Map::new();
430 owner.insert(schema.owner_key().to_string(), json!(ctx?.user_id));
431 let owner = Value::Object(owner);
432 match condition.filter(is_truthy) {
433 None => Some(owner),
434 Some(c) => Some(json!({ "$and": [c, owner] })),
435 }
436}
437
438pub fn get_writable_fields(
441 rules: &RoleRules,
442 schema: &Schema,
443 ctx: Option<&Context>,
444) -> Option<HashSet<String>> {
445 let c = ctx?;
446 let schema_writable = can_write_schema(rules, schema, ctx);
447 let mut allowed = HashSet::new();
448 for (key, field) in &schema.fields {
449 match &field.write {
450 Some(wl) => {
451 if evaluate(rules, Some(c), Some(wl), Doc::Missing, schema.owner_key()) {
452 allowed.insert(key.clone());
453 }
454 }
455 None => {
456 if schema_writable {
457 allowed.insert(key.clone());
458 }
459 }
460 }
461 }
462 Some(allowed)
463}
464
465pub fn filter_writable_data(
467 rules: &RoleRules,
468 schema: &Schema,
469 ctx: Option<&Context>,
470 data: &Value,
471) -> Value {
472 let Some(writable) = get_writable_fields(rules, schema, ctx) else {
473 return data.clone();
474 };
475 let Some(obj) = data.as_object() else {
476 return data.clone();
477 };
478 let mut result = Map::new();
479 for (key, val) in obj {
480 let root = key.split('.').next().unwrap_or(key);
481 if key == "_id" || writable.contains(root) {
484 result.insert(key.clone(), val.clone());
485 }
486 }
487 Value::Object(result)
488}