Files
s3/weed/s3api/policy_engine/sse_condition_test.go
zooqueen dd74d3e6d6 Replace AGPL MinIO fork with Apache-2.0 SeaweedFS fork; rebrand CLI weed->s3
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.

- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
  "Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
  SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
  (replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
  keep their upstream import paths with zero churn. Internal identifiers and
  import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
  help/usage/flag strings, shell autocomplete program name, and the product name
  in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
  (e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
  `cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
  updated to ignore the `s3` build output.
2026-06-18 23:41:25 -07:00

250 lines
8.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package policy_engine
import (
"net/http"
"testing"
)
// requiresSSEPolicy is a bucket policy that denies PutObject when the
// x-amz-server-side-encryption header is absent (Null == true).
const requiresSSEPolicy = `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::test-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
}
]
}`
// requiresAES256Policy denies PutObject unless AES256 is explicitly requested.
const requiresAES256Policy = `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAES256Only",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::test-bucket/*",
"Condition": {
"StringEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
}
]
}`
// requiresKMSPolicy allows PutObject only when aws:kms encryption is requested.
const requiresKMSPolicy = `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowKMSOnly",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::test-bucket/*",
"Condition": {
"StringEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}`
// multipartPolicy denies PutObject when SSE is absent but should NOT block UploadPart.
const multipartPolicy = `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:PutObject", "s3:UploadPart", "s3:UploadPartCopy"],
"Resource": "arn:aws:s3:::test-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
}
]
}`
func newEngineWithPolicy(t *testing.T, policy string) *PolicyEngine {
t.Helper()
engine := NewPolicyEngine()
if err := engine.SetBucketPolicy("test-bucket", policy); err != nil {
t.Fatalf("SetBucketPolicy: %v", err)
}
return engine
}
func evalArgs(action string, conditions map[string][]string) *PolicyEvaluationArgs {
return &PolicyEvaluationArgs{
Action: action,
Resource: "arn:aws:s3:::test-bucket/object.txt",
Principal: "*",
Conditions: conditions,
}
}
func evalArgsWithSSE(action, inheritedSSE string) *PolicyEvaluationArgs {
return &PolicyEvaluationArgs{
Action: action,
Resource: "arn:aws:s3:::test-bucket/object.txt",
Principal: "*",
Conditions: map[string][]string{},
InheritedSSEAlgorithm: inheritedSSE,
}
}
// TestSSEStringEqualsPresent – StringEquals with AES256 header present should Allow.
func TestSSEStringEqualsPresent(t *testing.T) {
engine := newEngineWithPolicy(t, requiresAES256Policy)
conditions := map[string][]string{
"s3:x-amz-server-side-encryption": {"AES256"},
}
result := engine.EvaluatePolicy("test-bucket", evalArgs("s3:PutObject", conditions))
if result != PolicyResultAllow {
t.Errorf("expected Allow, got %v", result)
}
}
// TestSSEStringEqualsWrongValue – StringEquals with wrong SSE value should not Allow.
func TestSSEStringEqualsWrongValue(t *testing.T) {
engine := newEngineWithPolicy(t, requiresAES256Policy)
conditions := map[string][]string{
"s3:x-amz-server-side-encryption": {"aws:kms"},
}
result := engine.EvaluatePolicy("test-bucket", evalArgs("s3:PutObject", conditions))
if result == PolicyResultAllow {
t.Errorf("expected non-Allow, got %v", result)
}
}
// TestSSENullConditionAbsent – Null("true") matches when header is absent → Deny.
func TestSSENullConditionAbsent(t *testing.T) {
engine := newEngineWithPolicy(t, requiresSSEPolicy)
// No SSE header → condition "Null == true" matches → Deny statement fires
result := engine.EvaluatePolicy("test-bucket", evalArgs("s3:PutObject", map[string][]string{}))
if result != PolicyResultDeny {
t.Errorf("expected Deny (no SSE header), got %v", result)
}
}
// TestSSENullConditionPresent – Null("true") does NOT match when header is present → not Deny.
func TestSSENullConditionPresent(t *testing.T) {
engine := newEngineWithPolicy(t, requiresSSEPolicy)
conditions := map[string][]string{
"s3:x-amz-server-side-encryption": {"AES256"},
}
result := engine.EvaluatePolicy("test-bucket", evalArgs("s3:PutObject", conditions))
// Deny condition not matched; no explicit Allow → Indeterminate
if result == PolicyResultDeny {
t.Errorf("expected non-Deny when SSE header present, got Deny")
}
}
// TestSSECaseInsensitiveNormalizationAES256 drives the AES256 normalisation
// through EvaluatePolicyForRequest so that a regression in the production
// code path would be caught. The request carries the header in lowercase
// ("aes256"); after normalisation it must match the policy's "AES256" value.
func TestSSECaseInsensitiveNormalizationAES256(t *testing.T) {
engine := newEngineWithPolicy(t, requiresAES256Policy)
req, _ := http.NewRequest(http.MethodPut, "/", nil)
req.RemoteAddr = "1.2.3.4:1234"
req.Header.Set("X-Amz-Server-Side-Encryption", "aes256") // lowercase
result := engine.EvaluatePolicyForRequest("test-bucket", "object.txt", "PutObject", "*", req)
if result != PolicyResultAllow {
t.Errorf("expected Allow after AES256 case normalisation, got %v", result)
}
}
// TestSSECaseInsensitiveNormalizationKMS drives the aws:kms branch of the
// normalisation through the production code path. The request carries
// "AWS:KMS" (mixed case); after normalisation it must match "aws:kms".
func TestSSECaseInsensitiveNormalizationKMS(t *testing.T) {
engine := newEngineWithPolicy(t, requiresKMSPolicy)
req, _ := http.NewRequest(http.MethodPut, "/", nil)
req.RemoteAddr = "1.2.3.4:1234"
req.Header.Set("X-Amz-Server-Side-Encryption", "AWS:KMS") // mixed case
result := engine.EvaluatePolicyForRequest("test-bucket", "object.txt", "PutObject", "*", req)
if result != PolicyResultAllow {
t.Errorf("expected Allow after aws:kms case normalisation, got %v", result)
}
}
// TestSSEMultipartAES256Exempt – UploadPart with AES256 inherited from
// CreateMultipartUpload is not blocked by the Null("true") deny condition.
func TestSSEMultipartAES256Exempt(t *testing.T) {
engine := newEngineWithPolicy(t, multipartPolicy)
result := engine.EvaluatePolicy("test-bucket", evalArgsWithSSE("s3:UploadPart", "AES256"))
if result == PolicyResultDeny {
t.Errorf("UploadPart with inherited AES256 should not be Deny, got Deny")
}
}
// TestSSEMultipartKMSExempt – UploadPart with aws:kms inherited from
// CreateMultipartUpload is not blocked by the Null("true") deny condition.
func TestSSEMultipartKMSExempt(t *testing.T) {
engine := newEngineWithPolicy(t, multipartPolicy)
result := engine.EvaluatePolicy("test-bucket", evalArgsWithSSE("s3:UploadPart", "aws:kms"))
if result == PolicyResultDeny {
t.Errorf("UploadPart with inherited aws:kms should not be Deny, got Deny")
}
}
// TestSSEMultipartNoSSEDenied – UploadPart for an upload that had no SSE
// must still be denied by the Null("true") deny condition.
func TestSSEMultipartNoSSEDenied(t *testing.T) {
engine := newEngineWithPolicy(t, multipartPolicy)
// inheritedSSE="" means CreateMultipartUpload was sent without SSE
result := engine.EvaluatePolicy("test-bucket", evalArgsWithSSE("s3:UploadPart", ""))
if result != PolicyResultDeny {
t.Errorf("UploadPart with no inherited SSE should be Deny, got %v", result)
}
}
// TestSSEUploadPartCopyKMSExempt – UploadPartCopy with aws:kms is also exempt.
func TestSSEUploadPartCopyKMSExempt(t *testing.T) {
engine := newEngineWithPolicy(t, multipartPolicy)
result := engine.EvaluatePolicy("test-bucket", evalArgsWithSSE("s3:UploadPartCopy", "aws:kms"))
if result == PolicyResultDeny {
t.Errorf("UploadPartCopy with inherited aws:kms should not be Deny, got Deny")
}
}
// TestSSEPutObjectStillBlockedWithoutHeader – regular PutObject still denied without SSE.
func TestSSEPutObjectStillBlockedWithoutHeader(t *testing.T) {
engine := newEngineWithPolicy(t, multipartPolicy)
result := engine.EvaluatePolicy("test-bucket", evalArgs("s3:PutObject", map[string][]string{}))
if result != PolicyResultDeny {
t.Errorf("PutObject without SSE should be Deny, got %v", result)
}
}