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!(
86 "未知 unconfigured_policy: {other}(仅 open / closed)"
87 )),
88 }
89 }
90 pub fn as_str(&self) -> &'static str {
91 match self {
92 UnconfiguredPolicy::Open => "open",
93 UnconfiguredPolicy::Closed => "closed",
94 }
95 }
96}
97
98#[derive(Debug, Clone, Default)]
101pub struct RoleRules {
102 pub exempt_roles: Vec<String>,
104 pub deny_write_roles: Vec<String>,
106 pub unconfigured: UnconfiguredPolicy,
108}
109
110#[derive(Debug, Clone, Default)]
115pub struct MetaPolicy {
116 pub closed: bool,
118 pub roles: Vec<String>,
120}
121
122pub fn can_register(policy: &MetaPolicy, ctx: Option<&Context>) -> bool {
127 if !policy.closed {
128 return true;
129 }
130 match ctx {
131 None => false,
132 Some(c) => c.internal || has_any(c, &policy.roles),
133 }
134}
135
136pub fn evaluate(
146 rules: &RoleRules,
147 ctx: Option<&Context>,
148 role_list: Option<&[String]>,
149 doc: Doc,
150 owner_key: &str,
151) -> bool {
152 let Some(c) = ctx else { return true };
153 if c.internal {
154 return true;
155 }
156 if has_any(c, &rules.exempt_roles) {
158 return true;
159 }
160 let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
161 if empty {
162 return rules.unconfigured == UnconfiguredPolicy::Open;
164 }
165
166 let effective = effective(c);
168 let Some(list) = role_list else { return true };
170 if list.iter().any(|r| effective.iter().any(|x| x == r)) {
171 return true;
172 }
173
174 match_creator(c, doc, list, owner_key)
176}
177
178fn match_creator(ctx: &Context, doc: Doc, role_list: &[String], owner_key: &str) -> bool {
179 if !role_list.iter().any(|r| r == "creator") {
180 return false;
181 }
182 match doc {
183 Doc::Missing => true,
184 Doc::NoResult => false,
185 Doc::Doc(d) => {
186 let uid = ctx.user_id.clone().unwrap_or_default();
187 if uid.is_empty() {
188 return false;
189 }
190 d.get(owner_key)
191 .and_then(|v| v.as_str())
192 .map(|cb| cb == uid)
193 .unwrap_or(false)
194 }
195 }
196}
197
198pub fn get_readable_fields(
202 rules: &RoleRules,
203 schema: &Schema,
204 ctx: Option<&Context>,
205) -> Option<HashSet<String>> {
206 let c = ctx?;
207 let mut allowed = HashSet::new();
208 for (key, field) in &schema.fields {
209 match &field.read {
210 Some(rl) => {
211 if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
212 allowed.insert(key.clone());
213 }
214 }
215 None => {
216 allowed.insert(key.clone());
217 }
218 }
219 }
220 Some(allowed)
221}
222
223pub fn get_readable_computes(
224 rules: &RoleRules,
225 schema: &Schema,
226 ctx: Option<&Context>,
227) -> Option<HashSet<String>> {
228 let c = ctx?;
229 let mut allowed = HashSet::new();
230 for (key, comp) in &schema.computes {
231 match &comp.read {
232 Some(rl) => {
233 if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
234 allowed.insert(key.clone());
235 }
236 }
237 None => {
238 allowed.insert(key.clone());
239 }
240 }
241 }
242 Some(allowed)
243}
244
245pub fn get_readable_relations(
246 rules: &RoleRules,
247 schema: &Schema,
248 ctx: Option<&Context>,
249) -> Option<HashSet<String>> {
250 let c = ctx?;
251 let mut allowed = HashSet::new();
252 for (key, rel) in &schema.relations {
253 match &rel.read {
254 Some(rl) => {
255 if evaluate(rules, Some(c), Some(rl), Doc::Missing, schema.owner_key()) {
256 allowed.insert(key.clone());
257 }
258 }
259 None => {
260 allowed.insert(key.clone());
261 }
262 }
263 }
264 Some(allowed)
265}
266
267pub fn is_field_readable(
273 rules: &RoleRules,
274 schema: &Schema,
275 ctx: Option<&Context>,
276 field: &str,
277) -> bool {
278 let root = field.split('.').next().unwrap_or(field);
279 if !schema.fields.contains_key(root) {
280 return true;
281 }
282 match get_readable_fields(rules, schema, ctx) {
283 None => true,
284 Some(allowed) => allowed.contains(root),
285 }
286}
287
288pub fn is_relation_readable(
290 rules: &RoleRules,
291 schema: &Schema,
292 ctx: Option<&Context>,
293 rel: &str,
294) -> bool {
295 match get_readable_relations(rules, schema, ctx) {
296 None => true,
297 Some(allowed) => allowed.contains(rel),
298 }
299}
300
301pub fn can_read_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
304 evaluate(
305 rules,
306 ctx,
307 schema.read.as_deref(),
308 Doc::Missing,
309 schema.owner_key(),
310 )
311}
312
313pub fn can_write_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
317 if let Some(c) = ctx {
318 if has_any(c, &rules.deny_write_roles) {
319 return false;
320 }
321 if !c.internal
322 && matches!(schema.write, Some(ref w) if w.is_empty())
323 && !has_any(c, &rules.exempt_roles)
324 {
325 return false;
326 }
327 }
328 evaluate(
329 rules,
330 ctx,
331 schema.write.as_deref(),
332 Doc::Missing,
333 schema.owner_key(),
334 )
335}
336
337fn effective(c: &Context) -> Vec<String> {
341 match &c.roles {
342 Some(r) if !r.is_empty() => r.clone(),
343 _ => vec![c.role.clone().unwrap_or_default()],
344 }
345}
346
347fn has_any(c: &Context, list: &[String]) -> bool {
349 effective(c).iter().any(|r| list.iter().any(|x| x == r))
350}
351
352pub(crate) fn has_any_role(c: &Context, list: &[String]) -> bool {
354 has_any(c, list)
355}
356
357pub fn should_inject_owner_condition(
360 rules: &RoleRules,
361 schema: &Schema,
362 ctx: Option<&Context>,
363) -> bool {
364 let Some(c) = ctx else { return false };
365 if c.user_id.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
367 return false;
368 }
369 if c.internal {
370 return false;
371 }
372 if has_any(c, &rules.exempt_roles) {
373 return false;
374 }
375 let effective = effective(c);
376 let read = schema.read.clone().unwrap_or_default();
377 let real_roles: Vec<&String> = read.iter().filter(|r| *r != "creator").collect();
378 if real_roles.iter().any(|r| effective.iter().any(|x| x == *r)) {
379 return false;
380 }
381 read.iter().any(|r| r == "creator")
382}
383
384pub fn merge_owner_condition(
387 rules: &RoleRules,
388 schema: &Schema,
389 ctx: Option<&Context>,
390 condition: Option<Value>,
391) -> Option<Value> {
392 if !should_inject_owner_condition(rules, schema, ctx) {
393 return condition;
394 }
395 let mut owner = Map::new();
396 owner.insert(schema.owner_key().to_string(), json!(ctx?.user_id));
397 let owner = Value::Object(owner);
398 match condition.filter(is_truthy) {
399 None => Some(owner),
400 Some(c) => Some(json!({ "$and": [c, owner] })),
401 }
402}
403
404pub fn get_writable_fields(
407 rules: &RoleRules,
408 schema: &Schema,
409 ctx: Option<&Context>,
410) -> Option<HashSet<String>> {
411 let c = ctx?;
412 let schema_writable = can_write_schema(rules, schema, ctx);
413 let mut allowed = HashSet::new();
414 for (key, field) in &schema.fields {
415 match &field.write {
416 Some(wl) => {
417 if evaluate(rules, Some(c), Some(wl), Doc::Missing, schema.owner_key()) {
418 allowed.insert(key.clone());
419 }
420 }
421 None => {
422 if schema_writable {
423 allowed.insert(key.clone());
424 }
425 }
426 }
427 }
428 Some(allowed)
429}
430
431pub fn filter_writable_data(
433 rules: &RoleRules,
434 schema: &Schema,
435 ctx: Option<&Context>,
436 data: &Value,
437) -> Value {
438 let Some(writable) = get_writable_fields(rules, schema, ctx) else {
439 return data.clone();
440 };
441 let Some(obj) = data.as_object() else {
442 return data.clone();
443 };
444 let mut result = Map::new();
445 for (key, val) in obj {
446 let root = key.split('.').next().unwrap_or(key);
447 if key == "_id" || writable.contains(root) {
450 result.insert(key.clone(), val.clone());
451 }
452 }
453 Value::Object(result)
454}