Compare commits

...
Author SHA1 Message Date
Chris Lu e222158de9 s3api: carry presign params onto synthesized policy requests
objectKeyPolicyRequest rebuilt the query with only versionId and the STS
token, so a presigned request's s3:authType condition value silently
changed from REST-QUERY-STRING to unset during phase-2 rechecks (version
read grant, copy source, delete keys, post-policy writes). A policy
conditioned on s3:authType would then deny a request it already allowed.
Carry the SigV2/SigV4 presign keys through; none of them steer
ResolveS3Action.
2026-10-21 15:09:36 -07:00
DevinandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> bdd9b1a8a9 s3api: reject a named versionId on buckets without versioning
Tagging, ACL, and attributes handlers answered versionId requests on
never-versioned buckets as the current object while evaluating the
version-scoped action, so a grant limited to the version action could
authorize changes to the live entry. Reject the request up front through
the existing checkVersionIdConfigured gate.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 14:54:49 -07:00
DevinandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 8e92be099a s3api: check the version read grant on a synthesized versioned GET
The versioned attributes check passed the literal s3:GetObjectVersion
action, but both policy paths re-resolve the action from the request, so
the attributes query parameter mapped it back to
s3:GetObjectVersionAttributes and a lone version-read Deny never fired.
The literal action also reached the legacy CanDo path, where a plain
Read grant no longer matched. Evaluate the grant on a request shaped
like a plain versioned GET instead.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 14:51:55 -07:00
DevinandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> d7c608ec51 s3api: require the version read grant for versioned attributes
AWS asks for s3:GetObjectVersion alongside s3:GetObjectVersionAttributes
when a GetObjectAttributes request names a versionId; the resolved
version-scoped action on its own left a lone s3:GetObjectVersion Deny
unable to cover the attributes read. Check the version read grant too:
the identity side through VerifyActionPermission, the bucket-policy side
through the same recheck that evaluates the scoped action.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 14:29:00 -07:00
DevinandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> c0034c9848 s3api: resolve HEAD ?acl as the head-object action, not an ACL action
No HEAD acl route exists, so HEAD /bucket/key?acl lands on
HeadObjectHandler — which serves object headers, not the ACL. Resolving
it to an ACL action denied readers who hold only GetObject(Version) and
let ACL grants reach a head-object read. Dropping HEAD from the acl
branch lets the request fall through to GetObject/GetObjectVersion,
matching how the tagging branch already treats HEAD.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 14:11:54 -07:00
DevinandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 0a08101acb s3api: recheck the version action on versioned tagging requests
The same phase-2 gap exists in the tagging handlers left by the earlier
version-tagging resolution change: versioned GetObjectTagging /
PutObjectTagging / DeleteObjectTagging rechecks evaluated the
current-object tagging action, so a Deny on s3:*VersionTagging could not
fire.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 14:07:18 -07:00
DevinandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 080e094b7e s3api: recheck the version action on versioned ACL/attributes requests
The phase-2 policy recheck inside GetObjectAclHandler,
PutObjectAclHandler, and GetObjectAttributesHandler passed the
current-object action even when the request carried versionId and phase 1
had resolved the version action. A Deny scoped to s3:*VersionAcl or
s3:GetObjectVersionAttributes could never fire in the recheck, and an
Allow limited to the version action went unevaluated. Select the action
the same way the resolver does — versioned whenever versionId is present.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 14:07:18 -07:00
Chris Lu f71620925d s3: resolve versionId attributes requests to the version attributes action
GET /bucket/key?attributes&versionId=<id> fetched the historical version's attributes but authorized against s3:GetObjectAttributes, so a grant limited to the current-object attributes action could reach noncurrent versions. The request now resolves to s3:GetObjectVersionAttributes, and both attributes actions map onto the Read identity action so policies can name them (s3:GetObjectAttributes was previously rejected as an unmapped action).
2026-10-21 14:02:07 -07:00
Chris Lu 95c31f3840 s3: resolve versionId ACL requests to the version ACL actions
A request for an object's ACL carrying versionId acted on the historical version but authorized against the current-object action (s3:GetObjectAcl/s3:PutObjectAcl), because the acl block in resolveFromQueryParameters returned before versionId was considered - the same gap closed for tagging. An identity granted only current-object ACL access - or explicitly denied s3:*ObjectVersionAcl - could still read and modify ACLs on noncurrent versions.

?acl&versionId=<id> on an object now resolves to s3:GetObjectVersionAcl/s3:PutObjectVersionAcl; ?acl alone keeps the current-object actions and bucket ACL is unaffected. s3:PutObjectVersionAcl is mapped onto the WriteAcp identity action so policies can name it.
2026-10-21 14:01:28 -07:00
11 changed files with 295 additions and 31 deletions
+8 -5
View File
@@ -98,11 +98,13 @@ var fineGrainedActionMap = map[string]string{
// prefixed (s3:Action) and unprefixed (Action) variants in init().
var baseS3ActionMap = map[string]string{
// Object operations
"DeleteObject": s3_constants.ACTION_WRITE,
"PutObject": s3_constants.ACTION_WRITE,
"GetObject": s3_constants.ACTION_READ,
"DeleteObjectVersion": s3_constants.ACTION_WRITE,
"GetObjectVersion": s3_constants.ACTION_READ,
"DeleteObject": s3_constants.ACTION_WRITE,
"PutObject": s3_constants.ACTION_WRITE,
"GetObject": s3_constants.ACTION_READ,
"DeleteObjectVersion": s3_constants.ACTION_WRITE,
"GetObjectVersion": s3_constants.ACTION_READ,
"GetObjectAttributes": s3_constants.ACTION_READ,
"GetObjectVersionAttributes": s3_constants.ACTION_READ,
// Tagging operations
"GetObjectTagging": s3_constants.ACTION_TAGGING,
"GetObjectVersionTagging": s3_constants.ACTION_TAGGING,
@@ -115,6 +117,7 @@ var baseS3ActionMap = map[string]string{
"DeleteBucketTagging": s3_constants.ACTION_TAGGING,
// ACL operations
"PutObjectAcl": s3_constants.ACTION_WRITE_ACP,
"PutObjectVersionAcl": s3_constants.ACTION_WRITE_ACP,
"GetObjectAcl": s3_constants.ACTION_READ_ACP,
"GetObjectVersionAcl": s3_constants.ACTION_READ_ACP,
"PutBucketAcl": s3_constants.ACTION_WRITE_ACP,
+11 -5
View File
@@ -3,6 +3,7 @@ package s3api
import (
"context"
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
@@ -251,9 +252,10 @@ func TestAuthorizeCopySource_PresignedURLSessionTokenPreserved(t *testing.T) {
}
// Presigned URL: session token is in the query string, no
// X-Amz-Security-Token header. The destination URL also carries unrelated
// SigV4 query params (X-Amz-Algorithm, etc.) that must not leak into the
// synthetic source action resolution.
// X-Amz-Security-Token header. The destination URL also carries the rest of
// the SigV4 presign params, which ride along so auth-derived policy
// conditions (s3:authType) match the real request; none of them steer
// ResolveS3Action.
req, err := http.NewRequest(http.MethodPut,
"http://s3.local/bucket-a/dst.bin?X-Amz-Algorithm=AWS4-HMAC-SHA256"+
"&X-Amz-Credential=stsKey%2F20260518%2Fus-east-1%2Fs3%2Faws4_request"+
@@ -267,8 +269,12 @@ func TestAuthorizeCopySource_PresignedURLSessionTokenPreserved(t *testing.T) {
assert.Equal(t, s3err.ErrNone, errCode)
assert.Equal(t, "presigned-session-token", capturedSessionToken,
"session token from presigned-URL query must reach the IAM check")
// versionId omitted, only the token should be present in the synthetic query.
assert.Equal(t, "X-Amz-Security-Token=presigned-session-token", capturedRawQuery)
capturedQuery, err := url.ParseQuery(capturedRawQuery)
require.NoError(t, err)
assert.Equal(t, "presigned-session-token", capturedQuery.Get("X-Amz-Security-Token"))
assert.Equal(t, "AWS4-HMAC-SHA256", capturedQuery.Get("X-Amz-Algorithm"))
assert.False(t, capturedQuery.Has("versionId"))
assert.False(t, capturedQuery.Has("acl"))
}
// TestAuthorizeCopySource_PreservesCopySourceHeader verifies that the synthetic
+14 -4
View File
@@ -3025,11 +3025,21 @@ func (iam *IdentityAccessManagement) bucketsNamedByAttachedPolicies(r *http.Requ
// its query describe. versionId, when set, and the STS session token ride along
// for policy conditions. Returns ErrNone when auth is disabled (checked by the
// callers) or the identity is an admin.
// authQueryParams are the query keys carrying a request's authentication
// material (SigV2 and SigV4 presign parameters). None steer ResolveS3Action,
// so they can ride along on a synthesized request.
var authQueryParams = []string{
"AWSAccessKeyId", "Signature", "Expires",
"X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Date", "X-Amz-Expires",
"X-Amz-Signature", "X-Amz-SignedHeaders", "X-Amz-Security-Token",
}
// objectKeyPolicyRequest synthesizes a <method> /<bucket>/<objectKey> request
// for policy evaluation against a key the real URL does not name. The query is
// built from scratch so a param on the real request (delete, uploadId,
// partNumber, ...) cannot steer ResolveS3Action off the intended action; the
// STS token and versionId ride along for policy conditions.
// versionId and the auth-material params ride along so auth-derived policy
// conditions (s3:authType) evaluate as they did on the real request.
func objectKeyPolicyRequest(r *http.Request, method, bucket, objectKey, versionId string) *http.Request {
keyReq := new(http.Request)
*keyReq = *r
@@ -3042,9 +3052,9 @@ func objectKeyPolicyRequest(r *http.Request, method, bucket, objectKey, versionI
if versionId != "" {
keyQuery.Set("versionId", versionId)
}
if strings.Contains(r.URL.RawQuery, "X-Amz-Security-Token") {
if token := r.URL.Query().Get("X-Amz-Security-Token"); token != "" {
keyQuery.Set("X-Amz-Security-Token", token)
for _, key := range authQueryParams {
if vals, ok := r.URL.Query()[key]; ok {
keyQuery[key] = vals
}
}
if len(keyQuery) > 0 {
+15 -2
View File
@@ -166,15 +166,25 @@ func resolveFromQueryParameters(query url.Values, method string, hasObject bool)
}
}
// ACL operations
// ACL operations. A versionId retargets the request at a specific
// version, so the version ACL action applies, not the current-object one.
if query.Has("acl") {
versioned := hasObject && query.Get("versionId") != ""
switch method {
case http.MethodGet, http.MethodHead:
// HEAD has no acl route; it lands on HeadObjectHandler and should
// resolve like a read — s3:GetObject, or s3:GetObjectVersion below.
case http.MethodGet:
if versioned {
return s3_constants.S3_ACTION_GET_OBJECT_VERSION_ACL
}
if hasObject {
return s3_constants.S3_ACTION_GET_OBJECT_ACL
}
return s3_constants.S3_ACTION_GET_BUCKET_ACL
case http.MethodPut:
if versioned {
return s3_constants.S3_ACTION_PUT_OBJECT_VERSION_ACL
}
if hasObject {
return s3_constants.S3_ACTION_PUT_OBJECT_ACL
}
@@ -220,6 +230,9 @@ func resolveFromQueryParameters(query url.Values, method string, hasObject bool)
// Must be checked before versionId, because GET /bucket/key?attributes&versionId=xyz
// is a GetObjectAttributes request, not a GetObjectVersion request
if hasObject && query.Has("attributes") && method == http.MethodGet {
if query.Get("versionId") != "" {
return s3_constants.S3_ACTION_GET_OBJECT_VERSION_ATTRIBUTES
}
return s3_constants.S3_ACTION_GET_OBJECT_ATTRIBUTES
}
+54 -1
View File
@@ -83,7 +83,7 @@ func TestResolveS3Action_AttributesBeforeVersionId(t *testing.T) {
method: http.MethodGet,
baseAction: s3_constants.ACTION_READ,
object: "key",
want: s3_constants.S3_ACTION_GET_OBJECT_ATTRIBUTES,
want: s3_constants.S3_ACTION_GET_OBJECT_VERSION_ATTRIBUTES,
},
{
name: "versionId only GET",
@@ -146,6 +146,59 @@ func TestResolveS3Action_VersionTagging(t *testing.T) {
}
}
// An ACL request carrying versionId targets a specific version and must
// resolve to the version ACL action so a grant limited to the current-object
// action cannot reach historical versions.
func TestResolveS3Action_VersionAcl(t *testing.T) {
tests := []struct {
name string
method string
object string
query string
want string
}{
{"get current acl", http.MethodGet, "key", "acl", s3_constants.S3_ACTION_GET_OBJECT_ACL},
{"put current acl", http.MethodPut, "key", "acl", s3_constants.S3_ACTION_PUT_OBJECT_ACL},
{"get version acl", http.MethodGet, "key", "acl&versionId=v1", s3_constants.S3_ACTION_GET_OBJECT_VERSION_ACL},
{"head acl resolves as head object", http.MethodHead, "key", "acl", s3_constants.S3_ACTION_GET_OBJECT},
{"head version acl resolves as head version", http.MethodHead, "key", "acl&versionId=v1", s3_constants.S3_ACTION_GET_OBJECT_VERSION},
{"put version acl", http.MethodPut, "key", "acl&versionId=v1", s3_constants.S3_ACTION_PUT_OBJECT_VERSION_ACL},
{"empty versionId stays current", http.MethodGet, "key", "acl&versionId=", s3_constants.S3_ACTION_GET_OBJECT_ACL},
{"bucket acl ignores versionId", http.MethodGet, "", "acl&versionId=v1", s3_constants.S3_ACTION_GET_BUCKET_ACL},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, _ := http.NewRequest(tt.method, "http://localhost/bucket/"+tt.object+"?"+tt.query, nil)
if got := ResolveS3Action(r, s3_constants.ACTION_READ, "bucket", tt.object); got != tt.want {
t.Errorf("ResolveS3Action() = %q, want %q", got, tt.want)
}
})
}
}
// An attributes request carrying versionId targets a specific version and
// must resolve to the version attributes action.
func TestResolveS3Action_VersionAttributes(t *testing.T) {
tests := []struct {
name string
method string
object string
query string
want string
}{
{"get current attributes", http.MethodGet, "key", "attributes", s3_constants.S3_ACTION_GET_OBJECT_ATTRIBUTES},
{"get version attributes", http.MethodGet, "key", "attributes&versionId=v1", s3_constants.S3_ACTION_GET_OBJECT_VERSION_ATTRIBUTES},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, _ := http.NewRequest(tt.method, "http://localhost/bucket/"+tt.object+"?"+tt.query, nil)
if got := ResolveS3Action(r, s3_constants.ACTION_READ, "bucket", tt.object); got != tt.want {
t.Errorf("ResolveS3Action() = %q, want %q", got, tt.want)
}
})
}
}
// Bucket subresources registered with ACTION_ADMIN must resolve to their own
// S3 actions so a policy granting one of them does not need s3:*, and so no
// broader grant sweeps them in.
+11 -6
View File
@@ -4,17 +4,22 @@ package s3_constants
// These match the official AWS S3 action format used in IAM and bucket policies
const (
// Object operations
S3_ACTION_GET_OBJECT = "s3:GetObject"
S3_ACTION_PUT_OBJECT = "s3:PutObject"
S3_ACTION_DELETE_OBJECT = "s3:DeleteObject"
S3_ACTION_DELETE_OBJECT_VERSION = "s3:DeleteObjectVersion"
S3_ACTION_GET_OBJECT_VERSION = "s3:GetObjectVersion"
S3_ACTION_GET_OBJECT_ATTRIBUTES = "s3:GetObjectAttributes"
S3_ACTION_GET_OBJECT = "s3:GetObject"
S3_ACTION_PUT_OBJECT = "s3:PutObject"
S3_ACTION_DELETE_OBJECT = "s3:DeleteObject"
S3_ACTION_DELETE_OBJECT_VERSION = "s3:DeleteObjectVersion"
S3_ACTION_GET_OBJECT_VERSION = "s3:GetObjectVersion"
S3_ACTION_GET_OBJECT_ATTRIBUTES = "s3:GetObjectAttributes"
S3_ACTION_GET_OBJECT_VERSION_ATTRIBUTES = "s3:GetObjectVersionAttributes"
// Object ACL operations
S3_ACTION_GET_OBJECT_ACL = "s3:GetObjectAcl"
S3_ACTION_PUT_OBJECT_ACL = "s3:PutObjectAcl"
// Version-specific ACL operations (?acl&versionId)
S3_ACTION_GET_OBJECT_VERSION_ACL = "s3:GetObjectVersionAcl"
S3_ACTION_PUT_OBJECT_VERSION_ACL = "s3:PutObjectVersionAcl"
// Object tagging operations
S3_ACTION_GET_OBJECT_TAGGING = "s3:GetObjectTagging"
S3_ACTION_PUT_OBJECT_TAGGING = "s3:PutObjectTagging"
+94
View File
@@ -576,3 +576,97 @@ func TestRecheckObjectAclTagConditionedPolicy(t *testing.T) {
assert.Equal(t, s3err.ErrNone,
s3a.recheckPolicyWithObjectEntry(putReq, "b", "secret.txt", s3_constants.S3_ACTION_PUT_OBJECT_ACL, publicEntry, "test"))
}
// A versioned GetObjectAttributes request is authorized against the version
// read grant, not the attributes action the original request resolves to: an
// explicit bucket-policy Deny on s3:GetObjectVersion blocks it, while a legacy
// identity holding only the coarse Read grant is still accepted.
func TestCheckVersionReadGrant(t *testing.T) {
s3a := &S3ApiServer{
policyEngine: NewBucketPolicyEngine(),
iam: &IdentityAccessManagement{},
}
denyPolicyJSON := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::deny-bucket", "arn:aws:s3:::deny-bucket/*"]
},
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObjectVersion",
"Resource": "arn:aws:s3:::deny-bucket/key"
}
]
}`
require.NoError(t, s3a.policyEngine.engine.SetBucketPolicy("deny-bucket", denyPolicyJSON))
allowPolicyJSON := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::allow-bucket", "arn:aws:s3:::allow-bucket/*"]
}
]
}`
require.NoError(t, s3a.policyEngine.engine.SetBucketPolicy("allow-bucket", allowPolicyJSON))
identity := &Identity{
Name: "reader",
Actions: []Action{s3_constants.ACTION_READ},
PrincipalArn: "arn:aws:iam::000000000000:user/reader",
}
attrsReq := func(bucket string) *http.Request {
req := httptest.NewRequest(http.MethodGet, "/"+bucket+"/key?attributes&versionId=v1", nil)
return req.WithContext(s3_constants.SetIdentityInContext(req.Context(), identity))
}
assert.Equal(t, s3err.ErrAccessDenied,
s3a.checkVersionReadGrant(attrsReq("deny-bucket"), "deny-bucket", "key", "v1", nil))
assert.Equal(t, s3err.ErrNone,
s3a.checkVersionReadGrant(attrsReq("allow-bucket"), "allow-bucket", "key", "v1", nil))
assert.Equal(t, s3err.ErrNone,
s3a.checkVersionReadGrant(attrsReq("no-policy-bucket"), "no-policy-bucket", "key", "v1", nil))
}
// Presigned-URL credentials ride along on the synthesized version-read
// request, so an s3:authType=REST-QUERY-STRING policy condition decides the
// recheck the same way it decided the real request.
func TestCheckVersionReadGrantPreservesQueryAuth(t *testing.T) {
s3a := &S3ApiServer{
policyEngine: NewBucketPolicyEngine(),
iam: &IdentityAccessManagement{},
}
policyJSON := `{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::qs-bucket", "arn:aws:s3:::qs-bucket/*"],
"Condition": {"StringEquals": {"s3:authType": "REST-QUERY-STRING"}}
}]
}`
require.NoError(t, s3a.policyEngine.engine.SetBucketPolicy("qs-bucket", policyJSON))
presigned := httptest.NewRequest(http.MethodGet,
"/qs-bucket/key?attributes&versionId=v1&AWSAccessKeyId=AK&Signature=sig&Expires=9999999999", nil)
assert.Equal(t, s3err.ErrNone,
s3a.checkVersionReadGrant(presigned, "qs-bucket", "key", "v1", nil))
keyReq := objectKeyPolicyRequest(presigned, http.MethodGet, "qs-bucket", "key", "v1")
q := keyReq.URL.Query()
assert.Equal(t, "AK", q.Get("AWSAccessKeyId"))
assert.Equal(t, "v1", q.Get("versionId"))
assert.False(t, q.Has("attributes"))
}
+18 -2
View File
@@ -30,6 +30,10 @@ func (s3a *S3ApiServer) GetObjectAclHandler(w http.ResponseWriter, r *http.Reque
// Check for specific version ID in query parameters
versionId := r.URL.Query().Get("versionId")
if errCode := s3a.checkVersionIdConfigured(bucket, versionId); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
// Check if versioning is configured for the bucket (Enabled or Suspended)
versioningConfigured, err := s3a.isVersioningConfigured(bucket)
@@ -89,7 +93,11 @@ func (s3a *S3ApiServer) GetObjectAclHandler(w http.ResponseWriter, r *http.Reque
return
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, s3_constants.S3_ACTION_GET_OBJECT_ACL, entry.Extended, "GetObjectAclHandler"); errCode != s3err.ErrNone {
action := s3_constants.S3_ACTION_GET_OBJECT_ACL
if versionId != "" {
action = s3_constants.S3_ACTION_GET_OBJECT_VERSION_ACL
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, action, entry.Extended, "GetObjectAclHandler"); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
@@ -142,6 +150,10 @@ func (s3a *S3ApiServer) PutObjectAclHandler(w http.ResponseWriter, r *http.Reque
// Check for specific version ID in query parameters
versionId := r.URL.Query().Get("versionId")
if errCode := s3a.checkVersionIdConfigured(bucket, versionId); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
// Check if versioning is configured for the bucket (Enabled or Suspended)
versioningConfigured, err := s3a.isVersioningConfigured(bucket)
@@ -201,7 +213,11 @@ func (s3a *S3ApiServer) PutObjectAclHandler(w http.ResponseWriter, r *http.Reque
return
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, s3_constants.S3_ACTION_PUT_OBJECT_ACL, entry.Extended, "PutObjectAclHandler"); errCode != s3err.ErrNone {
putAction := s3_constants.S3_ACTION_PUT_OBJECT_ACL
if versionId != "" {
putAction = s3_constants.S3_ACTION_PUT_OBJECT_VERSION_ACL
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, putAction, entry.Extended, "PutObjectAclHandler"); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
+18 -1
View File
@@ -129,6 +129,10 @@ func (s3a *S3ApiServer) GetObjectAttributesHandler(w http.ResponseWriter, r *htt
// Check for specific version ID
versionId := r.URL.Query().Get("versionId")
if errCode := s3a.checkVersionIdConfigured(bucket, versionId); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
var (
entry *filer_pb.Entry
@@ -219,11 +223,24 @@ func (s3a *S3ApiServer) GetObjectAttributesHandler(w http.ResponseWriter, r *htt
}
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, s3_constants.S3_ACTION_GET_OBJECT_ATTRIBUTES, entry.Extended, "GetObjectAttributesHandler"); errCode != s3err.ErrNone {
action := s3_constants.S3_ACTION_GET_OBJECT_ATTRIBUTES
if versionId != "" {
action = s3_constants.S3_ACTION_GET_OBJECT_VERSION_ATTRIBUTES
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, action, entry.Extended, "GetObjectAttributesHandler"); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
// AWS also asks for the version read grant on a versioned attributes
// request: s3:GetObjectVersionAttributes alone is not sufficient.
if versionId != "" {
if errCode := s3a.checkVersionReadGrant(r, bucket, object, versionId, entry.Extended); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
}
// Evaluate conditional headers against the resolved entry (after version resolution)
// This ensures conditions are checked against the correct version, not always the latest
if s3a.hasConditionalHeaders(r) {
+37 -5
View File
@@ -29,6 +29,10 @@ func (s3a *S3ApiServer) GetObjectTaggingHandler(w http.ResponseWriter, r *http.R
// Check for specific version ID in query parameters
versionId := r.URL.Query().Get("versionId")
if errCode := s3a.checkVersionIdConfigured(bucket, versionId); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
// Check if versioning is configured for the bucket (Enabled or Suspended)
versioningConfigured, err := s3a.isVersioningConfigured(bucket)
@@ -87,7 +91,11 @@ func (s3a *S3ApiServer) GetObjectTaggingHandler(w http.ResponseWriter, r *http.R
}
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, s3_constants.S3_ACTION_GET_OBJECT_TAGGING, entry.Extended, "GetObjectTaggingHandler"); errCode != s3err.ErrNone {
action := s3_constants.S3_ACTION_GET_OBJECT_TAGGING
if versionId != "" {
action = s3_constants.S3_ACTION_GET_OBJECT_VERSION_TAGGING
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, action, entry.Extended, "GetObjectTaggingHandler"); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
@@ -140,6 +148,10 @@ func (s3a *S3ApiServer) PutObjectTaggingHandler(w http.ResponseWriter, r *http.R
// Check for specific version ID in query parameters
versionId := r.URL.Query().Get("versionId")
if errCode := s3a.checkVersionIdConfigured(bucket, versionId); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
// Check if versioning is configured for the bucket (Enabled or Suspended)
versioningConfigured, err := s3a.isVersioningConfigured(bucket)
@@ -185,7 +197,11 @@ func (s3a *S3ApiServer) PutObjectTaggingHandler(w http.ResponseWriter, r *http.R
target := util.FullPath(fmt.Sprintf("%s/%s", s3a.bucketDir(bucket), object))
dir, name := target.DirAndName()
if errCode := s3a.recheckTagConditionedPolicyWithLookup(r, bucket, object, versionId, s3_constants.S3_ACTION_PUT_OBJECT_TAGGING, "PutObjectTaggingHandler"); errCode != s3err.ErrNone {
putAction := s3_constants.S3_ACTION_PUT_OBJECT_TAGGING
if versionId != "" {
putAction = s3_constants.S3_ACTION_PUT_OBJECT_VERSION_TAGGING
}
if errCode := s3a.recheckTagConditionedPolicyWithLookup(r, bucket, object, versionId, putAction, "PutObjectTaggingHandler"); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
@@ -206,7 +222,11 @@ func (s3a *S3ApiServer) PutObjectTaggingHandler(w http.ResponseWriter, r *http.R
return
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, s3_constants.S3_ACTION_PUT_OBJECT_TAGGING, entry.Extended, "PutObjectTaggingHandler"); errCode != s3err.ErrNone {
putAction := s3_constants.S3_ACTION_PUT_OBJECT_TAGGING
if versionId != "" {
putAction = s3_constants.S3_ACTION_PUT_OBJECT_VERSION_TAGGING
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, putAction, entry.Extended, "PutObjectTaggingHandler"); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
@@ -268,6 +288,10 @@ func (s3a *S3ApiServer) DeleteObjectTaggingHandler(w http.ResponseWriter, r *htt
// Check for specific version ID in query parameters
versionId := r.URL.Query().Get("versionId")
if errCode := s3a.checkVersionIdConfigured(bucket, versionId); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
// Check if versioning is configured for the bucket (Enabled or Suspended)
versioningConfigured, err := s3a.isVersioningConfigured(bucket)
@@ -313,7 +337,11 @@ func (s3a *S3ApiServer) DeleteObjectTaggingHandler(w http.ResponseWriter, r *htt
target := util.FullPath(fmt.Sprintf("%s/%s", s3a.bucketDir(bucket), object))
dir, name := target.DirAndName()
if errCode := s3a.recheckTagConditionedPolicyWithLookup(r, bucket, object, versionId, s3_constants.S3_ACTION_DELETE_OBJECT_TAGGING, "DeleteObjectTaggingHandler"); errCode != s3err.ErrNone {
deleteAction := s3_constants.S3_ACTION_DELETE_OBJECT_TAGGING
if versionId != "" {
deleteAction = s3_constants.S3_ACTION_DELETE_OBJECT_VERSION_TAGGING
}
if errCode := s3a.recheckTagConditionedPolicyWithLookup(r, bucket, object, versionId, deleteAction, "DeleteObjectTaggingHandler"); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
@@ -335,7 +363,11 @@ func (s3a *S3ApiServer) DeleteObjectTaggingHandler(w http.ResponseWriter, r *htt
return
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, s3_constants.S3_ACTION_DELETE_OBJECT_TAGGING, entry.Extended, "DeleteObjectTaggingHandler"); errCode != s3err.ErrNone {
deleteAction := s3_constants.S3_ACTION_DELETE_OBJECT_TAGGING
if versionId != "" {
deleteAction = s3_constants.S3_ACTION_DELETE_OBJECT_VERSION_TAGGING
}
if errCode := s3a.recheckPolicyWithObjectEntry(r, bucket, object, deleteAction, entry.Extended, "DeleteObjectTaggingHandler"); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
+15
View File
@@ -678,6 +678,21 @@ func (s3a *S3ApiServer) recheckPolicyWithObjectEntry(r *http.Request, bucket, ob
return errCode
}
// checkVersionReadGrant enforces the version read grant AWS requires alongside
// a version-scoped action such as s3:GetObjectVersionAttributes. The check runs
// on a request shaped like a plain versioned GET so the s3:GetObjectVersion
// action resolves from the request instead of the caller's subresource, and a
// legacy Read grant still applies.
func (s3a *S3ApiServer) checkVersionReadGrant(r *http.Request, bucket, object, versionId string, objectEntry map[string][]byte) s3err.ErrorCode {
versionReadReq := objectKeyPolicyRequest(r, http.MethodGet, bucket, object, versionId)
if identity, ok := GetIdentityFromContext(r).(*Identity); ok && identity != nil {
if errCode := s3a.iam.VerifyActionPermission(versionReadReq, identity, ACTION_READ, bucket, object); errCode != s3err.ErrNone {
return errCode
}
}
return s3a.recheckPolicyWithObjectEntry(versionReadReq, bucket, object, ACTION_READ, objectEntry, "checkVersionReadGrant")
}
// recheckCopySourcePolicy repeats the source object's policy check once its
// entry is fetched, so conditions on the object's existing tags apply to a
// CopyObject / UploadPartCopy / RenameObject source the same way they do to a