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) -> bool {
151 let Some(c) = ctx else { return true };
152 if c.internal {
153 return true;
154 }
155 if has_any(c, &rules.exempt_roles) {
157 return true;
158 }
159 let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
160 if empty {
161 return rules.unconfigured == UnconfiguredPolicy::Open;
163 }
164
165 let effective = effective(c);
167 let Some(list) = role_list else { return true };
169 if list.iter().any(|r| effective.iter().any(|x| x == r)) {
170 return true;
171 }
172
173 match_creator(c, doc, list)
175}
176
177fn match_creator(ctx: &Context, doc: Doc, role_list: &[String]) -> bool {
178 if !role_list.iter().any(|r| r == "creator") {
179 return false;
180 }
181 match doc {
182 Doc::Missing => true,
183 Doc::NoResult => false,
184 Doc::Doc(d) => {
185 let uid = ctx.user_id.clone().unwrap_or_default();
186 if uid.is_empty() {
187 return false;
188 }
189 d.get("createdBy")
190 .and_then(|v| v.as_str())
191 .map(|cb| cb == uid)
192 .unwrap_or(false)
193 }
194 }
195}
196
197pub fn get_readable_fields(
201 rules: &RoleRules,
202 schema: &Schema,
203 ctx: Option<&Context>,
204) -> Option<HashSet<String>> {
205 let c = ctx?;
206 let mut allowed = HashSet::new();
207 for (key, field) in &schema.fields {
208 match &field.read {
209 Some(rl) => {
210 if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
211 allowed.insert(key.clone());
212 }
213 }
214 None => {
215 allowed.insert(key.clone());
216 }
217 }
218 }
219 Some(allowed)
220}
221
222pub fn get_readable_computes(
223 rules: &RoleRules,
224 schema: &Schema,
225 ctx: Option<&Context>,
226) -> Option<HashSet<String>> {
227 let c = ctx?;
228 let mut allowed = HashSet::new();
229 for (key, comp) in &schema.computes {
230 match &comp.read {
231 Some(rl) => {
232 if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
233 allowed.insert(key.clone());
234 }
235 }
236 None => {
237 allowed.insert(key.clone());
238 }
239 }
240 }
241 Some(allowed)
242}
243
244pub fn get_readable_relations(
245 rules: &RoleRules,
246 schema: &Schema,
247 ctx: Option<&Context>,
248) -> Option<HashSet<String>> {
249 let c = ctx?;
250 let mut allowed = HashSet::new();
251 for (key, rel) in &schema.relations {
252 match &rel.read {
253 Some(rl) => {
254 if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
255 allowed.insert(key.clone());
256 }
257 }
258 None => {
259 allowed.insert(key.clone());
260 }
261 }
262 }
263 Some(allowed)
264}
265
266pub fn is_field_readable(
272 rules: &RoleRules,
273 schema: &Schema,
274 ctx: Option<&Context>,
275 field: &str,
276) -> bool {
277 let root = field.split('.').next().unwrap_or(field);
278 if !schema.fields.contains_key(root) {
279 return true;
280 }
281 match get_readable_fields(rules, schema, ctx) {
282 None => true,
283 Some(allowed) => allowed.contains(root),
284 }
285}
286
287pub fn is_relation_readable(
289 rules: &RoleRules,
290 schema: &Schema,
291 ctx: Option<&Context>,
292 rel: &str,
293) -> bool {
294 match get_readable_relations(rules, schema, ctx) {
295 None => true,
296 Some(allowed) => allowed.contains(rel),
297 }
298}
299
300pub fn can_read_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
303 evaluate(rules, ctx, schema.read.as_deref(), Doc::Missing)
304}
305
306pub fn can_write_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
310 if let Some(c) = ctx {
311 if has_any(c, &rules.deny_write_roles) {
312 return false;
313 }
314 if !c.internal
315 && matches!(schema.write, Some(ref w) if w.is_empty())
316 && !has_any(c, &rules.exempt_roles)
317 {
318 return false;
319 }
320 }
321 evaluate(rules, ctx, schema.write.as_deref(), Doc::Missing)
322}
323
324fn effective(c: &Context) -> Vec<String> {
328 match &c.roles {
329 Some(r) if !r.is_empty() => r.clone(),
330 _ => vec![c.role.clone().unwrap_or_default()],
331 }
332}
333
334fn has_any(c: &Context, list: &[String]) -> bool {
336 effective(c).iter().any(|r| list.iter().any(|x| x == r))
337}
338
339pub(crate) fn has_any_role(c: &Context, list: &[String]) -> bool {
341 has_any(c, list)
342}
343
344pub fn should_inject_owner_condition(
347 rules: &RoleRules,
348 schema: &Schema,
349 ctx: Option<&Context>,
350) -> bool {
351 let Some(c) = ctx else { return false };
352 if c.user_id.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
354 return false;
355 }
356 if c.internal {
357 return false;
358 }
359 if has_any(c, &rules.exempt_roles) {
360 return false;
361 }
362 let effective = effective(c);
363 let read = schema.read.clone().unwrap_or_default();
364 let real_roles: Vec<&String> = read.iter().filter(|r| *r != "creator").collect();
365 if real_roles.iter().any(|r| effective.iter().any(|x| x == *r)) {
366 return false;
367 }
368 read.iter().any(|r| r == "creator")
369}
370
371pub fn merge_owner_condition(
373 rules: &RoleRules,
374 schema: &Schema,
375 ctx: Option<&Context>,
376 condition: Option<Value>,
377) -> Option<Value> {
378 if !should_inject_owner_condition(rules, schema, ctx) {
379 return condition;
380 }
381 let owner = json!({ "createdBy": ctx?.user_id });
382 match condition.filter(is_truthy) {
383 None => Some(owner),
384 Some(c) => Some(json!({ "$and": [c, owner] })),
385 }
386}
387
388pub fn get_writable_fields(
391 rules: &RoleRules,
392 schema: &Schema,
393 ctx: Option<&Context>,
394) -> Option<HashSet<String>> {
395 let c = ctx?;
396 let schema_writable = can_write_schema(rules, schema, ctx);
397 let mut allowed = HashSet::new();
398 for (key, field) in &schema.fields {
399 match &field.write {
400 Some(wl) => {
401 if evaluate(rules, Some(c), Some(wl), Doc::Missing) {
402 allowed.insert(key.clone());
403 }
404 }
405 None => {
406 if schema_writable {
407 allowed.insert(key.clone());
408 }
409 }
410 }
411 }
412 Some(allowed)
413}
414
415pub fn filter_writable_data(
417 rules: &RoleRules,
418 schema: &Schema,
419 ctx: Option<&Context>,
420 data: &Value,
421) -> Value {
422 let Some(writable) = get_writable_fields(rules, schema, ctx) else {
423 return data.clone();
424 };
425 let Some(obj) = data.as_object() else {
426 return data.clone();
427 };
428 let mut result = Map::new();
429 for (key, val) in obj {
430 let root = key.split('.').next().unwrap_or(key);
431 if key == "_id" || writable.contains(root) {
434 result.insert(key.clone(), val.clone());
435 }
436 }
437 Value::Object(result)
438}