Merge pull request #2138 from versity/ben/implicit-dir-objs

fix: ignore implicit directories for  Get/HeadObject
This commit is contained in:
Ben McClelland
2026-05-13 11:47:15 -07:00
committed by GitHub
4 changed files with 86 additions and 0 deletions
+26
View File
@@ -4496,6 +4496,19 @@ func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.Ge
if !strings.HasSuffix(object, "/") && fid.IsDir() {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if fid.IsDir() {
// Only directories explicitly created via S3 (put-object with key ending
// in '/') have an etag attribute. Directories created incidentally on the
// filesystem or as parent directories during object upload should not be
// accessible via get-object.
_, derr := p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
if errors.Is(derr, meta.ErrNoSuchKey) || errors.Is(derr, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if derr != nil {
return nil, fmt.Errorf("get dir etag: %w", derr)
}
}
if p.versioningEnabled() {
isDelMarker, err := p.isObjDeleteMarker(bucket, object)
@@ -4821,6 +4834,19 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3.
if !strings.HasSuffix(object, "/") && fi.IsDir() {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if fi.IsDir() {
// Only directories explicitly created via S3 (put-object with key ending
// in '/') have an etag attribute. Directories created incidentally on the
// filesystem or as parent directories during object upload should not be
// accessible via head-object.
_, derr := p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
if errors.Is(derr, meta.ErrNoSuchKey) || errors.Is(derr, fs.ErrNotExist) {
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
}
if derr != nil {
return nil, fmt.Errorf("get dir etag: %w", derr)
}
}
if p.versioningEnabled() {
isDelMarker, err := p.isObjDeleteMarker(bucket, object)
+28
View File
@@ -829,6 +829,34 @@ func GetObject_not_enabled_checksum_mode(s *S3Conf) error {
})
}
// GetObject_incidental_dir_object verifies that a directory created incidentally
// as a parent during object upload (i.e. never explicitly PUT via S3 with a
// trailing-slash key) is not accessible via GetObject.
func GetObject_incidental_dir_object(s *S3Conf) error {
testName := "GetObject_incidental_dir_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
// Upload an object under a prefix; this creates the parent directory
// incidentally on posix but the directory was never PUT as an S3 object.
obj := "my-dir/my-obj"
_, err := putObjectWithData(int64(64), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
dir := "my-dir/"
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &dir,
})
cancel()
return checkSdkApiErr(err, "NoSuchKey")
})
}
func GetObject_non_existing_dir_object(s *S3Conf) error {
testName := "GetObject_non_existing_dir_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+28
View File
@@ -62,6 +62,34 @@ func HeadObject_invalid_part_number(s *S3Conf) error {
})
}
// HeadObject_incidental_dir_object verifies that a directory created incidentally
// as a parent during object upload (i.e. never explicitly PUT via S3 with a
// trailing-slash key) is not accessible via HeadObject.
func HeadObject_incidental_dir_object(s *S3Conf) error {
testName := "HeadObject_incidental_dir_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
// Upload an object under a prefix; this creates the parent directory
// incidentally on posix but the directory was never PUT as an S3 object.
obj := "my-dir/my-obj"
_, err := putObjectWithData(int64(64), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
dir := "my-dir/"
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &dir,
})
cancel()
return checkSdkApiErr(err, "NotFound")
})
}
func HeadObject_non_existing_dir_object(s *S3Conf) error {
testName := "HeadObject_non_existing_dir_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+4
View File
@@ -205,6 +205,7 @@ func TestHeadObject(ts *TestState) {
ts.Run(HeadObject_invalid_part_number)
ts.Run(HeadObject_directory_object_noslash)
ts.Run(HeadObject_non_existing_dir_object)
ts.Run(HeadObject_incidental_dir_object)
ts.Run(HeadObject_invalid_parent_dir)
ts.Run(HeadObject_with_range)
ts.Run(HeadObject_by_range_resp_status)
@@ -263,6 +264,7 @@ func TestGetObject(ts *TestState) {
ts.Run(GetObject_directory_success)
ts.Run(GetObject_by_range_resp_status)
ts.Run(GetObject_non_existing_dir_object)
ts.Run(GetObject_incidental_dir_object)
ts.Run(GetObject_overrides_success)
ts.Run(GetObject_overrides_presign_success)
ts.Run(GetObject_overrides_fail_public)
@@ -1412,6 +1414,7 @@ func GetIntTests() IntTests {
"HeadObject_invalid_part_number": HeadObject_invalid_part_number,
"HeadObject_directory_object_noslash": HeadObject_directory_object_noslash,
"HeadObject_non_existing_dir_object": HeadObject_non_existing_dir_object,
"HeadObject_incidental_dir_object": HeadObject_incidental_dir_object,
"HeadObject_name_too_long": HeadObject_name_too_long,
"HeadObject_invalid_parent_dir": HeadObject_invalid_parent_dir,
"HeadObject_with_range": HeadObject_with_range,
@@ -1456,6 +1459,7 @@ func GetIntTests() IntTests {
"GetObject_directory_success": GetObject_directory_success,
"GetObject_by_range_resp_status": GetObject_by_range_resp_status,
"GetObject_non_existing_dir_object": GetObject_non_existing_dir_object,
"GetObject_incidental_dir_object": GetObject_incidental_dir_object,
"GetObject_overrides_success": GetObject_overrides_success,
"GetObject_overrides_presign_success": GetObject_overrides_presign_success,
"GetObject_overrides_fail_public": GetObject_overrides_fail_public,