photoncloud-monorepo/iam/crates/iam-authz/src/context.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

220 lines
6.5 KiB
Rust

//! Authorization context
//!
//! Provides context for policy evaluation including principal, resource, and request metadata.
use std::collections::HashMap;
use std::net::IpAddr;
use iam_types::{Principal, Resource};
/// Context for authorization evaluation
#[derive(Debug, Clone)]
pub struct AuthzContext {
/// Source IP address of the request
pub source_ip: Option<IpAddr>,
/// Request timestamp (Unix seconds)
pub timestamp: u64,
/// Request metadata
pub metadata: HashMap<String, String>,
/// HTTP method (if applicable)
pub http_method: Option<String>,
/// Request path (if applicable)
pub request_path: Option<String>,
}
impl AuthzContext {
/// Create a new context with the current timestamp
pub fn new() -> Self {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
Self {
source_ip: None,
timestamp,
metadata: HashMap::new(),
http_method: None,
request_path: None,
}
}
/// Set the source IP
pub fn with_source_ip(mut self, ip: IpAddr) -> Self {
self.source_ip = Some(ip);
self
}
/// Set the timestamp
pub fn with_timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
/// Add metadata
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
/// Set HTTP method
pub fn with_http_method(mut self, method: impl Into<String>) -> Self {
self.http_method = Some(method.into());
self
}
/// Set request path
pub fn with_request_path(mut self, path: impl Into<String>) -> Self {
self.request_path = Some(path.into());
self
}
/// Get a value for condition evaluation
pub fn get_value(&self, key: &str) -> Option<String> {
match key {
"request.source_ip" => self.source_ip.map(|ip| ip.to_string()),
"request.time" => Some(self.timestamp.to_string()),
"request.method" => self.http_method.clone(),
"request.path" => self.request_path.clone(),
key if key.starts_with("request.metadata.") => {
let meta_key = &key["request.metadata.".len()..];
self.metadata.get(meta_key).cloned()
}
_ => None,
}
}
}
impl Default for AuthzContext {
fn default() -> Self {
Self::new()
}
}
/// Variable context for condition evaluation
/// Combines principal, resource, and request context
pub struct VariableContext<'a> {
pub principal: &'a Principal,
pub resource: &'a Resource,
pub context: &'a AuthzContext,
}
impl<'a> VariableContext<'a> {
/// Create a new variable context
pub fn new(
principal: &'a Principal,
resource: &'a Resource,
context: &'a AuthzContext,
) -> Self {
Self {
principal,
resource,
context,
}
}
/// Resolve a variable key to its value
pub fn resolve(&self, key: &str) -> Option<String> {
if let Some(prop) = key.strip_prefix("principal.") {
self.resolve_principal(prop)
} else if let Some(prop) = key.strip_prefix("resource.") {
self.resource.get_property(prop)
} else if key.starts_with("request.") {
self.context.get_value(key)
} else {
None
}
}
fn resolve_principal(&self, prop: &str) -> Option<String> {
match prop {
"id" => Some(self.principal.id.clone()),
"kind" => Some(self.principal.kind.to_string()),
"name" => Some(self.principal.name.clone()),
"org_id" => self.principal.org_id.clone(),
"project_id" => self.principal.project_id.clone(),
"node_id" => self.principal.node_id.clone(),
"email" => self.principal.email.clone(),
key if key.starts_with("metadata.") => {
let meta_key = &key["metadata.".len()..];
self.principal.metadata.get(meta_key).cloned()
}
_ => None,
}
}
/// Substitute variables in a string (${var} syntax)
pub fn substitute(&self, template: &str) -> String {
let mut result = template.to_string();
let mut start = 0;
while let Some(var_start) = result[start..].find("${") {
let absolute_start = start + var_start;
if let Some(var_end) = result[absolute_start..].find('}') {
let absolute_end = absolute_start + var_end;
let var_name = &result[absolute_start + 2..absolute_end];
if let Some(value) = self.resolve(var_name) {
result = format!(
"{}{}{}",
&result[..absolute_start],
value,
&result[absolute_end + 1..]
);
start = absolute_start + value.len();
} else {
// Variable not found, keep as is
start = absolute_end + 1;
}
} else {
break;
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[test]
fn test_context_values() {
let ctx = AuthzContext::new()
.with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))
.with_metadata("tenant", "acme");
assert_eq!(ctx.get_value("request.source_ip"), Some("10.0.0.1".into()));
assert_eq!(
ctx.get_value("request.metadata.tenant"),
Some("acme".into())
);
}
#[test]
fn test_variable_substitution() {
let principal = Principal::new_user("alice", "Alice Smith");
let resource = Resource::new("instance", "vm-123", "org-1", "proj-1").with_owner("alice");
let context = AuthzContext::new();
let var_ctx = VariableContext::new(&principal, &resource, &context);
// Test simple substitution
assert_eq!(var_ctx.substitute("user-${principal.id}"), "user-alice");
// Test multiple substitutions
assert_eq!(
var_ctx.substitute("${resource.kind}/${resource.id}"),
"instance/vm-123"
);
// Test unknown variable (kept as is)
assert_eq!(var_ctx.substitute("${unknown.var}"), "${unknown.var}");
}
}