Skip to main content

stateset_authz/
resource.rs

1//! Resources and actions for authorization checks.
2//!
3//! A [`Resource`] identifies *what* is being accessed, while an [`Action`]
4//! describes the operation being performed. Each action maps to a minimum
5//! [`PermissionLevel`] via [`Action::required_permission`].
6
7use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11use crate::PermissionLevel;
12
13/// A resource to be protected by authorization.
14///
15/// ```rust
16/// use stateset_authz::Resource;
17///
18/// let orders = Resource::new("orders");
19/// assert_eq!(orders.resource_type(), "orders");
20/// assert!(orders.resource_id().is_none());
21///
22/// let order = Resource::with_id("orders", "ord_123");
23/// assert_eq!(order.resource_id(), Some("ord_123"));
24/// ```
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub struct Resource {
27    resource_type: String,
28    resource_id: Option<String>,
29}
30
31impl Resource {
32    /// Creates a resource with only a type (e.g., "orders").
33    #[must_use]
34    pub fn new(resource_type: impl Into<String>) -> Self {
35        Self { resource_type: resource_type.into(), resource_id: None }
36    }
37
38    /// Creates a resource with a type and a specific ID.
39    #[must_use]
40    pub fn with_id(resource_type: impl Into<String>, resource_id: impl Into<String>) -> Self {
41        Self { resource_type: resource_type.into(), resource_id: Some(resource_id.into()) }
42    }
43
44    /// Returns the resource type (e.g., "orders", "customers").
45    #[must_use]
46    pub fn resource_type(&self) -> &str {
47        &self.resource_type
48    }
49
50    /// Returns the optional resource ID.
51    #[must_use]
52    pub fn resource_id(&self) -> Option<&str> {
53        self.resource_id.as_deref()
54    }
55}
56
57impl fmt::Display for Resource {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match &self.resource_id {
60            Some(id) => write!(f, "{}:{}", self.resource_type, id),
61            None => f.write_str(&self.resource_type),
62        }
63    }
64}
65
66/// An action to be performed on a resource.
67///
68/// Each action maps to a minimum required [`PermissionLevel`] via
69/// [`Action::required_permission`].
70///
71/// ```rust
72/// use stateset_authz::{Action, PermissionLevel};
73///
74/// assert_eq!(Action::Read.required_permission(), PermissionLevel::Read);
75/// assert_eq!(Action::Create.required_permission(), PermissionLevel::Write);
76/// assert_eq!(Action::Delete.required_permission(), PermissionLevel::Delete);
77/// ```
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
79#[non_exhaustive]
80pub enum Action {
81    /// Create a new resource.
82    Create,
83    /// Read / retrieve a resource.
84    Read,
85    /// Update an existing resource.
86    Update,
87    /// Delete a resource.
88    Delete,
89    /// List / query multiple resources.
90    List,
91    /// Execute a tool or command.
92    Execute,
93}
94
95impl Action {
96    /// Returns the minimum [`PermissionLevel`] required for this action.
97    ///
98    /// ```rust
99    /// use stateset_authz::{Action, PermissionLevel};
100    ///
101    /// assert_eq!(Action::List.required_permission(), PermissionLevel::Read);
102    /// assert_eq!(Action::Execute.required_permission(), PermissionLevel::Write);
103    /// ```
104    #[must_use]
105    pub const fn required_permission(self) -> PermissionLevel {
106        match self {
107            Self::Read | Self::List => PermissionLevel::Read,
108            Self::Create | Self::Update | Self::Execute => PermissionLevel::Write,
109            Self::Delete => PermissionLevel::Delete,
110        }
111    }
112
113    /// Returns all action variants.
114    #[must_use]
115    pub const fn all() -> &'static [Self] {
116        &[Self::Create, Self::Read, Self::Update, Self::Delete, Self::List, Self::Execute]
117    }
118}
119
120impl fmt::Display for Action {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        let s = match self {
123            Self::Create => "create",
124            Self::Read => "read",
125            Self::Update => "update",
126            Self::Delete => "delete",
127            Self::List => "list",
128            Self::Execute => "execute",
129        };
130        f.write_str(s)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    // -- Resource --
139
140    #[test]
141    fn resource_new_has_no_id() {
142        let r = Resource::new("orders");
143        assert_eq!(r.resource_type(), "orders");
144        assert!(r.resource_id().is_none());
145    }
146
147    #[test]
148    fn resource_with_id() {
149        let r = Resource::with_id("orders", "ord_123");
150        assert_eq!(r.resource_type(), "orders");
151        assert_eq!(r.resource_id(), Some("ord_123"));
152    }
153
154    #[test]
155    fn resource_display_no_id() {
156        let r = Resource::new("customers");
157        assert_eq!(r.to_string(), "customers");
158    }
159
160    #[test]
161    fn resource_display_with_id() {
162        let r = Resource::with_id("customers", "cust_42");
163        assert_eq!(r.to_string(), "customers:cust_42");
164    }
165
166    #[test]
167    fn resource_equality() {
168        let a = Resource::with_id("orders", "1");
169        let b = Resource::with_id("orders", "1");
170        let c = Resource::with_id("orders", "2");
171        assert_eq!(a, b);
172        assert_ne!(a, c);
173    }
174
175    #[test]
176    fn resource_serde_roundtrip() {
177        let r = Resource::with_id("products", "prod_99");
178        let json = serde_json::to_string(&r).unwrap();
179        let parsed: Resource = serde_json::from_str(&json).unwrap();
180        assert_eq!(parsed, r);
181    }
182
183    // -- Action --
184
185    #[test]
186    fn action_required_permission_read_ops() {
187        assert_eq!(Action::Read.required_permission(), PermissionLevel::Read);
188        assert_eq!(Action::List.required_permission(), PermissionLevel::Read);
189    }
190
191    #[test]
192    fn action_required_permission_write_ops() {
193        assert_eq!(Action::Create.required_permission(), PermissionLevel::Write);
194        assert_eq!(Action::Update.required_permission(), PermissionLevel::Write);
195        assert_eq!(Action::Execute.required_permission(), PermissionLevel::Write);
196    }
197
198    #[test]
199    fn action_required_permission_delete() {
200        assert_eq!(Action::Delete.required_permission(), PermissionLevel::Delete);
201    }
202
203    #[test]
204    fn action_display() {
205        assert_eq!(Action::Create.to_string(), "create");
206        assert_eq!(Action::Read.to_string(), "read");
207        assert_eq!(Action::Update.to_string(), "update");
208        assert_eq!(Action::Delete.to_string(), "delete");
209        assert_eq!(Action::List.to_string(), "list");
210        assert_eq!(Action::Execute.to_string(), "execute");
211    }
212
213    #[test]
214    fn action_all_returns_six() {
215        assert_eq!(Action::all().len(), 6);
216    }
217
218    #[test]
219    fn action_serde_roundtrip() {
220        for &action in Action::all() {
221            let json = serde_json::to_string(&action).unwrap();
222            let parsed: Action = serde_json::from_str(&json).unwrap();
223            assert_eq!(parsed, action);
224        }
225    }
226}