[wip] single runtime handling
This commit is contained in:
@@ -295,6 +295,10 @@ pub struct SecurityConfig {
|
||||
/// Enable authentication
|
||||
#[serde(default = "default_true")]
|
||||
pub enable_auth: bool,
|
||||
|
||||
/// Allow unauthenticated self-service user registration
|
||||
#[serde(default)]
|
||||
pub allow_self_registration: bool,
|
||||
}
|
||||
|
||||
fn default_jwt_access_expiration() -> u64 {
|
||||
@@ -676,6 +680,7 @@ impl Default for SecurityConfig {
|
||||
jwt_refresh_expiration: default_jwt_refresh_expiration(),
|
||||
encryption_key: None,
|
||||
enable_auth: true,
|
||||
allow_self_registration: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -924,6 +929,7 @@ mod tests {
|
||||
jwt_refresh_expiration: 604800,
|
||||
encryption_key: Some("a".repeat(32)),
|
||||
enable_auth: true,
|
||||
allow_self_registration: false,
|
||||
},
|
||||
worker: None,
|
||||
sensor: None,
|
||||
|
||||
@@ -15,6 +15,7 @@ pub mod models;
|
||||
pub mod mq;
|
||||
pub mod pack_environment;
|
||||
pub mod pack_registry;
|
||||
pub mod rbac;
|
||||
pub mod repositories;
|
||||
pub mod runtime_detection;
|
||||
pub mod schema;
|
||||
|
||||
@@ -430,6 +430,10 @@ pub mod runtime {
|
||||
#[serde(default)]
|
||||
pub interpreter: InterpreterConfig,
|
||||
|
||||
/// Strategy for inline code execution.
|
||||
#[serde(default)]
|
||||
pub inline_execution: InlineExecutionConfig,
|
||||
|
||||
/// Optional isolated environment configuration (venv, node_modules, etc.)
|
||||
#[serde(default)]
|
||||
pub environment: Option<EnvironmentConfig>,
|
||||
@@ -449,6 +453,33 @@ pub mod runtime {
|
||||
pub env_vars: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Controls how inline code is materialized before execution.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct InlineExecutionConfig {
|
||||
/// Whether inline code is passed directly to the interpreter or first
|
||||
/// written to a temporary file.
|
||||
#[serde(default)]
|
||||
pub strategy: InlineExecutionStrategy,
|
||||
|
||||
/// Optional extension for temporary inline files (e.g. ".sh").
|
||||
#[serde(default)]
|
||||
pub extension: Option<String>,
|
||||
|
||||
/// When true, inline wrapper files export the merged input map as shell
|
||||
/// environment variables (`PARAM_*` and bare names) before executing the
|
||||
/// script body.
|
||||
#[serde(default)]
|
||||
pub inject_shell_helpers: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InlineExecutionStrategy {
|
||||
#[default]
|
||||
Direct,
|
||||
TempFile,
|
||||
}
|
||||
|
||||
/// Describes the interpreter binary and how it invokes action scripts.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InterpreterConfig {
|
||||
|
||||
@@ -481,9 +481,8 @@ pub struct PackRegisteredPayload {
|
||||
|
||||
/// Payload for ExecutionCancelRequested message
|
||||
///
|
||||
/// Sent by the API to the worker that is running a specific execution,
|
||||
/// instructing it to gracefully terminate the process (SIGINT, then SIGTERM
|
||||
/// after a grace period).
|
||||
/// Sent by the API or executor to the worker that is running a specific
|
||||
/// execution, instructing it to terminate the process promptly.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExecutionCancelRequestedPayload {
|
||||
/// Execution ID to cancel
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
//! Pack Component Loader
|
||||
//!
|
||||
//! Reads runtime, action, trigger, and sensor YAML definitions from a pack directory
|
||||
//! Reads permission set, runtime, action, trigger, and sensor YAML definitions from a pack directory
|
||||
//! and registers them in the database. This is the Rust-native equivalent of
|
||||
//! the Python `load_core_pack.py` script used during init-packs.
|
||||
//!
|
||||
//! Components are loaded in dependency order:
|
||||
//! 1. Runtimes (no dependencies)
|
||||
//! 2. Triggers (no dependencies)
|
||||
//! 3. Actions (depend on runtime; workflow actions also create workflow_definition records)
|
||||
//! 4. Sensors (depend on triggers and runtime)
|
||||
//! 1. Permission sets (no dependencies)
|
||||
//! 2. Runtimes (no dependencies)
|
||||
//! 3. Triggers (no dependencies)
|
||||
//! 4. Actions (depend on runtime; workflow actions also create workflow_definition records)
|
||||
//! 5. Sensors (depend on triggers and runtime)
|
||||
//!
|
||||
//! All loaders use **upsert** semantics: if an entity with the same ref already
|
||||
//! exists it is updated in place (preserving its database ID); otherwise a new
|
||||
@@ -38,6 +39,9 @@ use tracing::{debug, info, warn};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::models::Id;
|
||||
use crate::repositories::action::{ActionRepository, UpdateActionInput};
|
||||
use crate::repositories::identity::{
|
||||
CreatePermissionSetInput, PermissionSetRepository, UpdatePermissionSetInput,
|
||||
};
|
||||
use crate::repositories::runtime::{CreateRuntimeInput, RuntimeRepository, UpdateRuntimeInput};
|
||||
use crate::repositories::runtime_version::{
|
||||
CreateRuntimeVersionInput, RuntimeVersionRepository, UpdateRuntimeVersionInput,
|
||||
@@ -56,6 +60,12 @@ use crate::workflow::parser::parse_workflow_yaml;
|
||||
/// Result of loading pack components into the database.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PackLoadResult {
|
||||
/// Number of permission sets created
|
||||
pub permission_sets_loaded: usize,
|
||||
/// Number of permission sets updated
|
||||
pub permission_sets_updated: usize,
|
||||
/// Number of permission sets skipped
|
||||
pub permission_sets_skipped: usize,
|
||||
/// Number of runtimes created
|
||||
pub runtimes_loaded: usize,
|
||||
/// Number of runtimes updated (already existed)
|
||||
@@ -88,15 +98,27 @@ pub struct PackLoadResult {
|
||||
|
||||
impl PackLoadResult {
|
||||
pub fn total_loaded(&self) -> usize {
|
||||
self.runtimes_loaded + self.triggers_loaded + self.actions_loaded + self.sensors_loaded
|
||||
self.permission_sets_loaded
|
||||
+ self.runtimes_loaded
|
||||
+ self.triggers_loaded
|
||||
+ self.actions_loaded
|
||||
+ self.sensors_loaded
|
||||
}
|
||||
|
||||
pub fn total_skipped(&self) -> usize {
|
||||
self.runtimes_skipped + self.triggers_skipped + self.actions_skipped + self.sensors_skipped
|
||||
self.permission_sets_skipped
|
||||
+ self.runtimes_skipped
|
||||
+ self.triggers_skipped
|
||||
+ self.actions_skipped
|
||||
+ self.sensors_skipped
|
||||
}
|
||||
|
||||
pub fn total_updated(&self) -> usize {
|
||||
self.runtimes_updated + self.triggers_updated + self.actions_updated + self.sensors_updated
|
||||
self.permission_sets_updated
|
||||
+ self.runtimes_updated
|
||||
+ self.triggers_updated
|
||||
+ self.actions_updated
|
||||
+ self.sensors_updated
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,22 +154,26 @@ impl<'a> PackComponentLoader<'a> {
|
||||
pack_dir.display()
|
||||
);
|
||||
|
||||
// 1. Load runtimes first (no dependencies)
|
||||
// 1. Load permission sets first (no dependencies)
|
||||
let permission_set_refs = self.load_permission_sets(pack_dir, &mut result).await?;
|
||||
|
||||
// 2. Load runtimes (no dependencies)
|
||||
let runtime_refs = self.load_runtimes(pack_dir, &mut result).await?;
|
||||
|
||||
// 2. Load triggers (no dependencies)
|
||||
// 3. Load triggers (no dependencies)
|
||||
let (trigger_ids, trigger_refs) = self.load_triggers(pack_dir, &mut result).await?;
|
||||
|
||||
// 3. Load actions (depend on runtime)
|
||||
// 4. Load actions (depend on runtime)
|
||||
let action_refs = self.load_actions(pack_dir, &mut result).await?;
|
||||
|
||||
// 4. Load sensors (depend on triggers and runtime)
|
||||
// 5. Load sensors (depend on triggers and runtime)
|
||||
let sensor_refs = self
|
||||
.load_sensors(pack_dir, &trigger_ids, &mut result)
|
||||
.await?;
|
||||
|
||||
// 5. Clean up entities that are no longer in the pack's YAML files
|
||||
// 6. Clean up entities that are no longer in the pack's YAML files
|
||||
self.cleanup_removed_entities(
|
||||
&permission_set_refs,
|
||||
&runtime_refs,
|
||||
&trigger_refs,
|
||||
&action_refs,
|
||||
@@ -169,6 +195,146 @@ impl<'a> PackComponentLoader<'a> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Load permission set definitions from `pack_dir/permission_sets/*.yaml`.
|
||||
///
|
||||
/// Permission sets are pack-scoped authorization metadata. Their `grants`
|
||||
/// payload is stored verbatim and interpreted by the API authorization
|
||||
/// layer at request time.
|
||||
async fn load_permission_sets(
|
||||
&self,
|
||||
pack_dir: &Path,
|
||||
result: &mut PackLoadResult,
|
||||
) -> Result<Vec<String>> {
|
||||
let permission_sets_dir = pack_dir.join("permission_sets");
|
||||
let mut loaded_refs = Vec::new();
|
||||
|
||||
if !permission_sets_dir.exists() {
|
||||
info!(
|
||||
"No permission_sets directory found for pack '{}'",
|
||||
self.pack_ref
|
||||
);
|
||||
return Ok(loaded_refs);
|
||||
}
|
||||
|
||||
let yaml_files = read_yaml_files(&permission_sets_dir)?;
|
||||
info!(
|
||||
"Found {} permission set definition(s) for pack '{}'",
|
||||
yaml_files.len(),
|
||||
self.pack_ref
|
||||
);
|
||||
|
||||
for (filename, content) in &yaml_files {
|
||||
let data: serde_yaml_ng::Value = serde_yaml_ng::from_str(content).map_err(|e| {
|
||||
Error::validation(format!(
|
||||
"Failed to parse permission set YAML {}: {}",
|
||||
filename, e
|
||||
))
|
||||
})?;
|
||||
|
||||
let permission_set_ref = match data.get("ref").and_then(|v| v.as_str()) {
|
||||
Some(r) => r.to_string(),
|
||||
None => {
|
||||
let msg = format!(
|
||||
"Permission set YAML {} missing 'ref' field, skipping",
|
||||
filename
|
||||
);
|
||||
warn!("{}", msg);
|
||||
result.warnings.push(msg);
|
||||
result.permission_sets_skipped += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let label = data
|
||||
.get("label")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let description = data
|
||||
.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let grants = data
|
||||
.get("grants")
|
||||
.and_then(|v| serde_json::to_value(v).ok())
|
||||
.unwrap_or_else(|| serde_json::json!([]));
|
||||
|
||||
if !grants.is_array() {
|
||||
let msg = format!(
|
||||
"Permission set '{}' has non-array 'grants', skipping",
|
||||
permission_set_ref
|
||||
);
|
||||
warn!("{}", msg);
|
||||
result.warnings.push(msg);
|
||||
result.permission_sets_skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(existing) =
|
||||
PermissionSetRepository::find_by_ref(self.pool, &permission_set_ref).await?
|
||||
{
|
||||
let update_input = UpdatePermissionSetInput {
|
||||
label,
|
||||
description,
|
||||
grants: Some(grants),
|
||||
};
|
||||
|
||||
match PermissionSetRepository::update(self.pool, existing.id, update_input).await {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"Updated permission set '{}' (ID: {})",
|
||||
permission_set_ref, existing.id
|
||||
);
|
||||
result.permission_sets_updated += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!(
|
||||
"Failed to update permission set '{}': {}",
|
||||
permission_set_ref, e
|
||||
);
|
||||
warn!("{}", msg);
|
||||
result.warnings.push(msg);
|
||||
result.permission_sets_skipped += 1;
|
||||
}
|
||||
}
|
||||
loaded_refs.push(permission_set_ref);
|
||||
continue;
|
||||
}
|
||||
|
||||
let input = CreatePermissionSetInput {
|
||||
r#ref: permission_set_ref.clone(),
|
||||
pack: Some(self.pack_id),
|
||||
pack_ref: Some(self.pack_ref.clone()),
|
||||
label,
|
||||
description,
|
||||
grants,
|
||||
};
|
||||
|
||||
match PermissionSetRepository::create(self.pool, input).await {
|
||||
Ok(permission_set) => {
|
||||
info!(
|
||||
"Created permission set '{}' (ID: {})",
|
||||
permission_set_ref, permission_set.id
|
||||
);
|
||||
result.permission_sets_loaded += 1;
|
||||
loaded_refs.push(permission_set_ref);
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!(
|
||||
"Failed to create permission set '{}': {}",
|
||||
permission_set_ref, e
|
||||
);
|
||||
warn!("{}", msg);
|
||||
result.warnings.push(msg);
|
||||
result.permission_sets_skipped += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(loaded_refs)
|
||||
}
|
||||
|
||||
/// Load runtime definitions from `pack_dir/runtimes/*.yaml`.
|
||||
///
|
||||
/// Runtimes define how actions and sensors are executed (interpreter,
|
||||
@@ -1308,12 +1474,37 @@ impl<'a> PackComponentLoader<'a> {
|
||||
/// removed.
|
||||
async fn cleanup_removed_entities(
|
||||
&self,
|
||||
permission_set_refs: &[String],
|
||||
runtime_refs: &[String],
|
||||
trigger_refs: &[String],
|
||||
action_refs: &[String],
|
||||
sensor_refs: &[String],
|
||||
result: &mut PackLoadResult,
|
||||
) {
|
||||
match PermissionSetRepository::delete_by_pack_excluding(
|
||||
self.pool,
|
||||
self.pack_id,
|
||||
permission_set_refs,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(count) => {
|
||||
if count > 0 {
|
||||
info!(
|
||||
"Removed {} stale permission set(s) from pack '{}'",
|
||||
count, self.pack_ref
|
||||
);
|
||||
result.removed += count as usize;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to clean up stale permission sets for pack '{}': {}",
|
||||
self.pack_ref, e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up sensors first (they depend on triggers/runtimes)
|
||||
match SensorRepository::delete_by_pack_excluding(self.pool, self.pack_id, sensor_refs).await
|
||||
{
|
||||
|
||||
292
crates/common/src/rbac.rs
Normal file
292
crates/common/src/rbac.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
//! 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));
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use crate::models::{identity::*, Id, JsonDict};
|
||||
use crate::Result;
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, List, Repository, Update};
|
||||
use super::{Create, Delete, FindById, FindByRef, List, Repository, Update};
|
||||
|
||||
pub struct IdentityRepository;
|
||||
|
||||
@@ -200,6 +200,22 @@ impl FindById for PermissionSetRepository {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindByRef for PermissionSetRepository {
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionSet>(
|
||||
"SELECT id, ref, pack, pack_ref, label, description, grants, created, updated FROM permission_set WHERE ref = $1"
|
||||
)
|
||||
.bind(ref_str)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for PermissionSetRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
@@ -287,6 +303,54 @@ impl Delete for PermissionSetRepository {
|
||||
}
|
||||
}
|
||||
|
||||
impl PermissionSetRepository {
|
||||
pub async fn find_by_identity<'e, E>(executor: E, identity_id: Id) -> Result<Vec<PermissionSet>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionSet>(
|
||||
"SELECT ps.id, ps.ref, ps.pack, ps.pack_ref, ps.label, ps.description, ps.grants, ps.created, ps.updated
|
||||
FROM permission_set ps
|
||||
INNER JOIN permission_assignment pa ON pa.permset = ps.id
|
||||
WHERE pa.identity = $1
|
||||
ORDER BY ps.ref ASC",
|
||||
)
|
||||
.bind(identity_id)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Delete permission sets belonging to a pack whose refs are NOT in the given set.
|
||||
///
|
||||
/// Used during pack reinstallation to clean up permission sets that were
|
||||
/// removed from the pack's metadata. Associated permission assignments are
|
||||
/// cascade-deleted by the FK constraint.
|
||||
pub async fn delete_by_pack_excluding<'e, E>(
|
||||
executor: E,
|
||||
pack_id: Id,
|
||||
keep_refs: &[String],
|
||||
) -> Result<u64>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = if keep_refs.is_empty() {
|
||||
sqlx::query("DELETE FROM permission_set WHERE pack = $1")
|
||||
.bind(pack_id)
|
||||
.execute(executor)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query("DELETE FROM permission_set WHERE pack = $1 AND ref != ALL($2)")
|
||||
.bind(pack_id)
|
||||
.bind(keep_refs)
|
||||
.execute(executor)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
// Permission Assignment Repository
|
||||
pub struct PermissionAssignmentRepository;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user