Files
seaweedFS/weed/s3api/policy_engine/sse_condition_test.go
Chris Lu 7d5cbfd547 s3: support s3:x-amz-server-side-encryption policy condition (#8806)
* s3: support s3:x-amz-server-side-encryption policy condition (#7680)

- Normalize x-amz-server-side-encryption header values to canonical form
  (aes256 → AES256, aws:kms mixed-case → aws:kms) so StringEquals
  conditions work regardless of client capitalisation
- Exempt UploadPart and UploadPartCopy from SSE Null conditions: these
  actions inherit SSE from the initial CreateMultipartUpload request and
  do not re-send the header, so Deny/Null("true") should not block them
- Add sse_condition_test.go covering StringEquals, Null, case-insensitive
  normalisation, and multipart continuation action exemption

* s3: address review comments on SSE condition support

- Replace "inherited" sentinel in injectSSEForMultipart with "AES256" so
  that StringEquals/Null conditions evaluate against a meaningful value;
  add TODO noting that KMS multipart uploads need the actual algorithm
  looked up from the upload state
- Rewrite TestSSECaseInsensitiveNormalization to drive normalisation
  through EvaluatePolicyForRequest with a real *http.Request so regressions
  in the production code path are caught; split into AES256 and aws:kms
  variants to cover both normalisation branches

* s3: plumb real inherited SSE from multipart upload state into policy eval

Instead of injecting a static "AES256" sentinel for UploadPart/UploadPartCopy,
look up the actual SSE algorithm from the stored CreateMultipartUpload entry
and pass it through the evaluation chain.

Changes:
- PolicyEvaluationArgs gains InheritedSSEAlgorithm string; set by the
  BucketPolicyEngine wrapper for multipart continuation actions
- injectSSEForMultipart(conditions, inheritedSSE) now accepts the real
  algorithm; empty string means no SSE → Null("true") fires correctly
- IsMultipartContinuationAction exported so the s3api wrapper can use it
- BucketPolicyEngine gets a MultipartSSELookup callback (set by S3ApiServer)
  that fetches the upload entry and reads SeaweedFSSSEKMSKeyID /
  SeaweedFSSSES3Encryption to determine the algorithm
- S3ApiServer.getMultipartSSEAlgorithm implements the lookup via getEntry
- Tests updated: three multipart cases (AES256, aws:kms, no-SSE-must-deny)
  plus UploadPartCopy coverage
2026-03-27 23:15:01 -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)
}
}