use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Principal {
id: String,
name: String,
principal_type: PrincipalType,
tenant_id: String,
roles: HashSet<String>,
attributes: std::collections::HashMap<String, String>,
expires_at: Option<DateTime<Utc>>,
authenticated_at: DateTime<Utc>,
auth_method: AuthMethod,
}
impl Principal {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
principal_type: PrincipalType,
tenant_id: impl Into<String>,
auth_method: AuthMethod,
) -> Self {
Self {
id: id.into(),
name: name.into(),
principal_type,
tenant_id: tenant_id.into(),
roles: HashSet::new(),
attributes: std::collections::HashMap::new(),
expires_at: None,
authenticated_at: Utc::now(),
auth_method,
}
}
pub fn anonymous() -> Self {
Self::new(
"anonymous",
"Anonymous User",
PrincipalType::Anonymous,
"default",
AuthMethod::None,
)
}
pub fn system() -> Self {
let mut principal = Self::new(
"system",
"System",
PrincipalType::System,
"system",
AuthMethod::Internal,
);
principal.roles.insert("system".to_string());
principal
}
#[inline]
pub fn id(&self) -> &str {
&self.id
}
#[inline]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
pub fn principal_type(&self) -> &PrincipalType {
&self.principal_type
}
#[inline]
pub fn tenant_id(&self) -> &str {
&self.tenant_id
}
#[inline]
pub fn roles(&self) -> &HashSet<String> {
&self.roles
}
#[inline]
pub fn has_role(&self, role: &str) -> bool {
self.roles.contains(role)
}
#[inline]
pub fn attributes(&self) -> &std::collections::HashMap<String, String> {
&self.attributes
}
#[inline]
pub fn get_attribute(&self, key: &str) -> Option<&str> {
self.attributes.get(key).map(|s| s.as_str())
}
#[inline]
pub fn expires_at(&self) -> Option<DateTime<Utc>> {
self.expires_at
}
pub fn is_expired(&self) -> bool {
self.expires_at.map(|exp| Utc::now() > exp).unwrap_or(false)
}
#[inline]
pub fn authenticated_at(&self) -> DateTime<Utc> {
self.authenticated_at
}
#[inline]
pub fn auth_method(&self) -> &AuthMethod {
&self.auth_method
}
#[inline]
pub fn is_system(&self) -> bool {
matches!(self.principal_type, PrincipalType::System)
}
#[inline]
pub fn is_anonymous(&self) -> bool {
matches!(self.principal_type, PrincipalType::Anonymous)
}
}
#[derive(Debug, Default)]
pub struct PrincipalBuilder {
id: String,
name: String,
principal_type: PrincipalType,
tenant_id: String,
roles: HashSet<String>,
attributes: std::collections::HashMap<String, String>,
expires_at: Option<DateTime<Utc>>,
auth_method: AuthMethod,
}
impl PrincipalBuilder {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
principal_type: PrincipalType,
tenant_id: impl Into<String>,
auth_method: AuthMethod,
) -> Self {
Self {
id: id.into(),
name: name.into(),
principal_type,
tenant_id: tenant_id.into(),
roles: HashSet::new(),
attributes: std::collections::HashMap::new(),
expires_at: None,
auth_method,
}
}
pub fn with_role(mut self, role: impl Into<String>) -> Self {
self.roles.insert(role.into());
self
}
pub fn with_roles(mut self, roles: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.roles.extend(roles.into_iter().map(|r| r.into()));
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
pub fn expires_at(mut self, expires_at: DateTime<Utc>) -> Self {
self.expires_at = Some(expires_at);
self
}
pub fn build(self) -> Principal {
Principal {
id: self.id,
name: self.name,
principal_type: self.principal_type,
tenant_id: self.tenant_id,
roles: self.roles,
attributes: self.attributes,
expires_at: self.expires_at,
authenticated_at: Utc::now(),
auth_method: self.auth_method,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PrincipalType {
User,
Service,
ApiKey,
#[default]
Anonymous,
System,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AuthMethod {
#[default]
None,
ApiKey,
Bearer,
Basic,
MutualTls,
Internal,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_principal_creation() {
let principal = Principal::new(
"user-123",
"Test User",
PrincipalType::User,
"tenant-1",
AuthMethod::ApiKey,
);
assert_eq!(principal.id(), "user-123");
assert_eq!(principal.name(), "Test User");
assert_eq!(principal.tenant_id(), "tenant-1");
assert!(!principal.is_expired());
assert!(!principal.is_system());
assert!(!principal.is_anonymous());
}
#[test]
fn test_principal_builder() {
let principal = PrincipalBuilder::new(
"user-456",
"Builder User",
PrincipalType::User,
"tenant-2",
AuthMethod::Bearer,
)
.with_role("admin")
.with_role("reader")
.with_attribute("department", "engineering")
.build();
assert!(principal.has_role("admin"));
assert!(principal.has_role("reader"));
assert!(!principal.has_role("writer"));
assert_eq!(principal.get_attribute("department"), Some("engineering"));
}
#[test]
fn test_anonymous_principal() {
let anon = Principal::anonymous();
assert!(anon.is_anonymous());
assert!(!anon.is_system());
assert_eq!(anon.id(), "anonymous");
}
#[test]
fn test_system_principal() {
let system = Principal::system();
assert!(system.is_system());
assert!(system.has_role("system"));
}
#[test]
fn test_principal_expiration() {
let expired = PrincipalBuilder::new(
"user-exp",
"Expired User",
PrincipalType::User,
"tenant",
AuthMethod::ApiKey,
)
.expires_at(Utc::now() - chrono::Duration::hours(1))
.build();
assert!(expired.is_expired());
let valid = PrincipalBuilder::new(
"user-valid",
"Valid User",
PrincipalType::User,
"tenant",
AuthMethod::ApiKey,
)
.expires_at(Utc::now() + chrono::Duration::hours(1))
.build();
assert!(!valid.is_expired());
}
}