use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::security::AuthenticatedUser;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityContext {
pub user_id: String,
pub roles: Vec<String>,
pub tenant_id: Option<String>,
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>,
}
impl SecurityContext {
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,
}
}
#[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()
}
pub fn with_role(mut self, role: String) -> Self {
self.roles.push(role);
self
}
pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
self.scopes = scopes;
self
}
pub fn with_tenant(mut self, tenant_id: String) -> Self {
self.tenant_id = Some(tenant_id);
self
}
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
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_has_role() {
let context = SecurityContext {
user_id: "user123".to_string(),
roles: vec!["admin".to_string(), "moderator".to_string()],
tenant_id: None,
scopes: vec![],
attributes: HashMap::new(),
request_id: "req-1".to_string(),
ip_address: None,
authenticated_at: Utc::now(),
expires_at: Utc::now() + chrono::Duration::hours(1),
issuer: None,
audience: None,
};
assert!(context.has_role("admin"));
assert!(context.has_role("moderator"));
assert!(!context.has_role("superadmin"));
}
#[test]
fn test_has_scope() {
let context = SecurityContext {
user_id: "user123".to_string(),
roles: vec![],
tenant_id: None,
scopes: vec!["read:user".to_string(), "write:post".to_string()],
attributes: HashMap::new(),
request_id: "req-1".to_string(),
ip_address: None,
authenticated_at: Utc::now(),
expires_at: Utc::now() + chrono::Duration::hours(1),
issuer: None,
audience: None,
};
assert!(context.has_scope("read:user"));
assert!(context.has_scope("write:post"));
assert!(!context.has_scope("admin:*"));
}
#[test]
fn test_wildcard_scopes() {
let context = SecurityContext {
user_id: "user123".to_string(),
roles: vec![],
tenant_id: None,
scopes: vec!["admin:*".to_string()],
attributes: HashMap::new(),
request_id: "req-1".to_string(),
ip_address: None,
authenticated_at: Utc::now(),
expires_at: Utc::now() + chrono::Duration::hours(1),
issuer: None,
audience: None,
};
assert!(context.has_scope("admin:read"));
assert!(context.has_scope("admin:write"));
assert!(!context.has_scope("user:read"));
}
#[test]
fn test_builder_pattern() {
let now = Utc::now();
let context = SecurityContext {
user_id: "user123".to_string(),
roles: vec![],
tenant_id: None,
scopes: vec![],
attributes: HashMap::new(),
request_id: "req-1".to_string(),
ip_address: None,
authenticated_at: now,
expires_at: now + chrono::Duration::hours(1),
issuer: None,
audience: None,
}
.with_role("admin".to_string())
.with_scopes(vec!["read:user".to_string()])
.with_tenant("tenant-1".to_string());
assert!(context.has_role("admin"));
assert!(context.has_scope("read:user"));
assert_eq!(context.tenant_id, Some("tenant-1".to_string()));
}
}