photoncloud-monorepo/iam/crates/iam-authz/src/condition.rs
centra 3eeb303dcb feat: Batch commit for T039.S3 deployment
Includes all pending changes needed for nixos-anywhere:
- fiberlb: L7 policy, rule, certificate types
- deployer: New service for cluster management
- nix-nos: Generic network modules
- Various service updates and fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 04:34:51 +09:00

348 lines
12 KiB
Rust

//! Condition evaluation for ABAC
//!
//! Evaluates condition expressions against the current authorization context.
use std::net::IpAddr;
use ipnetwork::IpNetwork;
use iam_types::{Condition, ConditionExpr, Error, IamError, Result};
use crate::context::VariableContext;
/// Evaluate a condition against the given context
pub fn evaluate_condition(condition: &Condition, ctx: &VariableContext<'_>) -> Result<bool> {
evaluate_expr(&condition.expression, ctx)
}
/// Evaluate a condition expression
fn evaluate_expr(expr: &ConditionExpr, ctx: &VariableContext<'_>) -> Result<bool> {
match expr {
ConditionExpr::StringEquals { key, value } => {
let actual = resolve_key(key, ctx);
let expected = ctx.substitute(value);
Ok(actual.as_deref() == Some(&expected))
}
ConditionExpr::StringNotEquals { key, value } => {
let actual = resolve_key(key, ctx);
let expected = ctx.substitute(value);
Ok(actual.as_deref() != Some(&expected))
}
ConditionExpr::StringLike { key, pattern } => {
let actual = require_key(key, ctx)?;
let pattern = ctx.substitute(pattern);
Ok(glob_match::glob_match(&pattern, &actual))
}
ConditionExpr::StringNotLike { key, pattern } => {
let actual = require_key(key, ctx)?;
let pattern = ctx.substitute(pattern);
Ok(!glob_match::glob_match(&pattern, &actual))
}
ConditionExpr::NumericEquals { key, value } => {
let actual = require_key(key, ctx)?;
let actual_num: i64 = actual.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Cannot parse '{}' as number for key '{}'",
actual, key
)))
})?;
Ok(actual_num == *value)
}
ConditionExpr::NumericLessThan { key, value } => {
let actual = require_key(key, ctx)?;
let actual_num: i64 = actual.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Cannot parse '{}' as number",
actual
)))
})?;
Ok(actual_num < *value)
}
ConditionExpr::NumericLessThanEquals { key, value } => {
let actual = require_key(key, ctx)?;
let actual_num: i64 = actual.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Cannot parse '{}' as number",
actual
)))
})?;
Ok(actual_num <= *value)
}
ConditionExpr::NumericGreaterThan { key, value } => {
let actual = require_key(key, ctx)?;
let actual_num: i64 = actual.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Cannot parse '{}' as number",
actual
)))
})?;
Ok(actual_num > *value)
}
ConditionExpr::NumericGreaterThanEquals { key, value } => {
let actual = require_key(key, ctx)?;
let actual_num: i64 = actual.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Cannot parse '{}' as number",
actual
)))
})?;
Ok(actual_num >= *value)
}
ConditionExpr::IpAddress { key, cidr } => {
let actual = require_key(key, ctx)?;
let ip: IpAddr = actual.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Cannot parse '{}' as IP address",
actual
)))
})?;
let network: IpNetwork = cidr.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Cannot parse '{}' as CIDR",
cidr
)))
})?;
Ok(network.contains(ip))
}
ConditionExpr::NotIpAddress { key, cidr } => {
let actual = require_key(key, ctx)?;
let ip: IpAddr = actual.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Cannot parse '{}' as IP address",
actual
)))
})?;
let network: IpNetwork = cidr.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Cannot parse '{}' as CIDR",
cidr
)))
})?;
Ok(!network.contains(ip))
}
ConditionExpr::TimeBetween { start, end } => {
let now = ctx.context.timestamp;
// Parse start/end as either Unix timestamp or HH:MM format
let start_ts = parse_time(start, now)?;
let end_ts = parse_time(end, now)?;
Ok(now >= start_ts && now <= end_ts)
}
ConditionExpr::Exists { key } => Ok(resolve_key(key, ctx).is_some()),
ConditionExpr::StringEqualsAny { key, values } => {
let actual = resolve_key(key, ctx);
match actual {
Some(actual_val) => {
let substituted: Vec<String> =
values.iter().map(|v| ctx.substitute(v)).collect();
Ok(substituted.contains(&actual_val))
}
None => Ok(false),
}
}
ConditionExpr::Bool { key, value } => {
let actual = require_key(key, ctx)?;
let actual_bool = matches!(actual.to_lowercase().as_str(), "true" | "1" | "yes");
Ok(actual_bool == *value)
}
ConditionExpr::And(exprs) => {
for e in exprs {
if !evaluate_expr(e, ctx)? {
return Ok(false);
}
}
Ok(true)
}
ConditionExpr::Or(exprs) => {
for e in exprs {
if evaluate_expr(e, ctx)? {
return Ok(true);
}
}
Ok(false)
}
ConditionExpr::Not(inner) => Ok(!evaluate_expr(inner, ctx)?),
}
}
/// Resolve a key to its value, returning Option
fn resolve_key(key: &str, ctx: &VariableContext<'_>) -> Option<String> {
ctx.resolve(key)
}
/// Resolve a key, returning an error if not found
fn require_key(key: &str, ctx: &VariableContext<'_>) -> Result<String> {
ctx.resolve(key).ok_or_else(|| {
Error::Iam(IamError::InvalidCondition(format!(
"Key not found: {}",
key
)))
})
}
/// Parse time string to Unix timestamp
fn parse_time(time_str: &str, reference_time: u64) -> Result<u64> {
// Try parsing as Unix timestamp
if let Ok(ts) = time_str.parse::<u64>() {
return Ok(ts);
}
// Try parsing as HH:MM (relative to current day)
if let Some((hours, minutes)) = time_str.split_once(':') {
let hours: u64 = hours.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Invalid time format: {}",
time_str
)))
})?;
let minutes: u64 = minutes.parse().map_err(|_| {
Error::Iam(IamError::InvalidCondition(format!(
"Invalid time format: {}",
time_str
)))
})?;
// Calculate seconds since midnight
let time_of_day = hours * 3600 + minutes * 60;
// Get current day start
let day_start = (reference_time / 86400) * 86400;
return Ok(day_start + time_of_day);
}
// Try parsing as ISO 8601
// For simplicity, just return an error for unsupported formats
Err(Error::Iam(IamError::InvalidCondition(format!(
"Unsupported time format: {}",
time_str
))))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::AuthzContext;
use iam_types::{Principal, Resource};
use std::net::{IpAddr, Ipv4Addr};
fn test_context<'a>(
principal: &'a Principal,
resource: &'a Resource,
context: &'a AuthzContext,
) -> VariableContext<'a> {
VariableContext::new(principal, resource, context)
}
#[test]
fn test_string_equals() {
let principal = Principal::new_user("alice", "Alice");
let resource = Resource::new("instance", "vm-1", "org-1", "proj-1").with_owner("alice");
let ctx = AuthzContext::new();
let var_ctx = test_context(&principal, &resource, &ctx);
// Direct comparison
let cond = Condition::string_equals("resource.owner", "alice");
assert!(evaluate_condition(&cond, &var_ctx).unwrap());
// Variable substitution
let cond = Condition::string_equals("resource.owner", "${principal.id}");
assert!(evaluate_condition(&cond, &var_ctx).unwrap());
// Non-matching
let cond = Condition::string_equals("resource.owner", "bob");
assert!(!evaluate_condition(&cond, &var_ctx).unwrap());
}
#[test]
fn test_string_like() {
let principal = Principal::new_user("alice", "Alice");
let resource = Resource::new("instance", "vm-prod-001", "org-1", "proj-1");
let ctx = AuthzContext::new();
let var_ctx = test_context(&principal, &resource, &ctx);
let cond = Condition::string_like("resource.id", "vm-prod-*");
assert!(evaluate_condition(&cond, &var_ctx).unwrap());
let cond = Condition::string_like("resource.id", "vm-dev-*");
assert!(!evaluate_condition(&cond, &var_ctx).unwrap());
}
#[test]
fn test_ip_address() {
let principal = Principal::new_user("alice", "Alice");
let resource = Resource::new("instance", "vm-1", "org-1", "proj-1");
let ctx = AuthzContext::new().with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 1, 50)));
let var_ctx = test_context(&principal, &resource, &ctx);
let cond = Condition::ip_address("request.source_ip", "10.0.0.0/8");
assert!(evaluate_condition(&cond, &var_ctx).unwrap());
let cond = Condition::ip_address("request.source_ip", "192.168.0.0/16");
assert!(!evaluate_condition(&cond, &var_ctx).unwrap());
}
#[test]
fn test_and_condition() {
let principal = Principal::new_user("alice", "Alice");
let resource = Resource::new("instance", "vm-1", "org-1", "proj-1").with_owner("alice");
let ctx = AuthzContext::new().with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
let var_ctx = test_context(&principal, &resource, &ctx);
let cond = Condition::and(vec![
Condition::string_equals("resource.owner", "${principal.id}"),
Condition::ip_address("request.source_ip", "10.0.0.0/8"),
]);
assert!(evaluate_condition(&cond, &var_ctx).unwrap());
// One condition fails
let cond = Condition::and(vec![
Condition::string_equals("resource.owner", "bob"),
Condition::ip_address("request.source_ip", "10.0.0.0/8"),
]);
assert!(!evaluate_condition(&cond, &var_ctx).unwrap());
}
#[test]
fn test_or_condition() {
let principal = Principal::new_user("alice", "Alice");
let resource = Resource::new("instance", "vm-1", "org-1", "proj-1").with_owner("bob");
let ctx = AuthzContext::new();
let var_ctx = test_context(&principal, &resource, &ctx);
let cond = Condition::or(vec![
Condition::string_equals("resource.owner", "${principal.id}"), // false
Condition::string_equals("principal.id", "alice"), // true
]);
assert!(evaluate_condition(&cond, &var_ctx).unwrap());
}
#[test]
fn test_not_condition() {
let principal = Principal::new_user("alice", "Alice");
let resource = Resource::new("instance", "vm-1", "org-1", "proj-1");
let ctx = AuthzContext::new();
let var_ctx = test_context(&principal, &resource, &ctx);
let cond = Condition::not(Condition::string_equals("principal.id", "bob"));
assert!(evaluate_condition(&cond, &var_ctx).unwrap());
}
}