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    Skill,
89    Hook,
90}
91
92impl EntityKind {
93    pub const fn as_str(self) -> &'static str {
94        match self {
95            Self::GatewayRoute => "gateway_route",
96            Self::McpServer => "mcp_server",
97            Self::Plugin => "plugin",
98            Self::Agent => "agent",
99            Self::Marketplace => "marketplace",
100            Self::Skill => "skill",
101            Self::Hook => "hook",
102        }
103    }
104}
105
106impl FromStr for EntityKind {
107    type Err = AuthzError;
108
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        match s {
111            "gateway_route" => Ok(Self::GatewayRoute),
112            "mcp_server" => Ok(Self::McpServer),
113            "plugin" => Ok(Self::Plugin),
114            "agent" => Ok(Self::Agent),
115            "marketplace" => Ok(Self::Marketplace),
116            "skill" => Ok(Self::Skill),
117            "hook" => Ok(Self::Hook),
118            other => Err(AuthzError::Validation(format!(
119                "unknown entity_type: {other}"
120            ))),
121        }
122    }
123}
124
125impl fmt::Display for EntityKind {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        f.write_str(self.as_str())
128    }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
132pub struct AccessRule {
133    pub id: RuleId,
134    pub rule_type: RuleType,
135    pub rule_value: String,
136    pub access: Access,
137    pub default_included: bool,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub justification: Option<String>,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(tag = "decision", rename_all = "lowercase")]
144pub enum Decision {
145    Allow,
146    Deny {
147        reason: String,
148        #[serde(default, skip_serializing_if = "Option::is_none")]
149        justification: Option<String>,
150    },
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct AuthzRequest {
155    pub entity_type: EntityKind,
156    pub entity_id: String,
157    pub user_id: UserId,
158    #[serde(default)]
159    pub roles: Vec<String>,
160    #[serde(default)]
161    pub department: String,
162    pub trace_id: TraceId,
163    #[serde(default)]
164    // JSON: extension hook contract — context is forwarded verbatim to webhook
165    // handlers and is intentionally schema-free at this boundary.
166    pub context: serde_json::Value,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(tag = "decision", rename_all = "lowercase")]
171pub enum AuthzDecision {
172    Allow,
173    Deny { reason: String, policy: String },
174}