use std::fmt;
use soaprs_core::{SoapError, SoapResult};
macro_rules! identifier {
($name:ident, $description:literal, $validator:ident) => {
#[doc = $description]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> SoapResult<Self> {
let value = value.into();
$validator(stringify!($name), &value)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl TryFrom<String> for $name {
type Error = SoapError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<&str> for $name {
type Error = SoapError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
};
}
identifier!(
AuthorizationName,
"Stable authentication strategy, authorization policy, role, or permission name.",
validate_logical_name
);
identifier!(
PrincipalId,
"Opaque authenticated principal identity.",
validate_opaque_id
);
identifier!(
SessionId,
"Opaque session identity safe to transport as a cookie value.",
validate_session_id
);
fn validate_logical_name(kind: &str, value: &str) -> SoapResult<()> {
if value.is_empty()
|| !value.chars().all(|character| {
character == '.'
|| character == '_'
|| character == '-'
|| character == ':'
|| character.is_ascii_alphanumeric()
})
{
return Err(SoapError::validation(format!("invalid {kind} `{value}`")));
}
Ok(())
}
fn validate_opaque_id(kind: &str, value: &str) -> SoapResult<()> {
if value.is_empty()
|| value.trim() != value
|| value.len() > 1024
|| value.chars().any(char::is_control)
{
return Err(SoapError::validation(format!("invalid {kind}")));
}
Ok(())
}
fn validate_session_id(kind: &str, value: &str) -> SoapResult<()> {
if value.is_empty()
|| value.len() > 1024
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~'))
{
return Err(SoapError::validation(format!("invalid {kind}")));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{AuthorizationName, PrincipalId, SessionId};
#[test]
fn identifiers_reject_transport_and_control_fragments() {
assert!(AuthorizationName::new("jwt.access").is_ok());
assert!(AuthorizationName::new("bad policy").is_err());
assert!(PrincipalId::new("urn:user:42").is_ok());
assert!(PrincipalId::new("user\n42").is_err());
assert!(SessionId::new("session_42-token").is_ok());
assert!(SessionId::new("session=42").is_err());
}
}