1use std::sync::Arc;
27
28use anyhow::{Context, Result};
29use sqlx::{PgPool, Row};
30
31use super::resolve::resolve;
32use super::store::ZanzibarStore;
33use super::types::{
34 CheckResult, Consistency, MAX_DEPTH, NamespaceSchema, ObjectRef, SubjectRef, TreeOp, Tuple,
35 UsersetTree,
36};
37
38#[derive(Clone)]
41pub struct PostgresZanzibarStore {
42 pool: PgPool,
43}
44
45impl PostgresZanzibarStore {
46 pub fn new(pool: PgPool) -> Self {
47 Self { pool }
48 }
49
50 pub fn into_dyn(self) -> Arc<dyn ZanzibarStore> {
52 Arc::new(self)
53 }
54}
55
56#[async_trait::async_trait]
57impl ZanzibarStore for PostgresZanzibarStore {
58 async fn define_namespace(&self, schema: &NamespaceSchema) -> Result<()> {
59 let json = serde_json::to_value(schema).context("zanzibar serialize NamespaceSchema")?;
60 sqlx::query(
61 "INSERT INTO auth.zanzibar_namespaces (name, schema_json, updated_at)
62 VALUES ($1, $2, EXTRACT(EPOCH FROM NOW()))
63 ON CONFLICT (name) DO UPDATE
64 SET schema_json = EXCLUDED.schema_json,
65 updated_at = EXCLUDED.updated_at",
66 )
67 .bind(&schema.name)
68 .bind(json)
69 .execute(&self.pool)
70 .await
71 .context("auth.zanzibar_namespaces upsert")?;
72 Ok(())
73 }
74
75 async fn get_namespace(&self, name: &str) -> Result<Option<NamespaceSchema>> {
76 let row: Option<(serde_json::Value,)> =
77 sqlx::query_as("SELECT schema_json FROM auth.zanzibar_namespaces WHERE name = $1")
78 .bind(name)
79 .fetch_optional(&self.pool)
80 .await
81 .context("auth.zanzibar_namespaces get")?;
82 Ok(match row {
83 Some((json,)) => {
84 Some(serde_json::from_value(json).context("zanzibar deserialize NamespaceSchema")?)
85 }
86 None => None,
87 })
88 }
89
90 async fn list_namespaces(&self) -> Result<Vec<NamespaceSchema>> {
91 let rows: Vec<(serde_json::Value,)> =
92 sqlx::query_as("SELECT schema_json FROM auth.zanzibar_namespaces ORDER BY name")
93 .fetch_all(&self.pool)
94 .await
95 .context("auth.zanzibar_namespaces list")?;
96 rows.into_iter()
97 .map(|(json,)| {
98 serde_json::from_value(json).context("zanzibar deserialize NamespaceSchema")
99 })
100 .collect()
101 }
102
103 async fn write_tuple(&self, t: &Tuple) -> Result<()> {
104 sqlx::query(
105 "INSERT INTO auth.zanzibar_tuples
106 (object_type, object_id, relation,
107 subject_type, subject_id, subject_rel, created_at)
108 VALUES ($1, $2, $3, $4, $5, $6, EXTRACT(EPOCH FROM NOW()))
109 ON CONFLICT DO NOTHING",
110 )
111 .bind(&t.object_type)
112 .bind(&t.object_id)
113 .bind(&t.relation)
114 .bind(&t.subject_type)
115 .bind(&t.subject_id)
116 .bind(&t.subject_rel)
117 .execute(&self.pool)
118 .await
119 .context("auth.zanzibar_tuples insert")?;
120 Ok(())
121 }
122
123 async fn write_tuples(&self, tuples: &[Tuple]) -> Result<()> {
124 if tuples.is_empty() {
125 return Ok(());
126 }
127 let mut tx = self.pool.begin().await.context("begin tuples txn")?;
128 for t in tuples {
129 sqlx::query(
130 "INSERT INTO auth.zanzibar_tuples
131 (object_type, object_id, relation,
132 subject_type, subject_id, subject_rel, created_at)
133 VALUES ($1, $2, $3, $4, $5, $6, EXTRACT(EPOCH FROM NOW()))
134 ON CONFLICT DO NOTHING",
135 )
136 .bind(&t.object_type)
137 .bind(&t.object_id)
138 .bind(&t.relation)
139 .bind(&t.subject_type)
140 .bind(&t.subject_id)
141 .bind(&t.subject_rel)
142 .execute(&mut *tx)
143 .await
144 .context("auth.zanzibar_tuples batch insert")?;
145 }
146 tx.commit().await.context("commit tuples txn")?;
147 Ok(())
148 }
149
150 async fn delete_tuple(&self, t: &Tuple) -> Result<bool> {
151 let res = sqlx::query(
154 "DELETE FROM auth.zanzibar_tuples
155 WHERE object_type = $1 AND object_id = $2 AND relation = $3
156 AND subject_type = $4 AND subject_id = $5
157 AND subject_rel = $6",
158 )
159 .bind(&t.object_type)
160 .bind(&t.object_id)
161 .bind(&t.relation)
162 .bind(&t.subject_type)
163 .bind(&t.subject_id)
164 .bind(&t.subject_rel)
165 .execute(&self.pool)
166 .await
167 .context("auth.zanzibar_tuples delete")?;
168 Ok(res.rows_affected() > 0)
169 }
170
171 async fn check(
172 &self,
173 resource: &ObjectRef,
174 permission: &str,
175 subject: &SubjectRef,
176 _consistency: Consistency,
177 ) -> Result<CheckResult> {
178 let Some(schema) = self.get_namespace(&resource.object_type).await? else {
181 return Ok(CheckResult::Denied);
182 };
183 let Some(resolved) = resolve(&schema, permission) else {
184 return Ok(CheckResult::CycleDetected);
187 };
188 if resolved.union_relations.is_empty() {
189 return Ok(CheckResult::Denied);
190 }
191 let relation_list: Vec<String> = resolved.union_relations.into_iter().collect();
192
193 let row: Option<(i32,)> = sqlx::query_as(
199 r#"
200 WITH RECURSIVE walk(subject_type, subject_id, subject_rel, depth, path) AS (
201 SELECT t.subject_type,
202 t.subject_id,
203 t.subject_rel,
204 1 AS depth,
205 ARRAY[t.subject_type || ':' || t.subject_id] AS path
206 FROM auth.zanzibar_tuples t
207 WHERE t.object_type = $1 AND t.object_id = $2 AND t.relation = ANY($3)
208 UNION ALL
209 SELECT t.subject_type,
210 t.subject_id,
211 t.subject_rel,
212 w.depth + 1,
213 w.path || (t.subject_type || ':' || t.subject_id)
214 FROM auth.zanzibar_tuples t
215 JOIN walk w
216 ON t.object_type = w.subject_type
217 AND t.object_id = w.subject_id
218 AND w.subject_rel <> ''
219 AND t.relation = w.subject_rel
220 WHERE w.depth < $4
221 AND NOT (t.subject_type || ':' || t.subject_id) = ANY(w.path)
222 )
223 SELECT CASE
224 WHEN EXISTS (
225 SELECT 1 FROM walk
226 WHERE subject_type = $5 AND subject_id = $6 AND subject_rel = ''
227 ) THEN 1
228 WHEN EXISTS (SELECT 1 FROM walk WHERE depth >= $4) THEN 2
229 ELSE 0
230 END AS verdict
231 "#,
232 )
233 .bind(&resource.object_type)
234 .bind(&resource.object_id)
235 .bind(&relation_list)
236 .bind(MAX_DEPTH as i32)
237 .bind(&subject.subject_type)
238 .bind(&subject.subject_id)
239 .fetch_optional(&self.pool)
240 .await
241 .context("auth.zanzibar check CTE")?;
242
243 let verdict = row.map(|(v,)| v).unwrap_or(0);
244 Ok(match verdict {
245 1 => CheckResult::Allowed {
246 resolved_via: Vec::new(),
247 },
248 2 => CheckResult::DepthExceeded,
249 _ => CheckResult::Denied,
250 })
251 }
252
253 async fn expand(
254 &self,
255 resource: &ObjectRef,
256 relation: &str,
257 depth_limit: u32,
258 ) -> Result<UsersetTree> {
259 let depth = depth_limit.min(MAX_DEPTH);
264 Ok(UsersetTree::Node {
265 op: TreeOp::Direct,
266 children: expand_pg(self, resource, relation, depth, &mut Vec::new()).await?,
267 })
268 }
269
270 async fn lookup_resources(
271 &self,
272 resource_type: &str,
273 permission: &str,
274 subject: &SubjectRef,
275 ) -> Result<Vec<ObjectRef>> {
276 let Some(schema) = self.get_namespace(resource_type).await? else {
283 return Ok(Vec::new());
284 };
285 let Some(resolved) = resolve(&schema, permission) else {
286 return Ok(Vec::new());
287 };
288 if resolved.union_relations.is_empty() {
289 return Ok(Vec::new());
290 }
291 let relation_list: Vec<String> = resolved.union_relations.into_iter().collect();
292 let rows = sqlx::query(
296 "SELECT DISTINCT object_type, object_id
297 FROM auth.zanzibar_tuples
298 WHERE object_type = $1 AND relation = ANY($2)",
299 )
300 .bind(resource_type)
301 .bind(&relation_list)
302 .fetch_all(&self.pool)
303 .await
304 .context("auth.zanzibar_tuples candidate resources")?;
305 let mut out = Vec::new();
306 for row in rows {
307 let object_type: String = row.get("object_type");
308 let object_id: String = row.get("object_id");
309 let r = ObjectRef::new(object_type, object_id);
310 if self
311 .check(&r, permission, subject, Consistency::Minimum)
312 .await?
313 .is_allowed()
314 {
315 out.push(r);
316 }
317 }
318 Ok(out)
319 }
320
321 async fn lookup_subjects(
322 &self,
323 subject_type: &str,
324 resource: &ObjectRef,
325 permission: &str,
326 ) -> Result<Vec<SubjectRef>> {
327 let Some(schema) = self.get_namespace(&resource.object_type).await? else {
331 return Ok(Vec::new());
332 };
333 let Some(resolved) = resolve(&schema, permission) else {
334 return Ok(Vec::new());
335 };
336 if resolved.union_relations.is_empty() {
337 return Ok(Vec::new());
338 }
339 let relation_list: Vec<String> = resolved.union_relations.into_iter().collect();
340 let rows = sqlx::query(
341 r#"
342 WITH RECURSIVE walk(subject_type, subject_id, subject_rel, depth, path) AS (
343 SELECT t.subject_type, t.subject_id, t.subject_rel, 1,
344 ARRAY[t.subject_type || ':' || t.subject_id]
345 FROM auth.zanzibar_tuples t
346 WHERE t.object_type = $1 AND t.object_id = $2 AND t.relation = ANY($3)
347 UNION ALL
348 SELECT t.subject_type, t.subject_id, t.subject_rel, w.depth + 1,
349 w.path || (t.subject_type || ':' || t.subject_id)
350 FROM auth.zanzibar_tuples t
351 JOIN walk w
352 ON t.object_type = w.subject_type
353 AND t.object_id = w.subject_id
354 AND w.subject_rel <> ''
355 AND t.relation = w.subject_rel
356 WHERE w.depth < $4
357 AND NOT (t.subject_type || ':' || t.subject_id) = ANY(w.path)
358 )
359 SELECT DISTINCT subject_type, subject_id
360 FROM walk
361 WHERE subject_type = $5 AND subject_rel = ''
362 "#,
363 )
364 .bind(&resource.object_type)
365 .bind(&resource.object_id)
366 .bind(&relation_list)
367 .bind(MAX_DEPTH as i32)
368 .bind(subject_type)
369 .fetch_all(&self.pool)
370 .await
371 .context("auth.zanzibar lookup_subjects CTE")?;
372 Ok(rows
373 .into_iter()
374 .map(|row| {
375 SubjectRef::direct(
376 row.get::<String, _>("subject_type"),
377 row.get::<String, _>("subject_id"),
378 )
379 })
380 .collect())
381 }
382}
383
384fn expand_pg<'a>(
387 store: &'a PostgresZanzibarStore,
388 resource: &'a ObjectRef,
389 relation: &'a str,
390 depth: u32,
391 seen: &'a mut Vec<String>,
392) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<UsersetTree>>> + Send + 'a>> {
393 Box::pin(async move {
394 if depth == 0 {
395 return Ok(Vec::new());
396 }
397 let key = format!(
398 "{}:{}#{}",
399 resource.object_type, resource.object_id, relation
400 );
401 if seen.contains(&key) {
402 return Ok(Vec::new());
403 }
404 seen.push(key);
405 let rows = sqlx::query(
406 "SELECT subject_type, subject_id, subject_rel
407 FROM auth.zanzibar_tuples
408 WHERE object_type = $1 AND object_id = $2 AND relation = $3",
409 )
410 .bind(&resource.object_type)
411 .bind(&resource.object_id)
412 .bind(relation)
413 .fetch_all(&store.pool)
414 .await
415 .context("auth.zanzibar_tuples expand fetch")?;
416 let mut children = Vec::new();
417 for row in rows {
418 let st: String = row.get("subject_type");
419 let sid: String = row.get("subject_id");
420 let sr: String = row.get("subject_rel");
421 if sr.is_empty() {
422 children.push(UsersetTree::Leaf {
423 subject: SubjectRef::direct(st, sid),
424 });
425 } else {
426 let inner_resource = ObjectRef::new(st.clone(), sid.clone());
427 let sub = expand_pg(store, &inner_resource, &sr, depth - 1, seen).await?;
428 children.push(UsersetTree::Node {
429 op: TreeOp::TuplesetArrow,
430 children: vec![
431 UsersetTree::Leaf {
432 subject: SubjectRef::userset(st, sid, sr.clone()),
433 },
434 UsersetTree::Node {
435 op: TreeOp::Direct,
436 children: sub,
437 },
438 ],
439 });
440 }
441 }
442 Ok(children)
443 })
444}