use std::{fmt, str::FromStr};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
use crate::{Error, Result};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SubjectKind {
PrincipalId,
PrincipalName,
GroupId,
GroupName,
Role,
PrincipalApiKey,
Unknown,
}
impl SubjectKind {
pub fn as_str(self) -> &'static str {
match self {
Self::PrincipalId => "principal_id",
Self::PrincipalName => "principal_name",
Self::GroupId => "group_id",
Self::GroupName => "group_name",
Self::Role => "role",
Self::PrincipalApiKey => "principal_api_key",
Self::Unknown => "unknown",
}
}
fn prefix(self) -> &'static str {
match self {
Self::PrincipalId => "p:",
Self::PrincipalName => "P:",
Self::GroupId => "g:",
Self::GroupName => "G:",
Self::Role => "r:",
Self::PrincipalApiKey => "a:",
Self::Unknown => "",
}
}
}
const KINDS: [SubjectKind; 6] = [
SubjectKind::PrincipalId,
SubjectKind::PrincipalName,
SubjectKind::GroupId,
SubjectKind::GroupName,
SubjectKind::Role,
SubjectKind::PrincipalApiKey,
];
impl FromStr for SubjectKind {
type Err = Error;
fn from_str(value: &str) -> Result<Self> {
if value == "unknown" {
return Ok(Self::Unknown);
}
KINDS
.into_iter()
.find(|kind| kind.as_str() == value)
.ok_or_else(|| invalid("Invalid subject kind"))
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Subject {
kind: SubjectKind,
value: String,
}
fn invalid(message: &str) -> Error {
Error::InvalidInput {
message: message.into(),
}
}
impl Subject {
pub fn new(kind: SubjectKind, value: impl Into<String>) -> Result<Self> {
let value = value.into();
if value.is_empty() {
return Err(invalid("Subject value must be a nonempty string"));
}
if kind == SubjectKind::Unknown
&& (!value
.split_once(':')
.is_some_and(|(prefix, value)| !prefix.is_empty() && !value.is_empty())
|| KINDS.iter().any(|kind| value.starts_with(kind.prefix())))
{
return Err(invalid("Invalid unknown subject selector"));
}
Ok(Self { kind, value })
}
pub fn principal_id(id: impl Into<String>) -> Result<Self> {
Self::new(SubjectKind::PrincipalId, id)
}
pub fn principal_name(name: impl Into<String>) -> Result<Self> {
Self::new(SubjectKind::PrincipalName, name)
}
pub fn group_id(id: impl Into<String>) -> Result<Self> {
Self::new(SubjectKind::GroupId, id)
}
pub fn group_name(name: impl Into<String>) -> Result<Self> {
Self::new(SubjectKind::GroupName, name)
}
pub fn role(name: impl Into<String>) -> Result<Self> {
Self::new(SubjectKind::Role, name)
}
pub fn principal_api_key(key: impl Into<String>) -> Result<Self> {
Self::new(SubjectKind::PrincipalApiKey, key)
}
pub fn kind(&self) -> SubjectKind {
self.kind
}
pub fn value(&self) -> &str {
&self.value
}
pub fn display_value(&self) -> &str {
if self.kind == SubjectKind::PrincipalApiKey {
"<redacted>"
} else {
&self.value
}
}
pub fn to_wire(&self) -> String {
format!("{}{}", self.kind.prefix(), self.value)
}
pub fn from_canonical(value: &str) -> Result<Self> {
let subject: Self = value.parse()?;
if !matches!(
subject.kind,
SubjectKind::PrincipalId
| SubjectKind::GroupId
| SubjectKind::Role
| SubjectKind::Unknown
) {
return Err(invalid("Server returned a non-canonical subject"));
}
Ok(subject)
}
pub fn deserialize_canonical<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
Self::from_canonical(&String::deserialize(deserializer)?).map_err(D::Error::custom)
}
pub fn validate_principal(&self) -> Result<()> {
if matches!(
self.kind,
SubjectKind::PrincipalId | SubjectKind::PrincipalName | SubjectKind::PrincipalApiKey
) {
Ok(())
} else {
Err(invalid("Subject kind is not valid for a principal lookup"))
}
}
pub fn validate_group(&self) -> Result<()> {
if matches!(self.kind, SubjectKind::GroupId | SubjectKind::GroupName) {
Ok(())
} else {
Err(invalid("Subject kind is not valid for a group lookup"))
}
}
pub fn validate_role_member(&self) -> Result<()> {
if self.kind == SubjectKind::Role {
Err(invalid("Roles cannot be members of other roles"))
} else {
Ok(())
}
}
}
impl FromStr for Subject {
type Err = Error;
fn from_str(value: &str) -> Result<Self> {
for kind in KINDS {
if let Some(value) = value.strip_prefix(kind.prefix()) {
return Self::new(kind, value);
}
}
Self::new(SubjectKind::Unknown, value)
}
}
impl fmt::Debug for Subject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Subject")
.field("kind", &self.kind)
.field("value", &self.display_value())
.finish()
}
}
impl fmt::Display for Subject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.kind.prefix(), self.display_value())
}
}
impl Serialize for Subject {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_wire())
}
}
impl<'de> Deserialize<'de> for Subject {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
String::deserialize(deserializer)?
.parse()
.map_err(D::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn selectors_roundtrip_without_normalizing_values() {
for kind in KINDS {
for value in [
"alice@example.com",
"p:literal-id",
"Engineering / R&D",
"名前",
] {
let subject = Subject::new(kind, value).unwrap();
assert_eq!(subject.to_wire().parse::<Subject>().unwrap(), subject);
let json = serde_json::to_string(&subject).unwrap();
assert_eq!(serde_json::from_str::<Subject>(&json).unwrap(), subject);
assert_eq!(subject.value(), value);
assert!(Subject::new(kind, "").is_err());
}
}
}
#[test]
fn contexts_and_canonical_ids() {
for kind in KINDS {
let subject = Subject::new(kind, "value").unwrap();
assert_eq!(
subject.validate_principal().is_ok(),
matches!(
kind,
SubjectKind::PrincipalId
| SubjectKind::PrincipalName
| SubjectKind::PrincipalApiKey
)
);
assert_eq!(
subject.validate_group().is_ok(),
matches!(kind, SubjectKind::GroupId | SubjectKind::GroupName)
);
assert_eq!(
subject.validate_role_member().is_ok(),
kind != SubjectKind::Role
);
assert_eq!(
Subject::from_canonical(&subject.to_wire()).is_ok(),
matches!(
kind,
SubjectKind::PrincipalId | SubjectKind::GroupId | SubjectKind::Role
)
);
}
}
#[test]
fn unknown_subjects_preserve_canonical_wire_strings() {
for wire in ["x:future-subject", "future:literal:identifier"] {
let subject = Subject::from_canonical(wire).unwrap();
assert_eq!(subject.kind(), SubjectKind::Unknown);
assert_eq!(subject.value(), wire);
assert_eq!(subject.to_wire(), wire);
assert_eq!(serde_json::to_value(&subject).unwrap(), wire);
assert_eq!(
serde_json::from_value::<Subject>(serde_json::json!(wire)).unwrap(),
subject
);
assert!(subject.validate_principal().is_err());
assert!(subject.validate_group().is_err());
}
for wire in [
"",
"no-prefix",
":value",
"x:",
"p:value",
"P:value",
"g:value",
"G:value",
"r:value",
"a:secret",
] {
assert!(Subject::new(SubjectKind::Unknown, wire).is_err());
}
}
#[test]
fn acl_page_preserves_known_and_unknown_subjects() {
let payload = serde_json::json!({"entries": [
{"subject_id": "p:known", "object": "system", "privilege": "USAGE"},
{"subject_id": "x:future-subject", "object": "system", "privilege": "USAGE"}
]});
let page: crate::authz::ListAccessControlEntriesResponse =
serde_json::from_value(payload.clone()).unwrap();
assert_eq!(page.entries.len(), 2);
assert_eq!(page.entries[0].subject_id.kind(), SubjectKind::PrincipalId);
assert_eq!(page.entries[1].subject_id.kind(), SubjectKind::Unknown);
assert_eq!(serde_json::to_value(page).unwrap(), payload);
}
#[test]
fn secrets_are_only_exposed_by_explicit_serialization() {
let secret = "never-log-this-key";
let subject = Subject::principal_api_key(secret).unwrap();
assert!(!format!("{subject:?} {subject}").contains(secret));
assert!(serde_json::to_string(&subject).unwrap().contains(secret));
let error = Subject::from_canonical(&subject.to_wire()).unwrap_err();
assert!(!error.to_string().contains(secret));
assert!(!format!("{error:?}").contains(secret));
let error = secret.parse::<Subject>().unwrap_err();
assert!(!error.to_string().contains(secret));
}
}