use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use wyrd_spec::DataTenantId;
use wyrd_spec::auth::{CardAccess, PrincipalKindTag, WorkloadFamily};
use wyrd_spec::ids::CardUid;
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>,
pub card_access: CardAccess,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workload_uid: Option<CardUid>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PrincipalKind {
TenantAdmin,
User,
Service,
System,
}
impl PrincipalKind {
#[must_use]
pub const fn tag(self) -> PrincipalKindTag {
match self {
Self::TenantAdmin => PrincipalKindTag::TenantAdmin,
Self::User => PrincipalKindTag::User,
Self::Service => PrincipalKindTag::Service,
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 {
let card_access = match kind {
PrincipalKind::System => CardAccess::Scoped { cards: Vec::new() },
PrincipalKind::TenantAdmin | PrincipalKind::User | PrincipalKind::Service => {
CardAccess::AnyRegistered
}
};
Self {
id,
kind,
tenant_id,
roles,
effective_permissions,
credential_id: None,
card_access,
workload_uid: 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 with_card_access(
mut self,
card_access: CardAccess,
workload_uid: Option<CardUid>,
) -> Self {
self.card_access = card_access;
self.workload_uid = workload_uid;
self
}
#[must_use]
pub fn card_scope(&self) -> Option<&[CardUid]> {
match &self.card_access {
CardAccess::Scoped { cards } => Some(cards),
CardAccess::AnyRegistered | CardAccess::RootRequired { .. } => None,
}
}
#[must_use]
pub fn card_attribution(&self) -> CardAttribution<'_> {
match &self.card_access {
CardAccess::Scoped { cards } => CardAttribution::Scoped(cards),
CardAccess::AnyRegistered => CardAttribution::AnyRegistered,
CardAccess::RootRequired { restriction } => {
CardAttribution::RootRequired(restriction.family())
}
}
}
#[must_use]
pub fn authorizes_card_uid(&self, card: &CardUid) -> bool {
self.card_scope().is_some_and(|cards| cards.contains(card))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CardAttribution<'a> {
Scoped(&'a [CardUid]),
AnyRegistered,
RootRequired(&'a WorkloadFamily),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PrincipalRef {
pub id: PrincipalId,
pub kind: PrincipalKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workload_uid: Option<CardUid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credential_id: Option<Uuid>,
}
impl PrincipalRef {
#[must_use]
pub fn from_principal(principal: &Principal) -> Self {
Self {
id: principal.id,
kind: principal.kind,
workload_uid: principal.workload_uid.clone(),
credential_id: principal.credential_id,
}
}
}
#[cfg(test)]
mod tests {
use super::{CardAttribution, Principal, PrincipalId, PrincipalKind, PrincipalRef, RoleRef};
use crate::permission::PermissionSet;
use wyrd_spec::auth::{CardAccess, FamilyRestriction, WorkloadFamily, WorkloadKind};
use wyrd_spec::ids::{CardName, CardUid};
fn uid() -> CardUid {
CardUid::from_uuid(uuid::Uuid::now_v7()).expect("v7 Card UID")
}
fn principal(kind: PrincipalKind) -> Principal {
Principal::new(
PrincipalId::new(uuid::Uuid::now_v7()),
kind,
wyrd_spec::DataTenantId::new_v7(),
Vec::new(),
PermissionSet::new(),
)
}
#[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 card_attribution_follows_the_signed_access_state() {
for kind in [
PrincipalKind::TenantAdmin,
PrincipalKind::User,
PrincipalKind::Service,
] {
assert_eq!(
principal(kind).card_attribution(),
CardAttribution::AnyRegistered
);
}
assert_eq!(
principal(PrincipalKind::System).card_attribution(),
CardAttribution::Scoped(&[])
);
let (root, member, outside) = (uid(), uid(), uid());
let scoped = principal(PrincipalKind::Service).with_card_access(
CardAccess::Scoped {
cards: vec![root.clone(), member.clone()],
},
Some(root.clone()),
);
assert!(scoped.authorizes_card_uid(&member));
assert!(!scoped.authorizes_card_uid(&outside));
let projected = PrincipalRef::from_principal(&scoped);
assert_eq!(projected.workload_uid, Some(root));
let family = WorkloadFamily {
kind: WorkloadKind::Service,
name: CardName::new("checkout").expect("static name"),
};
let unresolved = principal(PrincipalKind::Service).with_card_access(
CardAccess::RootRequired {
restriction: FamilyRestriction::Family(family.clone()),
},
None,
);
assert_eq!(
unresolved.card_attribution(),
CardAttribution::RootRequired(&family)
);
assert!(!unresolved.authorizes_card_uid(&member));
}
}
#[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);
}
}