Skip to main content

systemprompt_security/authz/repository/
rules.rs

1//! Access-control rule persistence.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use 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}