1use crate::error::{Error, Result};
2use std::borrow::Borrow;
3use std::fmt;
4
5const MAX_PERMISSION_PART_LEN: usize = 128;
6
7fn normalize(value: &str) -> String {
9 value.trim().to_ascii_lowercase()
10}
11
12fn validate_segment(value: &str, kind: &str, allow_slash: bool) -> Result<()> {
14 if value.is_empty() {
15 return Err(Error::InvalidPermission(format!(
16 "{kind} must not be empty"
17 )));
18 }
19 if value.len() > MAX_PERMISSION_PART_LEN {
20 return Err(Error::InvalidPermission(format!(
21 "{kind} length must be <= {MAX_PERMISSION_PART_LEN}"
22 )));
23 }
24 if value == "*" {
25 return Ok(());
26 }
27 for segment in value.split('/') {
28 if segment.is_empty() {
29 return Err(Error::InvalidPermission(format!(
30 "{kind} contains empty segment"
31 )));
32 }
33 if !allow_slash && segment != value {
34 return Err(Error::InvalidPermission(format!(
35 "{kind} must not contain '/'"
36 )));
37 }
38 if !segment
39 .chars()
40 .all(|ch| matches!(ch, 'a'..='z' | '0'..='9' | '_' | '-'))
41 {
42 return Err(Error::InvalidPermission(format!(
43 "{kind} contains invalid characters"
44 )));
45 }
46 }
47 Ok(())
48}
49
50macro_rules! define_permission_part {
51 ($(#[$doc:meta])* $name:ident, $kind:expr, $allow_slash:expr) => {
52 $(#[$doc])*
53 #[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
54 pub struct $name(String);
55
56 impl $name {
57 pub fn parse(value: impl AsRef<str>) -> Result<Self> {
59 let normalized = normalize(value.as_ref());
60 validate_segment(&normalized, $kind, $allow_slash)?;
61 Ok(Self(normalized))
62 }
63
64 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68
69 pub fn is_wildcard(&self) -> bool {
71 self.0 == "*"
72 }
73 }
74
75 impl fmt::Display for $name {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.write_str(self.as_str())
78 }
79 }
80
81 impl AsRef<str> for $name {
82 fn as_ref(&self) -> &str {
83 self.as_str()
84 }
85 }
86
87 impl Borrow<str> for $name {
88 fn borrow(&self) -> &str {
89 self.as_str()
90 }
91 }
92
93 impl TryFrom<&str> for $name {
94 type Error = Error;
95
96 fn try_from(value: &str) -> Result<Self> {
97 Self::parse(value)
98 }
99 }
100
101 #[cfg(feature = "serde")]
102 impl serde::Serialize for $name {
103 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
104 where
105 S: serde::Serializer,
106 {
107 serializer.serialize_str(self.as_str())
108 }
109 }
110
111 #[cfg(feature = "serde")]
112 impl<'de> serde::Deserialize<'de> for $name {
113 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
114 where
115 D: serde::Deserializer<'de>,
116 {
117 let value = String::deserialize(deserializer)?;
118 Self::parse(value).map_err(serde::de::Error::custom)
119 }
120 }
121 };
122}
123
124define_permission_part!(
125 Resource,
127 "resource",
128 true
129);
130define_permission_part!(
131 Action,
133 "action",
134 false
135);
136
137#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
139pub struct Permission {
140 resource: Resource,
141 action: Action,
142}
143
144impl Permission {
145 pub fn new(resource: Resource, action: Action) -> Self {
147 Self { resource, action }
148 }
149
150 pub fn parse(value: impl AsRef<str>) -> Result<Self> {
152 let normalized = normalize(value.as_ref());
153 let mut parts = normalized.split(':');
154 let resource = parts.next().unwrap_or_default();
155 let action = parts.next().ok_or_else(|| {
156 Error::InvalidPermission("permission must be in resource:action format".to_string())
157 })?;
158 if parts.next().is_some() {
159 return Err(Error::InvalidPermission(
160 "permission must contain exactly one ':' separator".to_string(),
161 ));
162 }
163 Ok(Self::new(
164 Resource::parse(resource)?,
165 Action::parse(action)?,
166 ))
167 }
168
169 pub fn resource(&self) -> &Resource {
171 &self.resource
172 }
173
174 pub fn action(&self) -> &Action {
176 &self.action
177 }
178
179 pub fn has_wildcard(&self) -> bool {
181 self.resource.is_wildcard() || self.action.is_wildcard()
182 }
183
184 pub fn matches(&self, required: &Permission, enable_wildcard: bool) -> bool {
186 if !enable_wildcard && self.has_wildcard() {
187 return false;
188 }
189 if !enable_wildcard {
190 return self == required;
191 }
192 let resource_matches = self.resource.is_wildcard() || self.resource == required.resource;
193 let action_matches = self.action.is_wildcard() || self.action == required.action;
194 resource_matches && action_matches
195 }
196}
197
198impl fmt::Display for Permission {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 write!(f, "{}:{}", self.resource, self.action)
201 }
202}
203
204impl TryFrom<&str> for Permission {
205 type Error = Error;
206
207 fn try_from(value: &str) -> Result<Self> {
208 Self::parse(value)
209 }
210}
211
212#[cfg(feature = "serde")]
213impl serde::Serialize for Permission {
214 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
215 where
216 S: serde::Serializer,
217 {
218 serializer.serialize_str(&self.to_string())
219 }
220}
221
222#[cfg(feature = "serde")]
223impl<'de> serde::Deserialize<'de> for Permission {
224 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
225 where
226 D: serde::Deserializer<'de>,
227 {
228 let value = String::deserialize(deserializer)?;
229 Self::parse(value).map_err(serde::de::Error::custom)
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::{Action, MAX_PERMISSION_PART_LEN, Permission, Resource};
236 use crate::Error;
237
238 #[test]
239 fn permission_should_trim_and_lowercase() {
240 let permission = Permission::parse(" Billing/Invoice:Read ").expect("permission");
241 assert_eq!(permission.to_string(), "billing/invoice:read");
242 }
243
244 #[test]
245 fn permission_should_reject_resource_colon() {
246 let err = Permission::parse("billing:invoice:read").expect_err("must reject");
247 assert!(matches!(err, Error::InvalidPermission(_)));
248 }
249
250 #[test]
251 fn permission_should_reject_missing_separator_and_empty_segments() {
252 for value in ["invoice", "invoice:", ":read", "billing//invoice:read"] {
253 let err = Permission::parse(value).expect_err("must reject");
254 assert!(matches!(err, Error::InvalidPermission(_)));
255 }
256 }
257
258 #[test]
259 fn action_should_reject_slash_and_overlong_values() {
260 let slash_err = Action::parse("read/write").expect_err("must reject");
261 assert!(slash_err.to_string().contains("must not contain '/'"));
262
263 let oversized = "a".repeat(MAX_PERMISSION_PART_LEN + 1);
264 let length_err = Action::parse(oversized).expect_err("must reject");
265 assert!(length_err.to_string().contains("length must be"));
266 }
267
268 #[test]
269 fn wildcard_should_match_only_when_enabled() {
270 let granted = Permission::parse("invoice:*").expect("permission");
271 let required = Permission::parse("invoice:read").expect("permission");
272
273 assert!(!granted.matches(&required, false));
274 assert!(granted.matches(&required, true));
275 }
276
277 #[test]
278 fn resource_should_not_support_partial_wildcard() {
279 let err = Resource::parse("billing/*").expect_err("must reject");
280 assert!(matches!(err, Error::InvalidPermission(_)));
281 }
282
283 #[cfg(feature = "serde")]
284 #[test]
285 fn serde_should_validate_permission() {
286 let err = serde_json::from_str::<Permission>("\"billing:invoice:read\"")
287 .expect_err("must reject");
288 assert!(err.to_string().contains("exactly one"));
289 }
290}