systemprompt_security/authz/repository/
rules.rs1use std::collections::HashMap;
7use std::str::FromStr;
8
9use chrono::{DateTime, Utc};
10use systemprompt_identifiers::RuleId;
11
12use super::{AccessControlRepository, ExportRuleRow, UpsertRuleParams};
13use crate::authz::error::AuthzResult;
14use crate::authz::types::{Access, AccessRule, EntityKind, RuleType};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct ChainFingerprint {
18 pub rule_count: i64,
19 pub rules_updated_at: Option<DateTime<Utc>>,
20 pub entity_count: i64,
21 pub entities_updated_at: Option<DateTime<Utc>>,
22}
23
24impl AccessControlRepository {
25 pub async fn chain_fingerprint(&self) -> AuthzResult<ChainFingerprint> {
26 let row = sqlx::query!(
27 r#"
28 SELECT
29 (SELECT COUNT(*) FROM access_control_rules) AS "rule_count!",
30 (SELECT MAX(updated_at) FROM access_control_rules) AS "rules_updated_at?",
31 (SELECT COUNT(*) FROM access_control_entities) AS "entity_count!",
32 (SELECT MAX(updated_at) FROM access_control_entities) AS "entities_updated_at?"
33 "#,
34 )
35 .fetch_one(&*self.pool)
36 .await?;
37 Ok(ChainFingerprint {
38 rule_count: row.rule_count,
39 rules_updated_at: row.rules_updated_at,
40 entity_count: row.entity_count,
41 entities_updated_at: row.entities_updated_at,
42 })
43 }
44
45 pub async fn list_role_rules_for_export(&self) -> AuthzResult<Vec<ExportRuleRow>> {
46 let rows = sqlx::query_as!(
47 ExportRuleRow,
48 r#"
49 SELECT entity_type, entity_id, rule_type, rule_value, access, justification
50 FROM access_control_rules
51 WHERE rule_type = 'role'
52 ORDER BY entity_type, entity_id, access, rule_type, rule_value
53 "#,
54 )
55 .fetch_all(&*self.pool)
56 .await?;
57 Ok(rows)
58 }
59
60 pub async fn list_rules_for_entity(
61 &self,
62 entity_type: EntityKind,
63 entity_id: &str,
64 ) -> AuthzResult<Vec<AccessRule>> {
65 let rows = sqlx::query!(
66 r#"
67 SELECT id, rule_type, rule_value, access, justification
68 FROM access_control_rules
69 WHERE entity_type = $1 AND entity_id = $2
70 ORDER BY rule_type, rule_value
71 "#,
72 entity_type.as_str(),
73 entity_id,
74 )
75 .fetch_all(&*self.pool)
76 .await?;
77
78 let mut out = Vec::with_capacity(rows.len());
79 for row in rows {
80 out.push(AccessRule {
81 id: RuleId::new(row.id),
82 rule_type: RuleType::from(row.rule_type.as_str()),
83 rule_value: row.rule_value,
84 access: Access::from_str(&row.access)?,
85 justification: row.justification,
86 });
87 }
88 Ok(out)
89 }
90
91 pub async fn list_rules_bulk(
92 &self,
93 entity_type: EntityKind,
94 entity_ids: &[String],
95 ) -> AuthzResult<HashMap<String, Vec<AccessRule>>> {
96 let mut out: HashMap<String, Vec<AccessRule>> = HashMap::with_capacity(entity_ids.len());
97 for id in entity_ids {
98 out.entry(id.clone()).or_default();
99 }
100 if entity_ids.is_empty() {
101 return Ok(out);
102 }
103
104 let rows = sqlx::query!(
105 r#"
106 SELECT entity_id, id, rule_type, rule_value, access, justification
107 FROM access_control_rules
108 WHERE entity_type = $1 AND entity_id = ANY($2)
109 ORDER BY entity_id, rule_type, rule_value
110 "#,
111 entity_type.as_str(),
112 entity_ids,
113 )
114 .fetch_all(&*self.pool)
115 .await?;
116
117 for row in rows {
118 let rule = AccessRule {
119 id: RuleId::new(row.id),
120 rule_type: RuleType::from(row.rule_type.as_str()),
121 rule_value: row.rule_value,
122 access: Access::from_str(&row.access)?,
123 justification: row.justification,
124 };
125 out.entry(row.entity_id).or_default().push(rule);
126 }
127 Ok(out)
128 }
129
130 pub async fn upsert_rule(&self, params: UpsertRuleParams<'_>) -> AuthzResult<AccessRule> {
131 let id = RuleId::generate();
132 let rule_type_str = params.rule_type.to_string();
133 let access_str = params.access.to_string();
134 let row = sqlx::query!(
135 r#"
136 INSERT INTO access_control_rules
137 (id, entity_type, entity_id, rule_type, rule_value, access, justification)
138 VALUES ($1, $2, $3, $4, $5, $6, $7)
139 ON CONFLICT (entity_type, entity_id, rule_type, rule_value)
140 DO UPDATE SET
141 access = EXCLUDED.access,
142 justification = COALESCE(EXCLUDED.justification, access_control_rules.justification),
143 updated_at = NOW()
144 RETURNING id, rule_type, rule_value, access, justification
145 "#,
146 id.as_str(),
147 params.entity_type.as_str(),
148 params.entity_id,
149 rule_type_str,
150 params.rule_value,
151 access_str,
152 params.justification,
153 )
154 .fetch_one(&*self.write_pool)
155 .await?;
156
157 Ok(AccessRule {
158 id: RuleId::new(row.id),
159 rule_type: RuleType::from(row.rule_type.as_str()),
160 rule_value: row.rule_value,
161 access: Access::from_str(&row.access)?,
162 justification: row.justification,
163 })
164 }
165
166 pub async fn set_justification(
167 &self,
168 rule_id: &RuleId,
169 justification: Option<&str>,
170 ) -> AuthzResult<bool> {
171 let result = sqlx::query!(
172 r#"UPDATE access_control_rules SET justification = $2, updated_at = NOW() WHERE id = $1"#,
173 rule_id.as_str(),
174 justification,
175 )
176 .execute(&*self.write_pool)
177 .await?;
178 Ok(result.rows_affected() > 0)
179 }
180
181 pub async fn delete_rule(&self, rule_id: &RuleId) -> AuthzResult<bool> {
182 let result = sqlx::query!(
183 r#"DELETE FROM access_control_rules WHERE id = $1"#,
184 rule_id.as_str(),
185 )
186 .execute(&*self.write_pool)
187 .await?;
188 Ok(result.rows_affected() > 0)
189 }
190}