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