Skip to main content

systemprompt_security/authz/types/
kinds.rs

1//! Rule and entity kind tags with parse/display.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::borrow::Cow;
7use std::fmt;
8use std::str::FromStr;
9
10use serde::{Deserialize, Serialize};
11
12use crate::authz::error::AuthzError;
13
14/// Open subject-dimension vocabulary bound to `access_control_rules.rule_type`.
15///
16/// Core mints exactly two: [`RuleType::USER`] and [`RuleType::ROLE`]. Every
17/// other dimension (department, cost centre, clearance, ...) is an extension
18/// concern, minted with [`RuleType::extension`] and taught to the resolver via
19/// a [`SubjectDimension`][sd] registered by
20/// [`register_subject_attribute_provider!`][macro]. Core never interprets an
21/// extension slug.
22///
23/// This mirrors [`AuthzContext`][ctx]: the column is an open vocabulary
24/// validated at the Rust boundary rather than by a SQL `CHECK`, so an
25/// unrecognised-but-well-formed slug is data, not an error.
26///
27/// [sd]: crate::authz::subject::SubjectDimension
28/// [macro]: crate::register_subject_attribute_provider
29/// [ctx]: super::request::AuthzContext
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
31#[serde(transparent)]
32pub struct RuleType(Cow<'static, str>);
33
34impl RuleType {
35    pub const USER: Self = Self(Cow::Borrowed("user"));
36    pub const ROLE: Self = Self(Cow::Borrowed("role"));
37
38    pub fn extension(slug: impl Into<Cow<'static, str>>) -> Result<Self, AuthzError> {
39        let slug = slug.into();
40        let well_formed = !slug.is_empty()
41            && !slug.starts_with('_')
42            && !slug.ends_with('_')
43            && slug
44                .chars()
45                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
46        if !well_formed || slug == Self::USER.as_str() || slug == Self::ROLE.as_str() {
47            return Err(AuthzError::InvalidRuleType(slug.into_owned()));
48        }
49        Ok(Self(slug))
50    }
51
52    #[must_use]
53    pub fn as_str(&self) -> &str {
54        &self.0
55    }
56}
57
58impl fmt::Display for RuleType {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.write_str(self.as_str())
61    }
62}
63
64impl From<&str> for RuleType {
65    fn from(s: &str) -> Self {
66        match s {
67            "user" => Self::USER,
68            "role" => Self::ROLE,
69            other => Self(Cow::Owned(other.to_owned())),
70        }
71    }
72}
73
74impl sqlx::Type<sqlx::Postgres> for RuleType {
75    fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
76        <str as sqlx::Type<sqlx::Postgres>>::type_info()
77    }
78
79    fn compatible(ty: &<sqlx::Postgres as sqlx::Database>::TypeInfo) -> bool {
80        <str as sqlx::Type<sqlx::Postgres>>::compatible(ty)
81    }
82}
83
84impl<'q> sqlx::Encode<'q, sqlx::Postgres> for RuleType {
85    fn encode_by_ref(
86        &self,
87        buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer,
88    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
89        <&str as sqlx::Encode<'q, sqlx::Postgres>>::encode(self.as_str(), buf)
90    }
91}
92
93impl<'r> sqlx::Decode<'r, sqlx::Postgres> for RuleType {
94    fn decode(
95        value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
96    ) -> Result<Self, sqlx::error::BoxDynError> {
97        let raw = <&str as sqlx::Decode<'r, sqlx::Postgres>>::decode(value)?;
98        Ok(Self::from(raw))
99    }
100}
101
102#[derive(
103    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, sqlx::Type,
104)]
105#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
106#[serde(rename_all = "lowercase")]
107pub enum Access {
108    Allow,
109    Deny,
110}
111
112impl fmt::Display for Access {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.write_str(match *self {
115            Self::Allow => "allow",
116            Self::Deny => "deny",
117        })
118    }
119}
120
121impl FromStr for Access {
122    type Err = AuthzError;
123
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        match s {
126            "allow" => Ok(Self::Allow),
127            "deny" => Ok(Self::Deny),
128            other => Err(AuthzError::InvalidAccess(other.to_owned())),
129        }
130    }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum EntityKind {
136    GatewayRoute,
137    McpServer,
138    Plugin,
139    Agent,
140    Marketplace,
141    Skill,
142    Hook,
143    SlackWorkspace,
144    SlackChannel,
145    TeamsTenant,
146    TeamsConversation,
147}
148
149impl EntityKind {
150    pub const fn as_str(self) -> &'static str {
151        match self {
152            Self::GatewayRoute => "gateway_route",
153            Self::McpServer => "mcp_server",
154            Self::Plugin => "plugin",
155            Self::Agent => "agent",
156            Self::Marketplace => "marketplace",
157            Self::Skill => "skill",
158            Self::Hook => "hook",
159            Self::SlackWorkspace => "slack_workspace",
160            Self::SlackChannel => "slack_channel",
161            Self::TeamsTenant => "teams_tenant",
162            Self::TeamsConversation => "teams_conversation",
163        }
164    }
165}
166
167impl FromStr for EntityKind {
168    type Err = AuthzError;
169
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        match s {
172            "gateway_route" => Ok(Self::GatewayRoute),
173            "mcp_server" => Ok(Self::McpServer),
174            "plugin" => Ok(Self::Plugin),
175            "agent" => Ok(Self::Agent),
176            "marketplace" => Ok(Self::Marketplace),
177            "skill" => Ok(Self::Skill),
178            "hook" => Ok(Self::Hook),
179            "slack_workspace" => Ok(Self::SlackWorkspace),
180            "slack_channel" => Ok(Self::SlackChannel),
181            "teams_tenant" => Ok(Self::TeamsTenant),
182            "teams_conversation" => Ok(Self::TeamsConversation),
183            other => Err(AuthzError::Validation(format!(
184                "unknown entity_type: {other}"
185            ))),
186        }
187    }
188}
189
190impl fmt::Display for EntityKind {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        f.write_str(self.as_str())
193    }
194}