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