systemprompt_security/authz/repository/
entities.rs1use std::collections::HashMap;
7
8use sqlx::PgConnection;
9
10use super::AccessControlRepository;
11use crate::authz::error::AuthzResult;
12use crate::authz::types::{EntityKind, EntityRow};
13
14impl AccessControlRepository {
15 pub async fn get_entity(
16 &self,
17 entity_type: EntityKind,
18 entity_id: &str,
19 ) -> AuthzResult<Option<EntityRow>> {
20 let row = sqlx::query_as!(
21 EntityRow,
22 r#"
23 SELECT entity_type AS "kind: EntityKind", entity_id AS id, default_included, source
24 FROM access_control_entities
25 WHERE entity_type = $1 AND entity_id = $2
26 "#,
27 entity_type.as_str(),
28 entity_id,
29 )
30 .fetch_optional(&*self.pool)
31 .await?;
32 Ok(row)
33 }
34
35 pub async fn list_entities_bulk(
36 &self,
37 entity_type: EntityKind,
38 entity_ids: &[String],
39 ) -> AuthzResult<HashMap<String, EntityRow>> {
40 if entity_ids.is_empty() {
41 return Ok(HashMap::new());
42 }
43 let rows = sqlx::query_as!(
44 EntityRow,
45 r#"
46 SELECT entity_type AS "kind: EntityKind", entity_id AS id, default_included, source
47 FROM access_control_entities
48 WHERE entity_type = $1 AND entity_id = ANY($2)
49 "#,
50 entity_type.as_str(),
51 entity_ids,
52 )
53 .fetch_all(&*self.pool)
54 .await?;
55 Ok(rows.into_iter().map(|row| (row.id.clone(), row)).collect())
56 }
57
58 pub async fn upsert_entity(
59 &self,
60 entity_type: EntityKind,
61 entity_id: &str,
62 default_included: bool,
63 source: &str,
64 ) -> AuthzResult<()> {
65 sqlx::query!(
66 r#"
67 INSERT INTO access_control_entities (entity_type, entity_id, default_included, source)
68 VALUES ($1, $2, $3, $4)
69 ON CONFLICT (entity_type, entity_id) DO UPDATE
70 SET default_included = EXCLUDED.default_included,
71 source = EXCLUDED.source,
72 updated_at = NOW()
73 "#,
74 entity_type.as_str(),
75 entity_id,
76 default_included,
77 source,
78 )
79 .execute(&*self.write_pool)
80 .await?;
81 Ok(())
82 }
83
84 pub async fn ensure_entity(
88 &self,
89 entity_type: EntityKind,
90 entity_id: &str,
91 source: &str,
92 ) -> AuthzResult<()> {
93 sqlx::query!(
94 r#"
95 INSERT INTO access_control_entities (entity_type, entity_id, default_included, source)
96 VALUES ($1, $2, false, $3)
97 ON CONFLICT (entity_type, entity_id) DO NOTHING
98 "#,
99 entity_type.as_str(),
100 entity_id,
101 source,
102 )
103 .execute(&*self.write_pool)
104 .await?;
105 Ok(())
106 }
107
108 pub async fn reconcile_entities(
113 &self,
114 entity_type: EntityKind,
115 keep: &[&str],
116 default_included: bool,
117 source: &str,
118 ) -> AuthzResult<u64> {
119 let mut tx = self.write_pool.begin().await?;
120 upsert_entities_on(&mut tx, entity_type, keep, default_included, source).await?;
121 let keep_owned: Vec<String> = keep.iter().map(|id| (*id).to_owned()).collect();
122 let res = sqlx::query!(
123 r#"
124 DELETE FROM access_control_entities
125 WHERE entity_type = $1
126 AND entity_id <> ALL($2::text[])
127 "#,
128 entity_type.as_str(),
129 &keep_owned,
130 )
131 .execute(&mut *tx)
132 .await?;
133 tx.commit().await?;
134 Ok(res.rows_affected())
135 }
136
137 pub async fn list_entities(&self, entity_type: EntityKind) -> AuthzResult<Vec<EntityRow>> {
138 let rows = sqlx::query_as!(
139 EntityRow,
140 r#"
141 SELECT entity_type AS "kind: EntityKind", entity_id AS id, default_included, source
142 FROM access_control_entities
143 WHERE entity_type = $1
144 ORDER BY entity_id
145 "#,
146 entity_type.as_str(),
147 )
148 .fetch_all(&*self.pool)
149 .await?;
150 Ok(rows)
151 }
152}
153
154async fn upsert_entities_on(
155 conn: &mut PgConnection,
156 entity_type: EntityKind,
157 ids: &[&str],
158 default_included: bool,
159 source: &str,
160) -> AuthzResult<()> {
161 if ids.is_empty() {
162 return Ok(());
163 }
164 let ids_owned: Vec<String> = ids.iter().map(|id| (*id).to_owned()).collect();
165 sqlx::query!(
166 r#"
167 INSERT INTO access_control_entities (entity_type, entity_id, default_included, source)
168 SELECT $1, id, $3, $4
169 FROM UNNEST($2::text[]) AS id
170 ON CONFLICT (entity_type, entity_id) DO UPDATE
171 SET default_included = EXCLUDED.default_included,
172 source = EXCLUDED.source,
173 updated_at = NOW()
174 "#,
175 entity_type.as_str(),
176 &ids_owned,
177 default_included,
178 source,
179 )
180 .execute(conn)
181 .await?;
182 Ok(())
183}