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),
}
pub fn evaluate(ctx: Option<&Context>, role_list: Option<&[String]>, doc: Doc) -> bool {
let empty = role_list.map(|r| r.is_empty()).unwrap_or(true);
if empty {
return match ctx {
None => true,
Some(c) => {
if c.internal {
true
} else {
!c.roles
.clone()
.unwrap_or_default()
.iter()
.any(|r| r == "guest")
}
}
};
}
let Some(c) = ctx else { return true };
if c.internal {
return true;
}
let roles = c.roles.clone().unwrap_or_default();
if roles.iter().any(|r| r == "super_admin" || r == "admin") {
return true;
}
let effective = if roles.is_empty() {
vec![c.role.clone().unwrap_or_default()]
} else {
roles
};
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(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(Some(c), Some(rl), Doc::Missing) {
allowed.insert(key.clone());
}
}
None => {
allowed.insert(key.clone());
}
}
}
Some(allowed)
}
pub fn get_readable_computes(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(Some(c), Some(rl), Doc::Missing) {
allowed.insert(key.clone());
}
}
None => {
allowed.insert(key.clone());
}
}
}
Some(allowed)
}
pub fn get_readable_relations(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(Some(c), Some(rl), Doc::Missing) {
allowed.insert(key.clone());
}
}
None => {
allowed.insert(key.clone());
}
}
}
Some(allowed)
}
pub fn is_field_readable(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(schema, ctx) {
None => true,
Some(allowed) => allowed.contains(root),
}
}
pub fn is_relation_readable(schema: &Schema, ctx: Option<&Context>, rel: &str) -> bool {
match get_readable_relations(schema, ctx) {
None => true,
Some(allowed) => allowed.contains(rel),
}
}
pub fn can_read_schema(schema: &Schema, ctx: Option<&Context>) -> bool {
evaluate(ctx, schema.read.as_deref(), Doc::Missing)
}
pub fn can_write_schema(schema: &Schema, ctx: Option<&Context>) -> bool {
if let Some(c) = ctx {
if has_role(c, "guest") {
return false;
}
if !c.internal && matches!(schema.write, Some(ref w) if w.is_empty()) {
let roles = c.roles.clone().unwrap_or_default();
if !roles.iter().any(|r| r == "super_admin" || r == "admin") {
return false;
}
}
}
evaluate(ctx, schema.write.as_deref(), Doc::Missing)
}
fn has_role(ctx: &Context, role: &str) -> bool {
ctx.roles
.as_ref()
.map(|r| r.iter().any(|x| x == role))
.unwrap_or(false)
}
pub fn should_inject_owner_condition(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;
}
let roles = c.roles.clone().unwrap_or_default();
if roles.iter().any(|r| r == "super_admin" || r == "admin") {
return false;
}
let effective = if roles.is_empty() {
vec![c.role.clone().unwrap_or_default()]
} else {
roles
};
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(
schema: &Schema,
ctx: Option<&Context>,
condition: Option<Value>,
) -> Option<Value> {
if !should_inject_owner_condition(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(schema: &Schema, ctx: Option<&Context>) -> Option<HashSet<String>> {
let c = ctx?;
let schema_writable = can_write_schema(schema, ctx);
let mut allowed = HashSet::new();
for (key, field) in &schema.fields {
match &field.write {
Some(wl) => {
if evaluate(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(schema: &Schema, ctx: Option<&Context>, data: &Value) -> Value {
let Some(writable) = get_writable_fields(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)
}