use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Actor {
pub id: String,
}
impl Actor {
pub fn new(id: impl Into<String>) -> Self {
Self { id: id.into() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyContext {
pub actor: Option<Actor>,
pub resource_collection: String,
pub resource_id: String,
pub resource_attributes: BTreeMap<String, String>,
}
impl PolicyContext {
pub fn new(
actor: Option<Actor>,
collection: impl Into<String>,
resource_id: impl Into<String>,
) -> Self {
Self {
actor,
resource_collection: collection.into(),
resource_id: resource_id.into(),
resource_attributes: BTreeMap::new(),
}
}
pub fn with_attributes(mut self, attrs: BTreeMap<String, String>) -> Self {
self.resource_attributes = attrs;
self
}
pub fn set_attribute(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.resource_attributes.insert(key.into(), value.into());
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicySubject {
Authenticated,
Owner,
Member,
Self_ { field: String },
}
impl PolicySubject {
pub fn as_str(&self) -> &'static str {
match self {
PolicySubject::Authenticated => "authenticated",
PolicySubject::Owner => "owner",
PolicySubject::Member => "member",
PolicySubject::Self_ { .. } => "self",
}
}
pub fn from_str(s: &str) -> Option<Self> {
if let Some(field) = s.strip_prefix("self(").and_then(|s| s.strip_suffix(")")) {
return Some(PolicySubject::Self_ {
field: field.to_string(),
});
}
match s {
"authenticated" => Some(PolicySubject::Authenticated),
"owner" => Some(PolicySubject::Owner),
"member" => Some(PolicySubject::Member),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthorizationDecision {
Allow,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyAuthorizationError {
pub code: String,
pub message: String,
}
impl PolicyAuthorizationError {
pub fn new(code: &str, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
}
}
pub fn unauthenticated(reason: impl Into<String>) -> Self {
Self::new("UNAUTHENTICATED", reason)
}
pub fn unauthorized(reason: impl Into<String>) -> Self {
Self::new("UNAUTHORIZED", reason)
}
}
impl std::fmt::Display for PolicyAuthorizationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for PolicyAuthorizationError {}
#[derive(Debug, Clone)]
pub struct AuthorizationState {
pub schema: Arc<crate::state_contract::StateSchema>,
pub state_namespace: String,
pub snapshot_rows: Arc<Vec<crate::StoredRow>>,
}
impl AuthorizationState {
pub fn new(
schema: Arc<crate::state_contract::StateSchema>,
state_namespace: impl Into<String>,
snapshot_rows: Arc<Vec<crate::StoredRow>>,
) -> Self {
Self {
schema,
state_namespace: state_namespace.into(),
snapshot_rows,
}
}
pub fn lookup_record(&self, collection: &str, key: &str) -> Option<Value> {
let capability = format!("{}:{}", self.state_namespace, collection);
self.snapshot_rows
.iter()
.find(|row| row.capability == capability && row.key == key && !row.deleted)
.map(|row| row.value.clone())
}
pub fn records_in_collection(&self, collection: &str) -> Vec<Value> {
let capability = format!("{}:{}", self.state_namespace, collection);
self.snapshot_rows
.iter()
.filter(|row| row.capability == capability && !row.deleted)
.map(|row| row.value.clone())
.collect()
}
pub fn find_reference_field(
&self,
from_collection: &str,
to_collection: &str,
) -> Option<String> {
let collection = self
.schema
.collections
.iter()
.find(|c| c.name == from_collection)?;
collection.fields.iter().find_map(|field| {
if let crate::state_contract::FieldType::Reference {
collection: ref_col,
} = &field.field_type
{
if ref_col == to_collection {
return Some(field.name.clone());
}
}
None
})
}
pub fn find_membership_collection(&self) -> Option<String> {
for collection in &self.schema.collections {
let has_user_ref = collection.fields.iter().any(|f| {
if let crate::state_contract::FieldType::Reference {
collection: ref_col,
} = &f.field_type
{
ref_col == "User" || ref_col == "Users"
} else {
false
}
});
let has_org_ref = collection.fields.iter().any(|f| {
if let crate::state_contract::FieldType::Reference {
collection: ref_col,
} = &f.field_type
{
ref_col == "Organization" || ref_col == "Organizations"
} else {
false
}
});
if has_user_ref && has_org_ref {
return Some(collection.name.clone());
}
}
None
}
pub fn find_membership_fields(&self, collection_name: &str) -> Option<(String, String)> {
let collection = self
.schema
.collections
.iter()
.find(|c| c.name == collection_name)?;
let mut user_field = None;
let mut org_field = None;
for field in &collection.fields {
if let crate::state_contract::FieldType::Reference {
collection: ref_col,
} = &field.field_type
{
if (ref_col == "User" || ref_col == "Users") && user_field.is_none() {
user_field = Some(field.name.clone());
}
if (ref_col == "Organization" || ref_col == "Organizations") && org_field.is_none()
{
org_field = Some(field.name.clone());
}
}
}
match (user_field, org_field) {
(Some(u), Some(o)) => Some((u, o)),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct RecordAuthorizationContext {
pub actor: Option<Actor>,
pub collection: String,
pub record_id: String,
pub record_value: serde_json::Value,
pub authorization_state: Option<AuthorizationState>,
}
impl RecordAuthorizationContext {
pub fn new(
actor: Option<Actor>,
collection: impl Into<String>,
record_id: impl Into<String>,
record_value: serde_json::Value,
) -> Self {
Self {
actor,
collection: collection.into(),
record_id: record_id.into(),
record_value,
authorization_state: None,
}
}
pub fn with_state(mut self, state: AuthorizationState) -> Self {
self.authorization_state = Some(state);
self
}
pub fn field(&self, name: &str) -> Option<&serde_json::Value> {
self.record_value.get(name)
}
}
pub struct PolicyEvaluator;
impl PolicyEvaluator {
pub fn evaluate(
subject: PolicySubject,
context: &PolicyContext,
) -> Result<AuthorizationDecision, PolicyAuthorizationError> {
match subject {
PolicySubject::Authenticated => Self::evaluate_authenticated(context),
PolicySubject::Owner => Self::evaluate_owner(context),
PolicySubject::Member => Self::evaluate_member(context),
PolicySubject::Self_ { .. } => Err(PolicyAuthorizationError::new(
"INVALID_POLICY_CONTEXT",
"self policies require record-level context; use evaluate_record instead",
)),
}
}
pub fn evaluate_record(
subject: PolicySubject,
context: &RecordAuthorizationContext,
) -> Result<AuthorizationDecision, PolicyAuthorizationError> {
match subject {
PolicySubject::Authenticated => Self::evaluate_record_authenticated(context),
PolicySubject::Owner => Self::evaluate_record_owner(context),
PolicySubject::Member => Self::evaluate_record_member(context),
PolicySubject::Self_ { field } => Self::evaluate_record_self(&field, context),
}
}
fn evaluate_authenticated(
context: &PolicyContext,
) -> Result<AuthorizationDecision, PolicyAuthorizationError> {
if context.actor.is_some() {
Ok(AuthorizationDecision::Allow)
} else {
Err(PolicyAuthorizationError::unauthenticated(
"authenticated policy requires an authenticated actor",
))
}
}
fn evaluate_owner(
context: &PolicyContext,
) -> Result<AuthorizationDecision, PolicyAuthorizationError> {
let actor = context.actor.as_ref().ok_or_else(|| {
PolicyAuthorizationError::unauthenticated(
"owner policy requires an authenticated actor",
)
})?;
let owner_id = context.resource_attributes.get("owner_id").ok_or_else(|| {
PolicyAuthorizationError::new(
"OWNERSHIP_NOT_DEFINED",
"resource does not have an owner_id attribute; ownership is not supported for this collection",
)
})?;
if actor.id == *owner_id {
Ok(AuthorizationDecision::Allow)
} else {
Err(PolicyAuthorizationError::unauthorized(format!(
"actor {} is not the owner ({})",
actor.id, owner_id
)))
}
}
fn evaluate_member(
context: &PolicyContext,
) -> Result<AuthorizationDecision, PolicyAuthorizationError> {
let actor = context.actor.as_ref().ok_or_else(|| {
PolicyAuthorizationError::unauthenticated(
"member policy requires an authenticated actor",
)
})?;
let organization_id = context.resource_attributes.get("organization_id").ok_or_else(|| {
PolicyAuthorizationError::new(
"ORGANIZATION_NOT_DEFINED",
"resource does not have an organization_id attribute; member policy cannot be evaluated",
)
})?;
let is_member = context
.resource_attributes
.get("is_member")
.map(|v| v == "true")
.unwrap_or(false);
if is_member {
Ok(AuthorizationDecision::Allow)
} else {
Err(PolicyAuthorizationError::unauthorized(format!(
"actor {} is not a member of organization {}",
actor.id, organization_id
)))
}
}
fn evaluate_record_authenticated(
context: &RecordAuthorizationContext,
) -> Result<AuthorizationDecision, PolicyAuthorizationError> {
if context.actor.is_some() {
Ok(AuthorizationDecision::Allow)
} else {
Err(PolicyAuthorizationError::unauthenticated(
"authenticated policy requires an authenticated actor",
))
}
}
fn evaluate_record_owner(
context: &RecordAuthorizationContext,
) -> Result<AuthorizationDecision, PolicyAuthorizationError> {
let actor = context.actor.as_ref().ok_or_else(|| {
PolicyAuthorizationError::unauthenticated(
"owner policy requires an authenticated actor",
)
})?;
let owner_id = context.field("owner_id")
.and_then(|v| v.as_str())
.ok_or_else(|| {
PolicyAuthorizationError::new(
"OWNERSHIP_NOT_DEFINED",
"record does not have an owner_id field; ownership is not supported for this collection",
)
})?;
if actor.id == owner_id {
Ok(AuthorizationDecision::Allow)
} else {
Err(PolicyAuthorizationError::unauthorized(format!(
"actor {} is not the owner ({})",
actor.id, owner_id
)))
}
}
fn evaluate_record_member(
context: &RecordAuthorizationContext,
) -> Result<AuthorizationDecision, PolicyAuthorizationError> {
let actor = context.actor.as_ref().ok_or_else(|| {
PolicyAuthorizationError::unauthenticated(
"member policy requires an authenticated actor",
)
})?;
let auth_state = context.authorization_state.as_ref().ok_or_else(|| {
PolicyAuthorizationError::new(
"AUTHORIZATION_STATE_NOT_AVAILABLE",
"member policy requires access to database state (authorization_state not provided)",
)
})?;
let org_ref_field = auth_state
.find_reference_field(&context.collection, "Organization")
.or_else(|| auth_state.find_reference_field(&context.collection, "Organizations"))
.ok_or_else(|| {
PolicyAuthorizationError::new(
"ORGANIZATION_NOT_DEFINED",
"resource collection does not have a reference to Organization",
)
})?;
let organization_id = context
.field(&org_ref_field)
.and_then(|v| v.as_str())
.ok_or_else(|| {
PolicyAuthorizationError::new(
"ORGANIZATION_NOT_DEFINED",
format!("record does not have a valid {} field", org_ref_field),
)
})?;
let membership_collection = auth_state.find_membership_collection()
.ok_or_else(|| {
PolicyAuthorizationError::new(
"MEMBERSHIP_NOT_DEFINED",
"schema does not define a membership collection (collection with references to both User and Organization)",
)
})?;
let (user_field, org_field) = auth_state
.find_membership_fields(&membership_collection)
.ok_or_else(|| {
PolicyAuthorizationError::new(
"MEMBERSHIP_NOT_DEFINED",
"membership collection does not have both user and organization references",
)
})?;
let membership_records = auth_state.records_in_collection(&membership_collection);
let has_membership = membership_records.iter().any(|record| {
if let Some(obj) = record.as_object() {
let user_match = obj
.get(&user_field)
.and_then(|v| v.as_str())
.map(|u| u == actor.id)
.unwrap_or(false);
let org_match = obj
.get(&org_field)
.and_then(|v| v.as_str())
.map(|o| o == organization_id)
.unwrap_or(false);
user_match && org_match
} else {
false
}
});
if has_membership {
Ok(AuthorizationDecision::Allow)
} else {
Err(PolicyAuthorizationError::unauthorized(format!(
"actor {} is not a member of organization {}",
actor.id, organization_id
)))
}
}
fn evaluate_record_self(
field: &str,
context: &RecordAuthorizationContext,
) -> Result<AuthorizationDecision, PolicyAuthorizationError> {
let actor = context.actor.as_ref().ok_or_else(|| {
PolicyAuthorizationError::unauthenticated("self policy requires an authenticated actor")
})?;
let field_value = context
.field(field)
.and_then(|v| v.as_str())
.ok_or_else(|| {
PolicyAuthorizationError::new(
"IDENTITY_NOT_FOUND",
format!("record does not have a valid {} field", field),
)
})?;
if actor.id == field_value {
Ok(AuthorizationDecision::Allow)
} else {
Err(PolicyAuthorizationError::unauthorized(format!(
"actor {} does not match identity field {}",
actor.id, field
)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn authenticated_allows_with_actor() {
let context = PolicyContext::new(Some(Actor::new("user123")), "Todo", "todo-1");
let result = PolicyEvaluator::evaluate(PolicySubject::Authenticated, &context);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
#[test]
fn authenticated_denies_without_actor() {
let context = PolicyContext::new(None, "Todo", "todo-1");
let result = PolicyEvaluator::evaluate(PolicySubject::Authenticated, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHENTICATED");
}
#[test]
fn owner_allows_when_actor_is_owner() {
let mut context = PolicyContext::new(Some(Actor::new("user123")), "Project", "project-1");
context.set_attribute("owner_id", "user123");
let result = PolicyEvaluator::evaluate(PolicySubject::Owner, &context);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
#[test]
fn owner_denies_when_actor_is_not_owner() {
let mut context = PolicyContext::new(Some(Actor::new("user123")), "Project", "project-1");
context.set_attribute("owner_id", "user456");
let result = PolicyEvaluator::evaluate(PolicySubject::Owner, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
}
#[test]
fn owner_denies_without_actor() {
let context = PolicyContext::new(None, "Project", "project-1");
let result = PolicyEvaluator::evaluate(PolicySubject::Owner, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHENTICATED");
}
#[test]
fn owner_denies_without_owner_id_attribute() {
let context = PolicyContext::new(Some(Actor::new("user123")), "Project", "project-1");
let result = PolicyEvaluator::evaluate(PolicySubject::Owner, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "OWNERSHIP_NOT_DEFINED");
}
#[test]
fn policy_subject_from_str() {
assert_eq!(
PolicySubject::from_str("authenticated"),
Some(PolicySubject::Authenticated)
);
assert_eq!(PolicySubject::from_str("owner"), Some(PolicySubject::Owner));
assert_eq!(
PolicySubject::from_str("member"),
Some(PolicySubject::Member)
);
assert_eq!(PolicySubject::from_str("invalid"), None);
}
#[test]
fn member_allows_when_actor_is_member() {
let mut context = PolicyContext::new(Some(Actor::new("user123")), "Project", "project-1");
context.set_attribute("organization_id", "org-abc");
context.set_attribute("is_member", "true");
let result = PolicyEvaluator::evaluate(PolicySubject::Member, &context);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
#[test]
fn member_denies_when_actor_is_not_member() {
let mut context = PolicyContext::new(Some(Actor::new("user123")), "Project", "project-1");
context.set_attribute("organization_id", "org-abc");
context.set_attribute("is_member", "false");
let result = PolicyEvaluator::evaluate(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
}
#[test]
fn member_denies_when_is_member_attribute_missing() {
let mut context = PolicyContext::new(Some(Actor::new("user123")), "Project", "project-1");
context.set_attribute("organization_id", "org-abc");
let result = PolicyEvaluator::evaluate(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
}
#[test]
fn member_denies_without_actor() {
let mut context = PolicyContext::new(None, "Project", "project-1");
context.set_attribute("organization_id", "org-abc");
context.set_attribute("is_member", "true");
let result = PolicyEvaluator::evaluate(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHENTICATED");
}
#[test]
fn member_denies_without_organization_id_attribute() {
let mut context = PolicyContext::new(Some(Actor::new("user123")), "Project", "project-1");
context.set_attribute("is_member", "true");
let result = PolicyEvaluator::evaluate(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "ORGANIZATION_NOT_DEFINED");
}
fn make_stored_row(collection: &str, key: &str, value: Value) -> crate::StoredRow {
crate::StoredRow {
recorded_version: None,
capability: format!("tenant:123:{}", collection),
key: key.into(),
rust_type: "json".into(),
value,
unix_ms: 0,
content_hash: None,
flow_ref: None,
deleted: false,
operation: None,
}
}
#[test]
fn record_context_without_state() {
let record = serde_json::json!({"_id": "rec1", "title": "Test"});
let context = RecordAuthorizationContext::new(
Some(Actor::new("user1")),
"TestCollection",
"rec1",
record,
);
assert!(context.authorization_state.is_none());
}
#[test]
fn record_context_with_state_builder() {
use crate::state_contract::StateSchema;
let record = serde_json::json!({"_id": "rec1", "title": "Test"});
let schema = Arc::new(StateSchema {
contract_version: 1,
schema_version: 1,
application_id: "app".into(),
revision_id: "rev".into(),
collections: vec![],
});
let auth_state = AuthorizationState {
schema: schema.clone(),
state_namespace: "tenant:123".into(),
snapshot_rows: Arc::new(vec![]),
};
let context = RecordAuthorizationContext::new(
Some(Actor::new("user1")),
"TestCollection",
"rec1",
record,
)
.with_state(auth_state.clone());
assert!(context.authorization_state.is_some());
assert_eq!(
context.authorization_state.unwrap().state_namespace,
"tenant:123"
);
}
#[test]
fn authorization_state_lookup_record() {
use crate::state_contract::StateSchema;
let schema = Arc::new(StateSchema {
contract_version: 1,
schema_version: 1,
application_id: "app".into(),
revision_id: "rev".into(),
collections: vec![],
});
let record1 = serde_json::json!({"_id": "id1", "name": "Alice"});
let stored_row = make_stored_row("Users", "id1", record1.clone());
let snapshot_rows = Arc::new(vec![stored_row]);
let state = AuthorizationState::new(schema, "tenant:123", snapshot_rows);
let found = state.lookup_record("Users", "id1");
assert_eq!(found, Some(record1));
}
#[test]
fn authorization_state_lookup_missing_record() {
use crate::state_contract::StateSchema;
let schema = Arc::new(StateSchema {
contract_version: 1,
schema_version: 1,
application_id: "app".into(),
revision_id: "rev".into(),
collections: vec![],
});
let snapshot_rows = Arc::new(vec![]);
let state = AuthorizationState::new(schema, "tenant:123", snapshot_rows);
let found = state.lookup_record("Users", "nonexistent");
assert_eq!(found, None);
}
#[test]
fn authorization_state_lookup_deleted_record() {
use crate::state_contract::StateSchema;
let schema = Arc::new(StateSchema {
contract_version: 1,
schema_version: 1,
application_id: "app".into(),
revision_id: "rev".into(),
collections: vec![],
});
let record1 = serde_json::json!({"_id": "id1", "name": "Alice"});
let mut stored_row = make_stored_row("Users", "id1", record1);
stored_row.deleted = true;
let snapshot_rows = Arc::new(vec![stored_row]);
let state = AuthorizationState::new(schema, "tenant:123", snapshot_rows);
let found = state.lookup_record("Users", "id1");
assert_eq!(found, None); }
#[test]
fn authorization_state_records_in_collection() {
use crate::state_contract::StateSchema;
let schema = Arc::new(StateSchema {
contract_version: 1,
schema_version: 1,
application_id: "app".into(),
revision_id: "rev".into(),
collections: vec![],
});
let record1 = serde_json::json!({"_id": "id1", "name": "Alice"});
let record2 = serde_json::json!({"_id": "id2", "name": "Bob"});
let snapshot_rows = Arc::new(vec![
make_stored_row("Users", "id1", record1.clone()),
make_stored_row("Users", "id2", record2.clone()),
]);
let state = AuthorizationState::new(schema, "tenant:123", snapshot_rows);
let records = state.records_in_collection("Users");
assert_eq!(records.len(), 2);
assert!(records.contains(&record1));
assert!(records.contains(&record2));
}
#[test]
fn existing_owner_policy_works_with_state() {
let record = serde_json::json!({"_id": "rec1", "owner_id": "user123"});
let context =
RecordAuthorizationContext::new(Some(Actor::new("user123")), "Project", "rec1", record);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Owner, &context);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
#[test]
fn existing_authenticated_policy_works_with_state() {
let record = serde_json::json!({"_id": "rec1"});
let context =
RecordAuthorizationContext::new(Some(Actor::new("user123")), "Project", "rec1", record);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Authenticated, &context);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
fn make_schema_with_collections(
collections: Vec<crate::state_contract::CollectionSchema>,
) -> Arc<crate::state_contract::StateSchema> {
Arc::new(crate::state_contract::StateSchema {
contract_version: 1,
schema_version: 1,
application_id: "app".into(),
revision_id: "rev".into(),
collections,
})
}
fn make_collection(
name: &str,
fields: Vec<crate::state_contract::FieldSchema>,
) -> crate::state_contract::CollectionSchema {
crate::state_contract::CollectionSchema {
name: name.into(),
version: 1,
fields,
indexes: vec![],
}
}
fn make_field(
name: &str,
field_type: crate::state_contract::FieldType,
) -> crate::state_contract::FieldSchema {
crate::state_contract::FieldSchema {
name: name.into(),
field_type,
nullable: false,
required: true,
default: None,
constraints: crate::state_contract::FieldConstraints::default(),
computed: None,
}
}
#[test]
fn member_policy_allows_authenticated_member() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org1"});
let membership =
serde_json::json!({"_id": "mem1", "user": "alice", "organization": "org1"});
let snapshot = Arc::new(vec![
make_stored_row("Project", "proj1", project),
make_stored_row("Membership", "mem1", membership),
]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org1"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
#[test]
fn member_policy_denies_unauthenticated() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org1"});
let membership =
serde_json::json!({"_id": "mem1", "user": "alice", "organization": "org1"});
let snapshot = Arc::new(vec![
make_stored_row("Project", "proj1", project),
make_stored_row("Membership", "mem1", membership),
]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
None, "Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org1"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHENTICATED");
}
#[test]
fn member_policy_denies_non_member() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org1"});
let membership =
serde_json::json!({"_id": "mem1", "user": "alice", "organization": "org1"});
let snapshot = Arc::new(vec![
make_stored_row("Project", "proj1", project),
make_stored_row("Membership", "mem1", membership),
]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("bob")), "Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org1"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
}
#[test]
fn member_policy_cross_org_isolation() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org2"});
let membership =
serde_json::json!({"_id": "mem1", "user": "alice", "organization": "org1"});
let snapshot = Arc::new(vec![
make_stored_row("Project", "proj1", project),
make_stored_row("Membership", "mem1", membership),
]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org2"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
}
#[test]
fn member_policy_no_state_fails() {
let project = serde_json::json!({"_id": "proj1", "organization": "org1"});
let context =
RecordAuthorizationContext::new(Some(Actor::new("alice")), "Project", "proj1", project);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().code,
"AUTHORIZATION_STATE_NOT_AVAILABLE"
);
}
#[test]
fn member_policy_missing_organization_reference() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1"});
let snapshot = Arc::new(vec![make_stored_row("Project", "proj1", project)]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "ORGANIZATION_NOT_DEFINED");
}
#[test]
fn member_policy_missing_organization_field_in_record() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1"}); let snapshot = Arc::new(vec![make_stored_row("Project", "proj1", project)]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "ORGANIZATION_NOT_DEFINED");
}
#[test]
fn member_policy_deleted_membership_not_honored() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org1"});
let mut membership = make_stored_row(
"Membership",
"mem1",
serde_json::json!({"_id": "mem1", "user": "alice", "organization": "org1"}),
);
membership.deleted = true;
let snapshot = Arc::new(vec![
make_stored_row("Project", "proj1", project),
membership,
]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org1"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
}
#[test]
fn member_policy_multiple_memberships_finds_match() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org2"});
let snapshot = Arc::new(vec![
make_stored_row("Project", "proj1", project),
make_stored_row(
"Membership",
"mem1",
serde_json::json!({"_id": "mem1", "user": "alice", "organization": "org1"}),
),
make_stored_row(
"Membership",
"mem2",
serde_json::json!({"_id": "mem2", "user": "alice", "organization": "org2"}),
),
make_stored_row(
"Membership",
"mem3",
serde_json::json!({"_id": "mem3", "user": "bob", "organization": "org2"}),
),
]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org2"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
#[test]
fn member_policy_spoofing_fake_is_member_attribute() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project =
serde_json::json!({"_id": "proj1", "organization": "org1", "is_member": "true"});
let snapshot = Arc::new(vec![make_stored_row("Project", "proj1", project)]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org1", "is_member": "true"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
}
#[test]
fn member_policy_spoofing_fake_organization_id() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org1"});
let membership =
serde_json::json!({"_id": "mem1", "user": "alice", "organization": "org1"});
let snapshot = Arc::new(vec![
make_stored_row("Project", "proj1", project),
make_stored_row("Membership", "mem1", membership),
]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org2"}), )
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
}
#[test]
fn member_policy_discovers_user_field_variations() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user_id",
FieldType::Reference {
collection: "User".into(),
},
), make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org1"});
let membership =
serde_json::json!({"_id": "mem1", "user_id": "alice", "organization": "org1"});
let snapshot = Arc::new(vec![
make_stored_row("Project", "proj1", project),
make_stored_row("Membership", "mem1", membership),
]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org1"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
#[test]
fn member_policy_spoofing_fake_role_attribute() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "User".into(),
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org1", "role": "admin"});
let snapshot = Arc::new(vec![make_stored_row("Project", "proj1", project)]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org1", "role": "admin"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
}
#[test]
fn member_policy_no_membership_collection() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organization".into(),
},
),
],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org1"});
let snapshot = Arc::new(vec![make_stored_row("Project", "proj1", project)]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org1"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "MEMBERSHIP_NOT_DEFINED");
}
#[test]
fn member_policy_handles_plural_collection_names() {
use crate::state_contract::FieldType;
let schema = make_schema_with_collections(vec![
make_collection(
"Project",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"organization",
FieldType::Reference {
collection: "Organizations".into(),
},
), ],
),
make_collection(
"Membership",
vec![
make_field(
"_id",
FieldType::Primitive {
primitive: crate::state_contract::PrimitiveType::String,
},
),
make_field(
"user",
FieldType::Reference {
collection: "Users".into(),
},
), make_field(
"organization",
FieldType::Reference {
collection: "Organizations".into(),
},
), ],
),
]);
let project = serde_json::json!({"_id": "proj1", "organization": "org1"});
let membership =
serde_json::json!({"_id": "mem1", "user": "alice", "organization": "org1"});
let snapshot = Arc::new(vec![
make_stored_row("Project", "proj1", project),
make_stored_row("Membership", "mem1", membership),
]);
let auth_state = AuthorizationState::new(schema, "tenant:123", snapshot);
let context = RecordAuthorizationContext::new(
Some(Actor::new("alice")),
"Project",
"proj1",
serde_json::json!({"_id": "proj1", "organization": "org1"}),
)
.with_state(auth_state);
let result = PolicyEvaluator::evaluate_record(PolicySubject::Member, &context);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
#[test]
fn self_allows_when_field_matches_actor_id() {
let context = RecordAuthorizationContext::new(
Some(Actor::new("user-bob")),
"Invitation",
"inv-1",
serde_json::json!({"_id": "inv-1", "user": "user-bob"}),
);
let result = PolicyEvaluator::evaluate_record(
PolicySubject::Self_ {
field: "user".to_string(),
},
&context,
);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
#[test]
fn self_denies_when_field_does_not_match_actor_id() {
let context = RecordAuthorizationContext::new(
Some(Actor::new("user-bob")),
"Invitation",
"inv-1",
serde_json::json!({"_id": "inv-1", "user": "user-alice"}),
);
let result = PolicyEvaluator::evaluate_record(
PolicySubject::Self_ {
field: "user".to_string(),
},
&context,
);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
}
#[test]
fn self_denies_without_actor() {
let context = RecordAuthorizationContext::new(
None,
"Invitation",
"inv-1",
serde_json::json!({"_id": "inv-1", "user": "user-bob"}),
);
let result = PolicyEvaluator::evaluate_record(
PolicySubject::Self_ {
field: "user".to_string(),
},
&context,
);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "UNAUTHENTICATED");
}
#[test]
fn self_denies_when_field_is_missing() {
let context = RecordAuthorizationContext::new(
Some(Actor::new("user-bob")),
"Invitation",
"inv-1",
serde_json::json!({"_id": "inv-1"}),
);
let result = PolicyEvaluator::evaluate_record(
PolicySubject::Self_ {
field: "user".to_string(),
},
&context,
);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "IDENTITY_NOT_FOUND");
}
#[test]
fn self_denies_when_field_is_null() {
let context = RecordAuthorizationContext::new(
Some(Actor::new("user-bob")),
"Invitation",
"inv-1",
serde_json::json!({"_id": "inv-1", "user": null}),
);
let result = PolicyEvaluator::evaluate_record(
PolicySubject::Self_ {
field: "user".to_string(),
},
&context,
);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "IDENTITY_NOT_FOUND");
}
#[test]
fn self_works_with_different_field_names() {
let context = RecordAuthorizationContext::new(
Some(Actor::new("user-alice")),
"APIKey",
"key-1",
serde_json::json!({"_id": "key-1", "owner": "user-alice"}),
);
let result = PolicyEvaluator::evaluate_record(
PolicySubject::Self_ {
field: "owner".to_string(),
},
&context,
);
assert_eq!(result, Ok(AuthorizationDecision::Allow));
}
#[test]
fn self_denies_wrong_actor() {
let context = RecordAuthorizationContext::new(
Some(Actor::new("user-alice")),
"APIKey",
"key-1",
serde_json::json!({"_id": "key-1", "owner": "user-bob"}),
);
let result = PolicyEvaluator::evaluate_record(
PolicySubject::Self_ {
field: "owner".to_string(),
},
&context,
);
assert!(result.is_err());
}
#[test]
fn policy_subject_parses_self_syntax() {
let subject = PolicySubject::from_str("self(user)");
assert!(matches!(subject, Some(PolicySubject::Self_ { field }) if field == "user"));
}
#[test]
fn policy_subject_parses_self_with_different_field() {
let subject = PolicySubject::from_str("self(owner)");
assert!(matches!(subject, Some(PolicySubject::Self_ { field }) if field == "owner"));
}
#[test]
fn policy_subject_as_str_for_self() {
let subject = PolicySubject::Self_ {
field: "user".to_string(),
};
assert_eq!(subject.as_str(), "self");
}
}