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(row.rule_type.as_str()),
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(row.rule_type.as_str()),
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> {
102 let id = RuleId::generate();
103 let rule_type_str = params.rule_type.to_string();
104 let access_str = params.access.to_string();
105 let row = sqlx::query!(
106 r#"
107 INSERT INTO access_control_rules
108 (id, entity_type, entity_id, rule_type, rule_value, access, justification)
109 VALUES ($1, $2, $3, $4, $5, $6, $7)
110 ON CONFLICT (entity_type, entity_id, rule_type, rule_value)
111 DO UPDATE SET
112 access = EXCLUDED.access,
113 justification = COALESCE(EXCLUDED.justification, access_control_rules.justification),
114 updated_at = NOW()
115 RETURNING id, rule_type, rule_value, access, justification
116 "#,
117 id.as_str(),
118 params.entity_type.as_str(),
119 params.entity_id,
120 rule_type_str,
121 params.rule_value,
122 access_str,
123 params.justification,
124 )
125 .fetch_one(&*self.write_pool)
126 .await?;
127
128 Ok(AccessRule {
129 id: RuleId::new(row.id),
130 rule_type: RuleType::from(row.rule_type.as_str()),
131 rule_value: row.rule_value,
132 access: Access::from_str(&row.access)?,
133 justification: row.justification,
134 })
135 }
136
137 pub async fn set_justification(
138 &self,
139 rule_id: &RuleId,
140 justification: Option<&str>,
141 ) -> AuthzResult<bool> {
142 let result = sqlx::query!(
143 r#"UPDATE access_control_rules SET justification = $2, updated_at = NOW() WHERE id = $1"#,
144 rule_id.as_str(),
145 justification,
146 )
147 .execute(&*self.write_pool)
148 .await?;
149 Ok(result.rows_affected() > 0)
150 }
151
152 pub async fn delete_rule(&self, rule_id: &RuleId) -> AuthzResult<bool> {
153 let result = sqlx::query!(
154 r#"DELETE FROM access_control_rules WHERE id = $1"#,
155 rule_id.as_str(),
156 )
157 .execute(&*self.write_pool)
158 .await?;
159 Ok(result.rows_affected() > 0)
160 }
161}