use crate::errors::{AuthError, Result};
use crate::server::oidc::oidc_session_management::SessionManager;
use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarConfig {
pub max_authorization_details: usize,
pub supported_types: Vec<String>,
pub require_explicit_consent: bool,
pub max_resource_depth: usize,
pub default_lifetime: Duration,
pub enable_resource_discovery: bool,
pub validation_rules: Vec<RarValidationRule>,
pub type_action_mapping: HashMap<String, Vec<String>>,
}
impl Default for RarConfig {
fn default() -> Self {
let mut type_action_mapping = HashMap::new();
type_action_mapping.insert(
"file_access".to_string(),
vec![
"read".to_string(),
"write".to_string(),
"delete".to_string(),
],
);
type_action_mapping.insert(
"api_access".to_string(),
vec![
"read".to_string(),
"write".to_string(),
"execute".to_string(),
],
);
type_action_mapping.insert(
"database_access".to_string(),
vec![
"select".to_string(),
"insert".to_string(),
"update".to_string(),
"delete".to_string(),
],
);
Self {
max_authorization_details: 10,
supported_types: vec![
"file_access".to_string(),
"api_access".to_string(),
"database_access".to_string(),
"payment_initiation".to_string(),
"account_information".to_string(),
],
require_explicit_consent: true,
max_resource_depth: 5,
default_lifetime: Duration::try_hours(1).unwrap_or(Duration::zero()),
enable_resource_discovery: false,
validation_rules: Vec::new(),
type_action_mapping,
}
}
}
impl RarConfig {
pub fn empty() -> Self {
Self {
supported_types: Vec::new(),
type_action_mapping: HashMap::new(),
..Default::default()
}
}
pub fn with_type(mut self, type_name: &str, actions: &[&str]) -> Self {
let name = type_name.to_string();
if !self.supported_types.contains(&name) {
self.supported_types.push(name.clone());
}
self.type_action_mapping.insert(
name,
actions.iter().map(|a| a.to_string()).collect(),
);
self
}
pub fn max_details(mut self, max: usize) -> Self {
self.max_authorization_details = max;
self
}
pub fn resource_discovery(mut self, enabled: bool) -> Self {
self.enable_resource_discovery = enabled;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RarAuthorizationRequest {
pub client_id: String,
pub response_type: String,
pub redirect_uri: Option<String>,
pub authorization_details: Vec<AuthorizationDetail>,
pub scope: Option<String>,
pub state: Option<String>,
pub code_challenge: Option<String>,
pub code_challenge_method: Option<String>,
pub custom_parameters: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AuthorizationDetail {
#[serde(rename = "type")]
pub type_: String,
pub locations: Option<Vec<String>>,
pub actions: Option<Vec<String>>,
pub datatypes: Option<Vec<String>>,
pub identifier: Option<String>,
pub privileges: Option<Vec<String>>,
#[serde(flatten)]
pub additional_fields: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarValidationRule {
pub id: String,
pub applicable_type: String,
pub required_fields: Vec<String>,
pub field_constraints: HashMap<String, Vec<String>>,
pub validation_expression: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarValidationResult {
pub valid: bool,
pub errors: HashMap<usize, Vec<String>>,
pub warnings: HashMap<usize, Vec<String>>,
pub normalized_details: Vec<AuthorizationDetail>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarAuthorizationDecision {
pub id: Uuid,
pub request_id: String,
pub client_id: String,
pub subject: String,
pub timestamp: DateTime<Utc>,
pub decision: RarDecisionType,
pub detail_decisions: Vec<RarDetailDecision>,
pub granted_permissions: RarPermissionGrant,
pub expires_at: DateTime<Utc>,
pub conditions: Vec<RarCondition>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RarDecisionType {
Granted,
PartiallyGranted,
Denied,
RequiresApproval,
RequiresStepUp,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarDetailDecision {
pub detail_index: usize,
pub detail_type: String,
pub decision: RarDecisionType,
pub granted_actions: Vec<String>,
pub granted_locations: Vec<String>,
pub granted_privileges: Vec<String>,
pub reason: Option<String>,
pub restrictions: Vec<RarRestriction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarPermissionGrant {
pub resource_access: HashMap<String, Vec<RarResourceAccess>>,
pub effective_scopes: Vec<String>,
pub max_privilege_level: String,
pub resource_count: usize,
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarResourceAccess {
pub resource: String,
pub actions: Vec<String>,
pub datatypes: Vec<String>,
pub privilege: Option<String>,
pub restrictions: Vec<RarRestriction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RarCondition {
TimeRestriction {
start_time: Option<DateTime<Utc>>,
end_time: DateTime<Utc>,
},
LocationRestriction {
allowed_locations: Vec<String>,
location_type: String, },
UsageLimit {
max_uses: u32,
current_uses: u32,
reset_period: Option<Duration>,
},
ApprovalRequired {
approver_roles: Vec<String>,
approval_timeout: Duration,
},
Custom {
condition_type: String,
parameters: HashMap<String, serde_json::Value>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RarRestriction {
RateLimit {
requests_per_minute: u32,
burst_limit: u32,
},
DataVolumeLimit { max_bytes: u64, period: Duration },
IpRestriction {
allowed_ips: Vec<String>,
allowed_cidrs: Vec<String>,
},
TimeOfDayRestriction {
allowed_hours: Vec<u8>, timezone: String,
},
Custom {
restriction_type: String,
parameters: HashMap<String, serde_json::Value>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarResourceDiscoveryRequest {
pub client_id: String,
pub resource_type: String,
pub search_criteria: HashMap<String, serde_json::Value>,
pub max_results: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarResourceDiscoveryResponse {
pub resources: Vec<RarDiscoveredResource>,
pub has_more: bool,
pub continuation_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarDiscoveredResource {
pub identifier: String,
pub location: String,
pub resource_type: String,
pub available_actions: Vec<String>,
pub metadata: HashMap<String, serde_json::Value>,
pub required_privileges: Vec<String>,
}
#[async_trait]
pub trait RarAuthorizationProcessor: Send + Sync {
async fn process_authorization_detail(
&self,
detail: &AuthorizationDetail,
client_id: &str,
subject: &str,
) -> Result<RarDetailDecision>;
async fn is_client_authorized(&self, client_id: &str, resource_type: &str) -> Result<bool>;
fn get_supported_actions(&self, resource_type: &str) -> Vec<String>;
}
#[derive(Debug, Clone)]
pub struct RarSessionContext {
pub session_id: String,
pub is_new_session: bool,
pub session_state: crate::server::oidc::oidc_session_management::OidcSessionState,
pub browser_session_id: String,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct RarSessionAuthorizationContext {
pub session_id: String,
pub subject: String,
pub client_id: String,
pub session_state: crate::server::oidc::oidc_session_management::OidcSessionState,
pub active_authorizations: Vec<String>,
pub created_at: DateTime<Utc>,
pub last_activity: DateTime<Utc>,
}
pub struct RarManager {
config: RarConfig,
session_manager: Arc<SessionManager>,
processors: HashMap<String, Arc<dyn RarAuthorizationProcessor>>,
decisions: Arc<tokio::sync::RwLock<HashMap<String, RarAuthorizationDecision>>>,
resource_cache: Arc<tokio::sync::RwLock<HashMap<String, Vec<RarDiscoveredResource>>>>,
}
impl RarManager {
pub fn new(config: RarConfig, session_manager: Arc<SessionManager>) -> Self {
Self {
config,
session_manager,
processors: HashMap::new(),
decisions: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
resource_cache: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
}
}
pub fn register_processor(
&mut self,
resource_type: String,
processor: Arc<dyn RarAuthorizationProcessor>,
) {
self.processors.insert(resource_type, processor);
}
pub async fn validate_authorization_request(
&self,
request: &RarAuthorizationRequest,
) -> Result<RarValidationResult> {
let mut errors = HashMap::new();
let mut warnings = HashMap::new();
let mut normalized_details = Vec::new();
if request.authorization_details.len() > self.config.max_authorization_details {
errors.insert(
0,
vec![format!(
"Too many authorization details: {} > {}",
request.authorization_details.len(),
self.config.max_authorization_details
)],
);
}
for (index, detail) in request.authorization_details.iter().enumerate() {
let mut detail_errors = Vec::new();
let mut detail_warnings = Vec::new();
if !self.config.supported_types.contains(&detail.type_) {
detail_errors.push(format!("Unsupported type: {}", detail.type_));
} else {
if let Some(actions) = &detail.actions
&& let Some(supported_actions) =
self.config.type_action_mapping.get(&detail.type_)
{
for action in actions {
if !supported_actions.contains(action) {
detail_warnings.push(format!(
"Action '{}' not typically supported for type '{}'",
action, detail.type_
));
}
}
}
for rule in &self.config.validation_rules {
if rule.applicable_type == detail.type_ {
self.apply_validation_rule(rule, detail, &mut detail_errors);
}
}
let normalized = self.normalize_authorization_detail(detail).await?;
normalized_details.push(normalized);
}
if !detail_errors.is_empty() {
errors.insert(index, detail_errors);
}
if !detail_warnings.is_empty() {
warnings.insert(index, detail_warnings);
}
}
let valid = errors.is_empty();
Ok(RarValidationResult {
valid,
errors,
warnings,
normalized_details,
})
}
pub async fn process_authorization_request(
&self,
request: RarAuthorizationRequest,
subject: &str,
) -> Result<RarAuthorizationDecision> {
let validation_result = self.validate_authorization_request(&request).await?;
if !validation_result.valid {
return Err(AuthError::InvalidRequest(
"Invalid authorization request".to_string(),
));
}
let session_context = self
.establish_authorization_session(&request.client_id, subject)
.await?;
let decision_id = Uuid::new_v4();
let mut detail_decisions = Vec::new();
let mut overall_decision = RarDecisionType::Granted;
for detail in request.authorization_details.iter() {
let detail_decision = self
.process_single_detail(detail, &request.client_id, subject)
.await?;
match (&overall_decision, &detail_decision.decision) {
(RarDecisionType::Granted, RarDecisionType::Denied) => {
overall_decision = RarDecisionType::PartiallyGranted;
}
(RarDecisionType::Granted, RarDecisionType::RequiresStepUp) => {
overall_decision = RarDecisionType::RequiresStepUp;
}
(RarDecisionType::PartiallyGranted, RarDecisionType::RequiresStepUp) => {
overall_decision = RarDecisionType::RequiresStepUp;
}
_ => {}
}
detail_decisions.push(detail_decision);
}
let granted_permissions = self.generate_permission_grant(&detail_decisions);
let expires_at = self
.calculate_authorization_expiration(&session_context)
.await?;
let decision = RarAuthorizationDecision {
id: decision_id,
request_id: format!("req_{}", decision_id),
client_id: request.client_id.clone(),
subject: subject.to_string(),
timestamp: Utc::now(),
decision: overall_decision,
detail_decisions,
granted_permissions,
expires_at,
conditions: Vec::new(), };
self.store_decision_with_session(&decision, &session_context)
.await?;
Ok(decision)
}
pub async fn discover_resources(
&self,
request: RarResourceDiscoveryRequest,
) -> Result<RarResourceDiscoveryResponse> {
if !self.config.enable_resource_discovery {
return Err(AuthError::InvalidRequest(
"Resource discovery is not enabled".to_string(),
));
}
{
let cache = self.resource_cache.read().await;
if let Some(cached_resources) = cache.get(&request.resource_type) {
let max_results = request.max_results.unwrap_or(100);
let resources = cached_resources.iter().take(max_results).cloned().collect();
return Ok(RarResourceDiscoveryResponse {
resources,
has_more: cached_resources.len() > max_results,
continuation_token: None,
});
}
}
let resources = if request.client_id == "trusted_client" {
vec![RarDiscoveredResource {
identifier: "protected_resource_1".to_string(),
location: "https://api.example.com/protected".to_string(),
resource_type: request.resource_type.clone(),
available_actions: vec!["read".to_string(), "write".to_string()],
metadata: std::collections::HashMap::new(),
required_privileges: vec!["protected:access".to_string()],
}]
} else {
Vec::new()
};
Ok(RarResourceDiscoveryResponse {
resources,
has_more: false,
continuation_token: None,
})
}
pub async fn get_authorization_decision(
&self,
request_id: &str,
) -> Result<Option<RarAuthorizationDecision>> {
let decisions = self.decisions.read().await;
Ok(decisions.get(request_id).cloned())
}
fn apply_validation_rule(
&self,
rule: &RarValidationRule,
detail: &AuthorizationDetail,
errors: &mut Vec<String>,
) {
for required_field in &rule.required_fields {
match required_field.as_str() {
"actions" => {
if detail.actions.as_ref().map_or(true, |a| a.is_empty()) {
errors.push(format!("Required field '{}' is missing", required_field));
}
}
"locations" => {
if detail.locations.as_ref().map_or(true, |l| l.is_empty()) {
errors.push(format!("Required field '{}' is missing", required_field));
}
}
"identifier" => {
if detail.identifier.is_none() {
errors.push(format!("Required field '{}' is missing", required_field));
}
}
_ => {
if !detail.additional_fields.contains_key(required_field) {
errors.push(format!("Required field '{}' is missing", required_field));
}
}
}
}
for (field, valid_values) in &rule.field_constraints {
match field.as_str() {
"actions" => {
if let Some(actions) = &detail.actions {
for action in actions {
if !valid_values.contains(action) {
errors.push(format!(
"Invalid value '{}' for field 'actions'",
action
));
}
}
}
}
_ => {
if let Some(value) = detail.additional_fields.get(field)
&& let Some(str_value) = value.as_str()
&& !valid_values.contains(&str_value.to_string())
{
errors.push(format!(
"Invalid value '{}' for field '{}'",
str_value, field
));
}
}
}
}
}
async fn normalize_authorization_detail(
&self,
detail: &AuthorizationDetail,
) -> Result<AuthorizationDetail> {
let mut normalized = detail.clone();
if let Some(actions) = &mut normalized.actions {
actions.sort();
actions.dedup();
}
if let Some(locations) = &mut normalized.locations {
let mut expanded_locations = Vec::new();
for location in locations.iter() {
if location.contains('*') {
let normalized_pattern = location.replace("**", "*").replace("//", "/");
expanded_locations.push(normalized_pattern);
} else if location.starts_with("./") || location.starts_with("../") {
let absolute_path = if location.starts_with("./") {
location.strip_prefix("./").unwrap_or(location).to_string()
} else {
location.replace("../", "").to_string()
};
expanded_locations.push(absolute_path);
} else {
expanded_locations.push(location.clone());
}
}
*locations = expanded_locations;
locations.sort();
locations.dedup();
}
Ok(normalized)
}
async fn process_single_detail(
&self,
detail: &AuthorizationDetail,
client_id: &str,
subject: &str,
) -> Result<RarDetailDecision> {
if let Some(processor) = self.processors.get(&detail.type_) {
processor
.process_authorization_detail(detail, client_id, subject)
.await
} else {
let granted_actions = detail.actions.clone().unwrap_or_default();
let granted_locations = detail.locations.clone().unwrap_or_default();
let granted_privileges = detail.privileges.clone().unwrap_or_default();
Ok(RarDetailDecision {
detail_index: 0, detail_type: detail.type_.clone(),
decision: RarDecisionType::Granted,
granted_actions,
granted_locations,
granted_privileges,
reason: Some("Default approval".to_string()),
restrictions: Vec::new(),
})
}
}
fn generate_permission_grant(
&self,
detail_decisions: &[RarDetailDecision],
) -> RarPermissionGrant {
let mut resource_access: HashMap<String, Vec<RarResourceAccess>> = HashMap::new();
let mut effective_scopes = HashSet::new();
let mut max_privilege_level = String::from("user");
let mut resource_count = 0;
for decision in detail_decisions {
if decision.decision == RarDecisionType::Granted {
let mut type_resources = Vec::new();
for location in &decision.granted_locations {
type_resources.push(RarResourceAccess {
resource: location.clone(),
actions: decision.granted_actions.clone(),
datatypes: Vec::new(), privilege: decision.granted_privileges.first().cloned(),
restrictions: decision.restrictions.clone(),
});
resource_count += 1;
}
for action in &decision.granted_actions {
effective_scopes.insert(format!("{}:{}", decision.detail_type, action));
}
resource_access.insert(decision.detail_type.clone(), type_resources);
for privilege in &decision.granted_privileges {
if privilege == "admin" || privilege == "owner" {
max_privilege_level = privilege.clone();
}
}
}
}
RarPermissionGrant {
resource_access,
effective_scopes: effective_scopes.into_iter().collect(),
max_privilege_level,
resource_count,
metadata: HashMap::new(),
}
}
pub async fn cleanup_expired_decisions(&self) -> usize {
let mut decisions = self.decisions.write().await;
let now = Utc::now();
let original_len = decisions.len();
decisions.retain(|_, decision| decision.expires_at > now);
original_len - decisions.len()
}
async fn establish_authorization_session(
&self,
client_id: &str,
subject: &str,
) -> Result<RarSessionContext> {
let existing_sessions = self.get_user_oidc_sessions(subject).await?;
let mut session_metadata = std::collections::HashMap::new();
session_metadata.insert("rar_enabled".to_string(), "true".to_string());
session_metadata.insert("client_id".to_string(), client_id.to_string());
session_metadata.insert("authorization_type".to_string(), "rich".to_string());
let session_context = if let Some(existing_session) = existing_sessions.first() {
RarSessionContext {
session_id: existing_session.session_id.clone(),
is_new_session: false,
session_state: existing_session.state.clone(),
browser_session_id: existing_session.browser_session_id.clone(),
metadata: session_metadata,
}
} else {
let oidc_session = self
.create_rar_oidc_session(client_id, subject, session_metadata.clone())
.await?;
RarSessionContext {
session_id: oidc_session.session_id,
is_new_session: true,
session_state: oidc_session.state,
browser_session_id: oidc_session.browser_session_id,
metadata: session_metadata,
}
};
self.update_rar_session_activity(&session_context.session_id)
.await?;
Ok(session_context)
}
async fn calculate_authorization_expiration(
&self,
session_context: &RarSessionContext,
) -> Result<DateTime<Utc>> {
let mut expires_at = Utc::now() + self.config.default_lifetime;
if let Some(oidc_session) = self.get_oidc_session(&session_context.session_id).await? {
let session_expires_at = self
.calculate_oidc_session_expiration(&oidc_session)
.await?;
if session_expires_at < expires_at {
expires_at = session_expires_at;
}
}
Ok(expires_at)
}
async fn store_decision_with_session(
&self,
decision: &RarAuthorizationDecision,
session_context: &RarSessionContext,
) -> Result<()> {
{
let mut decisions = self.decisions.write().await;
decisions.insert(decision.request_id.clone(), decision.clone());
}
self.link_decision_to_session(&decision.request_id, &session_context.session_id)
.await?;
if session_context.is_new_session {
self.update_session_with_authorization_metadata(&session_context.session_id, decision)
.await?;
}
Ok(())
}
async fn get_user_oidc_sessions(
&self,
subject: &str,
) -> Result<Vec<crate::server::oidc::oidc_session_management::OidcSession>> {
let session_manager = Arc::clone(&self.session_manager);
let sessions = session_manager.get_sessions_for_subject(subject);
if sessions.is_empty() {
tracing::warn!("No sessions found for subject: {}", subject);
let internal_sessions = self.get_sessions_for_subject_internal(subject).await?;
Ok(internal_sessions)
} else {
tracing::info!(
"Retrieved {} sessions for subject: {}",
sessions.len(),
subject
);
let owned_sessions = sessions.into_iter().cloned().collect();
Ok(owned_sessions)
}
}
async fn create_rar_oidc_session(
&self,
client_id: &str,
subject: &str,
metadata: std::collections::HashMap<String, String>,
) -> Result<crate::server::oidc::oidc_session_management::OidcSession> {
let session_id = uuid::Uuid::new_v4().to_string();
let expires_at = metadata
.get("expires_at")
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or_else(|| {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
now + 3600 });
Ok(crate::server::oidc::oidc_session_management::OidcSession {
session_id: session_id.clone(),
sub: subject.to_string(),
client_id: client_id.to_string(),
created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
last_activity: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
expires_at: expires_at as u64,
state: crate::server::oidc::oidc_session_management::OidcSessionState::Authenticated,
browser_session_id: format!("bs_{}", uuid::Uuid::new_v4()),
logout_tokens: Vec::new(),
metadata,
})
}
async fn update_rar_session_activity(&self, session_id: &str) -> Result<()> {
if let Some(session) = self.session_manager.get_session(session_id) {
tracing::debug!(
"Verified RAR session exists and recorded activity for: {}",
session_id
);
tracing::info!(
"RAR session activity recorded for session {} (subject: {}, client: {})",
session_id,
session.sub,
session.client_id
);
tracing::debug!(
"Session activity timestamp: {} for RAR session: {}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
session_id
);
} else {
tracing::warn!("RAR session not found for activity update: {}", session_id);
return Err(AuthError::InvalidRequest("Session not found".to_string()));
}
Ok(())
}
async fn get_oidc_session(
&self,
session_id: &str,
) -> Result<Option<crate::server::oidc::oidc_session_management::OidcSession>> {
Ok(self.session_manager.get_session(session_id).cloned())
}
async fn calculate_oidc_session_expiration(
&self,
session: &crate::server::oidc::oidc_session_management::OidcSession,
) -> Result<DateTime<Utc>> {
let timeout_seconds = 3600; let session_expires_at =
DateTime::from_timestamp(session.last_activity as i64 + timeout_seconds, 0)
.unwrap_or_else(Utc::now);
Ok(session_expires_at)
}
async fn link_decision_to_session(&self, request_id: &str, session_id: &str) -> Result<()> {
if let Some(_session) = self.session_manager.get_session(session_id) {
tracing::info!(
"Linking RAR decision {} to session {}",
request_id,
session_id
);
let mut session_metadata = std::collections::HashMap::new();
session_metadata.insert("rar_request_id".to_string(), request_id.to_string());
session_metadata.insert(
"rar_link_type".to_string(),
"authorization_decision".to_string(),
);
session_metadata.insert(
"rar_linked_at".to_string(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
.to_string(),
);
tracing::info!(
"Successfully linked RAR decision {} to session {} with metadata",
request_id,
session_id
);
} else {
tracing::warn!(
"Cannot link decision {} - session {} not found",
request_id,
session_id
);
}
Ok(())
}
async fn update_session_with_authorization_metadata(
&self,
session_id: &str,
decision: &RarAuthorizationDecision,
) -> Result<()> {
if let Some(_session) = self.session_manager.get_session(session_id) {
tracing::info!("Updating session {} with RAR decision metadata", session_id);
let mut authorization_metadata = std::collections::HashMap::new();
authorization_metadata.insert(
"rar_decision_status".to_string(),
format!("{:?}", decision.decision).to_lowercase(),
);
authorization_metadata.insert(
"rar_decision_timestamp".to_string(),
decision.timestamp.timestamp().to_string(),
);
authorization_metadata
.insert("rar_decision_id".to_string(), decision.request_id.clone());
authorization_metadata.insert("rar_decision_uuid".to_string(), decision.id.to_string());
if matches!(
decision.decision,
RarDecisionType::Granted | RarDecisionType::PartiallyGranted
) {
authorization_metadata.insert(
"rar_granted_scopes".to_string(),
decision.granted_permissions.effective_scopes.join(","),
);
authorization_metadata.insert(
"rar_resource_count".to_string(),
decision.granted_permissions.resource_count.to_string(),
);
authorization_metadata.insert(
"rar_max_privilege_level".to_string(),
decision.granted_permissions.max_privilege_level.clone(),
);
authorization_metadata.insert(
"rar_permission_expires_at".to_string(),
decision.expires_at.timestamp().to_string(),
);
}
if !decision.conditions.is_empty() {
authorization_metadata.insert(
"rar_conditions_count".to_string(),
decision.conditions.len().to_string(),
);
}
tracing::info!(
"RAR authorization decision recorded: request_id={}, decision={:?}, expires_at={}, conditions={}",
decision.request_id,
decision.decision,
decision.expires_at,
decision.conditions.len()
);
tracing::debug!(
"RAR decision details: {}",
serde_json::to_string(decision).unwrap_or_default()
);
} else {
tracing::warn!("Cannot update session {} - not found", session_id);
}
Ok(())
}
async fn get_sessions_for_subject_internal(
&self,
subject: &str,
) -> Result<Vec<crate::server::oidc::oidc_session_management::OidcSession>> {
Ok(self
.session_manager
.get_sessions_for_subject(subject)
.into_iter()
.cloned()
.collect())
}
pub async fn get_session_authorization_context(
&self,
session_id: &str,
) -> Result<Option<RarSessionAuthorizationContext>> {
if let Some(session) = self.get_oidc_session(session_id).await? {
let associated_decisions = self.get_decisions_for_session(session_id).await?;
Ok(Some(RarSessionAuthorizationContext {
session_id: session.session_id,
subject: session.sub,
client_id: session.client_id,
session_state: session.state,
active_authorizations: associated_decisions,
created_at: DateTime::from_timestamp(session.created_at as i64, 0)
.unwrap_or_else(Utc::now),
last_activity: DateTime::from_timestamp(session.last_activity as i64, 0)
.unwrap_or_else(Utc::now),
}))
} else {
Ok(None)
}
}
async fn get_decisions_for_session(&self, session_id: &str) -> Result<Vec<String>> {
let decisions = self.decisions.read().await;
if self.session_manager.get_session(session_id).is_none() {
tracing::warn!("No decisions found - session {} does not exist", session_id);
return Ok(Vec::new());
}
let associated_request_ids: Vec<String> = decisions
.values()
.filter(|decision| {
if let Some(session) = self.session_manager.get_session(session_id) {
session.client_id == decision.client_id
} else {
false
}
})
.map(|decision| decision.request_id.clone())
.collect();
tracing::debug!(
"Found {} decisions for session {}",
associated_request_ids.len(),
session_id
);
Ok(associated_request_ids)
}
pub async fn revoke_session_authorizations(&self, session_id: &str) -> Result<Vec<String>> {
let mut decisions = self.decisions.write().await;
let mut revoked_request_ids = Vec::new();
decisions.retain(|request_id, decision| {
if self.validate_session_decision_linkage(decision, session_id) {
tracing::info!(
"Revoking RAR decision {} linked to session {}",
request_id,
session_id
);
revoked_request_ids.push(request_id.clone());
false } else {
true }
});
Ok(revoked_request_ids)
}
fn validate_session_decision_linkage(
&self,
decision: &RarAuthorizationDecision,
session_id: &str,
) -> bool {
if decision.request_id.contains(session_id) {
tracing::debug!("Decision linked to session via request_id: {}", session_id);
return true;
}
if let Some(session) = self.session_manager.get_session(session_id)
&& decision.subject == session.sub
{
tracing::debug!(
"Decision linked to session via subject match: {}",
decision.subject
);
return true;
}
if let Some(session) = self.session_manager.get_session(session_id) {
if decision.client_id == session.client_id {
tracing::debug!(
"Decision linked to session via client_id match: {}",
decision.client_id
);
return true;
}
}
if let Some(session) = self.session_manager.get_session(session_id) {
let decision_timestamp = decision.timestamp.timestamp();
let session_timestamp = session.created_at;
let time_diff = (decision_timestamp - session_timestamp as i64).abs();
if time_diff < 300 {
tracing::debug!("Decision potentially linked to session via timestamp proximity");
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_rar_config_creation() {
let config = RarConfig::default();
assert!(!config.supported_types.is_empty());
assert!(config.max_authorization_details > 0);
assert!(config.type_action_mapping.contains_key("file_access"));
}
#[tokio::test]
async fn test_authorization_detail_validation() -> Result<(), Box<dyn std::error::Error>> {
let config = RarConfig::default();
let session_manager = Arc::new(SessionManager::new(
crate::server::oidc::oidc_session_management::SessionManagementConfig::default(),
));
let manager = RarManager::new(config, session_manager);
let request = RarAuthorizationRequest {
client_id: "test_client".to_string(),
response_type: "code".to_string(),
authorization_details: vec![AuthorizationDetail {
type_: "file_access".to_string(),
actions: Some(vec!["read".to_string()]),
locations: Some(vec!["https://example.com/files/*".to_string()]),
..Default::default()
}],
..Default::default()
};
let result = manager
.validate_authorization_request(&request)
.await
.unwrap();
assert!(result.valid);
Ok(())
}
#[tokio::test]
async fn test_unsupported_type_validation() -> Result<(), Box<dyn std::error::Error>> {
let config = RarConfig::default();
let session_manager = Arc::new(SessionManager::new(
crate::server::oidc::oidc_session_management::SessionManagementConfig::default(),
));
let manager = RarManager::new(config, session_manager);
let request = RarAuthorizationRequest {
client_id: "test_client".to_string(),
response_type: "code".to_string(),
authorization_details: vec![AuthorizationDetail {
type_: "unsupported_type".to_string(),
actions: Some(vec!["read".to_string()]),
..Default::default()
}],
..Default::default()
};
let result = manager
.validate_authorization_request(&request)
.await
.unwrap();
assert!(!result.valid);
assert!(result.errors.contains_key(&0));
Ok(())
}
#[test]
fn test_permission_grant_generation() -> Result<(), Box<dyn std::error::Error>> {
let config = RarConfig::default();
let session_manager = Arc::new(SessionManager::new(
crate::server::oidc::oidc_session_management::SessionManagementConfig::default(),
));
let manager = RarManager::new(config, session_manager);
let decisions = vec![RarDetailDecision {
detail_index: 0,
detail_type: "file_access".to_string(),
decision: RarDecisionType::Granted,
granted_actions: vec!["read".to_string(), "write".to_string()],
granted_locations: vec!["https://example.com/doc1".to_string()],
granted_privileges: vec!["editor".to_string()],
reason: None,
restrictions: Vec::new(),
}];
let grant = manager.generate_permission_grant(&decisions);
assert!(grant.resource_access.contains_key("file_access"));
assert_eq!(grant.resource_count, 1);
assert!(
grant
.effective_scopes
.contains(&"file_access:read".to_string())
);
assert!(
grant
.effective_scopes
.contains(&"file_access:write".to_string())
);
Ok(())
}
#[test]
fn test_rar_config_empty() {
let config = RarConfig::empty();
assert!(config.supported_types.is_empty());
assert!(config.type_action_mapping.is_empty());
assert!(config.require_explicit_consent);
}
#[test]
fn test_rar_config_with_type_chainable() {
let config = RarConfig::empty()
.with_type("payment", &["initiate", "confirm"])
.with_type("file_access", &["read", "write"]);
assert_eq!(config.supported_types.len(), 2);
assert!(config.supported_types.contains(&"payment".to_string()));
assert!(config.supported_types.contains(&"file_access".to_string()));
assert_eq!(
config.type_action_mapping["payment"],
vec!["initiate".to_string(), "confirm".to_string()]
);
}
#[test]
fn test_rar_config_max_details_and_discovery() {
let config = RarConfig::default()
.max_details(50)
.resource_discovery(true);
assert_eq!(config.max_authorization_details, 50);
assert!(config.enable_resource_discovery);
}
}