Skip to main content

systemprompt_security/authz/repository/
entities.rs

1//! Entity-catalog persistence for access control.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::str::FromStr;
7
8use super::AccessControlRepository;
9use crate::authz::error::AuthzResult;
10use crate::authz::types::{EntityKind, EntityRow};
11
12impl AccessControlRepository {
13    pub async fn get_entity(
14        &self,
15        entity_type: EntityKind,
16        entity_id: &str,
17    ) -> AuthzResult<Option<EntityRow>> {
18        let row = sqlx::query!(
19            r#"
20            SELECT entity_type, entity_id, default_included, source
21            FROM access_control_entities
22            WHERE entity_type = $1 AND entity_id = $2
23            "#,
24            entity_type.as_str(),
25            entity_id,
26        )
27        .fetch_optional(&*self.pool)
28        .await?;
29
30        let Some(row) = row else {
31            return Ok(None);
32        };
33        Ok(Some(EntityRow {
34            kind: EntityKind::from_str(&row.entity_type)?,
35            id: row.entity_id,
36            default_included: row.default_included,
37            source: row.source,
38        }))
39    }
40
41    pub async fn upsert_entity(
42        &self,
43        entity_type: EntityKind,
44        entity_id: &str,
45        default_included: bool,
46        source: &str,
47    ) -> AuthzResult<()> {
48        sqlx::query!(
49            r#"
50            INSERT INTO access_control_entities (entity_type, entity_id, default_included, source)
51            VALUES ($1, $2, $3, $4)
52            ON CONFLICT (entity_type, entity_id) DO UPDATE
53            SET default_included = EXCLUDED.default_included,
54                source = EXCLUDED.source,
55                updated_at = NOW()
56            "#,
57            entity_type.as_str(),
58            entity_id,
59            default_included,
60            source,
61        )
62        .execute(&*self.write_pool)
63        .await?;
64        Ok(())
65    }
66
67    pub async fn upsert_entities(
68        &self,
69        entity_type: EntityKind,
70        ids: &[&str],
71        default_included: bool,
72        source: &str,
73    ) -> AuthzResult<()> {
74        if ids.is_empty() {
75            return Ok(());
76        }
77        let ids_owned: Vec<String> = ids.iter().map(|id| (*id).to_owned()).collect();
78        sqlx::query!(
79            r#"
80            INSERT INTO access_control_entities (entity_type, entity_id, default_included, source)
81            SELECT $1, id, $3, $4
82            FROM UNNEST($2::text[]) AS id
83            ON CONFLICT (entity_type, entity_id) DO UPDATE
84            SET default_included = EXCLUDED.default_included,
85                source = EXCLUDED.source,
86                updated_at = NOW()
87            "#,
88            entity_type.as_str(),
89            &ids_owned,
90            default_included,
91            source,
92        )
93        .execute(&*self.write_pool)
94        .await?;
95        Ok(())
96    }
97
98    pub async fn list_entities(&self, entity_type: EntityKind) -> AuthzResult<Vec<EntityRow>> {
99        let rows = sqlx::query!(
100            r#"
101            SELECT entity_type, entity_id, default_included, source
102            FROM access_control_entities
103            WHERE entity_type = $1
104            ORDER BY entity_id
105            "#,
106            entity_type.as_str(),
107        )
108        .fetch_all(&*self.pool)
109        .await?;
110
111        let mut out = Vec::with_capacity(rows.len());
112        for row in rows {
113            out.push(EntityRow {
114                kind: EntityKind::from_str(&row.entity_type)?,
115                id: row.entity_id,
116                default_included: row.default_included,
117                source: row.source,
118            });
119        }
120        Ok(out)
121    }
122}