mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 04:06:44 +00:00
test: pin that an unusable version id is refused, never resolved (#10513)
* test: pin that an unusable version id is refused, never resolved A version id containing a path separator, or "." / "..", can never name a stored version. Resolving one to the null or latest version instead would let a caller destroy a live version by asking for one that does not exist, on a bucket configured for immutability. The guard exists today and holds on every verb; it had no test. Pins two properties: such a request is refused with a client error rather than a 5xx (a 5xx invites endless retries of something that can never succeed), and the version that does exist survives every refused request. * test: require exactly 400 for an unusable version id
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
smithyhttp "github.com/aws/smithy-go/transport/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Version ids that can never name a stored version. A request carrying one has
|
||||
// to be refused: resolving it to the null or latest version instead would let a
|
||||
// caller destroy a live version by asking for one that does not exist.
|
||||
var unusableVersionIDs = []string{"a/b", `a\b`, "..", "."}
|
||||
|
||||
func statusOf(t *testing.T, err error) int {
|
||||
t.Helper()
|
||||
require.Error(t, err)
|
||||
var respErr *smithyhttp.ResponseError
|
||||
require.True(t, errors.As(err, &respErr), "expected an HTTP response error, got %v", err)
|
||||
return respErr.HTTPStatusCode()
|
||||
}
|
||||
|
||||
func TestUnusableVersionIDIsRefusedNotResolved(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := getNewBucketName()
|
||||
|
||||
createBucketWithObjectLock(t, client, bucketName)
|
||||
defer deleteBucket(t, client, bucketName)
|
||||
|
||||
objectKey := "arbitration.lock"
|
||||
put, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: bytes.NewReader([]byte("held")),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, put.VersionId)
|
||||
|
||||
for _, versionID := range unusableVersionIDs {
|
||||
t.Run("delete "+versionID, func(t *testing.T) {
|
||||
_, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionID),
|
||||
})
|
||||
// 400, not merely "some 4xx": the id is malformed, which is a bad
|
||||
// argument rather than a missing resource. A 5xx would be the real
|
||||
// damage — it invites endless retries of a request that can never
|
||||
// succeed — but pinning the exact code keeps the contract honest.
|
||||
assert.Equal(t, 400, statusOf(t, err))
|
||||
})
|
||||
}
|
||||
|
||||
// The point of the whole test: none of those requests may have been resolved
|
||||
// to the version that does exist.
|
||||
head, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: put.VersionId,
|
||||
})
|
||||
require.NoError(t, err, "the stored version must survive every refused request")
|
||||
require.NotNil(t, head.VersionId)
|
||||
assert.Equal(t, *put.VersionId, *head.VersionId)
|
||||
|
||||
get, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
require.NoError(t, err, "the latest version must still resolve")
|
||||
defer get.Body.Close()
|
||||
}
|
||||
|
||||
// Read paths must agree with the write path: the same id is unusable everywhere,
|
||||
// so a caller cannot be told the object is missing by one verb and served by another.
|
||||
func TestUnusableVersionIDRefusedOnReadPaths(t *testing.T) {
|
||||
client := getS3Client(t)
|
||||
bucketName := getNewBucketName()
|
||||
|
||||
createBucketWithObjectLock(t, client, bucketName)
|
||||
defer deleteBucket(t, client, bucketName)
|
||||
|
||||
objectKey := "arbitration.lock"
|
||||
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: bytes.NewReader([]byte("held")),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, versionID := range unusableVersionIDs {
|
||||
t.Run("get "+versionID, func(t *testing.T) {
|
||||
_, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionID),
|
||||
})
|
||||
assert.Equal(t, 400, statusOf(t, err))
|
||||
})
|
||||
t.Run("head "+versionID, func(t *testing.T) {
|
||||
_, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
VersionId: aws.String(versionID),
|
||||
})
|
||||
assert.Equal(t, 400, statusOf(t, err))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user