use std::collections::HashMap;
use std::sync::Arc;
use modkit_security::{AccessScope, SecurityContext};
use super::IntoPropertyValue;
use uuid::Uuid;
use crate::api::AuthZResolverClient;
use crate::error::AuthZResolverError;
use crate::models::{
Action, BarrierMode, Capability, EvaluationRequest, EvaluationRequestContext, Resource,
Subject, TenantContext, TenantMode,
};
use crate::pep::compiler::{ConstraintCompileError, compile_to_access_scope};
#[derive(Debug, thiserror::Error)]
pub enum EnforcerError {
#[error("access denied by PDP")]
Denied {
deny_reason: Option<crate::models::DenyReason>,
},
#[error("authorization evaluation failed: {0}")]
EvaluationFailed(#[from] AuthZResolverError),
#[error("constraint compilation failed: {0}")]
CompileFailed(#[from] ConstraintCompileError),
}
#[derive(Debug, Clone, Default)]
pub struct AccessRequest {
resource_properties: HashMap<String, serde_json::Value>,
tenant_context: Option<TenantContext>,
require_constraints: Option<bool>,
}
impl AccessRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn resource_property(
mut self,
key: impl Into<String>,
value: impl IntoPropertyValue,
) -> Self {
self.resource_properties
.insert(key.into(), value.into_filter_value());
self
}
#[must_use]
pub fn resource_properties(mut self, props: HashMap<String, serde_json::Value>) -> Self {
self.resource_properties = props;
self
}
#[must_use]
pub fn context_tenant_id(mut self, id: Uuid) -> Self {
self.tenant_context.get_or_insert_default().root_id = Some(id);
self
}
#[must_use]
pub fn tenant_mode(mut self, mode: TenantMode) -> Self {
self.tenant_context.get_or_insert_default().mode = mode;
self
}
#[must_use]
pub fn barrier_mode(mut self, mode: BarrierMode) -> Self {
self.tenant_context.get_or_insert_default().barrier_mode = mode;
self
}
#[must_use]
pub fn tenant_status(mut self, statuses: Vec<String>) -> Self {
self.tenant_context.get_or_insert_default().tenant_status = Some(statuses);
self
}
#[must_use]
pub fn tenant_context(mut self, tc: TenantContext) -> Self {
self.tenant_context = Some(tc);
self
}
#[must_use]
pub fn require_constraints(mut self, require: bool) -> Self {
self.require_constraints = Some(require);
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct ResourceType {
pub name: &'static str,
pub supported_properties: &'static [&'static str],
}
#[derive(Clone)]
pub struct PolicyEnforcer {
authz: Arc<dyn AuthZResolverClient>,
capabilities: Vec<Capability>,
}
impl PolicyEnforcer {
pub fn new(authz: Arc<dyn AuthZResolverClient>) -> Self {
Self {
authz,
capabilities: Vec::new(),
}
}
#[must_use]
pub fn with_capabilities(mut self, capabilities: Vec<Capability>) -> Self {
self.capabilities = capabilities;
self
}
#[must_use]
pub fn build_request(
&self,
ctx: &SecurityContext,
resource: &ResourceType,
action: &str,
resource_id: Option<Uuid>,
require_constraints: bool,
) -> EvaluationRequest {
self.build_request_with(
ctx,
resource,
action,
resource_id,
require_constraints,
&AccessRequest::default(),
)
}
#[must_use]
pub fn build_request_with(
&self,
ctx: &SecurityContext,
resource: &ResourceType,
action: &str,
resource_id: Option<Uuid>,
require_constraints: bool,
request: &AccessRequest,
) -> EvaluationRequest {
let tenant_context = request.tenant_context.clone();
let mut subject_properties = HashMap::new();
subject_properties.insert(
"tenant_id".to_owned(),
serde_json::Value::String(ctx.subject_tenant_id().to_string()),
);
let bearer_token = ctx.bearer_token().cloned();
EvaluationRequest {
subject: Subject {
id: ctx.subject_id(),
subject_type: ctx.subject_type().map(ToOwned::to_owned),
properties: subject_properties,
},
action: Action {
name: action.to_owned(),
},
resource: Resource {
resource_type: resource.name.to_owned(),
id: resource_id,
properties: request.resource_properties.clone(),
},
context: EvaluationRequestContext {
tenant_context,
token_scopes: ctx.token_scopes().to_vec(),
require_constraints,
capabilities: self.capabilities.clone(),
supported_properties: resource
.supported_properties
.iter()
.map(|s| (*s).to_owned())
.collect(),
bearer_token,
},
}
}
pub async fn access_scope(
&self,
ctx: &SecurityContext,
resource: &ResourceType,
action: &str,
resource_id: Option<Uuid>,
) -> Result<AccessScope, EnforcerError> {
self.access_scope_with(
ctx,
resource,
action,
resource_id,
&AccessRequest::default(),
)
.await
}
pub async fn access_scope_with(
&self,
ctx: &SecurityContext,
resource: &ResourceType,
action: &str,
resource_id: Option<Uuid>,
request: &AccessRequest,
) -> Result<AccessScope, EnforcerError> {
let require = request.require_constraints.unwrap_or(true);
let eval_request =
self.build_request_with(ctx, resource, action, resource_id, require, request);
let response = self.authz.evaluate(eval_request).await?;
if !response.decision {
return Err(EnforcerError::Denied {
deny_reason: response.context.deny_reason,
});
}
Ok(compile_to_access_scope(
&response,
require,
resource.supported_properties,
)?)
}
}
impl std::fmt::Debug for PolicyEnforcer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PolicyEnforcer")
.field("capabilities", &self.capabilities)
.finish_non_exhaustive()
}
}
#[cfg(test)]
#[path = "enforcer_tests.rs"]
mod enforcer_tests;