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>
220 lines
6.5 KiB
Rust
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}");
|
|
}
|
|
}
|