293 lines
8.9 KiB
Rust
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));
|
|
}
|
|
}
|