108 lines
3.1 KiB
Rust
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()
|
||
|
|
}
|