use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::{
security::AuthenticatedUser,
types::{TenantId, UserId},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityContext {
pub user_id: UserId,
pub roles: Vec<String>,
pub tenant_id: Option<TenantId>,
pub scopes: Vec<String>,
pub attributes: HashMap<String, serde_json::Value>,
pub request_id: String,
pub ip_address: Option<String>,
pub authenticated_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub issuer: Option<String>,
pub audience: Option<String>,
pub email: Option<String>,
pub display_name: Option<String>,
}
impl SecurityContext {
#[must_use]
pub fn from_user(user: &AuthenticatedUser, request_id: String) -> Self {
SecurityContext {
user_id: user.user_id.clone(),
roles: vec![], tenant_id: None,
scopes: user.scopes.clone(),
attributes: HashMap::new(),
request_id,
ip_address: None,
authenticated_at: Utc::now(),
expires_at: user.expires_at,
issuer: None,
audience: None,
email: user.email.clone(),
display_name: user.display_name.clone(),
}
}
#[must_use]
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
#[must_use]
pub fn has_scope(&self, scope: &str) -> bool {
self.scopes.iter().any(|s| {
if s == scope {
return true;
}
if s.ends_with(':') {
scope.starts_with(s)
} else if s.ends_with('*') {
let prefix = &s[..s.len() - 1];
scope.starts_with(prefix)
} else {
false
}
})
}
#[must_use]
pub fn get_attribute(&self, key: &str) -> Option<&serde_json::Value> {
self.attributes.get(key)
}
#[must_use]
pub fn is_expired(&self) -> bool {
self.expires_at <= Utc::now()
}
#[must_use]
pub fn ttl_secs(&self) -> i64 {
(self.expires_at - Utc::now()).num_seconds()
}
#[must_use]
pub fn is_admin(&self) -> bool {
self.has_role("admin")
}
#[must_use]
pub const fn is_multi_tenant(&self) -> bool {
self.tenant_id.is_some()
}
#[must_use]
pub fn with_role(mut self, role: String) -> Self {
self.roles.push(role);
self
}
#[must_use]
pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
self.scopes = scopes;
self
}
pub fn with_tenant(mut self, tenant_id: impl Into<TenantId>) -> Self {
self.tenant_id = Some(tenant_id.into());
self
}
#[must_use]
pub fn with_attribute(mut self, key: String, value: serde_json::Value) -> Self {
self.attributes.insert(key, value);
self
}
#[must_use]
pub fn can_access_scope(
&self,
security_config: &crate::schema::SecurityConfig,
required_scope: &str,
) -> bool {
self.roles
.iter()
.any(|role_name| security_config.role_has_scope(role_name, required_scope))
}
}
impl std::fmt::Display for SecurityContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SecurityContext(user_id={}, roles={:?}, scopes={}, tenant={:?})",
self.user_id,
self.roles,
self.scopes.len(),
self.tenant_id
)
}
}