Files
markbase/markbase-core/src/s3_policy.rs
Warren 02d98419e1
Some checks failed
Test / build (push) Has been cancelled
Test / test (push) Has been cancelled
P3: Bucket Policy implementation complete
- BucketPolicy struct with Version + Statement array
- PolicyStatement: Effect, Principal, Action, Resource, Condition
- Principal matching (wildcard + user-specific)
- Action/Resource pattern matching with wildcards
- GetBucketPolicy: GET /s3/policy/:bucket
- PutBucketPolicy: PUT /s3/policy/:bucket
- DeleteBucketPolicy: DELETE /s3/policy/:bucket
- Policy persistence to data/s3_policies/:bucket/policy.json
- check_bucket_policy() for authorization
- 6 unit tests

Tests: 299 passed, 0 failed
2026-06-21 22:50:53 +08:00

252 lines
7.5 KiB
Rust

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BucketPolicy {
#[serde(rename = "Version")]
pub version: String,
#[serde(rename = "Statement")]
pub statement: Vec<PolicyStatement>,
}
impl Default for BucketPolicy {
fn default() -> Self {
Self {
version: "2012-10-17".to_string(),
statement: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyStatement {
#[serde(rename = "Sid")]
pub sid: Option<String>,
#[serde(rename = "Effect")]
pub effect: PolicyEffect,
#[serde(rename = "Principal")]
pub principal: Principal,
#[serde(rename = "Action")]
pub action: Vec<String>,
#[serde(rename = "Resource")]
pub resource: Vec<String>,
#[serde(rename = "Condition")]
pub condition: Option<HashMap<String, HashMap<String, String>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PolicyEffect {
Allow,
Deny,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Principal {
Wildcard(String),
Specific(HashMap<String, Vec<String>>),
}
impl Principal {
pub fn is_public(&self) -> bool {
match self {
Principal::Wildcard(s) => s == "*",
Principal::Specific(_) => false,
}
}
pub fn matches_user(&self, user_id: &str) -> bool {
match self {
Principal::Wildcard(s) => s == "*",
Principal::Specific(map) => {
if let Some(aws_users) = map.get("AWS") {
aws_users.iter().any(|u| u == user_id || u == "*")
} else {
false
}
}
}
}
}
impl BucketPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn is_allowed(&self, action: &str, resource: &str, user_id: &str) -> bool {
let mut allowed = false;
for stmt in &self.statement {
if stmt.matches_action(action) && stmt.matches_resource(resource) {
if stmt.principal.matches_user(user_id) {
match stmt.effect {
PolicyEffect::Allow => {
if stmt.matches_condition(user_id) {
allowed = true;
}
}
PolicyEffect::Deny => {
if stmt.matches_condition(user_id) {
return false;
}
}
}
}
}
}
allowed
}
}
impl PolicyStatement {
pub fn matches_action(&self, action: &str) -> bool {
self.action.iter().any(|a| {
a == action || a == "s3:*" || a == "*" ||
(a.ends_with('*') && action.starts_with(&a[..a.len()-1]))
})
}
pub fn matches_resource(&self, resource: &str) -> bool {
self.resource.iter().any(|r| {
r == resource || r == "*" ||
(r.ends_with('*') && resource.starts_with(&r[..r.len()-1]))
})
}
pub fn matches_condition(&self, _user_id: &str) -> bool {
if let Some(cond) = &self.condition {
for (operator, values) in cond {
for (key, value) in values {
if operator == "StringEquals" && key == "aws:userid" {
if value != _user_id {
return false;
}
}
}
}
}
true
}
}
pub fn default_public_policy(bucket: &str) -> BucketPolicy {
BucketPolicy {
version: "2012-10-17".to_string(),
statement: vec![
PolicyStatement {
sid: Some("PublicRead".to_string()),
effect: PolicyEffect::Allow,
principal: Principal::Wildcard("*".to_string()),
action: vec!["s3:GetObject".to_string()],
resource: vec![format!("arn:aws:s3:::{}/*", bucket)],
condition: None,
},
],
}
}
pub fn default_private_policy(bucket: &str, user_id: &str) -> BucketPolicy {
BucketPolicy {
version: "2012-10-17".to_string(),
statement: vec![
PolicyStatement {
sid: Some("OwnerFullAccess".to_string()),
effect: PolicyEffect::Allow,
principal: Principal::Specific({
let mut map = HashMap::new();
map.insert("AWS".to_string(), vec![format!("arn:aws:iam:::user/{}", user_id)]);
map
}),
action: vec!["s3:*".to_string()],
resource: vec![
format!("arn:aws:s3:::{}/*", bucket),
format!("arn:aws:s3:::{}/*", bucket),
],
condition: None,
},
],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_policy_parse() {
let policy_json = r#"{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::mybucket/*"]
}
]
}"#;
let policy: BucketPolicy = serde_json::from_str(policy_json).unwrap();
assert_eq!(policy.version, "2012-10-17");
assert_eq!(policy.statement.len(), 1);
assert_eq!(policy.statement[0].effect, PolicyEffect::Allow);
}
#[test]
fn test_policy_evaluation_allow() {
let policy = default_public_policy("testbucket");
assert!(policy.is_allowed("s3:GetObject", "arn:aws:s3:::testbucket/file.txt", "anonymous"));
}
#[test]
fn test_policy_evaluation_deny() {
let policy = default_public_policy("testbucket");
assert!(!policy.is_allowed("s3:PutObject", "arn:aws:s3:::testbucket/file.txt", "anonymous"));
}
#[test]
fn test_action_wildcard() {
let stmt = PolicyStatement {
sid: None,
effect: PolicyEffect::Allow,
principal: Principal::Wildcard("*".to_string()),
action: vec!["s3:*".to_string()],
resource: vec!["*".to_string()],
condition: None,
};
assert!(stmt.matches_action("s3:GetObject"));
assert!(stmt.matches_action("s3:PutObject"));
assert!(stmt.matches_action("s3:DeleteObject"));
}
#[test]
fn test_resource_pattern() {
let stmt = PolicyStatement {
sid: None,
effect: PolicyEffect::Allow,
principal: Principal::Wildcard("*".to_string()),
action: vec!["s3:GetObject".to_string()],
resource: vec!["arn:aws:s3:::mybucket/home/*".to_string()],
condition: None,
};
assert!(stmt.matches_resource("arn:aws:s3:::mybucket/home/user/file.txt"));
assert!(!stmt.matches_resource("arn:aws:s3:::mybucket/public/file.txt"));
}
#[test]
fn test_principal_user_match() {
let principal = Principal::Specific({
let mut map = HashMap::new();
map.insert("AWS".to_string(), vec!["warren".to_string()]);
map
});
assert!(principal.matches_user("warren"));
assert!(!principal.matches_user("demo"));
}
}