Skip to main content

systemprompt_security/authz/
types.rs

1//! Wire and storage types for authorization decisions.
2//!
3//! Types fall into two groups:
4//!
5//! 1. **Storage** — [`RuleType`], [`Access`], [`AccessRule`] map to columns in
6//!    `access_control_rules`. They round-trip through serde and sqlx.
7//! 2. **Decision** — [`Decision`] is the in-process resolver output;
8//!    [`AuthzRequest`] / [`AuthzDecision`] are the webhook wire format sent to
9//!    and parsed back from extension hook handlers.
10
11use std::fmt;
12use std::str::FromStr;
13
14use serde::{Deserialize, Serialize};
15use systemprompt_identifiers::{RuleId, TraceId, UserId};
16
17use super::error::AuthzError;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
20#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
21#[serde(rename_all = "lowercase")]
22pub enum RuleType {
23    User,
24    Role,
25    Department,
26}
27
28impl fmt::Display for RuleType {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        f.write_str(match *self {
31            Self::User => "user",
32            Self::Role => "role",
33            Self::Department => "department",
34        })
35    }
36}
37
38impl FromStr for RuleType {
39    type Err = AuthzError;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        match s {
43            "user" => Ok(Self::User),
44            "role" => Ok(Self::Role),
45            "department" => Ok(Self::Department),
46            other => Err(AuthzError::InvalidRuleType(other.to_owned())),
47        }
48    }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
52#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
53#[serde(rename_all = "lowercase")]
54pub enum Access {
55    Allow,
56    Deny,
57}
58
59impl fmt::Display for Access {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.write_str(match *self {
62            Self::Allow => "allow",
63            Self::Deny => "deny",
64        })
65    }
66}
67
68impl FromStr for Access {
69    type Err = AuthzError;
70
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        match s {
73            "allow" => Ok(Self::Allow),
74            "deny" => Ok(Self::Deny),
75            other => Err(AuthzError::InvalidAccess(other.to_owned())),
76        }
77    }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum EntityKind {
83    GatewayRoute,
84    McpServer,
85    Plugin,
86    Agent,
87    Marketplace,
88}
89
90impl EntityKind {
91    pub const fn as_str(self) -> &'static str {
92        match self {
93            Self::GatewayRoute => "gateway_route",
94            Self::McpServer => "mcp_server",
95            Self::Plugin => "plugin",
96            Self::Agent => "agent",
97            Self::Marketplace => "marketplace",
98        }
99    }
100}
101
102impl FromStr for EntityKind {
103    type Err = AuthzError;
104
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        match s {
107            "gateway_route" => Ok(Self::GatewayRoute),
108            "mcp_server" => Ok(Self::McpServer),
109            "plugin" => Ok(Self::Plugin),
110            "agent" => Ok(Self::Agent),
111            "marketplace" => Ok(Self::Marketplace),
112            other => Err(AuthzError::Validation(format!(
113                "unknown entity_type: {other}"
114            ))),
115        }
116    }
117}
118
119impl fmt::Display for EntityKind {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        f.write_str(self.as_str())
122    }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
126pub struct AccessRule {
127    pub id: RuleId,
128    pub rule_type: RuleType,
129    pub rule_value: String,
130    pub access: Access,
131    pub default_included: bool,
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub justification: Option<String>,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(tag = "decision", rename_all = "lowercase")]
138pub enum Decision {
139    Allow,
140    Deny {
141        reason: String,
142        #[serde(default, skip_serializing_if = "Option::is_none")]
143        justification: Option<String>,
144    },
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct AuthzRequest {
149    pub entity_type: EntityKind,
150    pub entity_id: String,
151    pub user_id: UserId,
152    #[serde(default)]
153    pub roles: Vec<String>,
154    #[serde(default)]
155    pub department: String,
156    pub trace_id: TraceId,
157    #[serde(default)]
158    // JSON: extension hook contract — context is forwarded verbatim to webhook
159    // handlers and is intentionally schema-free at this boundary.
160    pub context: serde_json::Value,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(tag = "decision", rename_all = "lowercase")]
165pub enum AuthzDecision {
166    Allow,
167    Deny { reason: String, policy: String },
168}