Files
rustfs/crates/protocols/src/swift/acl.rs
T
housemeandGitHub efa89a98ed refactor(logging): standardize protocol and observability events (#3419)
* refactor(logging): standardize object capacity events

* refactor(logging): standardize protocol server events

* refactor(logging): standardize swift protocol events

* refactor(logging): standardize observability events

* refactor(logging): move masking helper and extend guardrails
2026-06-14 07:14:45 +08:00

849 lines
30 KiB
Rust

// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Access Control List (ACL) Support for Swift API
//!
//! Implements Swift container ACLs for fine-grained access control.
//!
//! # ACL Types
//!
//! ## Read ACLs (X-Container-Read)
//!
//! - **Public read**: `.r:*` - Anyone can read
//! - **Referrer restriction**: `.r:*.example.com` - Only specific referrers
//! - **Account access**: `AUTH_project123` - Specific account
//! - **User access**: `AUTH_project123:user1` - Specific user in account
//!
//! ## Write ACLs (X-Container-Write)
//!
//! - **Account access**: `AUTH_project123` - Specific account can write
//! - **User access**: `AUTH_project123:user1` - Specific user can write
//! - **No public write** - Public write is not supported for security
//!
//! # Examples
//!
//! ```text
//! # Public read container
//! X-Container-Read: .r:*
//!
//! # Referrer-restricted public read
//! X-Container-Read: .r:*.example.com,.r:*.cdn.com
//!
//! # Specific accounts can read
//! X-Container-Read: AUTH_abc123,AUTH_def456
//!
//! # Mixed ACL
//! X-Container-Read: .r:*.example.com,AUTH_abc123,AUTH_def456:user1
//!
//! # Write access
//! X-Container-Write: AUTH_abc123,AUTH_def456:user1
//! ```
//!
//! # Storage
//!
//! ACLs are stored in S3 bucket tags:
//! - Tag key: `swift-acl-read` with comma-separated grants
//! - Tag key: `swift-acl-write` with comma-separated grants
use super::{SwiftError, SwiftResult};
use std::fmt;
use tracing::debug;
const LOG_COMPONENT_PROTOCOLS: &str = "protocols";
const LOG_SUBSYSTEM_SWIFT_ACL: &str = "swift_acl";
const EVENT_SWIFT_ACL_DECISION: &str = "swift_acl_decision";
/// Container ACL configuration
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ContainerAcl {
/// Grants allowing read access (GET, HEAD)
pub read: Vec<AclGrant>,
/// Grants allowing write access (PUT, POST, DELETE)
pub write: Vec<AclGrant>,
}
/// ACL grant entry
#[derive(Debug, Clone, PartialEq)]
pub enum AclGrant {
/// Public read access (.r:*)
/// Allows anyone to read without authentication
PublicRead,
/// Public read with referrer restriction (.r:*.example.com)
/// Allows read only if HTTP Referer header matches pattern
PublicReadReferrer(String),
/// Specific account access (AUTH_project_id)
/// Allows all users in the account
Account(String),
/// Specific user access (AUTH_project_id:user_id)
/// Allows only the specific user in the account
User { account: String, user: String },
}
impl fmt::Display for AclGrant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AclGrant::PublicRead => write!(f, ".r:*"),
AclGrant::PublicReadReferrer(pattern) => write!(f, ".r:{}", pattern),
AclGrant::Account(account) => write!(f, "{}", account),
AclGrant::User { account, user } => write!(f, "{}:{}", account, user),
}
}
}
impl ContainerAcl {
/// Create a new empty ACL
pub fn new() -> Self {
Self::default()
}
/// Parse read ACL from header value
///
/// Format: comma-separated list of grants
/// - `.r:*` = public read
/// - `.r:*.example.com` = referrer restriction
/// - `AUTH_abc123` = account access
/// - `AUTH_abc123:user1` = user access
///
/// # Example
/// ```ignore
/// let acl = ContainerAcl::parse_read(".r:*,AUTH_abc123")?;
/// ```
pub fn parse_read(header: &str) -> SwiftResult<Vec<AclGrant>> {
let header = header.trim();
if header.is_empty() {
return Ok(Vec::new());
}
let mut grants = Vec::new();
for grant_str in header.split(',') {
let grant_str = grant_str.trim();
if grant_str.is_empty() {
continue;
}
let grant = Self::parse_grant(grant_str, true)?;
grants.push(grant);
}
Ok(grants)
}
/// Parse write ACL from header value
///
/// Format: comma-separated list of grants
/// - `AUTH_abc123` = account access
/// - `AUTH_abc123:user1` = user access
/// - Public write is NOT allowed
///
/// # Example
/// ```ignore
/// let acl = ContainerAcl::parse_write("AUTH_abc123,AUTH_def456:user1")?;
/// ```
pub fn parse_write(header: &str) -> SwiftResult<Vec<AclGrant>> {
let header = header.trim();
if header.is_empty() {
return Ok(Vec::new());
}
let mut grants = Vec::new();
for grant_str in header.split(',') {
let grant_str = grant_str.trim();
if grant_str.is_empty() {
continue;
}
let grant = Self::parse_grant(grant_str, false)?;
grants.push(grant);
}
Ok(grants)
}
/// Parse a single ACL grant string
fn parse_grant(grant_str: &str, allow_public: bool) -> SwiftResult<AclGrant> {
// Check for public read patterns (.r:*)
if let Some(pattern) = grant_str.strip_prefix(".r:") {
if !allow_public {
return Err(SwiftError::BadRequest("Public access not allowed in write ACL".to_string()));
}
// Skip ".r:"
if pattern == "*" {
Ok(AclGrant::PublicRead)
} else if !pattern.is_empty() {
Ok(AclGrant::PublicReadReferrer(pattern.to_string()))
} else {
Err(SwiftError::BadRequest("Invalid referrer pattern".to_string()))
}
}
// Check for account or user pattern (AUTH_*)
else if grant_str.starts_with("AUTH_") {
if let Some(colon_pos) = grant_str.find(':') {
// User-specific: AUTH_project:user
let account = grant_str[..colon_pos].to_string();
let user = grant_str[colon_pos + 1..].to_string();
if user.is_empty() {
return Err(SwiftError::BadRequest("Empty user ID in ACL".to_string()));
}
Ok(AclGrant::User { account, user })
} else {
// Account-level: AUTH_project
Ok(AclGrant::Account(grant_str.to_string()))
}
} else {
Err(SwiftError::BadRequest(format!("Invalid ACL grant format: {}", grant_str)))
}
}
/// Check if a request has read access based on this ACL
///
/// # Arguments
/// * `request_account` - The account making the request (if authenticated)
/// * `request_user` - The user making the request (if known)
/// * `referrer` - The HTTP Referer header value (if present)
///
/// # Returns
/// `true` if access is allowed, `false` otherwise
pub fn check_read_access(&self, request_account: Option<&str>, request_user: Option<&str>, referrer: Option<&str>) -> bool {
if self.read.is_empty() {
// No read ACL means default behavior: owner can read
return request_account.is_some();
}
for grant in &self.read {
match grant {
AclGrant::PublicRead => {
debug!(
event = EVENT_SWIFT_ACL_DECISION,
component = LOG_COMPONENT_PROTOCOLS,
subsystem = LOG_SUBSYSTEM_SWIFT_ACL,
action = "read",
result = "granted",
reason = "public_read",
"swift acl decision"
);
return true;
}
AclGrant::PublicReadReferrer(pattern) => {
if let Some(ref_header) = referrer
&& Self::matches_referrer_pattern(ref_header, pattern)
{
debug!(event = EVENT_SWIFT_ACL_DECISION, component = LOG_COMPONENT_PROTOCOLS, subsystem = LOG_SUBSYSTEM_SWIFT_ACL, action = "read", result = "granted", reason = "referrer_match", pattern = %pattern, "swift acl decision");
return true;
}
}
AclGrant::Account(account) => {
if let Some(req_account) = request_account
&& req_account == account
{
debug!(event = EVENT_SWIFT_ACL_DECISION, component = LOG_COMPONENT_PROTOCOLS, subsystem = LOG_SUBSYSTEM_SWIFT_ACL, action = "read", result = "granted", reason = "account_match", account = %account, "swift acl decision");
return true;
}
}
AclGrant::User {
account,
user: grant_user,
} => {
if let (Some(req_account), Some(req_user)) = (request_account, request_user)
&& req_account == account
&& req_user == grant_user
{
debug!(event = EVENT_SWIFT_ACL_DECISION, component = LOG_COMPONENT_PROTOCOLS, subsystem = LOG_SUBSYSTEM_SWIFT_ACL, action = "read", result = "granted", reason = "user_match", account = %account, user = %grant_user, "swift acl decision");
return true;
}
}
}
}
debug!(
event = EVENT_SWIFT_ACL_DECISION,
component = LOG_COMPONENT_PROTOCOLS,
subsystem = LOG_SUBSYSTEM_SWIFT_ACL,
action = "read",
result = "denied",
reason = "no_matching_grant",
"swift acl decision"
);
false
}
/// Check if a request has write access based on this ACL
///
/// # Arguments
/// * `request_account` - The account making the request
/// * `request_user` - The user making the request (if known)
///
/// # Returns
/// `true` if access is allowed, `false` otherwise
pub fn check_write_access(&self, request_account: &str, request_user: Option<&str>) -> bool {
if self.write.is_empty() {
// No write ACL means default behavior: owner can write
return true;
}
for grant in &self.write {
match grant {
AclGrant::PublicRead | AclGrant::PublicReadReferrer(_) => {
// These should never appear in write ACL (validated during parse)
continue;
}
AclGrant::Account(account) => {
if request_account == account {
debug!(event = EVENT_SWIFT_ACL_DECISION, component = LOG_COMPONENT_PROTOCOLS, subsystem = LOG_SUBSYSTEM_SWIFT_ACL, action = "write", result = "granted", reason = "account_match", account = %account, "swift acl decision");
return true;
}
}
AclGrant::User {
account,
user: grant_user,
} => {
if let Some(req_user) = request_user
&& request_account == account
&& req_user == grant_user
{
debug!(event = EVENT_SWIFT_ACL_DECISION, component = LOG_COMPONENT_PROTOCOLS, subsystem = LOG_SUBSYSTEM_SWIFT_ACL, action = "write", result = "granted", reason = "user_match", account = %account, user = %grant_user, "swift acl decision");
return true;
}
}
}
}
debug!(
event = EVENT_SWIFT_ACL_DECISION,
component = LOG_COMPONENT_PROTOCOLS,
subsystem = LOG_SUBSYSTEM_SWIFT_ACL,
action = "write",
result = "denied",
reason = "no_matching_grant",
"swift acl decision"
);
false
}
/// Check if a referrer header matches a pattern
///
/// Pattern matching rules:
/// - `*` at start matches any subdomain: `*.example.com` matches `www.example.com`
/// - Exact match otherwise
fn matches_referrer_pattern(referrer: &str, pattern: &str) -> bool {
if let Some(suffix) = pattern.strip_prefix('*') {
// Wildcard match: *.example.com matches www.example.com, api.example.com, etc.
// Remove leading *
referrer.ends_with(suffix)
} else {
// Exact match
referrer == pattern
}
}
/// Convert read grants to header value
pub fn read_to_header(&self) -> Option<String> {
if self.read.is_empty() {
None
} else {
Some(self.read.iter().map(|g| g.to_string()).collect::<Vec<_>>().join(","))
}
}
/// Convert write grants to header value
pub fn write_to_header(&self) -> Option<String> {
if self.write.is_empty() {
None
} else {
Some(self.write.iter().map(|g| g.to_string()).collect::<Vec<_>>().join(","))
}
}
/// Check if this ACL allows public read access
pub fn is_public_read(&self) -> bool {
self.read
.iter()
.any(|g| matches!(g, AclGrant::PublicRead | AclGrant::PublicReadReferrer(_)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_public_read() {
let grants = ContainerAcl::parse_read(".r:*").unwrap();
assert_eq!(grants.len(), 1);
assert_eq!(grants[0], AclGrant::PublicRead);
}
#[test]
fn test_parse_referrer_restriction() {
let grants = ContainerAcl::parse_read(".r:*.example.com").unwrap();
assert_eq!(grants.len(), 1);
assert_eq!(grants[0], AclGrant::PublicReadReferrer("*.example.com".to_string()));
}
#[test]
fn test_parse_account() {
let grants = ContainerAcl::parse_read("AUTH_abc123").unwrap();
assert_eq!(grants.len(), 1);
assert_eq!(grants[0], AclGrant::Account("AUTH_abc123".to_string()));
}
#[test]
fn test_parse_user() {
let grants = ContainerAcl::parse_read("AUTH_abc123:user1").unwrap();
assert_eq!(grants.len(), 1);
assert_eq!(
grants[0],
AclGrant::User {
account: "AUTH_abc123".to_string(),
user: "user1".to_string()
}
);
}
#[test]
fn test_parse_multiple_grants() {
let grants = ContainerAcl::parse_read(".r:*,AUTH_abc123,AUTH_def456:user1").unwrap();
assert_eq!(grants.len(), 3);
assert_eq!(grants[0], AclGrant::PublicRead);
assert_eq!(grants[1], AclGrant::Account("AUTH_abc123".to_string()));
assert_eq!(
grants[2],
AclGrant::User {
account: "AUTH_def456".to_string(),
user: "user1".to_string()
}
);
}
#[test]
fn test_parse_write_no_public() {
// Public read in write ACL should fail
let result = ContainerAcl::parse_write(".r:*");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Public access not allowed"));
}
#[test]
fn test_parse_write_accounts() {
let grants = ContainerAcl::parse_write("AUTH_abc123,AUTH_def456:user1").unwrap();
assert_eq!(grants.len(), 2);
assert_eq!(grants[0], AclGrant::Account("AUTH_abc123".to_string()));
assert_eq!(
grants[1],
AclGrant::User {
account: "AUTH_def456".to_string(),
user: "user1".to_string()
}
);
}
#[test]
fn test_parse_empty() {
let grants = ContainerAcl::parse_read("").unwrap();
assert_eq!(grants.len(), 0);
let grants = ContainerAcl::parse_write(" ").unwrap();
assert_eq!(grants.len(), 0);
}
#[test]
fn test_parse_invalid_format() {
// Invalid patterns
assert!(ContainerAcl::parse_read("invalid_format").is_err());
assert!(ContainerAcl::parse_read(".r:").is_err());
assert!(ContainerAcl::parse_read("AUTH_abc:").is_err());
}
#[test]
fn test_check_public_read_access() {
let mut acl = ContainerAcl::new();
acl.read.push(AclGrant::PublicRead);
// Anyone can read
assert!(acl.check_read_access(None, None, None));
assert!(acl.check_read_access(Some("AUTH_other"), None, None));
}
#[test]
fn test_check_referrer_access() {
let mut acl = ContainerAcl::new();
acl.read.push(AclGrant::PublicReadReferrer("*.example.com".to_string()));
// Matches wildcard
assert!(acl.check_read_access(None, None, Some("www.example.com")));
assert!(acl.check_read_access(None, None, Some("api.example.com")));
// Doesn't match
assert!(!acl.check_read_access(None, None, Some("www.other.com")));
assert!(!acl.check_read_access(None, None, None));
}
#[test]
fn test_check_account_access() {
let mut acl = ContainerAcl::new();
acl.read.push(AclGrant::Account("AUTH_abc123".to_string()));
// Matches account
assert!(acl.check_read_access(Some("AUTH_abc123"), None, None));
// Doesn't match
assert!(!acl.check_read_access(Some("AUTH_other"), None, None));
assert!(!acl.check_read_access(None, None, None));
}
#[test]
fn test_check_user_access() {
let mut acl = ContainerAcl::new();
acl.read.push(AclGrant::User {
account: "AUTH_abc123".to_string(),
user: "user1".to_string(),
});
// Matches user
assert!(acl.check_read_access(Some("AUTH_abc123"), Some("user1"), None));
// Doesn't match (wrong user)
assert!(!acl.check_read_access(Some("AUTH_abc123"), Some("user2"), None));
// Doesn't match (no user)
assert!(!acl.check_read_access(Some("AUTH_abc123"), None, None));
}
#[test]
fn test_check_write_access() {
let mut acl = ContainerAcl::new();
acl.write.push(AclGrant::Account("AUTH_abc123".to_string()));
// Matches account
assert!(acl.check_write_access("AUTH_abc123", None));
// Doesn't match
assert!(!acl.check_write_access("AUTH_other", None));
}
#[test]
fn test_default_access_no_acl() {
let acl = ContainerAcl::new();
// No ACL means default: authenticated users can read
assert!(acl.check_read_access(Some("AUTH_owner"), None, None));
assert!(!acl.check_read_access(None, None, None));
// No write ACL means owner can write (default)
assert!(acl.check_write_access("AUTH_owner", None));
}
#[test]
fn test_to_header() {
let mut acl = ContainerAcl::new();
acl.read.push(AclGrant::PublicRead);
acl.read.push(AclGrant::Account("AUTH_abc123".to_string()));
let header = acl.read_to_header().unwrap();
assert_eq!(header, ".r:*,AUTH_abc123");
}
#[test]
fn test_is_public_read() {
let mut acl = ContainerAcl::new();
assert!(!acl.is_public_read());
acl.read.push(AclGrant::PublicRead);
assert!(acl.is_public_read());
let mut acl2 = ContainerAcl::new();
acl2.read.push(AclGrant::PublicReadReferrer("*.example.com".to_string()));
assert!(acl2.is_public_read());
}
// Integration-style tests for ACL workflows
#[test]
fn test_acl_roundtrip_public_read() {
// Simulate: swift post container -r ".r:*"
let header = ".r:*";
let grants = ContainerAcl::parse_read(header).unwrap();
let mut acl = ContainerAcl::new();
acl.read = grants;
// Verify anyone can read
assert!(acl.check_read_access(None, None, None));
assert!(acl.check_read_access(Some("AUTH_other"), None, None));
}
#[test]
fn test_acl_roundtrip_referrer_restriction() {
// Simulate: swift post container -r ".r:*.example.com"
let header = ".r:*.example.com";
let grants = ContainerAcl::parse_read(header).unwrap();
let mut acl = ContainerAcl::new();
acl.read = grants;
// Verify referrer matching
assert!(acl.check_read_access(None, None, Some("www.example.com")));
assert!(acl.check_read_access(None, None, Some("api.example.com")));
assert!(!acl.check_read_access(None, None, Some("www.other.com")));
assert!(!acl.check_read_access(None, None, None));
}
#[test]
fn test_acl_roundtrip_account_access() {
// Simulate: swift post container -r "AUTH_abc123,AUTH_def456"
let header = "AUTH_abc123,AUTH_def456";
let grants = ContainerAcl::parse_read(header).unwrap();
let mut acl = ContainerAcl::new();
acl.read = grants;
// Verify account matching
assert!(acl.check_read_access(Some("AUTH_abc123"), None, None));
assert!(acl.check_read_access(Some("AUTH_def456"), None, None));
assert!(!acl.check_read_access(Some("AUTH_other"), None, None));
assert!(!acl.check_read_access(None, None, None));
}
#[test]
fn test_acl_roundtrip_mixed_grants() {
// Simulate: swift post container -r ".r:*.cdn.com,AUTH_abc123,AUTH_def456:user1"
let header = ".r:*.cdn.com,AUTH_abc123,AUTH_def456:user1";
let grants = ContainerAcl::parse_read(header).unwrap();
let mut acl = ContainerAcl::new();
acl.read = grants;
// Verify various access patterns
assert!(acl.check_read_access(None, None, Some("api.cdn.com"))); // Referrer
assert!(acl.check_read_access(Some("AUTH_abc123"), None, None)); // Account
assert!(acl.check_read_access(Some("AUTH_def456"), Some("user1"), None)); // User
assert!(!acl.check_read_access(Some("AUTH_def456"), Some("user2"), None)); // Wrong user
assert!(!acl.check_read_access(Some("AUTH_other"), None, None)); // Wrong account
}
#[test]
fn test_acl_write_account_only() {
// Simulate: swift post container -w "AUTH_abc123"
let header = "AUTH_abc123";
let grants = ContainerAcl::parse_write(header).unwrap();
let mut acl = ContainerAcl::new();
acl.write = grants;
// Verify write access
assert!(acl.check_write_access("AUTH_abc123", None));
assert!(!acl.check_write_access("AUTH_other", None));
}
#[test]
fn test_acl_write_user_specific() {
// Simulate: swift post container -w "AUTH_abc123:user1,AUTH_def456:user2"
let header = "AUTH_abc123:user1,AUTH_def456:user2";
let grants = ContainerAcl::parse_write(header).unwrap();
let mut acl = ContainerAcl::new();
acl.write = grants;
// Verify user-specific write access
assert!(acl.check_write_access("AUTH_abc123", Some("user1")));
assert!(acl.check_write_access("AUTH_def456", Some("user2")));
assert!(!acl.check_write_access("AUTH_abc123", Some("user2"))); // Wrong user
assert!(!acl.check_write_access("AUTH_abc123", None)); // No user specified
assert!(!acl.check_write_access("AUTH_other", Some("user1"))); // Wrong account
}
#[test]
fn test_acl_permission_denied_scenarios() {
// Test various permission denied scenarios
let mut acl = ContainerAcl::new();
acl.read.push(AclGrant::Account("AUTH_abc123".to_string()));
acl.write.push(AclGrant::Account("AUTH_abc123".to_string()));
// Read denied
assert!(!acl.check_read_access(Some("AUTH_other"), None, None));
assert!(!acl.check_read_access(None, None, None));
// Write denied
assert!(!acl.check_write_access("AUTH_other", None));
}
#[test]
fn test_acl_empty_means_owner_only() {
// When no ACL is set, only authenticated owner should have access
let acl = ContainerAcl::new();
// Empty read ACL: authenticated users can read
assert!(acl.check_read_access(Some("AUTH_owner"), None, None));
assert!(!acl.check_read_access(None, None, None)); // Unauthenticated denied
// Empty write ACL: owner can write (default behavior)
assert!(acl.check_write_access("AUTH_owner", None));
}
#[test]
fn test_acl_remove_scenario() {
// Simulate removing ACLs (setting to empty)
let mut acl = ContainerAcl::new();
acl.read.push(AclGrant::PublicRead);
acl.write.push(AclGrant::Account("AUTH_abc123".to_string()));
// Initially has ACLs
assert!(acl.is_public_read());
assert!(!acl.write.is_empty());
// Remove ACLs
acl.read.clear();
acl.write.clear();
// Now reverts to default behavior
assert!(!acl.is_public_read());
assert!(acl.read.is_empty());
assert!(acl.write.is_empty());
}
#[test]
fn test_acl_wildcard_referrer_patterns() {
let mut acl = ContainerAcl::new();
acl.read.push(AclGrant::PublicReadReferrer("*.example.com".to_string()));
// Test various subdomain patterns
assert!(acl.check_read_access(None, None, Some("www.example.com")));
assert!(acl.check_read_access(None, None, Some("api.example.com")));
assert!(acl.check_read_access(None, None, Some("cdn.example.com")));
assert!(acl.check_read_access(None, None, Some("a.b.c.example.com")));
// Should not match
assert!(!acl.check_read_access(None, None, Some("example.com"))); // No subdomain
assert!(!acl.check_read_access(None, None, Some("example.org")));
assert!(!acl.check_read_access(None, None, Some("notexample.com")));
assert!(!acl.check_read_access(None, None, None)); // No referrer
}
#[test]
fn test_acl_exact_referrer_match() {
let mut acl = ContainerAcl::new();
acl.read.push(AclGrant::PublicReadReferrer("cdn.example.com".to_string()));
// Exact match only
assert!(acl.check_read_access(None, None, Some("cdn.example.com")));
// Should not match
assert!(!acl.check_read_access(None, None, Some("api.cdn.example.com")));
assert!(!acl.check_read_access(None, None, Some("www.example.com")));
assert!(!acl.check_read_access(None, None, None));
}
#[test]
fn test_acl_header_serialization() {
// Test round-trip: parse → serialize → parse
let original = ".r:*,AUTH_abc123,AUTH_def456:user1";
let grants = ContainerAcl::parse_read(original).unwrap();
let mut acl = ContainerAcl::new();
acl.read = grants;
let serialized = acl.read_to_header().unwrap();
let reparsed = ContainerAcl::parse_read(&serialized).unwrap();
// Should match original parsing
assert_eq!(reparsed.len(), 3);
assert!(matches!(reparsed[0], AclGrant::PublicRead));
assert!(matches!(reparsed[1], AclGrant::Account(_)));
assert!(matches!(reparsed[2], AclGrant::User { .. }));
}
#[test]
fn test_acl_whitespace_handling() {
// Test that whitespace is properly trimmed
let header = " .r:* , AUTH_abc123 , AUTH_def456:user1 ";
let grants = ContainerAcl::parse_read(header).unwrap();
assert_eq!(grants.len(), 3);
assert_eq!(grants[0], AclGrant::PublicRead);
assert_eq!(grants[1], AclGrant::Account("AUTH_abc123".to_string()));
}
#[test]
fn test_acl_multiple_referrer_patterns() {
// Multiple referrer restrictions
let header = ".r:*.example.com,.r:*.cdn.com";
let grants = ContainerAcl::parse_read(header).unwrap();
let mut acl = ContainerAcl::new();
acl.read = grants;
// Should match either pattern
assert!(acl.check_read_access(None, None, Some("www.example.com")));
assert!(acl.check_read_access(None, None, Some("api.cdn.com")));
assert!(!acl.check_read_access(None, None, Some("www.other.com")));
}
#[test]
fn test_acl_user_requires_both_account_and_user() {
let mut acl = ContainerAcl::new();
acl.read.push(AclGrant::User {
account: "AUTH_abc123".to_string(),
user: "user1".to_string(),
});
// Need both account and user to match
assert!(acl.check_read_access(Some("AUTH_abc123"), Some("user1"), None));
// Missing user
assert!(!acl.check_read_access(Some("AUTH_abc123"), None, None));
// Wrong user
assert!(!acl.check_read_access(Some("AUTH_abc123"), Some("user2"), None));
// Wrong account
assert!(!acl.check_read_access(Some("AUTH_other"), Some("user1"), None));
}
#[test]
fn test_acl_complex_scenario() {
// Complex real-world scenario: public CDN access + specific accounts
let read_header = ".r:*.cloudfront.net,AUTH_admin,AUTH_support:viewer";
let write_header = "AUTH_admin,AUTH_publisher:editor";
let read_grants = ContainerAcl::parse_read(read_header).unwrap();
let write_grants = ContainerAcl::parse_write(write_header).unwrap();
let mut acl = ContainerAcl::new();
acl.read = read_grants;
acl.write = write_grants;
// Read access scenarios
assert!(acl.check_read_access(None, None, Some("d111.cloudfront.net"))); // CDN
assert!(acl.check_read_access(Some("AUTH_admin"), None, None)); // Admin account
assert!(acl.check_read_access(Some("AUTH_support"), Some("viewer"), None)); // Support viewer
assert!(!acl.check_read_access(Some("AUTH_support"), Some("other"), None)); // Wrong user
assert!(!acl.check_read_access(Some("AUTH_other"), None, None)); // Unauthorized
// Write access scenarios
assert!(acl.check_write_access("AUTH_admin", None)); // Admin account
assert!(acl.check_write_access("AUTH_publisher", Some("editor"))); // Publisher editor
assert!(!acl.check_write_access("AUTH_publisher", Some("viewer"))); // Wrong role
assert!(!acl.check_write_access("AUTH_support", Some("viewer"))); // Read-only
assert!(!acl.check_write_access("AUTH_other", None)); // Unauthorized
}
}