snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
package s3_constants
import (
"github.com/aws/aws-sdk-go/service/s3"
)
const (
CannedAclPrivate = "private"
CannedAclPublicRead = "public-read"
CannedAclPublicReadWrite = "public-read-write"
CannedAclAuthenticatedRead = "authenticated-read"
CannedAclLogDeliveryWrite = "log-delivery-write"
CannedAclBucketOwnerRead = "bucket-owner-read"
CannedAclBucketOwnerFullControl = "bucket-owner-full-control"
CannedAclAwsExecRead = "aws-exec-read"
)
var (
PublicRead = []*s3.Grant{
{
Grantee: &s3.Grantee{
Type: &GrantTypeGroup,
URI: &GranteeGroupAllUsers,
},
Permission: &PermissionRead,
},
}
PublicReadWrite = []*s3.Grant{
{
Grantee: &s3.Grantee{
Type: &GrantTypeGroup,
URI: &GranteeGroupAllUsers,
},
Permission: &PermissionRead,
},
{
Grantee: &s3.Grantee{
Type: &GrantTypeGroup,
URI: &GranteeGroupAllUsers,
},
Permission: &PermissionWrite,
},
}
AuthenticatedRead = []*s3.Grant{
{
Grantee: &s3.Grantee{
Type: &GrantTypeGroup,
URI: &GranteeGroupAuthenticatedUsers,
},
Permission: &PermissionRead,
},
}
LogDeliveryWrite = []*s3.Grant{
{
Grantee: &s3.Grantee{
Type: &GrantTypeGroup,
URI: &GranteeGroupLogDelivery,
},
Permission: &PermissionWrite,
},
}
)

View File

@@ -0,0 +1,20 @@
package s3_constants
// Amazon S3 predefined groups
var (
GranteeGroupAllUsers = "http://acs.amazonaws.com/groups/global/AllUsers"
GranteeGroupAuthenticatedUsers = "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"
GranteeGroupLogDelivery = "http://acs.amazonaws.com/groups/s3/LogDelivery"
)
func ValidateGroup(group string) bool {
valid := true
switch group {
case GranteeGroupAllUsers:
case GranteeGroupLogDelivery:
case GranteeGroupAuthenticatedUsers:
default:
valid = false
}
return valid
}

View File

@@ -0,0 +1,7 @@
package s3_constants
var (
GrantTypeCanonicalUser = "CanonicalUser"
GrantTypeAmazonCustomerByEmail = "AmazonCustomerByEmail"
GrantTypeGroup = "Group"
)

View File

@@ -0,0 +1,27 @@
package s3_constants
var (
OwnershipBucketOwnerPreferred = "BucketOwnerPreferred"
OwnershipObjectWriter = "ObjectWriter"
OwnershipBucketOwnerEnforced = "BucketOwnerEnforced"
DefaultOwnershipForCreate = OwnershipObjectWriter
DefaultOwnershipForExists = OwnershipBucketOwnerEnforced
)
func ValidateOwnership(ownership string) bool {
if ownership == "" || (ownership != OwnershipBucketOwnerPreferred && ownership != OwnershipObjectWriter && ownership != OwnershipBucketOwnerEnforced) {
return false
} else {
return true
}
}
// EffectiveOwnership resolves a stored ownership setting to the one that governs
// the bucket: absent or invalid behaves as BucketOwnerEnforced.
func EffectiveOwnership(ownership string) string {
if !ValidateOwnership(ownership) {
return DefaultOwnershipForExists
}
return ownership
}

View File

@@ -0,0 +1,24 @@
package s3_constants
import "testing"
func TestEffectiveOwnership(t *testing.T) {
cases := []struct {
name string
input string
want string
}{
{name: "unset", input: "", want: OwnershipBucketOwnerEnforced},
{name: "invalid", input: "Bogus", want: OwnershipBucketOwnerEnforced},
{name: "object writer", input: OwnershipObjectWriter, want: OwnershipObjectWriter},
{name: "bucket owner preferred", input: OwnershipBucketOwnerPreferred, want: OwnershipBucketOwnerPreferred},
{name: "bucket owner enforced", input: OwnershipBucketOwnerEnforced, want: OwnershipBucketOwnerEnforced},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := EffectiveOwnership(tc.input); got != tc.want {
t.Fatalf("EffectiveOwnership(%q) = %q, want %q", tc.input, got, tc.want)
}
})
}
}

View File

@@ -0,0 +1,9 @@
package s3_constants
var (
PermissionFullControl = "FULL_CONTROL"
PermissionRead = "READ"
PermissionWrite = "WRITE"
PermissionReadAcp = "READ_ACP"
PermissionWriteAcp = "WRITE_ACP"
)

View File

@@ -0,0 +1,11 @@
package s3_constants
const (
// DefaultBucketsPath is the default path for S3 buckets in the filer
DefaultBucketsPath = "/buckets"
// ObjectWriteRouteKeyPrefix namespaces a full path into the ring key used to
// resolve and forward writes to it. Every writer of an object or bucket
// entry hashes this same key, so they serialize on one filer's path lock.
ObjectWriteRouteKeyPrefix = "s3.object.write:"
)

View File

@@ -0,0 +1,32 @@
package s3_constants
// Cryptographic constants
const (
// AES block and key sizes
AESBlockSize = 16 // 128 bits for AES block size (IV length)
AESKeySize = 32 // 256 bits for AES-256 keys
// SSE algorithm identifiers
SSEAlgorithmAES256 = "AES256"
SSEAlgorithmKMS = "aws:kms"
// SSE type identifiers for response headers and internal processing
SSETypeC = "SSE-C"
SSETypeKMS = "SSE-KMS"
SSETypeS3 = "SSE-S3"
// S3 multipart upload limits and offsets
S3MaxPartSize = 5 * 1024 * 1024 * 1024 // 5GB - AWS S3 maximum part size limit
// Multipart offset calculation for unique IV generation
// Using 8GB offset between parts (larger than max part size) to prevent IV collisions
// Critical for CTR mode encryption security in multipart uploads
PartOffsetMultiplier = int64(1) << 33 // 8GB per part offset
// KMS validation limits based on AWS KMS service constraints
MaxKMSEncryptionContextPairs = 10 // Maximum number of encryption context key-value pairs
MaxKMSKeyIDLength = 500 // Maximum length for KMS key identifiers
// S3 multipart upload limits based on AWS S3 service constraints
MaxS3MultipartParts = 10000 // Maximum number of parts in a multipart upload (1-10,000)
)

View File

@@ -0,0 +1,84 @@
package s3_constants
const (
ExtAmzPrefix = "Seaweed-X-Amz-"
ExtAmzOwnerKey = "Seaweed-X-Amz-Owner"
ExtAmzAclKey = "Seaweed-X-Amz-Acl"
ExtOwnershipKey = "Seaweed-X-Amz-Ownership"
ExtVersioningKey = "Seaweed-X-Amz-Versioning"
ExtVersionIdKey = "Seaweed-X-Amz-Version-Id"
ExtDeleteMarkerKey = "Seaweed-X-Amz-Delete-Marker"
ExtIsLatestKey = "Seaweed-X-Amz-Is-Latest"
ExtETagKey = "Seaweed-X-Amz-ETag"
ExtLatestVersionIdKey = "Seaweed-X-Amz-Latest-Version-Id"
ExtLatestVersionFileNameKey = "Seaweed-X-Amz-Latest-Version-File-Name"
ExtAllowEmptyFolders = "Seaweed-X-Amz-Allow-Empty-Folders"
// Cached list metadata in .versions directory for single-scan efficiency
ExtLatestVersionSizeKey = "Seaweed-X-Amz-Latest-Version-Size"
ExtLatestVersionETagKey = "Seaweed-X-Amz-Latest-Version-ETag"
ExtLatestVersionMtimeKey = "Seaweed-X-Amz-Latest-Version-Mtime"
ExtLatestVersionOwnerKey = "Seaweed-X-Amz-Latest-Version-Owner"
ExtLatestVersionIsDeleteMarker = "Seaweed-X-Amz-Latest-Version-Is-Delete-Marker"
ExtLatestVersionStorageClassKey = "Seaweed-X-Amz-Latest-Version-Storage-Class"
ExtMultipartObjectKey = "key"
// Wall-clock nanoseconds (int64 as decimal string) captured at the
// moment a versioned entry was demoted from current to noncurrent
// by a later PUT or delete marker. Read by the s3 lifecycle engine
// to compute NoncurrentDays due time; zero/missing falls back to
// the entry's own mtime so legacy data still expires.
ExtNoncurrentSinceNsKey = "Seaweed-X-Amz-Noncurrent-Since-Ns"
// Set on a .versions directory when a suspended-versioning write made the
// base-path null object the current version; cleared whenever a version in
// the directory becomes current. Unlike an absent latest pointer, which a
// replica may simply not have received yet, this is an explicit signal.
ExtNullVersionIsLatestKey = "Seaweed-X-Amz-Null-Version-Is-Latest"
// Per-bucket opt-in for the PutObject lifecycle TTL fast path ("true"
// to enable). When on, an Expiration.Days rule is stamped as a volume
// TTL at write time instead of being expired by the worker. Off by
// default: a baked-in TTL can't honor a later policy change (rule
// removed or lengthened) the way worker-driven expiration does.
ExtLifecycleTtlFastPathKey = "Seaweed-X-Amz-Lifecycle-Ttl-Fast-Path"
// S3 checksum storage keys (use x-seaweedfs- prefix to avoid leaking in generic header loop)
ExtChecksumAlgorithm = "x-seaweedfs-checksum-algorithm"
ExtChecksumValue = "x-seaweedfs-checksum-value"
ExtChecksumType = "x-seaweedfs-checksum-type"
// Bucket Policy
ExtBucketPolicyKey = "Seaweed-X-Amz-Bucket-Policy"
// Every attribute the s3tables catalog stores on a table bucket, namespace,
// table or view directory entry starts with this.
ExtS3TablesPrefix = "s3tables."
// Object Retention and Legal Hold
ExtObjectLockModeKey = "Seaweed-X-Amz-Object-Lock-Mode"
ExtRetentionUntilDateKey = "Seaweed-X-Amz-Retention-Until-Date"
ExtLegalHoldKey = "Seaweed-X-Amz-Legal-Hold"
ExtObjectLockEnabledKey = "Seaweed-X-Amz-Object-Lock-Enabled"
// Object Lock Bucket Configuration (individual components, not XML)
ExtObjectLockDefaultModeKey = "Lock-Default-Mode"
ExtObjectLockDefaultDaysKey = "Lock-Default-Days"
ExtObjectLockDefaultYearsKey = "Lock-Default-Years"
)
// Object Lock and Retention Constants
const (
// Retention modes
RetentionModeGovernance = "GOVERNANCE"
RetentionModeCompliance = "COMPLIANCE"
// Legal hold status
LegalHoldOn = "ON"
LegalHoldOff = "OFF"
// Object lock enabled status
ObjectLockEnabled = "Enabled"
// Bucket versioning status
VersioningEnabled = "Enabled"
VersioningSuspended = "Suspended"
)

View File

@@ -0,0 +1,464 @@
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package s3_constants
import (
"context"
"net/http"
"strings"
"sync/atomic"
"github.com/gorilla/mux"
)
// S3 XML namespace
const (
S3Namespace = "http://s3.amazonaws.com/doc/2006-03-01/"
)
// S3 object key limits
const (
MaxS3ObjectKeyLength = 1024
)
// Standard S3 HTTP request constants
const (
// S3 storage class
AmzStorageClass = "X-Amz-Storage-Class"
// S3 user-defined metadata
AmzUserMetaPrefix = "X-Amz-Meta-"
AmzUserMetaDirective = "X-Amz-Metadata-Directive"
AmzUserMetaMtime = "X-Amz-Meta-Mtime"
// S3 object tagging
AmzObjectTagging = "X-Amz-Tagging"
AmzObjectTaggingPrefix = "X-Amz-Tagging-"
AmzObjectTaggingDirective = "X-Amz-Tagging-Directive"
AmzTagCount = "x-amz-tagging-count"
// GetObjectAttributes headers
AmzObjectAttributes = "X-Amz-Object-Attributes"
AmzMaxParts = "X-Amz-Max-Parts"
AmzPartNumberMarker = "X-Amz-Part-Number-Marker"
AmzDeleteMarker = "X-Amz-Delete-Marker"
SeaweedFSUploadId = "X-Seaweedfs-Upload-Id"
SeaweedFSMultipartPartsCount = "X-Seaweedfs-Multipart-Parts-Count"
SeaweedFSMultipartPartBoundaries = "X-Seaweedfs-Multipart-Part-Boundaries" // JSON: [{part:1,start:0,end:2,etag:"abc"},{part:2,start:2,end:3,etag:"def"}]
SeaweedFSExpiresS3 = "X-Seaweedfs-Expires-S3"
AmzMpPartsCount = "x-amz-mp-parts-count"
// S3 ACL headers
AmzCannedAcl = "X-Amz-Acl"
AmzAclFullControl = "X-Amz-Grant-Full-Control"
AmzAclRead = "X-Amz-Grant-Read"
AmzAclWrite = "X-Amz-Grant-Write"
AmzAclReadAcp = "X-Amz-Grant-Read-Acp"
AmzAclWriteAcp = "X-Amz-Grant-Write-Acp"
// S3 Object Lock headers
AmzBucketObjectLockEnabled = "X-Amz-Bucket-Object-Lock-Enabled"
AmzObjectLockMode = "X-Amz-Object-Lock-Mode"
AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date"
AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold"
// S3 checksum headers
AmzChecksumAlgorithm = "X-Amz-Checksum-Algorithm"
AmzChecksumCRC32 = "X-Amz-Checksum-Crc32"
AmzChecksumCRC32C = "X-Amz-Checksum-Crc32c"
AmzChecksumCRC64NVME = "X-Amz-Checksum-Crc64nvme"
AmzChecksumSHA1 = "X-Amz-Checksum-Sha1"
AmzChecksumSHA256 = "X-Amz-Checksum-Sha256"
AmzTrailer = "X-Amz-Trailer"
AmzSdkChecksumAlgorithm = "X-Amz-Sdk-Checksum-Algorithm"
AmzChecksumType = "X-Amz-Checksum-Type"
// S3 checksum type values (x-amz-checksum-type). A COMPOSITE checksum is a
// checksum-of-per-part-checksums ("base64-N"); a FULL_OBJECT checksum is the
// checksum of the whole object as if uploaded in a single request ("base64").
ChecksumTypeComposite = "COMPOSITE"
ChecksumTypeFullObject = "FULL_OBJECT"
// S3 conditional headers
IfMatch = "If-Match"
IfNoneMatch = "If-None-Match"
IfModifiedSince = "If-Modified-Since"
IfUnmodifiedSince = "If-Unmodified-Since"
// S3 conditional copy headers
AmzCopySourceIfMatch = "X-Amz-Copy-Source-If-Match"
AmzCopySourceIfNoneMatch = "X-Amz-Copy-Source-If-None-Match"
AmzCopySourceIfModifiedSince = "X-Amz-Copy-Source-If-Modified-Since"
AmzCopySourceIfUnmodifiedSince = "X-Amz-Copy-Source-If-Unmodified-Since"
// RenameObject
// AmzClientToken makes a rename idempotent. The AWS SDKs fill it in on every
// call, so it arrives on requests that were never written with it in mind.
AmzClientToken = "X-Amz-Client-Token"
AmzRenameSource = "X-Amz-Rename-Source"
AmzRenameSourceIfMatch = "X-Amz-Rename-Source-If-Match"
AmzRenameSourceIfNoneMatch = "X-Amz-Rename-Source-If-None-Match"
AmzRenameSourceIfModifiedSince = "X-Amz-Rename-Source-If-Modified-Since"
AmzRenameSourceIfUnmodifiedSince = "X-Amz-Rename-Source-If-Unmodified-Since"
// S3 Server-Side Encryption with Customer-provided Keys (SSE-C)
AmzServerSideEncryptionCustomerAlgorithm = "X-Amz-Server-Side-Encryption-Customer-Algorithm"
AmzServerSideEncryptionCustomerKey = "X-Amz-Server-Side-Encryption-Customer-Key"
AmzServerSideEncryptionCustomerKeyMD5 = "X-Amz-Server-Side-Encryption-Customer-Key-MD5"
AmzServerSideEncryptionContext = "X-Amz-Server-Side-Encryption-Context"
// S3 Server-Side Encryption with KMS (SSE-KMS)
AmzServerSideEncryption = "X-Amz-Server-Side-Encryption"
AmzServerSideEncryptionAwsKmsKeyId = "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id"
AmzServerSideEncryptionBucketKeyEnabled = "X-Amz-Server-Side-Encryption-Bucket-Key-Enabled"
// S3 SSE-C copy source headers
AmzCopySourceServerSideEncryptionCustomerAlgorithm = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm"
AmzCopySourceServerSideEncryptionCustomerKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key"
AmzCopySourceServerSideEncryptionCustomerKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-MD5"
)
// Metadata keys for internal storage
const (
// SSE-KMS metadata keys
AmzEncryptedDataKey = "x-amz-encrypted-data-key"
AmzEncryptionContextMeta = "x-amz-encryption-context"
// SeaweedFS internal metadata prefix (used to filter internal headers from client responses)
SeaweedFSInternalPrefix = "x-seaweedfs-"
// SeaweedFSPrefixObject marks a directory entry that also holds the object named by
// its path without a trailing slash. S3 keys are flat, so "a/b" and "a/b/c" are
// independent keys, and the filer stores the first one on the directory the second
// one lives under.
SeaweedFSPrefixObject = "x-seaweedfs-prefix-object"
// SeaweedFS internal metadata keys for encryption (prefixed to avoid automatic HTTP header conversion)
SeaweedFSSSEKMSKey = "x-seaweedfs-sse-kms-key" // Key for storing serialized SSE-KMS metadata
SeaweedFSSSES3Key = "x-seaweedfs-sse-s3-key" // Key for storing serialized SSE-S3 metadata
SeaweedFSSSEIV = "x-seaweedfs-sse-c-iv" // Key for storing SSE-C IV
// Multipart upload metadata keys for SSE-KMS (consistent with internal metadata key pattern)
SeaweedFSSSEKMSKeyID = "x-seaweedfs-sse-kms-key-id" // Key ID for multipart upload SSE-KMS inheritance
SeaweedFSSSEKMSEncryption = "x-seaweedfs-sse-kms-encryption" // Encryption type for multipart upload SSE-KMS inheritance
SeaweedFSSSEKMSBucketKeyEnabled = "x-seaweedfs-sse-kms-bucket-key-enabled" // Bucket key setting for multipart upload SSE-KMS inheritance
SeaweedFSSSEKMSEncryptionContext = "x-seaweedfs-sse-kms-encryption-context" // Encryption context for multipart upload SSE-KMS inheritance
SeaweedFSSSEKMSBaseIV = "x-seaweedfs-sse-kms-base-iv" // Base IV for multipart upload SSE-KMS (for IV offset calculation)
// SeaweedFSRenameToken records the x-amz-client-token of the rename that put an
// object at its key, together with the source that rename named. It rides on the
// object itself, so every gateway reads the same answer for a retry of that
// rename, and it is dropped whenever the key is written again.
SeaweedFSRenameToken = "x-seaweedfs-rename-token"
// Multipart upload metadata keys for SSE-S3
SeaweedFSSSES3Encryption = "x-seaweedfs-sse-s3-encryption" // Encryption type for multipart upload SSE-S3 inheritance
SeaweedFSSSES3BaseIV = "x-seaweedfs-sse-s3-base-iv" // Base IV for multipart upload SSE-S3 (for IV offset calculation)
SeaweedFSSSES3KeyData = "x-seaweedfs-sse-s3-key-data" // Encrypted key data for multipart upload SSE-S3 inheritance
)
// SeaweedFS internal headers for filer communication
const (
SeaweedFSSSEKMSKeyHeader = "X-SeaweedFS-SSE-KMS-Key" // Header for passing SSE-KMS metadata to filer
SeaweedFSSSEIVHeader = "X-SeaweedFS-SSE-IV" // Header for passing SSE-C IV to filer (SSE-C only)
SeaweedFSSSEKMSBaseIVHeader = "X-SeaweedFS-SSE-KMS-Base-IV" // Header for passing base IV for multipart SSE-KMS
SeaweedFSSSES3BaseIVHeader = "X-SeaweedFS-SSE-S3-Base-IV" // Header for passing base IV for multipart SSE-S3
SeaweedFSSSES3KeyDataHeader = "X-SeaweedFS-SSE-S3-Key-Data" // Header for passing key data for multipart SSE-S3
// SeaweedFSPrincipalHeader and SeaweedFSSessionTokenHeader carry the
// server-derived principal ARN and session token from the auth layer to
// downstream authorization (IAM policy evaluation, S3Tables IAM checks).
// These headers are trusted: callers of AuthSignatureOnly /
// authenticateRequestInternal must scrub any client-supplied values at the
// very start of authentication to prevent privilege escalation via header
// injection. Prefer these constants over literals so that any future scrub
// or consumer cannot drift by typo.
SeaweedFSPrincipalHeader = "X-SeaweedFS-Principal"
SeaweedFSSessionTokenHeader = "X-SeaweedFS-Session-Token"
)
// Non-Standard S3 HTTP request constants
const (
AmzIdentityId = "s3-identity-id"
AmzAccountId = "s3-account-id"
AmzAuthType = "s3-auth-type"
)
func GetBucketAndObject(r *http.Request) (bucket, object string) {
vars := mux.Vars(r)
bucket = vars["bucket"]
object = NormalizeObjectKey(vars["object"])
return
}
// IsValidObjectKey rejects S3 object keys that — after normalization
// (backslash→slash, slash collapse) — contain a `.` or `..` path segment, or
// embed a NUL byte. Such keys are collapsed by filepath.Join inside the filer
// and would escape the bucket directory, so they must not reach the gRPC layer.
// Gorilla mux URL-decodes captured vars before this runs, so `%2e%2e` is
// already `..` here.
func IsValidObjectKey(object string) bool {
if object == "" {
return true
}
if strings.ContainsRune(object, '\x00') {
return false
}
object = strings.ReplaceAll(object, "\\", "/")
for _, seg := range strings.Split(object, "/") {
if seg == "." || seg == ".." {
return false
}
}
return true
}
// IsValidBucketName rejects bucket names captured from the URL path that are
// unsafe to use in filer path construction (`.`, `..`, contain `/` or `\`, or
// embed NUL). This is a path-safety check, not a full S3 naming-rule check.
func IsValidBucketName(bucket string) bool {
if bucket == "" {
return true
}
if bucket == "." || bucket == ".." {
return false
}
return !strings.ContainsAny(bucket, "/\\\x00")
}
// IsValidPathSegment reports whether value is safe to use as one filer path
// component. It intentionally does not enforce any application-specific
// format; callers can layer stricter checks on top.
func IsValidPathSegment(value string) bool {
if value == "" || value == "." || value == ".." {
return false
}
return !strings.ContainsAny(value, "/\\\x00")
}
// NormalizeObjectKey normalizes object keys by removing duplicate slashes and converting backslashes.
// This normalizes keys from various sources (URL path, form values, etc.) to a consistent format.
// It also converts Windows-style backslashes to forward slashes for cross-platform compatibility.
// Returns keys WITHOUT leading slash to match S3 API format (e.g., "foo/bar" not "/foo/bar").
// Preserves trailing slash if present (e.g., "foo/" stays "foo/").
func NormalizeObjectKey(object string) string {
// Preserve trailing slash if present
hasTrailingSlash := strings.HasSuffix(object, "/")
// Convert Windows-style backslashes to forward slashes
object = strings.ReplaceAll(object, "\\", "/")
object = removeDuplicateSlashes(object)
// Remove leading slash to match S3 API format
object = strings.TrimPrefix(object, "/")
// Restore trailing slash if it was present and result is not empty
if hasTrailingSlash && object != "" && !strings.HasSuffix(object, "/") {
object = object + "/"
}
return object
}
// removeDuplicateSlashes removes consecutive slashes from a path
func removeDuplicateSlashes(s string) string {
var result strings.Builder
result.Grow(len(s))
lastWasSlash := false
for _, r := range s {
if r == '/' {
if !lastWasSlash {
result.WriteRune(r)
}
lastWasSlash = true
} else {
result.WriteRune(r)
lastWasSlash = false
}
}
return result.String()
}
func GetPrefix(r *http.Request) string {
query := r.URL.Query()
prefix := query.Get("prefix")
prefix = removeDuplicateSlashes(prefix)
return prefix
}
var PassThroughHeaders = map[string]string{
"response-cache-control": "Cache-Control",
"response-content-disposition": "Content-Disposition",
"response-content-encoding": "Content-Encoding",
"response-content-language": "Content-Language",
"response-content-type": "Content-Type",
"response-expires": "Expires",
}
// IsSeaweedFSInternalHeader checks if a header key is a SeaweedFS internal header
// that should be filtered from client responses.
// Header names are case-insensitive in HTTP, so this function normalizes to lowercase.
func IsSeaweedFSInternalHeader(headerKey string) bool {
return strings.HasPrefix(strings.ToLower(headerKey), SeaweedFSInternalPrefix)
}
// Context keys for storing authenticated identity information
type contextKey string
const (
contextKeyIdentityName contextKey = "s3-identity-name"
contextKeyIdentityObject contextKey = "s3-identity-object"
contextKeyIdentityHolder contextKey = "s3-identity-holder"
contextKeyPrincipalArn contextKey = "s3-principal-arn"
contextKeyIdentityClaim contextKey = "s3-identity-claim"
)
// identityHolder is a mutable container for the authenticated identity name,
// stored by pointer in the request context. Authentication runs as an inner
// handler that records the identity with r.WithContext, which returns a request
// copy; that copy's new context values are invisible to outer middleware (e.g.
// the audit logger) holding an earlier copy of the request. A pointer to a
// holder installed before authentication is shared across all copies, so the
// name written by the inner handler is readable by the outer middleware.
type identityHolder struct {
name atomic.Pointer[string]
principalArn atomic.Pointer[string]
identityClaim atomic.Pointer[string]
}
// EnsureIdentityHolder attaches a mutable identity holder to the request context
// if one is not already present, returning the (possibly new) request. Install
// it before authentication so the authenticated requester remains recoverable
// from the original request, e.g. when emitting an audit entry for a handler
// that does not log the requester itself.
func EnsureIdentityHolder(r *http.Request) *http.Request {
if r == nil {
return nil
}
if h, ok := r.Context().Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
return r
}
return r.WithContext(context.WithValue(r.Context(), contextKeyIdentityHolder, &identityHolder{}))
}
// SetIdentityNameInContext stores the authenticated identity name in the request context
// This is the secure way to propagate identity - headers can be spoofed, context cannot
func SetIdentityNameInContext(ctx context.Context, identityName string) context.Context {
if identityName == "" {
return ctx
}
// Also record into the mutable holder (if installed) so the name survives the
// request-context copy and stays visible to outer middleware such as the
// audit logger.
if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
h.name.Store(&identityName)
}
return context.WithValue(ctx, contextKeyIdentityName, identityName)
}
// GetIdentityNameFromContext retrieves the authenticated identity name from the request context
// Returns empty string if no identity is set (unauthenticated request)
// This is the secure way to retrieve identity - never read from headers directly
func GetIdentityNameFromContext(r *http.Request) string {
// Prefer the per-request context value; fall back to the mutable holder for
// callers (e.g. the audit middleware) that hold a copy of the request taken
// before authentication set the identity.
if name, ok := r.Context().Value(contextKeyIdentityName).(string); ok && name != "" {
return name
}
if h, ok := r.Context().Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
if name := h.name.Load(); name != nil {
return *name
}
}
return ""
}
// SetPrincipalArnInContext stores the authenticated principal ARN in the request
// context. For an STS session the identity name is an opaque session subject, so
// the ARN is the only place the assumed role and session name survive to the
// audit log.
func SetPrincipalArnInContext(ctx context.Context, principalArn string) context.Context {
if principalArn == "" {
return ctx
}
if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
h.principalArn.Store(&principalArn)
}
return context.WithValue(ctx, contextKeyPrincipalArn, principalArn)
}
// GetPrincipalArnFromContext retrieves the authenticated principal ARN from the
// request context, or "" when the request is unauthenticated.
func GetPrincipalArnFromContext(r *http.Request) string {
if arn, ok := r.Context().Value(contextKeyPrincipalArn).(string); ok && arn != "" {
return arn
}
if h, ok := r.Context().Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
if arn := h.principalArn.Load(); arn != nil {
return *arn
}
}
return ""
}
// SetIdentityClaimInContext stores the authoritative OIDC identity claim (e.g.
// preferred_username, email, sub) for the audit log. For an STS-assumed OIDC
// session the requester name is an opaque session subject, so the claim is the
// only place a stable, human-readable federated identity survives to the audit
// log. Empty for non-federated sessions, where the requester name already
// carries the real username.
func SetIdentityClaimInContext(ctx context.Context, identityClaim string) context.Context {
if identityClaim == "" {
return ctx
}
if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
h.identityClaim.Store(&identityClaim)
}
return context.WithValue(ctx, contextKeyIdentityClaim, identityClaim)
}
// GetIdentityClaimFromContext retrieves the authoritative OIDC identity claim
// from the request context, or "" when the request is unauthenticated or not
// federated.
func GetIdentityClaimFromContext(r *http.Request) string {
if claim, ok := r.Context().Value(contextKeyIdentityClaim).(string); ok && claim != "" {
return claim
}
if h, ok := r.Context().Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
if claim := h.identityClaim.Load(); claim != nil {
return *claim
}
}
return ""
}
// SetIdentityInContext stores the full authenticated identity object in the request context
// This is used to pass the full identity (including for JWT users) to handlers
func SetIdentityInContext(ctx context.Context, identity interface{}) context.Context {
if identity != nil {
return context.WithValue(ctx, contextKeyIdentityObject, identity)
}
return ctx
}
// GetIdentityFromContext retrieves the full identity object from the request context
// Returns nil if no identity is set (unauthenticated request)
func GetIdentityFromContext(r *http.Request) interface{} {
return r.Context().Value(contextKeyIdentityObject)
}

View File

@@ -0,0 +1,267 @@
package s3_constants
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestNormalizeObjectKey(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple key",
input: "file.txt",
expected: "file.txt",
},
{
name: "key with leading slash",
input: "/file.txt",
expected: "file.txt",
},
{
name: "key with directory",
input: "folder/file.txt",
expected: "folder/file.txt",
},
{
name: "key with leading slash and directory",
input: "/folder/file.txt",
expected: "folder/file.txt",
},
{
name: "key with duplicate slashes",
input: "folder//subfolder///file.txt",
expected: "folder/subfolder/file.txt",
},
{
name: "Windows backslash - simple",
input: "folder\\file.txt",
expected: "folder/file.txt",
},
{
name: "Windows backslash - nested",
input: "folder\\subfolder\\file.txt",
expected: "folder/subfolder/file.txt",
},
{
name: "Windows backslash - with leading slash",
input: "/folder\\subfolder\\file.txt",
expected: "folder/subfolder/file.txt",
},
{
name: "mixed slashes",
input: "folder\\subfolder/another\\file.txt",
expected: "folder/subfolder/another/file.txt",
},
{
name: "Windows full path style (edge case)",
input: "C:\\Users\\test\\file.txt",
expected: "C:/Users/test/file.txt",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "just a slash",
input: "/",
expected: "",
},
{
name: "just a backslash",
input: "\\",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NormalizeObjectKey(tt.input)
if result != tt.expected {
t.Errorf("NormalizeObjectKey(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestIsValidObjectKey(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{"empty", "", true},
{"plain", "folder/file.txt", true},
{"leading slash", "/folder/file.txt", true},
{"trailing slash", "folder/", true},
{"hidden file ok", ".hidden", true},
{"dotdot in name ok", "..hidden", true},
{"double dots inside name", "foo..bar/baz", true},
{"bare dotdot", "..", false},
{"bare dot", ".", false},
{"leading dotdot segment", "../evil-bucket/test.txt", false},
{"leading dot-slash", "./evil/test.txt", false},
{"nested dotdot segment", "good/../evil/test.txt", false},
{"trailing dotdot segment", "good/..", false},
{"backslash dotdot", "..\\evil\\test.txt", false},
{"mixed-slash dotdot", "good\\..\\evil/test.txt", false},
{"dotdot after duplicate slash", "good//../evil", false},
{"nul byte", "foo\x00bar", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsValidObjectKey(tt.input); got != tt.want {
t.Errorf("IsValidObjectKey(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
func TestIsValidBucketName(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{"empty ok", "", true},
{"plain", "my-bucket", true},
{"name containing dots", "my.bucket.name", true},
{"bare dot", ".", false},
{"bare dotdot", "..", false},
{"with slash", "evil/bucket", false},
{"with backslash", "evil\\bucket", false},
{"with nul", "evil\x00bucket", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsValidBucketName(tt.input); got != tt.want {
t.Errorf("IsValidBucketName(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
func TestIsValidPathSegment(t *testing.T) {
tests := []struct {
input string
want bool
}{
{"opaque-id_123", true},
{"..hidden", true},
{"", false},
{".", false},
{"..", false},
{"dir/value", false},
{"dir\\value", false},
{"nul\x00value", false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
if got := IsValidPathSegment(tt.input); got != tt.want {
t.Errorf("IsValidPathSegment(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
func TestRemoveDuplicateSlashes(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "no duplicates",
input: "/folder/file.txt",
expected: "/folder/file.txt",
},
{
name: "double slash",
input: "/folder//file.txt",
expected: "/folder/file.txt",
},
{
name: "triple slash",
input: "/folder///file.txt",
expected: "/folder/file.txt",
},
{
name: "multiple duplicate locations",
input: "//folder//subfolder///file.txt",
expected: "/folder/subfolder/file.txt",
},
{
name: "empty string",
input: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := removeDuplicateSlashes(tt.input)
if result != tt.expected {
t.Errorf("removeDuplicateSlashes(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
// TestIdentityHolderPropagation reproduces the audit-log middleware chain: an
// outer handler installs the holder, an inner handler authenticates and records
// the identity on a request copy, and the outer handler must still recover the
// requester from its own (earlier) copy of the request.
func TestIdentityHolderPropagation(t *testing.T) {
outer := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
// Before the holder is installed there is no identity to recover.
if got := GetIdentityNameFromContext(outer); got != "" {
t.Fatalf("unauthenticated request reports requester %q, want empty", got)
}
outer = EnsureIdentityHolder(outer)
// The inner handler sets the identity on a copy, mirroring how auth wraps
// the request with r.WithContext before invoking the next handler.
innerCtx := SetIdentityNameInContext(outer.Context(), "admin")
inner := outer.WithContext(innerCtx)
if got := GetIdentityNameFromContext(inner); got != "admin" {
t.Errorf("inner request requester = %q, want admin", got)
}
if got := GetIdentityNameFromContext(outer); got != "admin" {
t.Errorf("outer request requester = %q, want admin (must propagate via holder)", got)
}
}
func TestEnsureIdentityHolderIdempotent(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
first := EnsureIdentityHolder(req)
if first == req {
t.Fatal("EnsureIdentityHolder should return a new request when no holder is present")
}
again := EnsureIdentityHolder(first)
if again != first {
t.Error("EnsureIdentityHolder should be idempotent when a holder is already present")
}
}
func TestGetIdentityNameFromContextWithoutHolder(t *testing.T) {
// Without a holder, the per-request context value is still honored so paths
// that set identity without installing a holder keep working.
req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
req = req.WithContext(SetIdentityNameInContext(req.Context(), "admin"))
if got := GetIdentityNameFromContext(req); got != "admin" {
t.Errorf("requester = %q, want admin", got)
}
}

View File

@@ -0,0 +1,6 @@
package s3_constants
const (
AccountAnonymousId = "anonymous"
AccountAdminId = "admin"
)

View File

@@ -0,0 +1,172 @@
package s3_constants
// S3 action strings for bucket policy evaluation
// 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"
// Object ACL operations
S3_ACTION_GET_OBJECT_ACL = "s3:GetObjectAcl"
S3_ACTION_PUT_OBJECT_ACL = "s3:PutObjectAcl"
// Object tagging operations
S3_ACTION_GET_OBJECT_TAGGING = "s3:GetObjectTagging"
S3_ACTION_PUT_OBJECT_TAGGING = "s3:PutObjectTagging"
S3_ACTION_DELETE_OBJECT_TAGGING = "s3:DeleteObjectTagging"
// Object retention and legal hold
S3_ACTION_GET_OBJECT_RETENTION = "s3:GetObjectRetention"
S3_ACTION_PUT_OBJECT_RETENTION = "s3:PutObjectRetention"
S3_ACTION_GET_OBJECT_LEGAL_HOLD = "s3:GetObjectLegalHold"
S3_ACTION_PUT_OBJECT_LEGAL_HOLD = "s3:PutObjectLegalHold"
S3_ACTION_BYPASS_GOVERNANCE = "s3:BypassGovernanceRetention"
// Multipart upload operations
S3_ACTION_CREATE_MULTIPART = "s3:CreateMultipartUpload"
S3_ACTION_UPLOAD_PART = "s3:UploadPart"
S3_ACTION_COMPLETE_MULTIPART = "s3:CompleteMultipartUpload"
S3_ACTION_ABORT_MULTIPART = "s3:AbortMultipartUpload"
S3_ACTION_UPLOAD_PART_COPY = "s3:UploadPartCopy"
S3_ACTION_LIST_PARTS = "s3:ListMultipartUploadParts"
S3_ACTION_LIST_MULTIPART_UPLOADS = "s3:ListBucketMultipartUploads"
// Bucket operations
S3_ACTION_CREATE_BUCKET = "s3:CreateBucket"
S3_ACTION_DELETE_BUCKET = "s3:DeleteBucket"
S3_ACTION_LIST_BUCKET = "s3:ListBucket"
S3_ACTION_LIST_BUCKET_VERSIONS = "s3:ListBucketVersions"
// Bucket ACL operations
S3_ACTION_GET_BUCKET_ACL = "s3:GetBucketAcl"
S3_ACTION_PUT_BUCKET_ACL = "s3:PutBucketAcl"
// Bucket policy operations
S3_ACTION_GET_BUCKET_POLICY = "s3:GetBucketPolicy"
S3_ACTION_PUT_BUCKET_POLICY = "s3:PutBucketPolicy"
S3_ACTION_DELETE_BUCKET_POLICY = "s3:DeleteBucketPolicy"
// Bucket tagging operations
S3_ACTION_GET_BUCKET_TAGGING = "s3:GetBucketTagging"
S3_ACTION_PUT_BUCKET_TAGGING = "s3:PutBucketTagging"
S3_ACTION_DELETE_BUCKET_TAGGING = "s3:DeleteBucketTagging"
// Bucket CORS operations
S3_ACTION_GET_BUCKET_CORS = "s3:GetBucketCors"
S3_ACTION_PUT_BUCKET_CORS = "s3:PutBucketCors"
S3_ACTION_DELETE_BUCKET_CORS = "s3:DeleteBucketCors"
// Bucket lifecycle operations
// Note: Both PUT and DELETE lifecycle operations use s3:PutLifecycleConfiguration
S3_ACTION_GET_BUCKET_LIFECYCLE = "s3:GetLifecycleConfiguration"
S3_ACTION_PUT_BUCKET_LIFECYCLE = "s3:PutLifecycleConfiguration"
// Bucket versioning operations
S3_ACTION_GET_BUCKET_VERSIONING = "s3:GetBucketVersioning"
S3_ACTION_PUT_BUCKET_VERSIONING = "s3:PutBucketVersioning"
// Bucket location
S3_ACTION_GET_BUCKET_LOCATION = "s3:GetBucketLocation"
// Bucket notification
S3_ACTION_GET_BUCKET_NOTIFICATION = "s3:GetBucketNotification"
S3_ACTION_PUT_BUCKET_NOTIFICATION = "s3:PutBucketNotification"
// Bucket object lock operations
S3_ACTION_GET_BUCKET_OBJECT_LOCK = "s3:GetBucketObjectLockConfiguration"
S3_ACTION_PUT_BUCKET_OBJECT_LOCK = "s3:PutBucketObjectLockConfiguration"
// Bucket encryption operations
// Note: DELETE bucket encryption uses s3:PutEncryptionConfiguration, matching AWS
S3_ACTION_GET_BUCKET_ENCRYPTION = "s3:GetEncryptionConfiguration"
S3_ACTION_PUT_BUCKET_ENCRYPTION = "s3:PutEncryptionConfiguration"
// Bucket request payment operations
S3_ACTION_GET_BUCKET_REQUEST_PAYMENT = "s3:GetBucketRequestPayment"
S3_ACTION_PUT_BUCKET_REQUEST_PAYMENT = "s3:PutBucketRequestPayment"
// Bucket public access block operations
// Note: DELETE uses s3:PutBucketPublicAccessBlock, matching AWS
S3_ACTION_GET_BUCKET_PUBLIC_ACCESS_BLOCK = "s3:GetBucketPublicAccessBlock"
S3_ACTION_PUT_BUCKET_PUBLIC_ACCESS_BLOCK = "s3:PutBucketPublicAccessBlock"
// Bucket ownership controls operations
// Note: DELETE uses s3:PutBucketOwnershipControls, matching AWS
S3_ACTION_GET_BUCKET_OWNERSHIP_CONTROLS = "s3:GetBucketOwnershipControls"
S3_ACTION_PUT_BUCKET_OWNERSHIP_CONTROLS = "s3:PutBucketOwnershipControls"
// SeaweedFS extension: bucket quota operations
// PUT /{bucket}?seaweedfs-quota and GET /{bucket}?seaweedfs-quota
// These are SeaweedFS-specific subresources, not part of the AWS S3 API.
S3_ACTION_PUT_BUCKET_QUOTA = "s3:PutBucketQuota"
S3_ACTION_GET_BUCKET_QUOTA = "s3:GetBucketQuota"
// Wildcard for all S3 actions
S3_ACTION_ALL = "s3:*"
)
// S3 Tables action strings for policy evaluation.
// Source of truth for the operation names: the dispatch switch in
// weed/s3api/s3tables/handler.go. Keep this list in sync with that switch
// when operations are added, renamed, or removed.
const (
// Table bucket operations
S3TABLES_ACTION_CREATE_TABLE_BUCKET = "s3tables:CreateTableBucket"
S3TABLES_ACTION_GET_TABLE_BUCKET = "s3tables:GetTableBucket"
S3TABLES_ACTION_LIST_TABLE_BUCKETS = "s3tables:ListTableBuckets"
S3TABLES_ACTION_DELETE_TABLE_BUCKET = "s3tables:DeleteTableBucket"
// Table bucket policy operations
S3TABLES_ACTION_PUT_TABLE_BUCKET_POLICY = "s3tables:PutTableBucketPolicy"
S3TABLES_ACTION_GET_TABLE_BUCKET_POLICY = "s3tables:GetTableBucketPolicy"
S3TABLES_ACTION_DELETE_TABLE_BUCKET_POLICY = "s3tables:DeleteTableBucketPolicy"
// Namespace operations
S3TABLES_ACTION_CREATE_NAMESPACE = "s3tables:CreateNamespace"
S3TABLES_ACTION_GET_NAMESPACE = "s3tables:GetNamespace"
S3TABLES_ACTION_UPDATE_NAMESPACE = "s3tables:UpdateNamespace"
S3TABLES_ACTION_LIST_NAMESPACES = "s3tables:ListNamespaces"
S3TABLES_ACTION_DELETE_NAMESPACE = "s3tables:DeleteNamespace"
// Table operations
S3TABLES_ACTION_CREATE_TABLE = "s3tables:CreateTable"
S3TABLES_ACTION_REGISTER_TABLE = "s3tables:RegisterTable"
S3TABLES_ACTION_GET_TABLE = "s3tables:GetTable"
S3TABLES_ACTION_LIST_TABLES = "s3tables:ListTables"
S3TABLES_ACTION_UPDATE_TABLE = "s3tables:UpdateTable"
S3TABLES_ACTION_DELETE_TABLE = "s3tables:DeleteTable"
S3TABLES_ACTION_RENAME_TABLE = "s3tables:RenameTable"
// View operations
S3TABLES_ACTION_CREATE_VIEW = "s3tables:CreateView"
S3TABLES_ACTION_GET_VIEW = "s3tables:GetView"
S3TABLES_ACTION_LIST_VIEWS = "s3tables:ListViews"
S3TABLES_ACTION_UPDATE_VIEW = "s3tables:UpdateView"
S3TABLES_ACTION_DELETE_VIEW = "s3tables:DeleteView"
S3TABLES_ACTION_RENAME_VIEW = "s3tables:RenameView"
// Table policy operations
S3TABLES_ACTION_PUT_TABLE_POLICY = "s3tables:PutTablePolicy"
S3TABLES_ACTION_GET_TABLE_POLICY = "s3tables:GetTablePolicy"
S3TABLES_ACTION_DELETE_TABLE_POLICY = "s3tables:DeleteTablePolicy"
// Maintenance configuration operations
S3TABLES_ACTION_PUT_TABLE_BUCKET_MAINTENANCE_CONFIGURATION = "s3tables:PutTableBucketMaintenanceConfiguration"
S3TABLES_ACTION_GET_TABLE_BUCKET_MAINTENANCE_CONFIGURATION = "s3tables:GetTableBucketMaintenanceConfiguration"
S3TABLES_ACTION_PUT_TABLE_MAINTENANCE_CONFIGURATION = "s3tables:PutTableMaintenanceConfiguration"
S3TABLES_ACTION_GET_TABLE_MAINTENANCE_CONFIGURATION = "s3tables:GetTableMaintenanceConfiguration"
S3TABLES_ACTION_GET_TABLE_MAINTENANCE_JOB_STATUS = "s3tables:GetTableMaintenanceJobStatus"
// Tagging operations
S3TABLES_ACTION_TAG_RESOURCE = "s3tables:TagResource"
S3TABLES_ACTION_LIST_TAGS_FOR_RESOURCE = "s3tables:ListTagsForResource"
S3TABLES_ACTION_UNTAG_RESOURCE = "s3tables:UntagResource"
// Wildcard for all S3 Tables actions
S3TABLES_ACTION_ALL = "s3tables:*"
)

View File

@@ -0,0 +1,29 @@
package s3_constants
const (
ACTION_READ = "Read"
ACTION_READ_ACP = "ReadAcp"
ACTION_WRITE = "Write"
ACTION_WRITE_ACP = "WriteAcp"
ACTION_ADMIN = "Admin"
ACTION_TAGGING = "Tagging"
ACTION_LIST = "List"
ACTION_DELETE_BUCKET = "DeleteBucket"
ACTION_BYPASS_GOVERNANCE_RETENTION = "BypassGovernanceRetention"
ACTION_GET_OBJECT_RETENTION = "GetObjectRetention"
ACTION_PUT_OBJECT_RETENTION = "PutObjectRetention"
ACTION_GET_OBJECT_LEGAL_HOLD = "GetObjectLegalHold"
ACTION_PUT_OBJECT_LEGAL_HOLD = "PutObjectLegalHold"
ACTION_GET_BUCKET_OBJECT_LOCK_CONFIG = "GetBucketObjectLockConfiguration"
ACTION_PUT_BUCKET_OBJECT_LOCK_CONFIG = "PutBucketObjectLockConfiguration"
ACTION_PUT_BUCKET_POLICY = "PutBucketPolicy"
ACTION_DELETE_BUCKET_POLICY = "DeleteBucketPolicy"
ACTION_PUT_BUCKET_QUOTA = "PutBucketQuota"
ACTION_GET_BUCKET_QUOTA = "GetBucketQuota"
SeaweedStorageDestinationHeader = "x-seaweedfs-destination"
MultipartUploadsFolder = ".uploads"
VersionsFolder = ".versions"
FolderMimeType = "httpd/unix-directory"
DirectoryMimeType = "application/x-directory"
)

View File

@@ -0,0 +1,18 @@
package s3_constants
import (
"strings"
)
var (
CircuitBreakerConfigDir = "/etc/s3"
CircuitBreakerConfigFile = "circuit_breaker.json"
AllowedActions = []string{ACTION_READ, ACTION_READ_ACP, ACTION_WRITE, ACTION_WRITE_ACP, ACTION_LIST, ACTION_TAGGING, ACTION_ADMIN, ACTION_DELETE_BUCKET}
LimitTypeCount = "Count"
LimitTypeBytes = "MB"
Separator = ":"
)
func Concat(elements ...string) string {
return strings.Join(elements, Separator)
}