use remora_domain::{OpaqueId, StorageScope, Target}; use crate::error::IdentityError; use crate::principal::Principal; use crate::target::{DefaultTarget, TargetOverride, resolve_target}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ManagementOperation { List, Read, Cancel, SoftDelete, Restore, HardDelete, } /// Indistinguishable missing-or-unauthorized outcome for single-resource reads. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ResourceVisibility { Visible, NotFound, } /// Minimal durable job projection needed for current-authorization decisions. #[derive(Clone, Debug, Eq, PartialEq)] pub struct AuthorizedResource { pub owner: OpaqueId, pub target: Target, } #[must_use] pub fn can_target(principal: &Principal, target: &Target) -> bool { if principal.administrator { return true; } principal.scopes.iter().any(|scope| scope.covers(target)) } /// Canonicalize defaults/overrides then authorize the result for the principal. /// /// Submission and other admission paths should use this rather than calling /// [`resolve_target`] without a subsequent authorization check. pub fn resolve_authorized_target( principal: &Principal, defaults: &DefaultTarget, override_: &TargetOverride, reserved_segments: &[&str], ) -> Result { let target = resolve_target(defaults, override_, reserved_segments)?; if can_target(principal, &target) { Ok(target) } else { Err(IdentityError::UnauthorizedTarget) } } #[must_use] pub fn may_manage( principal: &Principal, resource: &AuthorizedResource, _operation: ManagementOperation, ) -> bool { if principal.administrator { return true; } principal.id == resource.owner && can_target(principal, &resource.target) } /// Whether the principal is an administrator eligible to *request* revocation /// of an accepted execution grant. Does not mutate grants; durable revoke and /// cancellation mapping belong to admission/lifecycle packages. #[must_use] pub fn may_request_execution_grant_revocation(principal: &Principal) -> bool { principal.administrator } /// Single-resource access: missing and unauthorized are the same response class. #[must_use] pub fn resource_visibility( principal: &Principal, resource: Option<&AuthorizedResource>, operation: ManagementOperation, ) -> ResourceVisibility { match resource { Some(resource) if may_manage(principal, resource, operation) => ResourceVisibility::Visible, _ => ResourceVisibility::NotFound, } } /// List filtering never reveals unauthorized existence. pub fn filter_visible<'a>( principal: &Principal, resources: impl IntoIterator, ) -> Vec<&'a AuthorizedResource> { resources .into_iter() .filter(|resource| may_manage(principal, resource, ManagementOperation::List)) .collect() } #[must_use] pub fn covering_scopes<'a>(principal: &'a Principal, target: &Target) -> Vec<&'a StorageScope> { principal .scopes .iter() .filter(|scope| scope.covers(target)) .collect() }