use std::sync::Arc;
use anyhow::{Context, Result};
use sqlx::{Row, SqlitePool};
use super::eval::{LeafCheck, Verdict, evaluate};
use super::resolve::resolve;
use super::store::ZanzibarStore;
use super::types::{
CheckResult, Consistency, MAX_DEPTH, NamespaceSchema, ObjectRef, SubjectRef, TreeOp, Tuple,
TupleFilter, UsersetTree,
};
use std::future::Future;
use std::pin::Pin;
#[derive(Clone)]
pub struct SqliteZanzibarStore {
pool: SqlitePool,
}
impl SqliteZanzibarStore {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
pub fn into_dyn(self) -> Arc<dyn ZanzibarStore> {
Arc::new(self)
}
}
fn now_secs() -> f64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64()
}
#[async_trait::async_trait]
impl ZanzibarStore for SqliteZanzibarStore {
async fn define_namespace(&self, schema: &NamespaceSchema) -> Result<()> {
let json = serde_json::to_string(schema).context("zanzibar serialize NamespaceSchema")?;
let now = now_secs();
sqlx::query(
"INSERT INTO auth.zanzibar_namespaces (name, schema_json, updated_at)
VALUES (?, ?, ?)
ON CONFLICT (name) DO UPDATE
SET schema_json = excluded.schema_json,
updated_at = excluded.updated_at",
)
.bind(&schema.name)
.bind(json)
.bind(now)
.execute(&self.pool)
.await
.context("auth.zanzibar_namespaces upsert")?;
Ok(())
}
async fn get_namespace(&self, name: &str) -> Result<Option<NamespaceSchema>> {
let row: Option<(String,)> =
sqlx::query_as("SELECT schema_json FROM auth.zanzibar_namespaces WHERE name = ?")
.bind(name)
.fetch_optional(&self.pool)
.await
.context("auth.zanzibar_namespaces get")?;
Ok(match row {
Some((json,)) => {
Some(serde_json::from_str(&json).context("zanzibar deserialize NamespaceSchema")?)
}
None => None,
})
}
async fn list_namespaces(&self) -> Result<Vec<NamespaceSchema>> {
let rows: Vec<(String,)> =
sqlx::query_as("SELECT schema_json FROM auth.zanzibar_namespaces ORDER BY name")
.fetch_all(&self.pool)
.await
.context("auth.zanzibar_namespaces list")?;
rows.into_iter()
.map(|(json,)| {
serde_json::from_str(&json).context("zanzibar deserialize NamespaceSchema")
})
.collect()
}
async fn write_tuple(&self, t: &Tuple) -> Result<()> {
let now = now_secs();
sqlx::query(
"INSERT INTO auth.zanzibar_tuples
(object_type, object_id, relation,
subject_type, subject_id, subject_rel, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT DO NOTHING",
)
.bind(&t.object_type)
.bind(&t.object_id)
.bind(&t.relation)
.bind(&t.subject_type)
.bind(&t.subject_id)
.bind(&t.subject_rel)
.bind(now)
.execute(&self.pool)
.await
.context("auth.zanzibar_tuples insert")?;
Ok(())
}
async fn write_tuples(&self, tuples: &[Tuple]) -> Result<()> {
if tuples.is_empty() {
return Ok(());
}
let mut tx = self.pool.begin().await.context("begin tuples txn")?;
for t in tuples {
let now = now_secs();
sqlx::query(
"INSERT INTO auth.zanzibar_tuples
(object_type, object_id, relation,
subject_type, subject_id, subject_rel, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT DO NOTHING",
)
.bind(&t.object_type)
.bind(&t.object_id)
.bind(&t.relation)
.bind(&t.subject_type)
.bind(&t.subject_id)
.bind(&t.subject_rel)
.bind(now)
.execute(&mut *tx)
.await
.context("auth.zanzibar_tuples batch insert")?;
}
tx.commit().await.context("commit tuples txn")?;
Ok(())
}
async fn delete_tuple(&self, t: &Tuple) -> Result<bool> {
let res = sqlx::query(
"DELETE FROM auth.zanzibar_tuples
WHERE object_type = ? AND object_id = ? AND relation = ?
AND subject_type = ? AND subject_id = ?
AND subject_rel = ?",
)
.bind(&t.object_type)
.bind(&t.object_id)
.bind(&t.relation)
.bind(&t.subject_type)
.bind(&t.subject_id)
.bind(&t.subject_rel)
.execute(&self.pool)
.await
.context("auth.zanzibar_tuples delete")?;
Ok(res.rows_affected() > 0)
}
async fn list_tuples(&self, filter: &TupleFilter) -> Result<Vec<Tuple>> {
let rows: Vec<(String, String, String, String, String, String)> = sqlx::query_as(
"SELECT object_type, object_id, relation,
subject_type, subject_id, subject_rel
FROM auth.zanzibar_tuples
WHERE (? IS NULL OR object_type = ?)
AND (? IS NULL OR object_id = ?)
AND (? IS NULL OR relation = ?)
AND (? IS NULL OR subject_type = ?)
AND (? IS NULL OR subject_id = ?)
ORDER BY object_type, object_id, relation, subject_type, subject_id
LIMIT ? OFFSET ?",
)
.bind(&filter.object_type)
.bind(&filter.object_type)
.bind(&filter.object_id)
.bind(&filter.object_id)
.bind(&filter.relation)
.bind(&filter.relation)
.bind(&filter.subject_type)
.bind(&filter.subject_type)
.bind(&filter.subject_id)
.bind(&filter.subject_id)
.bind(filter.effective_limit())
.bind(filter.effective_offset())
.fetch_all(&self.pool)
.await
.context("auth.zanzibar_tuples list")?;
Ok(rows
.into_iter()
.map(|(ot, oid, rel, st, sid, srel)| Tuple {
object_type: ot,
object_id: oid,
relation: rel,
subject_type: st,
subject_id: sid,
subject_rel: srel,
})
.collect())
}
async fn check(
&self,
resource: &ObjectRef,
permission: &str,
subject: &SubjectRef,
_consistency: Consistency,
) -> Result<CheckResult> {
let Some(schema) = self.get_namespace(&resource.object_type).await? else {
return Ok(CheckResult::Denied);
};
evaluate(self, &schema, resource, permission, subject).await
}
async fn expand(
&self,
resource: &ObjectRef,
relation: &str,
depth_limit: u32,
) -> Result<UsersetTree> {
let depth = depth_limit.min(MAX_DEPTH);
Ok(UsersetTree::Node {
op: TreeOp::Direct,
children: expand_sqlite(self, resource, relation, depth, &mut Vec::new()).await?,
})
}
async fn lookup_resources(
&self,
resource_type: &str,
permission: &str,
subject: &SubjectRef,
) -> Result<Vec<ObjectRef>> {
let Some(schema) = self.get_namespace(resource_type).await? else {
return Ok(Vec::new());
};
let Some(resolved) = resolve(&schema, permission) else {
return Ok(Vec::new());
};
if resolved.union_relations.is_empty() {
return Ok(Vec::new());
}
let relation_list: Vec<String> = resolved.union_relations.into_iter().collect();
let relation_json =
serde_json::to_string(&relation_list).context("encode relation list as JSON")?;
let rows = sqlx::query(
"SELECT DISTINCT object_type, object_id
FROM auth.zanzibar_tuples
WHERE object_type = ?
AND relation IN (SELECT value FROM json_each(?))",
)
.bind(resource_type)
.bind(relation_json)
.fetch_all(&self.pool)
.await
.context("auth.zanzibar_tuples candidate resources")?;
let mut out = Vec::new();
for row in rows {
let object_type: String = row.get("object_type");
let object_id: String = row.get("object_id");
let r = ObjectRef::new(object_type, object_id);
if self
.check(&r, permission, subject, Consistency::Minimum)
.await?
.is_allowed()
{
out.push(r);
}
}
Ok(out)
}
async fn lookup_subjects(
&self,
subject_type: &str,
resource: &ObjectRef,
permission: &str,
) -> Result<Vec<SubjectRef>> {
let Some(schema) = self.get_namespace(&resource.object_type).await? else {
return Ok(Vec::new());
};
let Some(resolved) = resolve(&schema, permission) else {
return Ok(Vec::new());
};
if resolved.union_relations.is_empty() {
return Ok(Vec::new());
}
let relation_list: Vec<String> = resolved.union_relations.into_iter().collect();
let relation_json =
serde_json::to_string(&relation_list).context("encode relation list as JSON")?;
let rows = sqlx::query(
r#"
WITH RECURSIVE walk(subject_type, subject_id, subject_rel, depth, path) AS (
SELECT t.subject_type, t.subject_id, t.subject_rel, 1,
'|' || t.subject_type || ':' || t.subject_id || '|'
FROM auth.zanzibar_tuples t
WHERE t.object_type = ?1
AND t.object_id = ?2
AND t.relation IN (SELECT value FROM json_each(?3))
UNION ALL
SELECT t.subject_type, t.subject_id, t.subject_rel, w.depth + 1,
w.path || t.subject_type || ':' || t.subject_id || '|'
FROM auth.zanzibar_tuples t
JOIN walk w
ON t.object_type = w.subject_type
AND t.object_id = w.subject_id
AND w.subject_rel <> ''
AND t.relation = w.subject_rel
WHERE w.depth < ?4
AND instr(w.path, '|' || t.subject_type || ':' || t.subject_id || '|') = 0
)
SELECT DISTINCT subject_type, subject_id
FROM walk
WHERE subject_type = ?5 AND subject_rel = ''
"#,
)
.bind(&resource.object_type)
.bind(&resource.object_id)
.bind(relation_json)
.bind(MAX_DEPTH as i64)
.bind(subject_type)
.fetch_all(&self.pool)
.await
.context("auth.zanzibar lookup_subjects CTE")?;
Ok(rows
.into_iter()
.map(|row| {
SubjectRef::direct(
row.get::<String, _>("subject_type"),
row.get::<String, _>("subject_id"),
)
})
.collect())
}
}
impl LeafCheck for SqliteZanzibarStore {
fn check_relation_set<'a>(
&'a self,
object: &'a ObjectRef,
relations: &'a [String],
subject: &'a SubjectRef,
) -> Pin<Box<dyn Future<Output = Result<Verdict>> + Send + 'a>> {
Box::pin(async move {
if relations.is_empty() {
return Ok(Verdict::Denied);
}
let relation_json =
serde_json::to_string(relations).context("encode relation list as JSON")?;
let row: Option<(i64,)> = sqlx::query_as(
r#"
WITH RECURSIVE walk(subject_type, subject_id, subject_rel, depth, path) AS (
SELECT t.subject_type,
t.subject_id,
t.subject_rel,
1 AS depth,
'|' || t.subject_type || ':' || t.subject_id || '|' AS path
FROM auth.zanzibar_tuples t
WHERE t.object_type = ?1
AND t.object_id = ?2
AND t.relation IN (SELECT value FROM json_each(?3))
UNION ALL
SELECT t.subject_type,
t.subject_id,
t.subject_rel,
w.depth + 1,
w.path || t.subject_type || ':' || t.subject_id || '|'
FROM auth.zanzibar_tuples t
JOIN walk w
ON t.object_type = w.subject_type
AND t.object_id = w.subject_id
AND w.subject_rel <> ''
AND t.relation = w.subject_rel
WHERE w.depth < ?4
AND instr(w.path, '|' || t.subject_type || ':' || t.subject_id || '|') = 0
)
SELECT CASE
WHEN EXISTS (
SELECT 1 FROM walk
WHERE subject_type = ?5 AND subject_id = ?6 AND subject_rel = ''
) THEN 1
WHEN EXISTS (SELECT 1 FROM walk WHERE depth >= ?4) THEN 2
ELSE 0
END AS verdict
"#,
)
.bind(&object.object_type)
.bind(&object.object_id)
.bind(relation_json)
.bind(MAX_DEPTH as i64)
.bind(&subject.subject_type)
.bind(&subject.subject_id)
.fetch_optional(&self.pool)
.await
.context("auth.zanzibar check CTE")?;
Ok(match row.map(|(v,)| v).unwrap_or(0) {
1 => Verdict::Allowed,
2 => Verdict::DepthExceeded,
_ => Verdict::Denied,
})
})
}
fn arrow_targets<'a>(
&'a self,
object: &'a ObjectRef,
relation: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Vec<ObjectRef>>> + Send + 'a>> {
Box::pin(async move {
let rows = sqlx::query(
"SELECT subject_type, subject_id
FROM auth.zanzibar_tuples
WHERE object_type = ? AND object_id = ? AND relation = ?
AND subject_rel = ''",
)
.bind(&object.object_type)
.bind(&object.object_id)
.bind(relation)
.fetch_all(&self.pool)
.await
.context("auth.zanzibar arrow targets")?;
Ok(rows
.into_iter()
.map(|row| {
ObjectRef::new(
row.get::<String, _>("subject_type"),
row.get::<String, _>("subject_id"),
)
})
.collect())
})
}
fn schema_for<'a>(
&'a self,
object_type: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Option<NamespaceSchema>>> + Send + 'a>> {
Box::pin(async move { self.get_namespace(object_type).await })
}
}
fn expand_sqlite<'a>(
store: &'a SqliteZanzibarStore,
resource: &'a ObjectRef,
relation: &'a str,
depth: u32,
seen: &'a mut Vec<String>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<UsersetTree>>> + Send + 'a>> {
Box::pin(async move {
if depth == 0 {
return Ok(Vec::new());
}
let key = format!(
"{}:{}#{}",
resource.object_type, resource.object_id, relation
);
if seen.contains(&key) {
return Ok(Vec::new());
}
seen.push(key);
let rows = sqlx::query(
"SELECT subject_type, subject_id, subject_rel
FROM auth.zanzibar_tuples
WHERE object_type = ? AND object_id = ? AND relation = ?",
)
.bind(&resource.object_type)
.bind(&resource.object_id)
.bind(relation)
.fetch_all(&store.pool)
.await
.context("auth.zanzibar_tuples expand fetch")?;
let mut children = Vec::new();
for row in rows {
let st: String = row.get("subject_type");
let sid: String = row.get("subject_id");
let sr: String = row.get("subject_rel");
if sr.is_empty() {
children.push(UsersetTree::Leaf {
subject: SubjectRef::direct(st, sid),
});
} else {
let inner_resource = ObjectRef::new(st.clone(), sid.clone());
let sub = expand_sqlite(store, &inner_resource, &sr, depth - 1, seen).await?;
children.push(UsersetTree::Node {
op: TreeOp::TuplesetArrow,
children: vec![
UsersetTree::Leaf {
subject: SubjectRef::userset(st, sid, sr.clone()),
},
UsersetTree::Node {
op: TreeOp::Direct,
children: sub,
},
],
});
}
}
Ok(children)
})
}