mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-12 23:09:33 +01:00
* s3: ignore unsigned object headers on POST Object The POST policy signs form fields only, but the upload read tagging, user metadata, storage class, object lock and the stored content headers from the request headers as well. Drop them before the form fields are applied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3: test POST Object request headers against signed form fields Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
838 lines
36 KiB
Go
838 lines
36 KiB
Go
package s3api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sort"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
postAuthzAccessKey = "AKIATESTTESTTEST"
|
|
postAuthzSecretKey = "secret-key-for-tests"
|
|
postAuthzRegion = "us-east-1"
|
|
postAuthzService = "s3"
|
|
postAuthzBucket = "testbucket"
|
|
postAuthzPrincipal = "arn:aws:iam::000000000000:user/tester"
|
|
|
|
postAclAccessKey = "AKIAACLTESTTEST"
|
|
postAclSecretKey = "secret-key-for-acl-tests"
|
|
postAclPrincipal = "arn:aws:iam::000000000000:user/aclwriter"
|
|
|
|
postPolicyAccessKey = "AKIAPOLICYONLY0000"
|
|
postPolicySecretKey = "secret-key-for-policy-only"
|
|
postReadAccessKey = "AKIAPOLICYREAD0000"
|
|
postReadSecretKey = "secret-key-for-policy-read"
|
|
|
|
postAdminAccessKey = "AKIAADMINTEST000"
|
|
postAdminSecretKey = "secret-key-for-admin-tests"
|
|
)
|
|
|
|
// newPostAuthzServer builds an S3ApiServer whose non-admin "tester" identity
|
|
// holds the coarse Write action on the bucket and whose bucket policy denies it
|
|
// s3:PutObject under denied/: a writer confined by a bucket policy to part of a
|
|
// shared bucket.
|
|
func newPostAuthzServer(t *testing.T) *S3ApiServer {
|
|
t.Helper()
|
|
|
|
iam := &IdentityAccessManagement{
|
|
hashes: make(map[string]*sync.Pool),
|
|
hashCounters: make(map[string]*int32),
|
|
}
|
|
err := iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{
|
|
Name: "tester",
|
|
Account: &iam_pb.Account{Id: "000000000000", DisplayName: "tester"},
|
|
Credentials: []*iam_pb.Credential{{AccessKey: postAuthzAccessKey, SecretKey: postAuthzSecretKey}},
|
|
Actions: []string{"Read:" + postAuthzBucket, "Write:" + postAuthzBucket, "List:" + postAuthzBucket},
|
|
}, {
|
|
Name: "aclwriter",
|
|
Account: &iam_pb.Account{Id: "000000000000", DisplayName: "aclwriter"},
|
|
Credentials: []*iam_pb.Credential{{AccessKey: postAclAccessKey, SecretKey: postAclSecretKey}},
|
|
Actions: []string{"Read:" + postAuthzBucket, "Write:" + postAuthzBucket, "WriteAcp:" + postAuthzBucket},
|
|
}},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
policyEngine := NewBucketPolicyEngine()
|
|
err = policyEngine.engine.SetBucketPolicy(postAuthzBucket, fmt.Sprintf(`{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{
|
|
"Sid": "DenyTesterDeniedPrefix",
|
|
"Effect": "Deny",
|
|
"Principal": {"AWS": "%s"},
|
|
"Action": ["s3:PutObject"],
|
|
"Resource": ["arn:aws:s3:::%s/denied/*"]
|
|
}, {
|
|
"Sid": "DenyAclWriterAclPrefix",
|
|
"Effect": "Deny",
|
|
"Principal": {"AWS": "%s"},
|
|
"Action": ["s3:PutObjectAcl"],
|
|
"Resource": ["arn:aws:s3:::%s/acl-denied/*"]
|
|
}]
|
|
}`, postAuthzPrincipal, postAuthzBucket, postAclPrincipal, postAuthzBucket))
|
|
require.NoError(t, err)
|
|
iam.policyEngine = policyEngine
|
|
|
|
s3a := &S3ApiServer{
|
|
option: &S3ApiServerOption{BucketsPath: "/buckets"},
|
|
iam: iam,
|
|
policyEngine: policyEngine,
|
|
}
|
|
// Pre-populate the bucket registry so validateTableBucketObjectPath sees a
|
|
// non-table bucket without needing a live filer connection.
|
|
s3a.bucketRegistry = NewBucketRegistry(s3a)
|
|
s3a.bucketRegistry.setMetadataCache(&BucketMetaData{Name: postAuthzBucket, IsTableBucket: false})
|
|
return s3a
|
|
}
|
|
|
|
// newSignedPostRequest builds a signed multipart POST Object request whose POST
|
|
// policy conditions are satisfied by the form, so the handler passes signature
|
|
// and CheckPostPolicy verification and reaches the write-authorization stage.
|
|
func newSignedPostRequest(t *testing.T, key string) *http.Request {
|
|
return newSignedPostRequestFor(t, key, postAuthzAccessKey, postAuthzSecretKey, nil)
|
|
}
|
|
|
|
// extraFields are added to both the form and the policy conditions so
|
|
// CheckPostPolicy accepts them.
|
|
func newSignedPostRequestFor(t *testing.T, key, accessKey, secretKey string, extraFields map[string]string) *http.Request {
|
|
t.Helper()
|
|
|
|
now := time.Now().UTC()
|
|
amzDate := now.Format(iso8601Format)
|
|
yyyymmddStr := now.Format(yyyymmdd)
|
|
credential := fmt.Sprintf("%s/%s/%s/%s/aws4_request", accessKey, yyyymmddStr, postAuthzRegion, postAuthzService)
|
|
expiration := now.Add(1 * time.Hour).Format("2006-01-02T15:04:05.000Z")
|
|
|
|
extraKeys := make([]string, 0, len(extraFields))
|
|
for k := range extraFields {
|
|
extraKeys = append(extraKeys, k)
|
|
}
|
|
sort.Strings(extraKeys)
|
|
conditions := []any{
|
|
[]any{"eq", "$bucket", postAuthzBucket},
|
|
[]any{"eq", "$key", key},
|
|
[]any{"eq", "$x-amz-credential", credential},
|
|
[]any{"eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"},
|
|
[]any{"eq", "$x-amz-date", amzDate},
|
|
}
|
|
for _, k := range extraKeys {
|
|
conditions = append(conditions, []any{"eq", "$" + k, extraFields[k]})
|
|
}
|
|
policyJSON, err := json.Marshal(map[string]any{
|
|
"expiration": expiration,
|
|
"conditions": conditions,
|
|
})
|
|
require.NoError(t, err)
|
|
encodedPolicy := base64.StdEncoding.EncodeToString(policyJSON)
|
|
signature := getSignature(getSigningKey(secretKey, yyyymmddStr, postAuthzRegion, postAuthzService), encodedPolicy)
|
|
|
|
var buf bytes.Buffer
|
|
writer := multipart.NewWriter(&buf)
|
|
require.NoError(t, writer.WriteField("bucket", postAuthzBucket))
|
|
require.NoError(t, writer.WriteField("key", key))
|
|
require.NoError(t, writer.WriteField("x-amz-credential", credential))
|
|
require.NoError(t, writer.WriteField("x-amz-algorithm", "AWS4-HMAC-SHA256"))
|
|
require.NoError(t, writer.WriteField("x-amz-date", amzDate))
|
|
for _, k := range extraKeys {
|
|
require.NoError(t, writer.WriteField(k, extraFields[k]))
|
|
}
|
|
require.NoError(t, writer.WriteField("policy", encodedPolicy))
|
|
require.NoError(t, writer.WriteField("x-amz-signature", signature))
|
|
filePart, err := writer.CreateFormFile("file", "payload.txt")
|
|
require.NoError(t, err)
|
|
_, err = filePart.Write([]byte("payload"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, writer.Close())
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/"+postAuthzBucket, &buf)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
return mux.SetURLVars(req, map[string]string{"bucket": postAuthzBucket})
|
|
}
|
|
|
|
// TestPostPolicyBucketHandlerHonorsBucketPolicyDeny drives a signed POST Object
|
|
// upload whose key a bucket policy denies the caller, and asserts the handler
|
|
// rejects it with 403 -- the same result the equivalent PUT gives, now that POST
|
|
// runs the shared per-key authorization instead of only the coarse Write check.
|
|
func TestPostPolicyBucketHandlerHonorsBucketPolicyDeny(t *testing.T) {
|
|
s3a := newPostAuthzServer(t)
|
|
|
|
// Precondition: the policy really denies this principal the denied/ key, so a
|
|
// non-403 below is the POST path not consulting it, not a mis-configured policy.
|
|
putReq := httptest.NewRequest(http.MethodPut, "/"+postAuthzBucket+"/denied/secret.txt", nil)
|
|
allowed, evaluated, err := s3a.iam.policyEngine.EvaluatePolicy(
|
|
postAuthzBucket, "denied/secret.txt", s3_constants.ACTION_WRITE, postAuthzPrincipal, putReq, nil, nil)
|
|
require.NoError(t, err)
|
|
require.True(t, evaluated, "policy must match the denied key for the tester principal")
|
|
require.False(t, allowed, "policy must deny the tester principal on the denied key")
|
|
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequest(t, "denied/secret.txt"))
|
|
|
|
assert.Equal(t, http.StatusForbidden, rec.Code,
|
|
"POST Object into a bucket-policy-denied key must be rejected, body: %s", rec.Body.String())
|
|
assert.Contains(t, rec.Body.String(), "AccessDenied",
|
|
"response should identify AccessDenied, body: %s", rec.Body.String())
|
|
}
|
|
|
|
// TestAuthorizeObjectWrite covers the write-authorization decision the POST
|
|
// handler defers to: a bucket-policy Deny blocks the key, a key the policy does
|
|
// not deny clears (the coarse Write action still grants it), and admins are
|
|
// unaffected. Driving the full handler for the allowed case is not hermetic --
|
|
// past authorization it reaches the lifecycle/write path that needs a filer --
|
|
// so the counter-cases are checked at the authorization boundary directly.
|
|
func TestAuthorizeObjectWrite(t *testing.T) {
|
|
s3a := newPostAuthzServer(t)
|
|
tester, _, found := s3a.iam.lookupByAccessKey(postAuthzAccessKey)
|
|
require.True(t, found)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/"+postAuthzBucket, nil)
|
|
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
s3a.iam.AuthorizeObjectWrite(req, tester, postAuthzBucket, "denied/secret.txt"),
|
|
"a bucket-policy-denied key must be denied")
|
|
assert.Equal(t, s3err.ErrNone,
|
|
s3a.iam.AuthorizeObjectWrite(req, tester, postAuthzBucket, "allowed/report.txt"),
|
|
"a key the policy does not deny must clear on the coarse Write grant")
|
|
|
|
admin := &Identity{Name: "admin", Actions: []Action{s3_constants.ACTION_ADMIN}}
|
|
assert.Equal(t, s3err.ErrNone,
|
|
s3a.iam.AuthorizeObjectWrite(req, admin, postAuthzBucket, "denied/secret.txt"),
|
|
"an admin is not confined by the bucket policy")
|
|
}
|
|
|
|
// TestPostPolicyBucketHandlerGatesAclFields drives signed POST uploads carrying
|
|
// an acl form field and asserts the handler rejects them the way the PUT path
|
|
// does: a writer without the WriteAcp action is refused, and so is a writer the
|
|
// bucket policy denies s3:PutObjectAcl on the target key.
|
|
func TestPostPolicyBucketHandlerGatesAclFields(t *testing.T) {
|
|
s3a := newPostAuthzServer(t)
|
|
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, "allowed/report.txt",
|
|
postAuthzAccessKey, postAuthzSecretKey, map[string]string{"acl": "public-read"}))
|
|
assert.Equal(t, http.StatusForbidden, rec.Code,
|
|
"POST with an acl field needs WriteAcp like PUT, body: %s", rec.Body.String())
|
|
|
|
rec = httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, "acl-denied/secret.txt",
|
|
postAclAccessKey, postAclSecretKey, map[string]string{"acl": "public-read"}))
|
|
assert.Equal(t, http.StatusForbidden, rec.Code,
|
|
"POST with an acl field must honor the bucket-policy PutObjectAcl deny, body: %s", rec.Body.String())
|
|
|
|
for _, field := range []string{"x-amz-grant-read", "x-amz-grant-write", "x-amz-grant-read-acp", "x-amz-grant-write-acp", "x-amz-grant-full-control"} {
|
|
rec = httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, "allowed/report.txt",
|
|
postAuthzAccessKey, postAuthzSecretKey,
|
|
map[string]string{field: `id="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"`}))
|
|
assert.Equal(t, http.StatusForbidden, rec.Code,
|
|
"POST with an %s field needs WriteAcp like PUT, body: %s", field, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestPostRequestsAcl(t *testing.T) {
|
|
for _, header := range []string{
|
|
s3_constants.AmzCannedAcl, s3_constants.AmzAclFullControl, s3_constants.AmzAclRead,
|
|
s3_constants.AmzAclReadAcp, s3_constants.AmzAclWrite, s3_constants.AmzAclWriteAcp,
|
|
} {
|
|
req := httptest.NewRequest(http.MethodPost, "/"+postAuthzBucket, nil)
|
|
req.Header.Set(header, "x")
|
|
assert.True(t, postRequestsAcl(req), "%s must trip the ACL gate", header)
|
|
}
|
|
assert.False(t, postRequestsAcl(httptest.NewRequest(http.MethodPost, "/"+postAuthzBucket, nil)))
|
|
}
|
|
|
|
// TestAuthorizeObjectAclWrite covers the acl authorization decision at the
|
|
// boundary, for the same reason as TestAuthorizeObjectWrite.
|
|
func TestAuthorizeObjectAclWrite(t *testing.T) {
|
|
s3a := newPostAuthzServer(t)
|
|
tester, _, found := s3a.iam.lookupByAccessKey(postAuthzAccessKey)
|
|
require.True(t, found)
|
|
aclwriter, _, found := s3a.iam.lookupByAccessKey(postAclAccessKey)
|
|
require.True(t, found)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/"+postAuthzBucket, nil)
|
|
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
s3a.iam.AuthorizeObjectAclWrite(req, tester, postAuthzBucket, "allowed/report.txt"),
|
|
"a writer without the WriteAcp action cannot attach an ACL")
|
|
assert.Equal(t, s3err.ErrNone,
|
|
s3a.iam.AuthorizeObjectAclWrite(req, aclwriter, postAuthzBucket, "allowed/report.txt"),
|
|
"the WriteAcp holder clears keys the policy does not deny")
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
s3a.iam.AuthorizeObjectAclWrite(req, aclwriter, postAuthzBucket, "acl-denied/secret.txt"),
|
|
"a bucket-policy PutObjectAcl deny must block the ACL-bearing upload")
|
|
|
|
admin := &Identity{Name: "admin", Actions: []Action{s3_constants.ACTION_ADMIN}}
|
|
assert.Equal(t, s3err.ErrNone,
|
|
s3a.iam.AuthorizeObjectAclWrite(req, admin, postAuthzBucket, "acl-denied/secret.txt"),
|
|
"an admin is not confined by the bucket policy")
|
|
}
|
|
|
|
// newPostPolicyOnlyServer builds an S3ApiServer with identities that hold no
|
|
// legacy Write action: "poster" carries an attached IAM policy allowing
|
|
// s3:PutObject on the test bucket, and "reader" one allowing only s3:GetObject.
|
|
func newPostPolicyOnlyServer(t *testing.T) *S3ApiServer {
|
|
t.Helper()
|
|
|
|
iam := &IdentityAccessManagement{
|
|
hashes: make(map[string]*sync.Pool),
|
|
hashCounters: make(map[string]*int32),
|
|
}
|
|
err := iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{
|
|
Name: "poster",
|
|
Account: &iam_pb.Account{Id: "000000000000", DisplayName: "poster"},
|
|
Credentials: []*iam_pb.Credential{{AccessKey: postPolicyAccessKey, SecretKey: postPolicySecretKey}},
|
|
Actions: []string{"List:" + postAuthzBucket},
|
|
PolicyNames: []string{"post-write"},
|
|
}, {
|
|
Name: "reader",
|
|
Account: &iam_pb.Account{Id: "000000000000", DisplayName: "reader"},
|
|
Credentials: []*iam_pb.Credential{{AccessKey: postReadAccessKey, SecretKey: postReadSecretKey}},
|
|
Actions: []string{"Read:" + postAuthzBucket, "List:" + postAuthzBucket},
|
|
PolicyNames: []string{"post-read"},
|
|
}},
|
|
Policies: []*iam_pb.Policy{{
|
|
Name: "post-write",
|
|
Content: fmt.Sprintf(`{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{
|
|
"Effect": "Allow",
|
|
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
|
|
"Resource": ["arn:aws:s3:::%s/*"]
|
|
}, {
|
|
"Effect": "Allow",
|
|
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
|
|
"Resource": ["arn:aws:s3:::%s"]
|
|
}]
|
|
}`, postAuthzBucket, postAuthzBucket),
|
|
}, {
|
|
Name: "post-read",
|
|
Content: fmt.Sprintf(`{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{
|
|
"Effect": "Allow",
|
|
"Action": ["s3:GetObject"],
|
|
"Resource": ["arn:aws:s3:::%s/*"]
|
|
}]
|
|
}`, postAuthzBucket),
|
|
}},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
s3a := &S3ApiServer{
|
|
option: &S3ApiServerOption{BucketsPath: "/buckets"},
|
|
iam: iam,
|
|
}
|
|
s3a.bucketRegistry = NewBucketRegistry(s3a)
|
|
s3a.bucketRegistry.setMetadataCache(&BucketMetaData{Name: postAuthzBucket, IsTableBucket: false})
|
|
return s3a
|
|
}
|
|
|
|
// postFormValues canonicalizes the signed request's multipart fields the way
|
|
// extractPostPolicyFormValues does, for driving doesPolicySignatureMatch.
|
|
func postFormValues(t *testing.T, r *http.Request) http.Header {
|
|
t.Helper()
|
|
require.NoError(t, r.ParseMultipartForm(1<<20))
|
|
formValues := make(http.Header)
|
|
for k, v := range r.MultipartForm.Value {
|
|
formValues[http.CanonicalHeaderKey(k)] = v
|
|
}
|
|
return formValues
|
|
}
|
|
|
|
// TestPostPolicySignatureAllowsAttachedPolicyIdentity signs a POST Object form
|
|
// with a key whose holder has no legacy Write action - only an attached IAM
|
|
// policy granting s3:PutObject - and asserts the signature check accepts it
|
|
// and the shared object-write authorization allows the key. Gating the
|
|
// signature check on the Actions projection used to fail this with
|
|
// AccessDenied before authorization ever ran.
|
|
func TestPostPolicySignatureAllowsAttachedPolicyIdentity(t *testing.T) {
|
|
s3a := newPostPolicyOnlyServer(t)
|
|
|
|
req := newSignedPostRequestFor(t, "uploads/pic.png", postPolicyAccessKey, postPolicySecretKey, nil)
|
|
identity, errCode := s3a.iam.doesPolicySignatureMatch(req.Context(), postFormValues(t, req))
|
|
require.Equal(t, s3err.ErrNone, errCode,
|
|
"a verified POST signature must not require the legacy Write action")
|
|
require.NotNil(t, identity)
|
|
|
|
assert.Equal(t, s3err.ErrNone,
|
|
s3a.iam.AuthorizeObjectWrite(req, identity, postAuthzBucket, "uploads/pic.png"),
|
|
"an attached IAM policy granting s3:PutObject must authorize the key like the PUT path")
|
|
}
|
|
|
|
// TestPostPolicyAttachedPolicyDeny covers the deny leg: a verified identity
|
|
// whose attached policy does not grant s3:PutObject is still refused by
|
|
// AuthorizeObjectWrite and by the full handler.
|
|
func TestPostPolicyAttachedPolicyDeny(t *testing.T) {
|
|
s3a := newPostPolicyOnlyServer(t)
|
|
|
|
req := newSignedPostRequestFor(t, "uploads/pic.png", postReadAccessKey, postReadSecretKey, nil)
|
|
identity, errCode := s3a.iam.doesPolicySignatureMatch(req.Context(), postFormValues(t, req))
|
|
require.Equal(t, s3err.ErrNone, errCode)
|
|
require.NotNil(t, identity)
|
|
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
s3a.iam.AuthorizeObjectWrite(req, identity, postAuthzBucket, "uploads/pic.png"),
|
|
"an attached policy without s3:PutObject must still deny the upload")
|
|
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, "uploads/pic.png", postReadAccessKey, postReadSecretKey, nil))
|
|
assert.Equal(t, http.StatusForbidden, rec.Code,
|
|
"POST from a read-only identity must stay AccessDenied, body: %s", rec.Body.String())
|
|
assert.Contains(t, rec.Body.String(), "AccessDenied")
|
|
}
|
|
|
|
// newPostAutoCreateServer builds an S3ApiServer on the fake filer with
|
|
// auto-create enabled, an "admin" identity allowed to create buckets, and the
|
|
// non-admin "tester" writer.
|
|
func newPostAutoCreateServer(t *testing.T, filer *ambiguousPutFiler) *S3ApiServer {
|
|
t.Helper()
|
|
|
|
iam := &IdentityAccessManagement{
|
|
hashes: make(map[string]*sync.Pool),
|
|
hashCounters: make(map[string]*int32),
|
|
}
|
|
err := iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{
|
|
Name: "admin",
|
|
Account: &iam_pb.Account{Id: "000000000000", DisplayName: "admin"},
|
|
Credentials: []*iam_pb.Credential{{AccessKey: postAdminAccessKey, SecretKey: postAdminSecretKey}},
|
|
Actions: []string{"Admin"},
|
|
}, {
|
|
Name: "tester",
|
|
Account: &iam_pb.Account{Id: "000000000000", DisplayName: "tester"},
|
|
Credentials: []*iam_pb.Credential{{AccessKey: postAuthzAccessKey, SecretKey: postAuthzSecretKey}},
|
|
Actions: []string{"Read:" + postAuthzBucket, "Write:" + postAuthzBucket, "List:" + postAuthzBucket},
|
|
}},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
s3a := newFailoverTestServer(t, startFakeFiler(t, filer))
|
|
s3a.iam = iam
|
|
s3a.option.BucketsPath = "/buckets"
|
|
s3a.option.AutoCreateBucket = true
|
|
s3a.bucketConfigCache = NewBucketConfigCache(time.Minute)
|
|
return s3a
|
|
}
|
|
|
|
// A signed POST Object upload to a bucket that does not exist must lazy-create
|
|
// it the way PutObject does: an admin signer is allowed to create the bucket
|
|
// and the upload proceeds against it, landing the object entry and its chunk
|
|
// instead of answering NoSuchBucket.
|
|
func TestPostPolicyBucketHandlerAutoCreatesMissingBucket(t *testing.T) {
|
|
volume := startFakeVolumeServer(t)
|
|
filer := &ambiguousPutFiler{
|
|
volume: volume,
|
|
entries: map[string]*filer_pb.Entry{},
|
|
apply: true,
|
|
}
|
|
s3a := newPostAutoCreateServer(t, filer)
|
|
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, "uploads/pic.png",
|
|
postAdminAccessKey, postAdminSecretKey, map[string]string{"success_action_status": "201"}))
|
|
|
|
assert.Equal(t, http.StatusCreated, rec.Code,
|
|
"POST to a missing bucket must complete the upload after creating it, body: %s", rec.Body.String())
|
|
assert.Contains(t, rec.Body.String(), "<Key>uploads/pic.png</Key>")
|
|
|
|
filer.mu.Lock()
|
|
defer filer.mu.Unlock()
|
|
bucketEntry := filer.entries["/buckets/"+postAuthzBucket]
|
|
require.NotNil(t, bucketEntry, "POST to a missing bucket must create it like PUT")
|
|
assert.True(t, bucketEntry.IsDirectory)
|
|
assert.Equal(t, []byte("admin"), bucketEntry.Extended[s3_constants.AmzIdentityId])
|
|
assert.Equal(t, []byte("000000000000"), bucketEntry.Extended[s3_constants.ExtAmzOwnerKey])
|
|
objectEntry := filer.entries["/buckets/"+postAuthzBucket+"/uploads/pic.png"]
|
|
require.NotNil(t, objectEntry, "the upload must land its object entry on the new bucket")
|
|
assert.NotEmpty(t, objectEntry.Chunks)
|
|
|
|
volume.mu.Lock()
|
|
defer volume.mu.Unlock()
|
|
var stored []byte
|
|
for _, data := range volume.stored {
|
|
stored = append(stored, data...)
|
|
}
|
|
assert.Contains(t, string(stored), "payload", "the file body must reach the assigned volume")
|
|
}
|
|
|
|
// A non-admin signer keeps the PUT semantics: the upload itself is authorized
|
|
// but the lazy create is refused, surfacing AccessDenied rather than creating
|
|
// the bucket or answering NoSuchBucket.
|
|
func TestPostPolicyBucketHandlerAutoCreateDeniedForNonAdmin(t *testing.T) {
|
|
volume := startFakeVolumeServer(t)
|
|
filer := &ambiguousPutFiler{
|
|
volume: volume,
|
|
entries: map[string]*filer_pb.Entry{},
|
|
apply: true,
|
|
}
|
|
s3a := newPostAutoCreateServer(t, filer)
|
|
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, "uploads/pic.png", postAuthzAccessKey, postAuthzSecretKey, nil))
|
|
|
|
assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body.String())
|
|
assert.Contains(t, rec.Body.String(), "AccessDenied")
|
|
|
|
filer.mu.Lock()
|
|
defer filer.mu.Unlock()
|
|
assert.Empty(t, filer.entries, "a non-admin POST must not create the bucket")
|
|
}
|
|
|
|
// A bucket that blocks public ACLs refuses a POST upload whose acl or grant
|
|
// fields would make the object public, and still accepts a private one.
|
|
func TestPostPolicyBucketHandlerBlocksPublicAcls(t *testing.T) {
|
|
newServer := func(t *testing.T) (*S3ApiServer, *ambiguousPutFiler) {
|
|
filer := &ambiguousPutFiler{
|
|
volume: startFakeVolumeServer(t),
|
|
entries: map[string]*filer_pb.Entry{"/buckets/" + postAuthzBucket: {
|
|
Name: postAuthzBucket,
|
|
IsDirectory: true,
|
|
Attributes: &filer_pb.FuseAttributes{},
|
|
Extended: map[string][]byte{
|
|
s3_constants.ExtAmzOwnerKey: []byte("000000000000"),
|
|
s3_constants.ExtPublicAccessBlockKey: []byte(`<PublicAccessBlockConfiguration><BlockPublicAcls>true</BlockPublicAcls></PublicAccessBlockConfiguration>`),
|
|
},
|
|
}},
|
|
apply: true,
|
|
}
|
|
return newPostAutoCreateServer(t, filer), filer
|
|
}
|
|
const key = "uploads/pic.png"
|
|
for _, fields := range []map[string]string{
|
|
{"acl": "public-read"},
|
|
{"acl": "authenticated-read"},
|
|
{"x-amz-grant-read": `uri="` + s3_constants.GranteeGroupAllUsers + `"`},
|
|
} {
|
|
s3a, filer := newServer(t)
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAdminAccessKey, postAdminSecretKey, fields))
|
|
assert.Equal(t, http.StatusForbidden, rec.Code, "%v must be refused, body: %s", fields, rec.Body.String())
|
|
assert.Contains(t, rec.Body.String(), "AccessDenied")
|
|
filer.mu.Lock()
|
|
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/"+key], "%v must not write the object", fields)
|
|
filer.mu.Unlock()
|
|
}
|
|
|
|
s3a, filer := newServer(t)
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAdminAccessKey, postAdminSecretKey,
|
|
map[string]string{"acl": "private", "success_action_status": "201"}))
|
|
assert.Equal(t, http.StatusCreated, rec.Code, "a private POST must succeed, body: %s", rec.Body.String())
|
|
filer.mu.Lock()
|
|
defer filer.mu.Unlock()
|
|
assert.NotNil(t, filer.entries["/buckets/"+postAuthzBucket+"/"+key])
|
|
}
|
|
|
|
// A POST Object acl field is stored on the object like PUT's x-amz-acl, so a
|
|
// private upload into a public-read bucket is not anonymously readable.
|
|
func TestPostPolicyBucketHandlerStoresObjectAcl(t *testing.T) {
|
|
const key = "uploads/pic.png"
|
|
upload := func(t *testing.T, publicBucket bool, fields, headers map[string]string) (*httptest.ResponseRecorder, *S3ApiServer, *filer_pb.Entry) {
|
|
bucketEntry := &filer_pb.Entry{
|
|
Name: postAuthzBucket,
|
|
IsDirectory: true,
|
|
Attributes: &filer_pb.FuseAttributes{},
|
|
Extended: map[string][]byte{s3_constants.ExtAmzOwnerKey: []byte("000000000000")},
|
|
}
|
|
if publicBucket {
|
|
bucketEntry.Extended[s3_constants.ExtAmzAclKey] = anonymousReadGrant(s3_constants.PermissionRead)
|
|
}
|
|
filer := &ambiguousPutFiler{
|
|
volume: startFakeVolumeServer(t),
|
|
entries: map[string]*filer_pb.Entry{"/buckets/" + postAuthzBucket: bucketEntry},
|
|
apply: true,
|
|
}
|
|
s3a := newPostAutoCreateServer(t, filer)
|
|
req := newSignedPostRequestFor(t, key, postAdminAccessKey, postAdminSecretKey, fields)
|
|
for name, value := range headers {
|
|
req.Header.Set(name, value)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, req)
|
|
filer.mu.Lock()
|
|
defer filer.mu.Unlock()
|
|
return rec, s3a, filer.entries["/buckets/"+postAuthzBucket+"/"+key]
|
|
}
|
|
anonymousRead := func(s3a *S3ApiServer, entry *filer_pb.Entry) s3err.ErrorCode {
|
|
req := httptest.NewRequest(http.MethodGet, "/"+postAuthzBucket+"/"+key, nil)
|
|
return s3a.authorizeAnonymousObjectRead(req, postAuthzBucket, key, entry.Extended)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
publicBucket bool
|
|
fields map[string]string
|
|
headers map[string]string
|
|
want s3err.ErrorCode
|
|
}{
|
|
{"private on public-read bucket", true, map[string]string{"acl": "private"}, nil, s3err.ErrAccessDenied},
|
|
{"no acl on public-read bucket", true, nil, nil, s3err.ErrAccessDenied},
|
|
{"public-read on private bucket", false, map[string]string{"acl": "public-read"}, nil, s3err.ErrNone},
|
|
{"grant-read AllUsers on private bucket", false, map[string]string{"x-amz-grant-read": `uri="` + s3_constants.GranteeGroupAllUsers + `"`}, nil, s3err.ErrNone},
|
|
{"unsigned acl header", false, nil, map[string]string{s3_constants.AmzCannedAcl: "public-read"}, s3err.ErrAccessDenied},
|
|
{"unsigned grant header", false, nil, map[string]string{s3_constants.AmzAclRead: `uri="` + s3_constants.GranteeGroupAllUsers + `"`}, s3err.ErrAccessDenied},
|
|
{"unsigned header beside signed acl", false, map[string]string{"acl": "private"}, map[string]string{s3_constants.AmzAclRead: `uri="` + s3_constants.GranteeGroupAllUsers + `"`}, s3err.ErrAccessDenied},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rec, s3a, entry := upload(t, tc.publicBucket, tc.fields, tc.headers)
|
|
require.Equal(t, http.StatusNoContent, rec.Code, "body: %s", rec.Body.String())
|
|
require.NotNil(t, entry)
|
|
assert.Equal(t, []byte("000000000000"), entry.Extended[s3_constants.ExtAmzOwnerKey])
|
|
assert.NotEmpty(t, entry.Extended[s3_constants.ExtAmzAclKey], "the upload ACL must be stored")
|
|
assert.Equal(t, tc.want, anonymousRead(s3a, entry))
|
|
})
|
|
}
|
|
|
|
t.Run("invalid canned acl", func(t *testing.T) {
|
|
rec, _, entry := upload(t, false, map[string]string{"acl": "bogus-acl"}, nil)
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code, "body: %s", rec.Body.String())
|
|
assert.Nil(t, entry, "a rejected ACL must not write the object")
|
|
})
|
|
t.Run("canned and grant together", func(t *testing.T) {
|
|
rec, _, entry := upload(t, false, map[string]string{"acl": "private", "x-amz-grant-read": `uri="` + s3_constants.GranteeGroupAllUsers + `"`}, nil)
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code, "body: %s", rec.Body.String())
|
|
assert.Nil(t, entry)
|
|
})
|
|
}
|
|
|
|
// newPostAclWriterServer serves the policy-confined identities of
|
|
// newPostAuthzServer against a fake filer holding the test bucket.
|
|
func newPostAclWriterServer(t *testing.T) (*S3ApiServer, *ambiguousPutFiler) {
|
|
filer := &ambiguousPutFiler{
|
|
volume: startFakeVolumeServer(t),
|
|
entries: map[string]*filer_pb.Entry{"/buckets/" + postAuthzBucket: {
|
|
Name: postAuthzBucket,
|
|
IsDirectory: true,
|
|
Attributes: &filer_pb.FuseAttributes{},
|
|
Extended: map[string][]byte{s3_constants.ExtAmzOwnerKey: []byte("000000000000")},
|
|
}},
|
|
apply: true,
|
|
}
|
|
s3a := newPostAutoCreateServer(t, filer)
|
|
authz := newPostAuthzServer(t)
|
|
s3a.iam, s3a.policyEngine = authz.iam, authz.policyEngine
|
|
return s3a, filer
|
|
}
|
|
|
|
// Policy conditions on a POST grant see the canonical grant list, as on PUT,
|
|
// so a respelled grantee does not slip past a deny.
|
|
func TestPostPolicyBucketHandlerCanonicalGrantCondition(t *testing.T) {
|
|
for _, grant := range []string{`id="000000000000"`, `id = "000000000000"`, `id="00000000000\u0030"`} {
|
|
s3a, filer := newPostAclWriterServer(t)
|
|
require.NoError(t, s3a.policyEngine.engine.SetBucketPolicy(postAuthzBucket, fmt.Sprintf(`{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{
|
|
"Effect": "Deny",
|
|
"Principal": {"AWS": "%s"},
|
|
"Action": ["s3:PutObjectAcl"],
|
|
"Resource": ["arn:aws:s3:::%s/*"],
|
|
"Condition": {"StringEquals": {"s3:x-amz-grant-read": "id=\"000000000000\""}}
|
|
}]
|
|
}`, postAclPrincipal, postAuthzBucket)))
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, "allowed/report.txt",
|
|
postAclAccessKey, postAclSecretKey, map[string]string{"x-amz-grant-read": grant}))
|
|
assert.Equal(t, http.StatusForbidden, rec.Code, "grant %s, body: %s", grant, rec.Body.String())
|
|
filer.mu.Lock()
|
|
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/allowed/report.txt"], "grant %s", grant)
|
|
filer.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// With authentication off, a bucket policy still governs a POST upload ACL,
|
|
// as it does a PutObject ACL, including on the first request after start.
|
|
func TestPostPolicyBucketHandlerAclPolicyWithAuthDisabled(t *testing.T) {
|
|
const key = "uploads/pic.png"
|
|
s3a, filer := newPostAclWriterServer(t)
|
|
s3a.iam = &IdentityAccessManagement{}
|
|
s3a.policyEngine = NewBucketPolicyEngine()
|
|
filer.mu.Lock()
|
|
filer.entries["/buckets/"+postAuthzBucket].Extended[BUCKET_POLICY_METADATA_KEY] = []byte(fmt.Sprintf(`{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{"Effect": "Deny", "Principal": "*", "Action": ["s3:PutObjectAcl"], "Resource": ["arn:aws:s3:::%s/*"]}]
|
|
}`, postAuthzBucket))
|
|
filer.mu.Unlock()
|
|
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAdminAccessKey, postAdminSecretKey, map[string]string{"acl": "public-read"}))
|
|
assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body.String())
|
|
filer.mu.Lock()
|
|
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/"+key])
|
|
filer.mu.Unlock()
|
|
|
|
rec = httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAdminAccessKey, postAdminSecretKey, nil))
|
|
assert.Equal(t, http.StatusNoContent, rec.Code, "an upload without an ACL is not a PutObjectAcl, body: %s", rec.Body.String())
|
|
}
|
|
|
|
// newColdPostAclWriterServer is newPostAclWriterServer with an empty policy
|
|
// engine that only learns the bucket policy when the bucket loads.
|
|
func newColdPostAclWriterServer(t *testing.T, policy string) (*S3ApiServer, *ambiguousPutFiler) {
|
|
s3a, filer := newPostAclWriterServer(t)
|
|
s3a.policyEngine = NewBucketPolicyEngine()
|
|
s3a.iam.policyEngine = s3a.policyEngine
|
|
s3a.iam.primeBucketForIAM = func(bucket string) s3err.ErrorCode {
|
|
_, code := s3a.getBucketConfig(bucket)
|
|
return code
|
|
}
|
|
if policy != "" {
|
|
filer.mu.Lock()
|
|
filer.entries["/buckets/"+postAuthzBucket].Extended[BUCKET_POLICY_METADATA_KEY] = []byte(policy)
|
|
filer.mu.Unlock()
|
|
}
|
|
return s3a, filer
|
|
}
|
|
|
|
// With authentication on, a persisted bucket policy also governs a POST
|
|
// upload ACL on the first request after start.
|
|
func TestPostPolicyBucketHandlerAclPolicyOnColdBucket(t *testing.T) {
|
|
const key = "allowed/report.txt"
|
|
aclField := map[string]string{"acl": "public-read"}
|
|
|
|
s3a, _ := newColdPostAclWriterServer(t, "")
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAclAccessKey, postAclSecretKey, aclField))
|
|
assert.Equal(t, http.StatusNoContent, rec.Code, "body: %s", rec.Body.String())
|
|
|
|
s3a, filer := newColdPostAclWriterServer(t, fmt.Sprintf(`{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{"Effect": "Deny", "Principal": {"AWS": "%s"}, "Action": ["s3:PutObjectAcl"], "Resource": ["arn:aws:s3:::%s/*"]}]
|
|
}`, postAclPrincipal, postAuthzBucket))
|
|
rec = httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAclAccessKey, postAclSecretKey, aclField))
|
|
assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body.String())
|
|
filer.mu.Lock()
|
|
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/"+key])
|
|
filer.mu.Unlock()
|
|
|
|
rec = httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAclAccessKey, postAclSecretKey, nil))
|
|
assert.Equal(t, http.StatusNoContent, rec.Code, "an upload without an ACL is not a PutObjectAcl, body: %s", rec.Body.String())
|
|
}
|
|
|
|
// A bucket that fails to load cannot have its policy evaluated, so the
|
|
// upload stops instead of falling back to the identity's grants.
|
|
func TestPostPolicyBucketHandlerStopsWhenBucketLoadFails(t *testing.T) {
|
|
const key = "allowed/report.txt"
|
|
s3a, filer := newColdPostAclWriterServer(t, "")
|
|
s3a.iam.primeBucketForIAM = func(string) s3err.ErrorCode { return s3err.ErrInternalError }
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAclAccessKey, postAclSecretKey, nil))
|
|
assert.Equal(t, http.StatusInternalServerError, rec.Code, "body: %s", rec.Body.String())
|
|
filer.mu.Lock()
|
|
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/"+key])
|
|
filer.mu.Unlock()
|
|
}
|
|
|
|
func TestPostPolicyBucketHandlerRejectsConflictingAclFields(t *testing.T) {
|
|
for _, fields := range []map[string]string{
|
|
{"acl": "private", "x-amz-acl": "public-read"},
|
|
{"acl": "public-read", "x-amz-acl": ""},
|
|
{"acl": "", "x-amz-acl": "public-read"},
|
|
} {
|
|
s3a, filer := newPostAclWriterServer(t)
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, "allowed/report.txt", postAclAccessKey, postAclSecretKey, fields))
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code, "fields %v, body: %s", fields, rec.Body.String())
|
|
filer.mu.Lock()
|
|
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/allowed/report.txt"], "fields %v", fields)
|
|
filer.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// Object settings sent as request headers are outside the signed policy, so
|
|
// only the matching form fields reach the stored object.
|
|
func TestPostPolicyBucketHandlerIgnoresUnsignedHeaders(t *testing.T) {
|
|
const key = "uploads/header.txt"
|
|
upload := func(t *testing.T, fields, headers map[string]string) (*httptest.ResponseRecorder, *filer_pb.Entry) {
|
|
filer := &ambiguousPutFiler{
|
|
volume: startFakeVolumeServer(t),
|
|
entries: map[string]*filer_pb.Entry{"/buckets/" + postAuthzBucket: {
|
|
Name: postAuthzBucket,
|
|
IsDirectory: true,
|
|
Attributes: &filer_pb.FuseAttributes{},
|
|
Extended: map[string][]byte{s3_constants.ExtAmzOwnerKey: []byte("000000000000")},
|
|
}},
|
|
apply: true,
|
|
}
|
|
s3a := newPostAutoCreateServer(t, filer)
|
|
req := newSignedPostRequestFor(t, key, postAdminAccessKey, postAdminSecretKey, fields)
|
|
for name, value := range headers {
|
|
req.Header.Set(name, value)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
s3a.PostPolicyBucketHandler(rec, req)
|
|
filer.mu.Lock()
|
|
defer filer.mu.Unlock()
|
|
return rec, filer.entries["/buckets/"+postAuthzBucket+"/"+key]
|
|
}
|
|
settings := map[string]string{
|
|
s3_constants.AmzObjectTagging: "scan=clean",
|
|
"X-Amz-Meta-Role": "from-form-holder",
|
|
s3_constants.AmzStorageClass: "STANDARD_IA",
|
|
"Cache-Control": "max-age=60",
|
|
"Content-Disposition": "inline",
|
|
"Expires": "Wed, 21 Oct 2026 07:28:00 GMT",
|
|
}
|
|
stored := map[string]string{
|
|
s3_constants.AmzObjectTaggingPrefix + "scan": "clean",
|
|
"X-Amz-Meta-Role": "from-form-holder",
|
|
s3_constants.AmzStorageClass: "STANDARD_IA",
|
|
"Cache-Control": "max-age=60",
|
|
"Content-Disposition": "inline",
|
|
"Expires": "Wed, 21 Oct 2026 07:28:00 GMT",
|
|
}
|
|
|
|
t.Run("request headers", func(t *testing.T) {
|
|
headers := map[string]string{"Content-Language": "fr", "Response-Content-Disposition": "inline"}
|
|
for k, v := range settings {
|
|
headers[k] = v
|
|
}
|
|
rec, entry := upload(t, nil, headers)
|
|
require.Equal(t, http.StatusNoContent, rec.Code, "body: %s", rec.Body.String())
|
|
require.NotNil(t, entry)
|
|
for k := range stored {
|
|
assert.Empty(t, entry.Extended[k], k)
|
|
}
|
|
assert.Empty(t, entry.Extended["Content-Language"])
|
|
})
|
|
t.Run("object lock request headers", func(t *testing.T) {
|
|
rec, entry := upload(t, nil, map[string]string{
|
|
s3_constants.AmzObjectLockMode: "COMPLIANCE",
|
|
s3_constants.AmzObjectLockRetainUntilDate: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
|
|
s3_constants.AmzObjectLockLegalHold: "ON",
|
|
})
|
|
require.Equal(t, http.StatusNoContent, rec.Code, "body: %s", rec.Body.String())
|
|
require.NotNil(t, entry)
|
|
assert.Empty(t, entry.Extended[s3_constants.ExtObjectLockModeKey])
|
|
assert.Empty(t, entry.Extended[s3_constants.ExtRetentionUntilDateKey])
|
|
assert.Empty(t, entry.Extended[s3_constants.ExtLegalHoldKey])
|
|
})
|
|
t.Run("signed form fields", func(t *testing.T) {
|
|
rec, entry := upload(t, settings, nil)
|
|
require.Equal(t, http.StatusNoContent, rec.Code, "body: %s", rec.Body.String())
|
|
require.NotNil(t, entry)
|
|
for k, v := range stored {
|
|
assert.Equal(t, v, string(entry.Extended[k]), k)
|
|
}
|
|
})
|
|
}
|