use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use wyrd_spec::DataTenantId;
use wyrd_spec::auth::PrincipalKindTag;
use wyrd_spec::reference::{CardRef, CardRefScope};
use crate::permission::PermissionSet;
pub use wyrd_spec::auth::PrincipalId;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Principal {
pub id: PrincipalId,
pub kind: PrincipalKind,
pub tenant_id: DataTenantId,
pub roles: Vec<RoleRef>,
pub effective_permissions: PermissionSet,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credential_id: Option<Uuid>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind", deny_unknown_fields)]
pub enum PrincipalKind {
TenantAdmin,
User,
Service {
card_ref: Option<CardRef>,
card_ref_scope: CardRefScope,
},
Agent {
card_ref: CardRef,
card_ref_scope: CardRefScope,
},
System {
card_ref_scope: CardRefScope,
},
}
impl PrincipalKind {
#[must_use]
pub fn tag(&self) -> PrincipalKindTag {
match self {
Self::TenantAdmin => PrincipalKindTag::TenantAdmin,
Self::User => PrincipalKindTag::User,
Self::Service { .. } => PrincipalKindTag::Service,
Self::Agent { .. } => PrincipalKindTag::Agent,
Self::System { .. } => PrincipalKindTag::System,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RoleRef(String);
impl RoleRef {
pub fn new(value: &str) -> Result<Self, InvalidRoleName> {
if value.is_empty()
|| value.len() > 64
|| !value
.bytes()
.all(|byte| matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'_'))
{
return Err(InvalidRoleName);
}
Ok(Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for RoleRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for RoleRef {
type Err = InvalidRoleName;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Debug, thiserror::Error)]
#[error("role name must match ^[a-z0-9_]{{1,64}}$")]
pub struct InvalidRoleName;
impl Principal {
#[must_use]
pub fn new(
id: PrincipalId,
kind: PrincipalKind,
tenant_id: DataTenantId,
roles: Vec<RoleRef>,
effective_permissions: PermissionSet,
) -> Self {
Self {
id,
kind,
tenant_id,
roles,
effective_permissions,
credential_id: None,
}
}
#[must_use]
pub fn with_credential_id(mut self, credential_id: Option<Uuid>) -> Self {
self.credential_id = credential_id;
self
}
#[must_use]
pub fn card_ref(&self) -> Option<&CardRef> {
match &self.kind {
PrincipalKind::Service { card_ref, .. } => card_ref.as_ref(),
PrincipalKind::Agent { card_ref, .. } => Some(card_ref),
PrincipalKind::TenantAdmin | PrincipalKind::User | PrincipalKind::System { .. } => None,
}
}
#[must_use]
pub fn owner_service_uid(&self) -> Option<&wyrd_spec::ids::CardUid> {
match &self.kind {
PrincipalKind::Service {
card_ref: Some(card_ref),
..
} if card_ref.kind == wyrd_spec::envelope::CardKind::Service => card_ref.uid.as_ref(),
_ => None,
}
}
#[must_use]
pub fn card_ref_scope(&self) -> Option<&CardRefScope> {
match &self.kind {
PrincipalKind::Service { card_ref_scope, .. }
| PrincipalKind::Agent { card_ref_scope, .. }
| PrincipalKind::System { card_ref_scope } => Some(card_ref_scope),
PrincipalKind::TenantAdmin | PrincipalKind::User => None,
}
}
#[must_use]
pub fn card_attribution(&self) -> CardAttribution<'_> {
match &self.kind {
PrincipalKind::Service {
card_ref: Some(_),
card_ref_scope,
}
| PrincipalKind::Agent { card_ref_scope, .. }
| PrincipalKind::System { card_ref_scope } => CardAttribution::Scoped(card_ref_scope),
PrincipalKind::Service { card_ref: None, .. }
| PrincipalKind::TenantAdmin
| PrincipalKind::User => CardAttribution::AnyRegistered,
}
}
#[must_use]
pub fn authorizes_card(&self, card: &CardRef) -> bool {
self.card_ref_scope()
.is_some_and(|scope| scope.authorizes(card))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CardAttribution<'a> {
Scoped(&'a CardRefScope),
AnyRegistered,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PrincipalRef {
pub id: PrincipalId,
pub kind: PrincipalKind,
}
impl PrincipalRef {
#[must_use]
pub fn from_principal(principal: &Principal) -> Self {
Self {
id: principal.id,
kind: principal.kind.clone(),
}
}
#[must_use]
pub fn card_ref(&self) -> Option<&CardRef> {
match &self.kind {
PrincipalKind::Service { card_ref, .. } => card_ref.as_ref(),
PrincipalKind::Agent { card_ref, .. } => Some(card_ref),
PrincipalKind::TenantAdmin | PrincipalKind::User | PrincipalKind::System { .. } => None,
}
}
#[must_use]
pub fn card_ref_scope(&self) -> Option<&CardRefScope> {
match &self.kind {
PrincipalKind::Service { card_ref_scope, .. }
| PrincipalKind::Agent { card_ref_scope, .. }
| PrincipalKind::System { card_ref_scope } => Some(card_ref_scope),
PrincipalKind::TenantAdmin | PrincipalKind::User => None,
}
}
}
#[cfg(test)]
mod tests {
use super::{CardAttribution, Principal, PrincipalId, PrincipalKind, PrincipalRef, RoleRef};
use crate::permission::{Permission, PermissionSet};
use wyrd_semver::VersionBlock;
use wyrd_spec::envelope::CardKind;
use wyrd_spec::ids::{CardName, SpaceName};
use wyrd_spec::reference::{CardRef, CardRefScope};
fn service_card_ref() -> CardRef {
CardRef {
kind: CardKind::Service,
name: CardName::new("billing").expect("static card name is valid"),
version: VersionBlock::parse("1.0.0").expect("static version is valid"),
space: Some(SpaceName::new("prod").expect("static space is valid")),
uid: None,
}
}
#[test]
fn role_ref_accepts_locked_name_shape() {
let role = RoleRef::new("credential_admin").expect("role name is valid");
assert_eq!(role.as_str(), "credential_admin");
}
#[test]
fn role_ref_rejects_punctuation_and_uppercase() {
assert!(RoleRef::new("RuntimeAdmin").is_err());
assert!(RoleRef::new("runtime-admin").is_err());
}
#[test]
fn principal_id_roundtrips_as_uuid() {
let uuid = uuid::Uuid::now_v7();
let id = PrincipalId::new(uuid);
assert_eq!(id.to_string(), uuid.to_string());
assert_eq!(id.as_uuid(), uuid);
}
#[test]
fn service_kind_carries_card_ref() {
let card_ref = service_card_ref();
let principal = Principal {
id: PrincipalId::new(uuid::Uuid::now_v7()),
kind: PrincipalKind::Service {
card_ref: Some(card_ref.clone()),
card_ref_scope: CardRefScope::own(&card_ref),
},
tenant_id: wyrd_spec::DataTenantId::new_v7(),
roles: vec![RoleRef::new("agent").expect("static role is valid")],
effective_permissions: PermissionSet::from_iter([Permission::card_read()]),
credential_id: None,
};
assert_eq!(principal.card_ref(), Some(&card_ref));
}
#[test]
fn owner_service_uid_copies_only_a_resolved_service_card() {
let principal = |kind| Principal {
id: PrincipalId::new(uuid::Uuid::now_v7()),
kind,
tenant_id: wyrd_spec::DataTenantId::new_v7(),
roles: Vec::new(),
effective_permissions: PermissionSet::new(),
credential_id: None,
};
let uid = wyrd_spec::ids::CardUid::from_uuid(uuid::Uuid::now_v7()).expect("v7 Card UID");
let resolved = CardRef {
uid: Some(uid.clone()),
..service_card_ref()
};
let service = |card_ref: CardRef| PrincipalKind::Service {
card_ref_scope: CardRefScope::own(&card_ref),
card_ref: Some(card_ref),
};
assert_eq!(
principal(service(resolved.clone())).owner_service_uid(),
Some(&uid)
);
assert_eq!(
principal(service(service_card_ref())).owner_service_uid(),
None
);
let agent = CardRef {
kind: CardKind::Agent,
..resolved
};
assert_eq!(
principal(PrincipalKind::Agent {
card_ref_scope: CardRefScope::own(&agent),
card_ref: agent,
})
.owner_service_uid(),
None
);
}
#[test]
fn principal_ref_keeps_card_ref_projection() {
let card_ref = service_card_ref();
let principal = Principal {
id: PrincipalId::new(uuid::Uuid::now_v7()),
kind: PrincipalKind::Service {
card_ref: Some(card_ref.clone()),
card_ref_scope: CardRefScope::own(&card_ref),
},
tenant_id: wyrd_spec::DataTenantId::new_v7(),
roles: vec![RoleRef::new("agent").expect("static role is valid")],
effective_permissions: PermissionSet::new(),
credential_id: None,
};
let principal_ref = PrincipalRef::from_principal(&principal);
assert_eq!(principal_ref.id, principal.id);
assert_eq!(principal_ref.card_ref(), Some(&card_ref));
assert_eq!(
principal_ref.card_ref_scope(),
Some(&CardRefScope::own(&card_ref))
);
assert_eq!(principal_ref.kind, principal.kind);
}
#[test]
fn principal_authorizes_cards_from_scope() {
let own = service_card_ref();
let mut composed = service_card_ref();
composed.name = CardName::new("composed").expect("static card name is valid");
let principal = Principal {
id: PrincipalId::new(uuid::Uuid::now_v7()),
kind: PrincipalKind::Service {
card_ref: Some(own.clone()),
card_ref_scope: CardRefScope::from_root_and_members(&own, [composed.clone()]),
},
tenant_id: wyrd_spec::DataTenantId::new_v7(),
roles: Vec::new(),
effective_permissions: PermissionSet::new(),
credential_id: None,
};
assert!(principal.authorizes_card(&own));
assert!(principal.authorizes_card(&composed));
let mut unrelated = service_card_ref();
unrelated.name = CardName::new("unrelated").expect("static card name is valid");
assert!(!principal.authorizes_card(&unrelated));
}
#[test]
fn only_unbound_principals_attribute_to_any_registered_card() {
let own = service_card_ref();
let scope = CardRefScope::own(&own);
let principal = |kind| {
Principal::new(
PrincipalId::new(uuid::Uuid::now_v7()),
kind,
wyrd_spec::DataTenantId::new_v7(),
Vec::new(),
PermissionSet::new(),
)
};
let bound = principal(PrincipalKind::Service {
card_ref: Some(own),
card_ref_scope: scope.clone(),
});
assert_eq!(bound.card_attribution(), CardAttribution::Scoped(&scope));
for unbound in [
PrincipalKind::TenantAdmin,
PrincipalKind::User,
PrincipalKind::Service {
card_ref: None,
card_ref_scope: CardRefScope::default(),
},
] {
assert_eq!(
principal(unbound).card_attribution(),
CardAttribution::AnyRegistered
);
}
}
fn verifier_card_ref() -> CardRef {
CardRef {
kind: CardKind::Verifier,
name: CardName::new("drift").expect("static card name is valid"),
version: VersionBlock::parse("1.0.0").expect("static version is valid"),
space: Some(SpaceName::new("prod").expect("static space is valid")),
uid: Some(
wyrd_spec::ids::CardUid::from_uuid(uuid::Uuid::now_v7())
.expect("UUIDv7 is a valid card UID"),
),
}
}
#[test]
fn system_kind_exposes_its_verifier_scope_without_a_root_card() {
let verifier = verifier_card_ref();
let principal = Principal::new(
PrincipalId::new(uuid::Uuid::now_v7()),
PrincipalKind::System {
card_ref_scope: CardRefScope::own(&verifier),
},
wyrd_spec::DataTenantId::new_v7(),
Vec::new(),
PermissionSet::from_iter([Permission::bifrost_record_write()]),
);
assert_eq!(
principal.kind.tag(),
wyrd_spec::auth::PrincipalKindTag::System
);
assert_eq!(principal.card_ref(), None);
assert_eq!(
principal.card_ref_scope(),
Some(&CardRefScope::own(&verifier))
);
assert!(principal.authorizes_card(&verifier));
assert!(!principal.authorizes_card(&service_card_ref()));
let principal_ref = PrincipalRef::from_principal(&principal);
assert_eq!(principal_ref.card_ref(), None);
assert_eq!(principal_ref.card_ref_scope(), principal.card_ref_scope());
let wire = serde_json::to_value(&principal.kind).expect("kind serializes");
assert_eq!(wire["kind"], "system");
assert_eq!(
serde_json::from_value::<PrincipalKind>(wire).expect("kind deserializes"),
principal.kind
);
}
#[test]
fn user_principal_authorizes_no_cards() {
let principal = Principal {
id: PrincipalId::new(uuid::Uuid::now_v7()),
kind: PrincipalKind::User,
tenant_id: wyrd_spec::DataTenantId::new_v7(),
roles: Vec::new(),
effective_permissions: PermissionSet::new(),
credential_id: None,
};
assert!(!principal.authorizes_card(&service_card_ref()));
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformPrincipal {
pub id: PrincipalId,
pub kind: PrincipalKindTag,
pub effective_permissions: PermissionSet,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credential_id: Option<Uuid>,
}
impl PlatformPrincipal {
#[must_use]
pub const fn new(
id: PrincipalId,
kind: PrincipalKindTag,
effective_permissions: PermissionSet,
) -> Self {
Self {
id,
kind,
effective_permissions,
credential_id: None,
}
}
#[must_use]
pub const fn with_credential_id(mut self, credential_id: Option<Uuid>) -> Self {
self.credential_id = credential_id;
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "scope")]
pub enum AuthContext {
Platform(PlatformPrincipal),
Tenant(Box<Principal>),
}
impl AuthContext {
#[must_use]
pub fn platform(&self) -> Option<&PlatformPrincipal> {
match self {
Self::Platform(principal) => Some(principal),
Self::Tenant(_) => None,
}
}
#[must_use]
pub fn tenant(&self) -> Option<&Principal> {
match self {
Self::Tenant(principal) => Some(principal),
Self::Platform(_) => None,
}
}
#[must_use]
pub fn principal_id(&self) -> PrincipalId {
match self {
Self::Platform(principal) => principal.id,
Self::Tenant(principal) => principal.id,
}
}
#[must_use]
pub fn principal_kind(&self) -> PrincipalKindTag {
match self {
Self::Platform(principal) => principal.kind,
Self::Tenant(principal) => principal.kind.tag(),
}
}
#[must_use]
pub const fn credential_id(&self) -> Option<Uuid> {
match self {
Self::Platform(principal) => principal.credential_id,
Self::Tenant(principal) => principal.credential_id,
}
}
#[must_use]
pub fn effective_permissions(&self) -> &PermissionSet {
match self {
Self::Platform(principal) => &principal.effective_permissions,
Self::Tenant(principal) => &principal.effective_permissions,
}
}
#[must_use]
pub fn tenant_id(&self) -> Option<DataTenantId> {
match self {
Self::Tenant(principal) => Some(principal.tenant_id),
Self::Platform(_) => None,
}
}
}
impl From<Principal> for AuthContext {
fn from(principal: Principal) -> Self {
Self::Tenant(Box::new(principal))
}
}
impl From<PlatformPrincipal> for AuthContext {
fn from(principal: PlatformPrincipal) -> Self {
Self::Platform(principal)
}
}
#[cfg(test)]
mod auth_context_tests {
use wyrd_spec::DataTenantId;
use wyrd_spec::auth::PrincipalKindTag;
use super::{AuthContext, PlatformPrincipal, Principal, PrincipalId, PrincipalKind};
use crate::permission::{Permission, PermissionSet};
fn platform() -> PlatformPrincipal {
PlatformPrincipal::new(
PrincipalId::new(uuid::Uuid::now_v7()),
PrincipalKindTag::GlobalAdmin,
{
let mut set = PermissionSet::new();
set.insert(Permission::tenant_create());
set
},
)
}
fn tenant() -> Principal {
Principal::new(
PrincipalId::new(uuid::Uuid::now_v7()),
PrincipalKind::TenantAdmin,
DataTenantId::new_v7(),
Vec::new(),
{
let mut set = PermissionSet::new();
set.insert(Permission::card_read());
set
},
)
}
#[test]
fn platform_context_carries_no_tenant() {
let context = AuthContext::from(platform());
assert_eq!(context.tenant_id(), None);
assert!(context.tenant().is_none());
assert!(context.platform().is_some());
}
#[test]
fn tenant_context_carries_exactly_its_tenant() {
let principal = tenant();
let expected = principal.tenant_id;
let context = AuthContext::from(principal);
assert_eq!(context.tenant_id(), Some(expected));
assert!(context.platform().is_none());
assert!(context.tenant().is_some());
}
#[test]
fn permissions_resolve_from_whichever_plane_authenticated() {
let platform_context = AuthContext::from(platform());
let tenant_context = AuthContext::from(tenant());
assert!(
platform_context
.effective_permissions()
.contains(&Permission::tenant_create())
);
assert!(
!tenant_context
.effective_permissions()
.contains(&Permission::tenant_create()),
"a tenant principal never holds platform authority"
);
assert!(
tenant_context
.effective_permissions()
.contains(&Permission::card_read())
);
}
#[test]
fn principal_id_is_available_on_both_planes() {
let principal = platform();
let expected = principal.id;
assert_eq!(AuthContext::from(principal).principal_id(), expected);
let principal = tenant();
let expected = principal.id;
assert_eq!(AuthContext::from(principal).principal_id(), expected);
}
}