Files
seaweedfs/weed/s3api/s3_constants/header.go
T
Chris Lu 268424a5e1 s3: extract zip members on PutObject (#11786)
* filer: add zip archive extraction helpers

SpoolZipReader makes an upload body seekable for archive/zip,
SanitizeZipMemberName rejects zip-slip and non-regular members, and
CheckZipLimits caps member count and total uncompressed size.

For seaweedfs/seaweedfs#11722

* filer: extract zip members on ?extract=zip upload

POST/PUT to a directory path with ?extract=zip (or X-Seaweedfs-Extract:
zip) writes each archive member as its own entry, so a bundle of small
files lands in one request instead of one roundtrip each. Members keep
their mtime, mode and mime-by-extension; directories are created;
unsafe names are skipped and reported. extract.keep=true also stores
the archive itself.

For seaweedfs/seaweedfs#11722

* filer: bound zip spool memory by actual bytes

A chunked upload has no Content-Length, so the old size hint let an unbounded body go through io.ReadAll. Read up to the memory threshold plus one byte first, then spill the prefix and the rest to a temp file, capped at ZipExtractMaxBytes total.

* filer: guard zip member size total against overflow

Declared member sizes summing past math.MaxUint64 wrapped the running total and slipped past the 1 TiB cap.

* server: only take the inline fast path at a short first read

uploadReaderToChunks read at most chunkSize bytes, then treated any read below SaveToFilerLimit as the whole file. When SaveToFilerLimit exceeded chunkSize, longer uploads were stored truncated.

* server: resolve storage options per extracted zip member

A member can land under a storage rule more specific than the request path. Re-running detectStorageOption0 per destination applies that rule's collection, replication, TTL, filename limit, and read-only guard to files and directories alike, while explicit query overrides still win.

* server: accept raw request body for POST extract uploads

Every POST was forced through MultipartReader, so a raw zip body always got a 400. Only parse multipart when the Content-Type is actually multipart.

* server: extract zip directories before files and stop on cancel

Processing directory members first keeps their stored mtime/mode from being overwritten by parents auto-created for an earlier file member. Checking ctx.Err() between members keeps a disconnected client from driving thousands more writes.

* server: assert stored mtime and mode for extracted members

* s3: add zip extraction header constants

* s3: extract zip members on PutObject with extract header

x-amz-meta-snowball-auto-extract or x-seaweedfs-extract: zip on a PutObject fans out the archive to one object per member under the object's directory, reusing putToFiler so chunking, collections, storage rules, versioning, and bucket-default encryption apply to each member. The archive itself is not stored. Explicit SSE request headers are rejected since one encryption config cannot represent independently created member objects.

* s3: bound member fan-out by request cancellation

Stop starting new member writes once the request context is done, matching the filer extract loop.

* filer: bound compressed zip spool and reject non-local member names

The 1 TiB uncompressed limit was also capping the compressed bytes
spooled to the host's temp directory, so a handful of concurrent
extractions could fill the filer's disk before any ZIP validation ran.
SpoolZipReader now takes a separate compressed-size limit (the filer
passes its TUS max upload size) with a 5 GiB default.

SanitizeZipMemberName gains a filepath.IsLocal check: the segment loop
already rejects "..", but CodeQL only treats member names as sanitized
through a recognized guard, and the extra check also covers reserved
Windows names.

* server: honor destination rules for zip members and clamp maxMB

An x-seaweedfs-destination header picks the storage rules for a logical
destination while members still land under the request path, matching
PostHandler. Directory members now also enforce the actual path's
read-only rule instead of relying on option resolution to fail.

maxMB up to 2048 overflowed the int32 chunk size, so an oversized query
value made extraction (and ordinary autoChunk uploads) store members
empty yet report success. The parsing is shared and clamped at 2047 MB.

* s3: scope zip member writes through the per-key checks PutObject applies

The extraction loop bypassed most of what a standalone PutObject would
face: members were written without IAM authorization for their key,
directory members replaced markers without entry lock checks, and lock
headers persisted retention without the s3:PutObjectRetention or
s3:PutObjectLegalHold permissions. authorizeMember now runs IAM,
table-bucket path rules, bucket policy, lock-header validation, and
non-versioned lock enforcement per member key, and directory members
take the same guarded mkdir path as an explicit dir marker.

The archive's own Content-Md5 and x-amz-checksum-* values are verified
while the body spools instead of being silently dropped, members stop
inheriting the archive's Content-Encoding, and non-versioned members
resolve their lifecycle TTL like normal uploads.

* s3: fail malformed lock headers before spooling the archive

Per-member re-validation turned a bad object-lock header into one error
entry per member inside a 200 response. Validate once up front like
PutObjectHandler does, and keep only the dedicated retention/legal-hold
permission checks scoped to each member key.

* filer: return 413 for oversized archives and honor saveInside in extract

Map filer.ErrZipTooLarge to 413 instead of 500 for client-side archive
size violations. Extracted members written under saveInside now store
as inline entry content instead of unreachable volume chunks.

* s3: write extracted members under their own request context

Point member writes at the member key via mux.SetURLVars so SSE-KMS
builds the encryption context from the member ARN, not the archive's.
Map the spool limit error to ErrEntityTooLarge.
2026-10-16 21:07:05 -07:00

486 lines
20 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"
"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"
// 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"
contextKeyIgnoredSessionToken contextKey = "s3-ignored-session-token"
)
// 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 ""
}
// 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)
}