Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion env/default/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ features:
enabled: true

cloudserver:
image: ghcr.io/scality/cloudserver:9.2.32
image: ghcr.io/scality/cloudserver:9.2.34

vault:
image: ghcr.io/scality/vault:7.84.0
Expand Down
153 changes: 153 additions & 0 deletions test/e2e/acl_required_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package e2e_test

import (
"bytes"
"context"
"fmt"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("aclRequired field in access logs", func() {
var testCtx *E2ETestContext

BeforeEach(func() {
testCtx = setupE2ETest()
})

AfterEach(func() {
cleanupE2ETest(testCtx)
})

It("logs aclRequired as dash for bucket owner requests", func(ctx context.Context) {
testKey := "acl-owner-test.txt"
testContent := []byte("owner request test data")

_, err := testCtx.S3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(testCtx.SourceBucket),
Key: aws.String(testKey),
Body: bytes.NewReader(testContent),
})
Expect(err).NotTo(HaveOccurred(), "PUT should succeed")

resp, err := testCtx.S3Client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(testCtx.SourceBucket),
Key: aws.String(testKey),
})
Expect(err).NotTo(HaveOccurred(), "GET should succeed")
Comment thread
dvasilas marked this conversation as resolved.
_ = resp.Body.Close()

testCtx.VerifyLogs(
testCtx.ObjectOp("REST.PUT.OBJECT", testKey, 200).
WithObjectSize(int64(len(testContent))).
WithACLRequired("-"),
testCtx.ObjectOp("REST.GET.OBJECT", testKey, 200).
WithBytesSent(int64(len(testContent))).
WithObjectSize(int64(len(testContent))).
WithACLRequired("-"),
)
})

It("logs aclRequired as Yes for IAM user authorized by ACL", func(ctx context.Context) {
testKey := "acl-iam-test.txt"
testContent := []byte("IAM user ACL test data")

// PUT object as bucket owner
_, err := testCtx.S3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(testCtx.SourceBucket),
Key: aws.String(testKey),
Body: bytes.NewReader(testContent),
})
Expect(err).NotTo(HaveOccurred(), "PUT should succeed")

// Create IAM user with s3:GetObject permission
userName := fmt.Sprintf("e2e-acl-test-%d", time.Now().UnixNano())
policy := fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::%s/*"
}]
}`, testCtx.SourceBucket)
iamUser := createIAMUser(ctx, userName, "allow-get-object", policy)
defer iamUser.Cleanup()

// GET object as the IAM user — authorized via ACL (same account)
iamS3Client := iamUser.S3Client

resp, err := iamS3Client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(testCtx.SourceBucket),
Key: aws.String(testKey),
})
Expect(err).NotTo(HaveOccurred(), "GET as IAM user should succeed")
Comment thread
dvasilas marked this conversation as resolved.
_ = resp.Body.Close()

logs := testCtx.VerifyLogs(
testCtx.ObjectOp("REST.PUT.OBJECT", testKey, 200).
WithObjectSize(int64(len(testContent))).
WithACLRequired("-"),
testCtx.ObjectOp("REST.GET.OBJECT", testKey, 200).
WithBytesSent(int64(len(testContent))).
WithObjectSize(int64(len(testContent))).
WithACLRequired("Yes"),
)

// Additional verification: the two requests have different requesters
Expect(logs[0].Requester).NotTo(Equal(logs[1].Requester),
"PUT (owner) and GET (IAM user) should have different requesters")
})

It("logs aclRequired as Yes for both sides of a copy by IAM user", func(ctx context.Context) {
sourceKey := "acl-copy-source.txt"
destKey := "acl-copy-dest.txt"
testContent := []byte("copy test data")

// PUT object as bucket owner
_, err := testCtx.S3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(testCtx.SourceBucket),
Key: aws.String(sourceKey),
Body: bytes.NewReader(testContent),
})
Expect(err).NotTo(HaveOccurred(), "PUT should succeed")

// Create IAM user with GetObject + PutObject permissions for the copy
userName := fmt.Sprintf("e2e-acl-copy-%d", time.Now().UnixNano())
policy := fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::%s/*"
}]
}`, testCtx.SourceBucket)
iamUser := createIAMUser(ctx, userName, "allow-copy", policy)
defer iamUser.Cleanup()

// Copy as IAM user — both source GET and destination PUT go through ACL path
iamS3Client := iamUser.S3Client

_, err = iamS3Client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(testCtx.SourceBucket),
Key: aws.String(destKey),
CopySource: aws.String(fmt.Sprintf("%s/%s", testCtx.SourceBucket, sourceKey)),
})
Expect(err).NotTo(HaveOccurred(), "COPY as IAM user should succeed")

testCtx.VerifyLogs(
testCtx.ObjectOp("REST.PUT.OBJECT", sourceKey, 200).
WithObjectSize(int64(len(testContent))).
WithACLRequired("-"),
testCtx.ObjectOp("REST.COPY.OBJECT_GET", sourceKey, 200).
WithObjectSize(int64(len(testContent))).
WithACLRequired("Yes"),
testCtx.ObjectOp("REST.COPY.OBJECT", destKey, 200).
WithObjectSize(int64(len(testContent))).
WithACLRequired("Yes"),
)
})
})
52 changes: 3 additions & 49 deletions test/e2e/error_cases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/s3"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -183,56 +182,11 @@ var _ = Describe("Error Cases", func() {
time.Sleep(1 * time.Second)

// Create IAM user with no permissions
iamEndpoint := os.Getenv("E2E_IAM_ENDPOINT")
if iamEndpoint == "" {
iamEndpoint = testIAMEndpoint
}
accessKey := os.Getenv("E2E_S3_ACCESS_KEY_ID")
if accessKey == "" {
accessKey = testAccessKeyID
}
secretKey := os.Getenv("E2E_S3_SECRET_ACCESS_KEY")
if secretKey == "" {
secretKey = testSecretAccessKey
}

iamClient := iam.NewFromConfig(aws.Config{
Region: testRegion,
Credentials: aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) {
return aws.Credentials{
AccessKeyID: accessKey,
SecretAccessKey: secretKey,
}, nil
}),
}, func(o *iam.Options) {
o.BaseEndpoint = aws.String(iamEndpoint)
})

userName := fmt.Sprintf("e2e-test-user-%d", time.Now().UnixNano())
_, err = iamClient.CreateUser(ctx, &iam.CreateUserInput{
UserName: aws.String(userName),
})
Expect(err).NotTo(HaveOccurred(), "CreateUser should succeed")
iamUser := createIAMUser(ctx, userName, "", "")
defer iamUser.Cleanup()

createKeyResp, err := iamClient.CreateAccessKey(ctx, &iam.CreateAccessKeyInput{
UserName: aws.String(userName),
})
Expect(err).NotTo(HaveOccurred(), "CreateAccessKey should succeed")

defer func() {
_, _ = iamClient.DeleteAccessKey(ctx, &iam.DeleteAccessKeyInput{
UserName: aws.String(userName),
AccessKeyId: createKeyResp.AccessKey.AccessKeyId,
})
_, _ = iamClient.DeleteUser(ctx, &iam.DeleteUserInput{
UserName: aws.String(userName),
})
}()

unprivilegedClient := newS3ClientWithCredentials(
*createKeyResp.AccessKey.AccessKeyId,
*createKeyResp.AccessKey.SecretAccessKey,
)
unprivilegedClient := iamUser.S3Client

_, err = unprivilegedClient.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(testCtx.SourceBucket),
Expand Down
110 changes: 100 additions & 10 deletions test/e2e/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -64,7 +65,7 @@ type ParsedLogRecord struct {
HostHeader string
TLSVersion string
AccessPointARN string // Field 25 - always "-" (not supported)
ACLRequired string // Field 26 - always "-" (not supported)
ACLRequired string
BytesSent int64
ObjectSize int64
HTTPStatus int
Expand All @@ -74,13 +75,14 @@ type ParsedLogRecord struct {

// ExpectedLog defines expected log record fields for verification
type ExpectedLog struct {
Operation string
Bucket string
Key string // Optional, use "" to skip check
ErrorCode string // Optional, use "" to skip check
HTTPStatus int
BytesSent int64 // Optional, use -1 to skip check
ObjectSize int64 // Optional, use -1 to skip check
Operation string
Bucket string
Key string // Optional, use "" to skip check
ErrorCode string // Optional, use "" to skip check
ACLRequired string // Optional, use "" to skip check
BytesSent int64 // Optional, use -1 to skip check
ObjectSize int64 // Optional, use -1 to skip check
HTTPStatus int
}

// ExpectedLogBuilder allows fluent construction of ExpectedLog
Expand All @@ -106,6 +108,12 @@ func (b ExpectedLogBuilder) WithErrorCode(code string) ExpectedLogBuilder {
return b
}

// WithACLRequired sets the expected ACLRequired value
func (b ExpectedLogBuilder) WithACLRequired(value string) ExpectedLogBuilder {
b.log.ACLRequired = value
return b
}

// BucketOp creates an ExpectedLog for bucket-level operations (no key)
func (ctx *E2ETestContext) BucketOp(operation string, httpStatus int) ExpectedLogBuilder {
return ExpectedLogBuilder{
Expand Down Expand Up @@ -510,8 +518,8 @@ func verifyLogRecord(actual *ParsedLogRecord, expected ExpectedLogBuilder) {
"HostID should always be '-' (not supported)")
Expect(actual.AccessPointARN).To(Equal("-"),
"AccessPointARN should always be '-' (not supported)")
Expect(actual.ACLRequired).To(Equal("-"),
"ACLRequired should always be '-' (not supported)")
Expect(actual.ACLRequired).To(BeElementOf("-", "Yes"),
"ACLRequired should be '-' or 'Yes'")

// Verify timing fields relationship
Expect(actual.TotalTime).To(BeNumerically(">=", 0),
Expand All @@ -530,6 +538,10 @@ func verifyLogRecord(actual *ParsedLogRecord, expected ExpectedLogBuilder) {
Expect(actual.ObjectSize).To(Equal(exp.ObjectSize),
"ObjectSize mismatch")
}
if exp.ACLRequired != "" {
Expect(actual.ACLRequired).To(Equal(exp.ACLRequired),
"ACLRequired mismatch")
}
}

// verifyLogKeys verifies that logs contain exactly the expected keys with no duplicates.
Expand Down Expand Up @@ -647,6 +659,84 @@ func newS3ClientWithCredentials(accessKeyID, secretAccessKey string) *s3.Client
})
}

type IAMUserResult struct {
S3Client *s3.Client
Cleanup func()
}

// createIAMUser creates an IAM user with an optional inline policy.
// If policyName and policyDocument are non-empty, the policy is attached.
func createIAMUser(ctx context.Context, userName, policyName, policyDocument string) IAMUserResult {
GinkgoHelper()

iamEndpoint := os.Getenv("E2E_IAM_ENDPOINT")
if iamEndpoint == "" {
iamEndpoint = testIAMEndpoint
}
accessKey := os.Getenv("E2E_S3_ACCESS_KEY_ID")
if accessKey == "" {
accessKey = testAccessKeyID
}
secretKey := os.Getenv("E2E_S3_SECRET_ACCESS_KEY")
if secretKey == "" {
secretKey = testSecretAccessKey
}

iamClient := iam.NewFromConfig(aws.Config{
Region: testRegion,
Credentials: aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) {
return aws.Credentials{
AccessKeyID: accessKey,
SecretAccessKey: secretKey,
}, nil
}),
}, func(o *iam.Options) {
o.BaseEndpoint = aws.String(iamEndpoint)
})

_, err := iamClient.CreateUser(ctx, &iam.CreateUserInput{
UserName: aws.String(userName),
})
Expect(err).NotTo(HaveOccurred(), "CreateUser should succeed")

createKeyResp, err := iamClient.CreateAccessKey(ctx, &iam.CreateAccessKeyInput{
UserName: aws.String(userName),
})
Expect(err).NotTo(HaveOccurred(), "CreateAccessKey should succeed")

if policyName != "" && policyDocument != "" {
_, err = iamClient.PutUserPolicy(ctx, &iam.PutUserPolicyInput{
UserName: aws.String(userName),
PolicyName: aws.String(policyName),
PolicyDocument: aws.String(policyDocument),
})
Expect(err).NotTo(HaveOccurred(), "PutUserPolicy should succeed")
}

cleanup := func() {
if policyName != "" {
_, _ = iamClient.DeleteUserPolicy(ctx, &iam.DeleteUserPolicyInput{
UserName: aws.String(userName),
PolicyName: aws.String(policyName),
})
}
_, _ = iamClient.DeleteAccessKey(ctx, &iam.DeleteAccessKeyInput{
UserName: aws.String(userName),
AccessKeyId: createKeyResp.AccessKey.AccessKeyId,
})
_, _ = iamClient.DeleteUser(ctx, &iam.DeleteUserInput{
UserName: aws.String(userName),
})
}

s3Client := newS3ClientWithCredentials(
*createKeyResp.AccessKey.AccessKeyId,
*createKeyResp.AccessKey.SecretAccessKey,
)

return IAMUserResult{S3Client: s3Client, Cleanup: cleanup}
}

// setupE2ETest creates and initializes an E2E test context
func setupE2ETest() *E2ETestContext {
GinkgoHelper()
Expand Down
Loading
Loading