use crate::config::{MergedProfile, PolicyItems, ProfileSpec, SourceConstraints};
use crate::errors::{CompositionError, Result};
pub fn validate_constraints(
source_name: &str,
constraints: &SourceConstraints,
spec: &ProfileSpec,
) -> Result<()> {
if constraints.no_scripts
&& let Some(ref scripts) = spec.scripts
&& (!scripts.pre_apply.is_empty()
|| !scripts.post_apply.is_empty()
|| !scripts.pre_reconcile.is_empty()
|| !scripts.post_reconcile.is_empty()
|| !scripts.on_drift.is_empty()
|| !scripts.on_change.is_empty())
{
return Err(CompositionError::ScriptsNotAllowed {
source_name: source_name.to_string(),
}
.into());
}
if !constraints.allow_system_changes && !spec.system.is_empty() {
let first_key = spec.system.keys().next().cloned().unwrap_or_default();
return Err(CompositionError::SystemChangeNotAllowed {
source_name: source_name.to_string(),
setting: first_key,
}
.into());
}
if !constraints.allowed_target_paths.is_empty()
&& let Some(ref files) = spec.files
{
for managed in &files.managed {
let target_str = managed.target.to_string_lossy();
if !path_matches_any(&target_str, &constraints.allowed_target_paths) {
return Err(CompositionError::PathNotAllowed {
source_name: source_name.to_string(),
path: target_str.to_string(),
}
.into());
}
}
}
if let Some(ref enc_constraint) = constraints.encryption
&& !enc_constraint.required_targets.is_empty()
&& let Some(ref files) = spec.files
{
for managed in &files.managed {
let target_str = managed.target.to_string_lossy();
if let Some(matched_pattern) =
find_matching_pattern(&target_str, &enc_constraint.required_targets)
{
match managed.encryption.as_ref() {
None => {
return Err(CompositionError::EncryptionRequired {
source_name: source_name.to_string(),
path: target_str.to_string(),
pattern: matched_pattern,
}
.into());
}
Some(enc_spec) => {
if let Some(ref required_backend) = enc_constraint.backend
&& enc_spec.backend != *required_backend
{
return Err(CompositionError::EncryptionBackendMismatch {
source_name: source_name.to_string(),
path: target_str.to_string(),
pattern: matched_pattern.clone(),
actual_backend: enc_spec.backend.clone(),
required_backend: required_backend.clone(),
}
.into());
}
if let Some(ref required_mode) = enc_constraint.mode
&& enc_spec.mode != *required_mode
{
return Err(CompositionError::EncryptionModeMismatch {
source_name: source_name.to_string(),
path: target_str.to_string(),
pattern: matched_pattern,
actual_mode: format!("{:?}", enc_spec.mode),
required_mode: format!("{:?}", required_mode),
}
.into());
}
}
}
}
}
}
Ok(())
}
pub(super) fn path_matches_any(path: &str, allowed: &[String]) -> bool {
find_matching_pattern(path, allowed).is_some()
}
pub(super) fn find_matching_pattern(path: &str, patterns: &[String]) -> Option<String> {
for pattern in patterns {
if let Ok(glob_pattern) = glob::Pattern::new(pattern)
&& glob_pattern.matches(path)
{
return Some(pattern.clone());
}
if pattern.ends_with('/') && path.starts_with(pattern.as_str()) {
return Some(pattern.clone());
}
if path == pattern {
return Some(pattern.clone());
}
}
None
}
pub fn check_locked_violations(
source_name: &str,
locked: &PolicyItems,
local_merged: &MergedProfile,
) -> Result<()> {
for locked_file in &locked.files {
for local_file in &local_merged.files.managed {
if local_file.target == locked_file.target && local_file.source != locked_file.source {
return Err(CompositionError::LockedResource {
source_name: source_name.to_string(),
resource: locked_file.target.to_string_lossy().to_string(),
}
.into());
}
}
}
Ok(())
}