use std::collections::HashSet;
use serde_json::{json, Map, Value};
use crate::schema::Schema;
use crate::types::{is_truthy, str_list};
#[derive(Debug, Clone, Default)]
pub struct Context {
pub user_id: Option<String>,
pub roles: Option<Vec<String>>,
pub role: Option<String>,
pub internal: bool,
}
impl Context {
pub fn system() -> Self {
Self {
user_id: None,
roles: None,
role: None,
internal: true,
}
}
}
pub fn context_from_value(v: &Value) -> Option<Context> {
if v.is_null() {
return None;
}
let o = v.as_object()?;
Some(Context {
user_id: o.get("userId").and_then(|x| x.as_str()).map(String::from),
roles: str_list(o.get("roles")),
role: o.get("role").and_then(|x| x.as_str()).map(String::from),
internal: o.get("internal").map(is_truthy).unwrap_or(false),
})
}
#[derive(Debug, Clone, Copy)]
pub enum Doc<'a> {
Missing,
NoResult,
Doc(&'a Value),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UnconfiguredPolicy {
#[default]
Open,
Closed,
}
impl UnconfiguredPolicy {
pub fn from_str_or_err(s: &str) -> Result<Self, String> {
match s {
"open" => Ok(UnconfiguredPolicy::Open),
"closed" => Ok(UnconfiguredPolicy::Closed),
other => Err(format!(
"未知 unconfigured_policy: {other}(仅 open / closed)"
)),
}
}
pub fn as_str(&self) -> &'static str {
match self {
UnconfiguredPolicy::Open => "open",
UnconfiguredPolicy::Closed => "closed",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RoleRules {
pub exempt_roles: Vec<String>,
pub deny_write_roles: Vec<String>,
pub unconfigured: UnconfiguredPolicy,
}
#[derive(Debug, Clone, Default)]
pub struct MetaPolicy {
pub closed: bool,
pub roles: Vec<String>,
}
pub fn can_register(policy: &MetaPolicy, ctx: Option<&Context>) -> bool {
if !policy.closed {
return true;
}
match ctx {
None => false,
Some(c) => c.internal || has_any(c, &policy.roles),
}
}
pub fn evaluate(
rules: &RoleRules,
ctx: Option<&Context>,
role_list: Option<&[String]>,
doc: Doc,
) -> bool {
let Some(c) = ctx else { return true };
if c.internal {
return true;
}
if has_any(c, &rules.exempt_roles) {
return true;
}
let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
if empty {
return rules.unconfigured == UnconfiguredPolicy::Open;
}
let effective = effective(c);
let Some(list) = role_list else { return true };
if list.iter().any(|r| effective.iter().any(|x| x == r)) {
return true;
}
match_creator(c, doc, list)
}
fn match_creator(ctx: &Context, doc: Doc, role_list: &[String]) -> bool {
if !role_list.iter().any(|r| r == "creator") {
return false;
}
match doc {
Doc::Missing => true,
Doc::NoResult => false,
Doc::Doc(d) => {
let uid = ctx.user_id.clone().unwrap_or_default();
if uid.is_empty() {
return false;
}
d.get("createdBy")
.and_then(|v| v.as_str())
.map(|cb| cb == uid)
.unwrap_or(false)
}
}
}
pub fn get_readable_fields(
rules: &RoleRules,
schema: &Schema,
ctx: Option<&Context>,
) -> Option<HashSet<String>> {
let c = ctx?;
let mut allowed = HashSet::new();
for (key, field) in &schema.fields {
match &field.read {
Some(rl) => {
if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
allowed.insert(key.clone());
}
}
None => {
allowed.insert(key.clone());
}
}
}
Some(allowed)
}
pub fn get_readable_computes(
rules: &RoleRules,
schema: &Schema,
ctx: Option<&Context>,
) -> Option<HashSet<String>> {
let c = ctx?;
let mut allowed = HashSet::new();
for (key, comp) in &schema.computes {
match &comp.read {
Some(rl) => {
if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
allowed.insert(key.clone());
}
}
None => {
allowed.insert(key.clone());
}
}
}
Some(allowed)
}
pub fn get_readable_relations(
rules: &RoleRules,
schema: &Schema,
ctx: Option<&Context>,
) -> Option<HashSet<String>> {
let c = ctx?;
let mut allowed = HashSet::new();
for (key, rel) in &schema.relations {
match &rel.read {
Some(rl) => {
if evaluate(rules, Some(c), Some(rl), Doc::Missing) {
allowed.insert(key.clone());
}
}
None => {
allowed.insert(key.clone());
}
}
}
Some(allowed)
}
pub fn is_field_readable(
rules: &RoleRules,
schema: &Schema,
ctx: Option<&Context>,
field: &str,
) -> bool {
let root = field.split('.').next().unwrap_or(field);
if !schema.fields.contains_key(root) {
return true;
}
match get_readable_fields(rules, schema, ctx) {
None => true,
Some(allowed) => allowed.contains(root),
}
}
pub fn is_relation_readable(
rules: &RoleRules,
schema: &Schema,
ctx: Option<&Context>,
rel: &str,
) -> bool {
match get_readable_relations(rules, schema, ctx) {
None => true,
Some(allowed) => allowed.contains(rel),
}
}
pub fn can_read_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
evaluate(rules, ctx, schema.read.as_deref(), Doc::Missing)
}
pub fn can_write_schema(rules: &RoleRules, schema: &Schema, ctx: Option<&Context>) -> bool {
if let Some(c) = ctx {
if has_any(c, &rules.deny_write_roles) {
return false;
}
if !c.internal
&& matches!(schema.write, Some(ref w) if w.is_empty())
&& !has_any(c, &rules.exempt_roles)
{
return false;
}
}
evaluate(rules, ctx, schema.write.as_deref(), Doc::Missing)
}
fn effective(c: &Context) -> Vec<String> {
match &c.roles {
Some(r) if !r.is_empty() => r.clone(),
_ => vec![c.role.clone().unwrap_or_default()],
}
}
fn has_any(c: &Context, list: &[String]) -> bool {
effective(c).iter().any(|r| list.iter().any(|x| x == r))
}
pub(crate) fn has_any_role(c: &Context, list: &[String]) -> bool {
has_any(c, list)
}
pub fn should_inject_owner_condition(
rules: &RoleRules,
schema: &Schema,
ctx: Option<&Context>,
) -> bool {
let Some(c) = ctx else { return false };
if c.user_id.as_deref().map(|s| s.is_empty()).unwrap_or(true) {
return false;
}
if c.internal {
return false;
}
if has_any(c, &rules.exempt_roles) {
return false;
}
let effective = effective(c);
let read = schema.read.clone().unwrap_or_default();
let real_roles: Vec<&String> = read.iter().filter(|r| *r != "creator").collect();
if real_roles.iter().any(|r| effective.iter().any(|x| x == *r)) {
return false;
}
read.iter().any(|r| r == "creator")
}
pub fn merge_owner_condition(
rules: &RoleRules,
schema: &Schema,
ctx: Option<&Context>,
condition: Option<Value>,
) -> Option<Value> {
if !should_inject_owner_condition(rules, schema, ctx) {
return condition;
}
let owner = json!({ "createdBy": ctx?.user_id });
match condition.filter(is_truthy) {
None => Some(owner),
Some(c) => Some(json!({ "$and": [c, owner] })),
}
}
pub fn get_writable_fields(
rules: &RoleRules,
schema: &Schema,
ctx: Option<&Context>,
) -> Option<HashSet<String>> {
let c = ctx?;
let schema_writable = can_write_schema(rules, schema, ctx);
let mut allowed = HashSet::new();
for (key, field) in &schema.fields {
match &field.write {
Some(wl) => {
if evaluate(rules, Some(c), Some(wl), Doc::Missing) {
allowed.insert(key.clone());
}
}
None => {
if schema_writable {
allowed.insert(key.clone());
}
}
}
}
Some(allowed)
}
pub fn filter_writable_data(
rules: &RoleRules,
schema: &Schema,
ctx: Option<&Context>,
data: &Value,
) -> Value {
let Some(writable) = get_writable_fields(rules, schema, ctx) else {
return data.clone();
};
let Some(obj) = data.as_object() else {
return data.clone();
};
let mut result = Map::new();
for (key, val) in obj {
let root = key.split('.').next().unwrap_or(key);
if key == "_id" || writable.contains(root) {
result.insert(key.clone(), val.clone());
}
}
Value::Object(result)
}