Files
seaweedfs/weed/s3api/s3_constants/header.go
T
ad89f003a5 s3: end a catalog-vended session with the access key it was vended for (#11945)
* s3: end a catalog-vended session with the access key it was vended for

A table credential the Iceberg catalog vends is a self-contained STS session:
it stays valid until it expires (an hour by default) even after the access key
that loaded the table is deleted or deactivated, or its identity is removed
or disabled. An operator revoking a leaked key cannot cut off the table
credentials that key already obtained.

Sessions are stateless JWTs, so there is no revocation list to consult. This
change follows the same pattern as role sessions, which are already refused
once their role's ID no longer matches (checkSessionRoleBinding): a vended
session carries the access key it was vended for (src_ak, recorded by the
previous change), and validateSTSSessionToken checks it against the identity
table held in memory. A session of provider iceberg-catalog that carries a
source key is refused (InvalidAccessKeyId, as for an unknown key) once its
identity is gone or disabled, or the key is no longer one of that identity's
active, unexpired credentials.

Scope:
- Only catalog-vended sessions are checked; web-identity, AssumeRole and
  every other STS session are validated as before.
- The vendor binds a session to the caller's key only when the caller is a
  stored identity holding that key. A caller that signed with another STS
  session's key, an OIDC user, or a request that recorded no key gets an
  unbound session, as today; binding it would refuse every request made with
  it. A session minted before this change carries no key and is unbound.
- The check is one map lookup under the read lock and a scan of one
  identity's keys (about 11 ns, 0 allocations, with 1,000 identities).

Tests (s3api_iceberg_vended_session_test.go) mint real sessions through the
catalog vendor and validate them the way a data-path request is validated:
deactivating, expiring or deleting key A ends A's session while the session
vended through key B on the same identity keeps working; disabling or
deleting the identity ends it; callers that are not a stored identity holding
the key get unbound sessions that keep working; other STS sessions are not
checked. The first two tests and the third fail without this change.

* s3: keep the vended-session key binding when the key dies before vend

vendedSessionKeyLive gated the binding, so a key revoked between the
request's authentication and the vend skipped it and minted an unbound
session that stayed usable until it expired. Every catalog auth path
pairs the identity name in context with the key that authenticated, so
a stored-identity principal always means the request signed with that
identity's own key. Bind whenever the principal is a stored identity: a
session bound to a now-dead key is refused on its first request, which
is what the binding is for.

* s3: gate the vended-session key binding on the authenticated caller type

lookupByIdentityName(principal) named only that some identity exists, not
that the request signed with a stored key: an STS session or OIDC principal
that collides with an identity name got a session bound to its temporary
key, which vendedSessionKeyLive then refused on every request. Verifiers
now record the verified key as a stored credential, and the vendor binds
only when that marker is present — keeping the binding even when the key
was revoked between authentication and the vend.

* s3: enforce the vended key binding under a dedicated claim

Sessions minted before the binding existed carry the caller's key in the
audit-only SourceAccessKeyId — including temporary STS keys that can never
resolve in the identity table. Enforcing on that field would end live table
credentials at upgrade; the binding now travels under BoundAccessKeyId,
leaving legacy sessions unbound as before.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: bind vended sessions to a credential fingerprint

The key id alone cannot identify a credential: an operator can delete a
key and re-create it under the same id with a different secret, reviving
sessions minted for the original. Verifiers now record a fingerprint of
the secret at authentication; the session dies unless it still matches.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: keep recording the caller's key on every catalog session

SourceAccessKeyId was unconditional audit metadata before the binding
existed; gating it on the stored-credential marker dropped the record
for callers that authenticated another way. With the binding under its
own claim there is no ambiguity left, so the field records the caller's
key again regardless of how it authenticated.

* s3: key the vended-session credential fingerprint with the signing key

The bound-credential fingerprint travels inside the vended session
token. As a plain SHA-256 of the secret it gave any token holder an
offline guessing oracle for weak secrets, and CodeQL flags it twice.

Store the verified secret in the request marker instead and HMAC it
with the STS signing key at vend and at validation. The claim can no
longer be mined, and the per-request auth path stops hashing secrets.

---------

Co-authored-by: Javier de la Torre <jatorre@lakehousebox.com>
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-23 03:58:06 -07:00

682 lines
28 KiB
Go

/*
* 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"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"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"
SeaweedFSExtract = "X-Seaweedfs-Extract"
SeaweedFSExtractedCount = "X-Seaweedfs-Extracted-Count"
AmzMetaSnowballAutoExtract = "X-Amz-Meta-Snowball-Auto-Extract"
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"
AmzChecksumMode = "X-Amz-Checksum-Mode"
// 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"
// SeaweedFSWriteId identifies the single write that stored an entry: putToFiler
// stamps a fresh id into Extended on every call. Recovery for an ambiguously
// committed write compares it to tell its own entry from an identical older
// object — chunk lists cannot, since an empty object carries none.
SeaweedFSWriteId = "x-seaweedfs-write-id"
// 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
}
// allowBackslashInObjectKey keeps '\' a literal object-key character instead
// of normalizing it to '/'. It is a process-wide mode set once at S3 server
// startup; the filer only treats '/' as a path separator, so a verbatim '\'
// stays inside one filer entry name.
var allowBackslashInObjectKey atomic.Bool
func SetAllowBackslashInObjectKey(allow bool) {
allowBackslashInObjectKey.Store(allow)
}
// IsValidObjectKey rejects S3 object keys that — after the mode-dependent
// normalization NormalizeObjectKey applies — 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
}
if !allowBackslashInObjectKey.Load() {
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,
// unless allowBackslashInObjectKey is set.
// 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
if !allowBackslashInObjectKey.Load() {
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"
contextKeyIgnoredSessionToken contextKey = "s3-ignored-session-token"
contextKeyOriginalRequestPath contextKey = "s3-original-request-path"
contextKeyAccessKeyId contextKey = "s3-access-key-id"
contextKeyStoredAccessKeyId contextKey = "s3-stored-access-key-id"
)
// 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]
accessKeyId atomic.Pointer[string]
storedCredential atomic.Pointer[storedCredential]
}
// storedCredential is the stored-identity credential a request authenticated
// with: the access key id and its secret, so a credential vendor can bind
// what it mints to this exact credential instance.
type storedCredential struct {
accessKeyId string
secretKey string
}
// StoredCredentialFingerprint identifies one credential instance. The key id
// alone cannot: an operator can delete a key and re-create it under the same
// id with a different secret. The fingerprint travels inside the vended
// session token, so it is keyed with a server-held key — an unkeyed hash of
// the secret would hand the token holder an offline guessing oracle.
func StoredCredentialFingerprint(fingerprintKey []byte, accessKeyId, secretKey string) string {
if len(fingerprintKey) == 0 {
return ""
}
h := hmac.New(sha256.New, fingerprintKey)
h.Write([]byte("seaweedfs-stored-credential"))
h.Write([]byte{0})
h.Write([]byte(accessKeyId))
h.Write([]byte{0})
h.Write([]byte(secretKey))
return hex.EncodeToString(h.Sum(nil)[:16])
}
// 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 ""
}
// SetOriginalRequestPathInContext records the path a client actually sent
// before an internal rewrite (e.g. the Iceberg catalog collapsing a doubled
// /v1 prefix). SigV4 verification must run against this path: the client
// signed the URI it requested, not the normalized path it was routed with.
func SetOriginalRequestPathInContext(ctx context.Context, path string) context.Context {
if path == "" {
return ctx
}
return context.WithValue(ctx, contextKeyOriginalRequestPath, path)
}
// GetOriginalRequestPathFromContext returns the pre-rewrite request path
// recorded by SetOriginalRequestPathInContext, or "" when unset.
func GetOriginalRequestPathFromContext(r *http.Request) string {
if path, ok := r.Context().Value(contextKeyOriginalRequestPath).(string); ok {
return path
}
return ""
}
// SetAccessKeyIdInContext stores the access key id the request authenticated
// with: a static key's id, or an STS session's temporary key id. Several keys
// can belong to one identity, so the identity name alone cannot tell their
// holders apart in the audit log. An access key id is not a secret (AWS logs
// it in CloudTrail); the secret key never reaches the context.
func SetAccessKeyIdInContext(ctx context.Context, accessKeyId string) context.Context {
if accessKeyId == "" {
return ctx
}
if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
h.accessKeyId.Store(&accessKeyId)
}
return context.WithValue(ctx, contextKeyAccessKeyId, accessKeyId)
}
// RecordAccessKeyId stores the access key id of a credential whose signature
// was just verified into the request's identity holder, so the outer
// middleware (the audit fallback) and the handler both see it. A no-op without
// a holder (EnsureIdentityHolder) or without a key. Only a verifier calls it:
// the key comes from the credential that matched, never from re-reading the
// request, whose shape the verifier already resolved.
func RecordAccessKeyId(ctx context.Context, accessKeyId string) {
if ctx == nil || accessKeyId == "" {
return
}
if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
h.accessKeyId.Store(&accessKeyId)
}
}
// GetAccessKeyIdFromContext retrieves the access key id the request
// authenticated with, or "" when it carried none (anonymous, OIDC bearer).
func GetAccessKeyIdFromContext(r *http.Request) string {
if r == nil {
return ""
}
return AccessKeyIdFromContext(r.Context())
}
// AccessKeyIdFromContext is GetAccessKeyIdFromContext for code that holds
// only the request's context, such as the Iceberg credential vendor.
func AccessKeyIdFromContext(ctx context.Context) string {
if ctx == nil {
return ""
}
if id, ok := ctx.Value(contextKeyAccessKeyId).(string); ok && id != "" {
return id
}
if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
if id := h.accessKeyId.Load(); id != nil {
return *id
}
}
return ""
}
// SetStoredAccessKeyIdInContext records the stored-identity credential the
// request authenticated with — set by an authenticator that verified the
// credential against the identity store, so a credential vendor can tell the
// caller apart from an STS session or OIDC user holding a key the store never
// issued.
func SetStoredAccessKeyIdInContext(ctx context.Context, accessKeyId, secretKey string) context.Context {
if accessKeyId == "" {
return ctx
}
sc := storedCredential{accessKeyId, secretKey}
if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
h.storedCredential.Store(&sc)
}
return context.WithValue(ctx, contextKeyStoredAccessKeyId, sc)
}
// RecordStoredAccessKeyId stores the stored-credential marker into the
// request's identity holder: a verifier calls it when the credential that
// matched resolves to a key held by a configured identity. A no-op without a
// holder (EnsureIdentityHolder) or without a key.
func RecordStoredAccessKeyId(ctx context.Context, accessKeyId, secretKey string) {
if ctx == nil || accessKeyId == "" {
return
}
sc := storedCredential{accessKeyId, secretKey}
if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
h.storedCredential.Store(&sc)
}
}
// StoredAccessKeyIdFromContext retrieves the stored-identity access key id the
// request authenticated with, or "" when it authenticated another way (STS
// session, OIDC bearer, anonymous).
func StoredAccessKeyIdFromContext(ctx context.Context) string {
sc, _ := storedCredentialFromContext(ctx)
return sc.accessKeyId
}
// StoredCredentialFromContext retrieves the stored-identity access key id and
// secret the request authenticated with, or "", "" when it authenticated
// another way (STS session, OIDC bearer, anonymous).
func StoredCredentialFromContext(ctx context.Context) (string, string) {
sc, ok := storedCredentialFromContext(ctx)
if !ok {
return "", ""
}
return sc.accessKeyId, sc.secretKey
}
func storedCredentialFromContext(ctx context.Context) (storedCredential, bool) {
if ctx == nil {
return storedCredential{}, false
}
if sc, ok := ctx.Value(contextKeyStoredAccessKeyId).(storedCredential); ok && sc.accessKeyId != "" {
return sc, true
}
if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil {
if sc := h.storedCredential.Load(); sc != nil && sc.accessKeyId != "" {
return *sc, true
}
}
return storedCredential{}, false
}
// IgnoreSessionTokenInContext marks the request's session token as tolerated
// rather than authenticated: a statically configured access key signed the
// request, so the token (which credential vendors like Unity Catalog emit on
// static credentials too) must not steer authorization into the STS session
// path. A context value because headers can be spoofed; this cannot.
func IgnoreSessionTokenInContext(ctx context.Context) context.Context {
return context.WithValue(ctx, contextKeyIgnoredSessionToken, true)
}
// IsSessionTokenIgnored reports whether the request carries a session token
// that was tolerated for a statically configured access key.
func IsSessionTokenIgnored(ctx context.Context) bool {
ignored, _ := ctx.Value(contextKeyIgnoredSessionToken).(bool)
return ignored
}
// 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)
}