Add identity and target authorization package
OIDC validation, principal mapping, remora:v1 roles, and current-authz filtering with kind acceptance on Keycloak and ZITADEL.
This commit is contained in:
parent
91ac6dee85
commit
bf028161ae
1 changed files with 3153 additions and 21 deletions
109
crates/remora-identity/src/authz.rs
Normal file
109
crates/remora-identity/src/authz.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
use remora_domain::{OpaqueId, StorageScope, Target};
|
||||
|
||||
use crate::error::IdentityError;
|
||||
use crate::principal::Principal;
|
||||
use crate::target::{DefaultTarget, TargetOverride, resolve_target};
|
||||
|
||||
/// Management operations governed by current authorization (architecture A-3).
|
||||
#[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()
|
||||
}
|
||||
Loading…
Reference in a new issue