remora/crates/remora-identity/src/authz.rs

108 lines
3.1 KiB
Rust

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<Target, IdentityError> {
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<Item = &'a AuthorizedResource>,
) -> 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()
}