* 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
849 lines
30 KiB
Rust
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
|
|
}
|
|
}
|