use std::collections::BTreeSet;
use std::sync::Arc;
use serde_json::Value;
use tokio::sync::{RwLock, RwLockReadGuard};
use crate::shared_entity::{lookup, EntityFields};
use crate::websocket::auth::AuthContext;
pub const DEFAULT_MAX_SCOPE_PARTITIONS: usize = 10_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScopeField {
path: Arc<[String]>,
}
impl ScopeField {
pub fn new(dotted_path: &str) -> Self {
Self {
path: dotted_path.split('.').map(str::to_string).collect(),
}
}
pub fn path(&self) -> &[String] {
&self.path
}
pub fn scope_of<E: EntityFields + ?Sized>(&self, row: &E) -> RowScope {
match lookup(row, self.path.iter()) {
Some(value) => RowScope::of_value(&value),
None => RowScope::Public,
}
}
pub fn scope_in<E: EntityFields + ?Sized>(&self, data: &E) -> Option<RowScope> {
let mut path = self.path.iter();
let first = path.next()?;
let Some(mut current) = data.field(first) else {
return (!data.whole().is_object()).then_some(RowScope::Public);
};
for segment in path {
if !current.is_object() {
return Some(RowScope::Public);
}
current = current.get(segment)?;
}
Some(RowScope::of_value(current))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum RowScope {
Public,
Scoped(String),
Invalid,
}
impl RowScope {
fn of_value(value: &Value) -> Self {
match value {
Value::Null => Self::Public,
Value::String(scope) if scope.is_empty() => Self::Public,
Value::String(scope) => Self::Scoped(scope.clone()),
_ => Self::Invalid,
}
}
pub(crate) fn partition(&self) -> Option<&str> {
match self {
Self::Public => Some(""),
Self::Scoped(scope) => Some(scope),
Self::Invalid => None,
}
}
}
pub fn session_row_scopes(auth: Option<&AuthContext>) -> BTreeSet<String> {
auth.and_then(|auth| auth.row_scopes.as_ref())
.map(|scopes| {
scopes
.iter()
.filter(|scope| !scope.is_empty())
.cloned()
.collect()
})
.unwrap_or_default()
}
#[derive(Debug, Clone)]
pub(crate) struct RowFilter {
field: ScopeField,
scopes: Arc<BTreeSet<String>>,
lock: Arc<RwLock<u64>>,
generation: u64,
}
impl RowFilter {
pub(crate) fn new(
field: ScopeField,
scopes: BTreeSet<String>,
lock: Arc<RwLock<u64>>,
generation: u64,
) -> Self {
Self {
field,
scopes: Arc::new(scopes),
lock,
generation,
}
}
pub(crate) fn field(&self) -> &ScopeField {
&self.field
}
pub(crate) fn scopes(&self) -> &BTreeSet<String> {
&self.scopes
}
pub(crate) fn allows_scope(&self, scope: &RowScope) -> bool {
match scope {
RowScope::Public => true,
RowScope::Scoped(scope) => self.scopes.contains(scope),
RowScope::Invalid => false,
}
}
pub(crate) fn allows<E: EntityFields + ?Sized>(&self, row: &E) -> bool {
self.allows_scope(&self.field.scope_of(row))
}
pub(crate) fn denies_frame_data<E: EntityFields + ?Sized>(&self, data: &E) -> bool {
self.field
.scope_in(data)
.is_some_and(|scope| !self.allows_scope(&scope))
}
pub(crate) fn try_hold(&self) -> Option<RwLockReadGuard<'_, u64>> {
self.lock
.try_read()
.ok()
.filter(|generation| **generation == self.generation)
}
pub(crate) async fn hold(&self) -> Option<RwLockReadGuard<'_, u64>> {
let generation = self.lock.read().await;
(*generation == self.generation).then_some(generation)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn filter(scopes: &[&str]) -> RowFilter {
RowFilter::new(
ScopeField::new("meta.scope"),
scopes.iter().map(|scope| scope.to_string()).collect(),
Arc::new(RwLock::new(0)),
0,
)
}
#[test]
fn absent_null_and_empty_scopes_are_public() {
let field = ScopeField::new("meta.scope");
assert_eq!(field.scope_of(&json!({})), RowScope::Public);
assert_eq!(field.scope_of(&json!({"meta": {}})), RowScope::Public);
assert_eq!(
field.scope_of(&json!({"meta": {"scope": null}})),
RowScope::Public
);
assert_eq!(
field.scope_of(&json!({"meta": {"scope": ""}})),
RowScope::Public
);
assert_eq!(
field.scope_of(&json!({"meta": {"scope": "a"}})),
RowScope::Scoped("a".into())
);
assert_eq!(
field.scope_of(&json!({"meta": {"scope": 7}})),
RowScope::Invalid
);
}
#[test]
fn a_session_reads_public_rows_and_its_own_scopes_only() {
let scoped_to_a = filter(&["a"]);
assert!(scoped_to_a.allows(&json!({"meta": {"scope": "a"}})));
assert!(scoped_to_a.allows(&json!({"name": "public"})));
assert!(!scoped_to_a.allows(&json!({"meta": {"scope": "b"}})));
assert!(!scoped_to_a.allows(&json!({"meta": {"scope": ["a"]}})));
let unscoped = filter(&[]);
assert!(unscoped.allows(&json!({"meta": {"scope": ""}})));
assert!(!unscoped.allows(&json!({"meta": {"scope": "a"}})));
}
#[test]
fn frame_data_without_the_field_leaves_the_scope_alone() {
let scoped_to_a = filter(&["a"]);
assert!(!scoped_to_a.denies_frame_data(&json!({"count": 2})));
assert!(!scoped_to_a.denies_frame_data(&json!({"meta": {"scope": "a"}})));
assert!(scoped_to_a.denies_frame_data(&json!({"meta": {"scope": "b"}})));
}
#[test]
fn parent_replacements_clear_scope_but_partial_objects_keep_it() {
let field = ScopeField::new("meta.access.scope");
for patch in [
json!({"meta":null}),
json!({"meta":false}),
json!({"meta":[]}),
json!({"meta":"replaced"}),
json!({"meta":{"access":null}}),
json!({"meta":{"access":7}}),
Value::Null,
] {
assert_eq!(field.scope_in(&patch), Some(RowScope::Public), "{patch}");
assert_eq!(
field.scope_in(&crate::shared_entity::SharedEntity::new(patch)),
Some(RowScope::Public)
);
}
for patch in [
json!({"value":1}),
json!({"meta":{}}),
json!({"meta":{"access":{}}}),
json!({"meta":{"other":true}}),
] {
assert_eq!(field.scope_in(&patch), None, "{patch}");
}
assert_eq!(
field.scope_in(&json!({"meta":{"access":{"scope":false}}})),
Some(RowScope::Invalid)
);
assert_eq!(
field.scope_in(&json!({"meta":{"access":{"scope":"B"}}})),
Some(RowScope::Scoped("B".into()))
);
}
#[test]
fn session_row_scopes_come_from_the_claim_without_empty_ids() {
let claims = arete_auth::SessionClaims::builder("issuer", "subject", "audience")
.with_row_scopes(["a", "", "b"])
.build();
let context = AuthContext::from_claims(claims);
assert_eq!(
session_row_scopes(Some(&context)),
["a".to_string(), "b".to_string()].into_iter().collect()
);
assert!(session_row_scopes(None).is_empty());
}
#[tokio::test]
async fn a_bumped_generation_stops_the_old_filter() {
let lock = Arc::new(RwLock::new(0));
let old = RowFilter::new(ScopeField::new("scope"), BTreeSet::new(), lock.clone(), 0);
assert!(old.try_hold().is_some());
*lock.write().await += 1;
assert!(old.try_hold().is_none());
assert!(old.hold().await.is_none());
}
}