soaprs_auth/
identifier.rs1use std::fmt;
4
5use soaprs_core::{SoapError, SoapResult};
6
7macro_rules! identifier {
8 ($name:ident, $description:literal, $validator:ident) => {
9 #[doc = $description]
10 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
11 pub struct $name(String);
12
13 impl $name {
14 pub fn new(value: impl Into<String>) -> SoapResult<Self> {
16 let value = value.into();
17 $validator(stringify!($name), &value)?;
18 Ok(Self(value))
19 }
20
21 pub fn as_str(&self) -> &str {
23 &self.0
24 }
25 }
26
27 impl fmt::Display for $name {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29 formatter.write_str(&self.0)
30 }
31 }
32
33 impl TryFrom<String> for $name {
34 type Error = SoapError;
35
36 fn try_from(value: String) -> Result<Self, Self::Error> {
37 Self::new(value)
38 }
39 }
40
41 impl TryFrom<&str> for $name {
42 type Error = SoapError;
43
44 fn try_from(value: &str) -> Result<Self, Self::Error> {
45 Self::new(value)
46 }
47 }
48 };
49}
50
51identifier!(
52 AuthorizationName,
53 "Stable authentication strategy, authorization policy, role, or permission name.",
54 validate_logical_name
55);
56identifier!(
57 PrincipalId,
58 "Opaque authenticated principal identity.",
59 validate_opaque_id
60);
61identifier!(
62 SessionId,
63 "Opaque session identity safe to transport as a cookie value.",
64 validate_session_id
65);
66
67fn validate_logical_name(kind: &str, value: &str) -> SoapResult<()> {
68 if value.is_empty()
69 || !value.chars().all(|character| {
70 character == '.'
71 || character == '_'
72 || character == '-'
73 || character == ':'
74 || character.is_ascii_alphanumeric()
75 })
76 {
77 return Err(SoapError::validation(format!("invalid {kind} `{value}`")));
78 }
79 Ok(())
80}
81
82fn validate_opaque_id(kind: &str, value: &str) -> SoapResult<()> {
83 if value.is_empty()
84 || value.trim() != value
85 || value.len() > 1024
86 || value.chars().any(char::is_control)
87 {
88 return Err(SoapError::validation(format!("invalid {kind}")));
89 }
90 Ok(())
91}
92
93fn validate_session_id(kind: &str, value: &str) -> SoapResult<()> {
94 if value.is_empty()
95 || value.len() > 1024
96 || !value
97 .bytes()
98 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~'))
99 {
100 return Err(SoapError::validation(format!("invalid {kind}")));
101 }
102 Ok(())
103}
104
105#[cfg(test)]
106mod tests {
107 use super::{AuthorizationName, PrincipalId, SessionId};
108
109 #[test]
110 fn identifiers_reject_transport_and_control_fragments() {
111 assert!(AuthorizationName::new("jwt.access").is_ok());
112 assert!(AuthorizationName::new("bad policy").is_err());
113 assert!(PrincipalId::new("urn:user:42").is_ok());
114 assert!(PrincipalId::new("user\n42").is_err());
115 assert!(SessionId::new("session_42-token").is_ok());
116 assert!(SessionId::new("session=42").is_err());
117 }
118}