use std::sync::Arc;
use axum::extract::{FromRef, FromRequestParts};
use axum::http::request::Parts;
use axum::response::{IntoResponse, Response};
use cedar_policy::{Context, Entities, Request};
use pep::cedar::CedarAuthorizer;
use pep::oidc::types::JwtClaims;
use crate::auth::AuthenticatedUser;
use crate::error::ApiError;
use crate::models::Asset;
use crate::service::Services;
use crate::tenant::TenantPoolManager;
pub type AppRawState = (TenantPoolManager, Option<Arc<CedarAuthorizer>>);
#[derive(Clone)]
pub struct AppState {
pub services: Services,
pub authorizer: Option<Arc<CedarAuthorizer>>,
}
impl AppState {
pub fn services(&self) -> &Services {
&self.services
}
pub fn authorizer(&self) -> Option<&CedarAuthorizer> {
self.authorizer.as_ref().map(|v| v.as_ref())
}
}
#[derive(Clone)]
pub struct TenantState {
pub services: Services,
pub authorizer: Option<Arc<CedarAuthorizer>>,
pub instance_id: Option<String>,
}
impl TenantState {
pub fn services(&self) -> &Services {
&self.services
}
pub fn authorizer(&self) -> Option<&CedarAuthorizer> {
self.authorizer.as_ref().map(|v| v.as_ref())
}
pub fn instance_id(&self) -> Option<&str> {
self.instance_id.as_deref()
}
}
impl<S> FromRequestParts<S> for TenantState
where
S: Send + Sync,
TenantPoolManager: FromRef<S>,
AuthorizerRef: FromRef<S>,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
use axum::http::StatusCode;
let instance_id = parts
.headers
.get("X-Instance-Id")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
let manager = TenantPoolManager::from_ref(state);
let auth_ref = AuthorizerRef::from_ref(state);
let services = manager
.get_services(instance_id.as_deref())
.await
.map_err(|e| {
tracing::error!("Failed to resolve tenant services: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to resolve tenant: {}", e),
)
.into_response()
})?;
Ok(TenantState {
services,
authorizer: auth_ref.0,
instance_id,
})
}
}
#[derive(Clone)]
pub struct AuthorizerRef(pub Option<Arc<CedarAuthorizer>>);
impl AuthorizerRef {
pub fn as_ref(&self) -> Option<&CedarAuthorizer> {
self.0.as_ref().map(|v| v.as_ref())
}
}
impl FromRef<AppRawState> for AppState {
fn from_ref(state: &AppRawState) -> Self {
AppState {
services: state.0.global().clone(),
authorizer: state.1.clone(),
}
}
}
impl FromRef<AppRawState> for TenantPoolManager {
fn from_ref(state: &AppRawState) -> Self {
state.0.clone()
}
}
impl FromRef<AppRawState> for AuthorizerRef {
fn from_ref(state: &AppRawState) -> Self {
AuthorizerRef(state.1.clone())
}
}
pub fn extract_claims_for_cedar(user: &AuthenticatedUser) -> JwtClaims {
user.to_cedar_claims()
}
pub fn should_enforce_cedar(asset: &Asset) -> bool {
asset.auth_context.is_some()
}
pub fn check_permission(
authorizer: &CedarAuthorizer,
claims: &JwtClaims,
action: &str,
asset: &Asset,
) -> Result<(), ApiError> {
if !should_enforce_cedar(asset) {
return Ok(()); }
let allowed = evaluate_permission(authorizer, claims, action, asset)?;
if !allowed {
tracing::warn!(
"Cedar denied {} on asset {} for user {}",
action,
asset.id,
claims.sub
);
return Err(ApiError::Forbidden(format!(
"Access denied: you do not have '{}' permission on asset '{}'",
action, asset.id
)));
}
Ok(())
}
pub fn evaluate_permission(
authorizer: &CedarAuthorizer,
claims: &JwtClaims,
action: &str,
asset: &Asset,
) -> Result<bool, ApiError> {
let principal = pep::cedar::build_principal_uid(claims).map_err(|e| {
tracing::warn!("Cedar principal build failed: {}", e);
ApiError::Forbidden(format!("Authorization error: {}", e))
})?;
let action_uid = pep::cedar::build_action_uid(action).map_err(|e| {
tracing::warn!("Cedar action build failed: {}", e);
ApiError::Forbidden(format!("Authorization error: {}", e))
})?;
let resource = crate::cedar::entity::build_asset_resource_uid(asset).map_err(|e| {
tracing::warn!("Cedar resource build failed for asset {}: {}", asset.id, e);
ApiError::Forbidden(format!("Authorization error: {}", e))
})?;
let asset_entity = crate::cedar::entity::asset_to_cedar_entity(asset);
let principal_entity = crate::cedar::entity::user_to_cedar_principal(claims);
let entities = Entities::from_entities(
[asset_entity, principal_entity],
None,
)
.map_err(|e| {
tracing::warn!("Cedar entities build failed: {}", e);
ApiError::Forbidden(format!("Authorization error: {}", e))
})?;
let request = Request::new(principal, action_uid, resource, Context::empty(), None)
.map_err(|e| {
tracing::warn!("Cedar request build failed: {}", e);
ApiError::Forbidden(format!("Authorization error: {}", e))
})?;
let response = authorizer.is_allowed_with_entities(&request, &entities);
Ok(response.allowed())
}
pub async fn check_asset_permission_by_id(
services: &Services,
authorizer: &CedarAuthorizer,
claims: &JwtClaims,
action: &str,
asset_id: &str,
) -> Result<(), ApiError> {
let asset = services.assets().get_by_id(asset_id).await?;
if let Some(authz) = check_permission(authorizer, claims, action, &asset).err() {
return Err(authz);
}
Ok(())
}
pub async fn check_relation_permission(
services: &Services,
authorizer: &CedarAuthorizer,
claims: &JwtClaims,
action: &str,
from_asset_id: &str,
to_asset_id: &str,
) -> Result<(), ApiError> {
check_asset_permission_by_id(services, authorizer, claims, "View", from_asset_id).await?;
check_asset_permission_by_id(services, authorizer, claims, action, to_asset_id).await?;
Ok(())
}
pub fn filter_by_permission(
authorizer: &CedarAuthorizer,
claims: &JwtClaims,
action: &str,
assets: Vec<Asset>,
) -> Vec<Asset> {
assets
.into_iter()
.filter(|asset| {
if !should_enforce_cedar(asset) {
return true; }
match evaluate_permission(authorizer, claims, action, asset) {
Ok(allowed) => allowed,
Err(e) => {
tracing::warn!(
"Cedar evaluation error for asset {}: {}, denying access",
asset.id,
e
);
false
}
}
})
.collect()
}