Files
attune/crates/common/src/rbac.rs

293 lines
8.9 KiB
Rust

//! Role-based access control (RBAC) model and evaluator.
//!
//! Permission sets store `grants` as a JSON array of [`Grant`].
//! This module defines the canonical grant schema and matching logic.
use crate::models::{ArtifactVisibility, Id, OwnerType};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum Resource {
Packs,
Actions,
Rules,
Triggers,
Executions,
Events,
Enforcements,
Inquiries,
Keys,
Artifacts,
Workflows,
Webhooks,
Analytics,
History,
Identities,
Permissions,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum Action {
Read,
Create,
Update,
Delete,
Execute,
Cancel,
Respond,
Manage,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum OwnerConstraint {
#[serde(rename = "self")]
SelfOnly,
Any,
None,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionScopeConstraint {
#[serde(rename = "self")]
SelfOnly,
Descendants,
Any,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct GrantConstraints {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pack_refs: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<OwnerConstraint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner_types: Option<Vec<OwnerType>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visibility: Option<Vec<ArtifactVisibility>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execution_scope: Option<ExecutionScopeConstraint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refs: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ids: Option<Vec<Id>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub encrypted: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attributes: Option<HashMap<String, JsonValue>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Grant {
pub resource: Resource,
pub actions: Vec<Action>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub constraints: Option<GrantConstraints>,
}
#[derive(Debug, Clone)]
pub struct AuthorizationContext {
pub identity_id: Id,
pub identity_attributes: HashMap<String, JsonValue>,
pub target_id: Option<Id>,
pub target_ref: Option<String>,
pub pack_ref: Option<String>,
pub owner_identity_id: Option<Id>,
pub owner_type: Option<OwnerType>,
pub visibility: Option<ArtifactVisibility>,
pub encrypted: Option<bool>,
pub execution_owner_identity_id: Option<Id>,
pub execution_ancestor_identity_ids: Vec<Id>,
}
impl AuthorizationContext {
pub fn new(identity_id: Id) -> Self {
Self {
identity_id,
identity_attributes: HashMap::new(),
target_id: None,
target_ref: None,
pack_ref: None,
owner_identity_id: None,
owner_type: None,
visibility: None,
encrypted: None,
execution_owner_identity_id: None,
execution_ancestor_identity_ids: Vec::new(),
}
}
}
impl Grant {
pub fn allows(&self, resource: Resource, action: Action, ctx: &AuthorizationContext) -> bool {
self.resource == resource && self.actions.contains(&action) && self.constraints_match(ctx)
}
fn constraints_match(&self, ctx: &AuthorizationContext) -> bool {
let Some(constraints) = &self.constraints else {
return true;
};
if let Some(pack_refs) = &constraints.pack_refs {
let Some(pack_ref) = &ctx.pack_ref else {
return false;
};
if !pack_refs.contains(pack_ref) {
return false;
}
}
if let Some(owner) = constraints.owner {
let owner_match = match owner {
OwnerConstraint::SelfOnly => ctx.owner_identity_id == Some(ctx.identity_id),
OwnerConstraint::Any => true,
OwnerConstraint::None => ctx.owner_identity_id.is_none(),
};
if !owner_match {
return false;
}
}
if let Some(owner_types) = &constraints.owner_types {
let Some(owner_type) = ctx.owner_type else {
return false;
};
if !owner_types.contains(&owner_type) {
return false;
}
}
if let Some(visibility) = &constraints.visibility {
let Some(target_visibility) = ctx.visibility else {
return false;
};
if !visibility.contains(&target_visibility) {
return false;
}
}
if let Some(execution_scope) = constraints.execution_scope {
let execution_match = match execution_scope {
ExecutionScopeConstraint::SelfOnly => {
ctx.execution_owner_identity_id == Some(ctx.identity_id)
}
ExecutionScopeConstraint::Descendants => {
ctx.execution_owner_identity_id == Some(ctx.identity_id)
|| ctx
.execution_ancestor_identity_ids
.contains(&ctx.identity_id)
}
ExecutionScopeConstraint::Any => true,
};
if !execution_match {
return false;
}
}
if let Some(refs) = &constraints.refs {
let Some(target_ref) = &ctx.target_ref else {
return false;
};
if !refs.contains(target_ref) {
return false;
}
}
if let Some(ids) = &constraints.ids {
let Some(target_id) = ctx.target_id else {
return false;
};
if !ids.contains(&target_id) {
return false;
}
}
if let Some(encrypted) = constraints.encrypted {
let Some(target_encrypted) = ctx.encrypted else {
return false;
};
if encrypted != target_encrypted {
return false;
}
}
if let Some(attributes) = &constraints.attributes {
for (key, expected_value) in attributes {
let Some(actual_value) = ctx.identity_attributes.get(key) else {
return false;
};
if actual_value != expected_value {
return false;
}
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn grant_without_constraints_allows() {
let grant = Grant {
resource: Resource::Actions,
actions: vec![Action::Read],
constraints: None,
};
let ctx = AuthorizationContext::new(42);
assert!(grant.allows(Resource::Actions, Action::Read, &ctx));
assert!(!grant.allows(Resource::Actions, Action::Create, &ctx));
}
#[test]
fn key_constraint_owner_type_and_encrypted() {
let grant = Grant {
resource: Resource::Keys,
actions: vec![Action::Read],
constraints: Some(GrantConstraints {
owner_types: Some(vec![OwnerType::System]),
encrypted: Some(false),
..Default::default()
}),
};
let mut ctx = AuthorizationContext::new(1);
ctx.owner_type = Some(OwnerType::System);
ctx.encrypted = Some(false);
assert!(grant.allows(Resource::Keys, Action::Read, &ctx));
ctx.encrypted = Some(true);
assert!(!grant.allows(Resource::Keys, Action::Read, &ctx));
}
#[test]
fn attributes_constraint_requires_exact_value_match() {
let grant = Grant {
resource: Resource::Packs,
actions: vec![Action::Read],
constraints: Some(GrantConstraints {
attributes: Some(HashMap::from([("team".to_string(), json!("platform"))])),
..Default::default()
}),
};
let mut ctx = AuthorizationContext::new(1);
ctx.identity_attributes
.insert("team".to_string(), json!("platform"));
assert!(grant.allows(Resource::Packs, Action::Read, &ctx));
ctx.identity_attributes
.insert("team".to_string(), json!("infra"));
assert!(!grant.allows(Resource::Packs, Action::Read, &ctx));
}
}