vault/sdk/logical/request.go
Vault Automation 2ef4c50221
Add audit log entries for new token type (#12747) (#12908)
* Add audit log entries for enterprise JWT token fields



* Reduce enterprise token field comment detail

- simplify enterprise token comments in sdk/logical/request.go
- remove verbose wording about issuer/audience/authorization semantics



* Fix TestAudit_JWT_DelegationToken permission denied error

The test was failing with 'permission denied' when using a delegation token
(JWT with act claim) to access cubbyhole. The root causes were:

1. RAR (Rich Authorization Request) check: The JWT contained
   'authorization_details' constraints that only allowed access to
   'secret/data/users/alice' and 'secret/data/config/general', but the
   test was attempting to access 'cubbyhole/test'. The RAR check in
   PerformRARCheck() was correctly denying this mismatch.

2. Missing entity policies for actor ACL: For delegation tokens, the
   actor's ACL is built solely from entity identity policies (not token
   policies like 'default'). Without explicit policies on the actor
   entity, the delegation ACL intersection check would fail.

Fixes:
- Removed 'authorization_details' from the test JWT since the test is
  about verifying audit log entries for delegation tokens, not RAR
  constraints
- Added 'default' policy to both subject and actor entities to ensure
  both ACLs allow cubbyhole access for the delegation token intersection
- Updated test assertions to match the simplified JWT (removed
  authorization_details verification)



* Use require.NoError instead of t.Fatalf for error check



* Add explicit checks for auth field presence before type assertion

Adds separate checks to verify the 'auth' and 'metadata' fields exist in
the map before attempting type assertion, preventing potential panics
and improving test clarity.



* test: tighten request metadata merge assertions



* test: simplify enterprise metadata assertions



* test: split enterprise metadata merge coverage



* style: apply gofumpt to entry formatter tests



* test: add godoc for enterprise token metadata test



---------

Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-11 19:21:10 +01:00

681 lines
25 KiB
Go

// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: MPL-2.0
package logical
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/hashicorp/vault/sdk/rotation"
"github.com/mitchellh/copystructure"
)
// RequestWrapInfo is a struct that stores information about desired response
// and seal wrapping behavior
type RequestWrapInfo struct {
// Setting to non-zero specifies that the response should be wrapped.
// Specifies the desired TTL of the wrapping token.
TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl" sentinel:""`
// The format to use for the wrapped response; if not specified it's a bare
// token
Format string `json:"format" structs:"format" mapstructure:"format" sentinel:""`
// A flag to conforming backends that data for a given request should be
// seal wrapped
SealWrap bool `json:"seal_wrap" structs:"seal_wrap" mapstructure:"seal_wrap" sentinel:""`
}
func (r *RequestWrapInfo) SentinelGet(key string) (interface{}, error) {
if r == nil {
return nil, nil
}
switch key {
case "ttl":
return r.TTL, nil
case "ttl_seconds":
return int64(r.TTL.Seconds()), nil
}
return nil, nil
}
func (r *RequestWrapInfo) SentinelKeys() []string {
return []string{
"ttl",
"ttl_seconds",
}
}
//go:generate enumer -type=ClientTokenSource -trimprefix=ClientTokenFrom -transform=snake
type ClientTokenSource uint32
const (
NoClientToken ClientTokenSource = iota
ClientTokenFromVaultHeader
ClientTokenFromAuthzHeader
ClientTokenFromInternalAuth
)
type WALState struct {
ClusterID string
LocalIndex uint64
ReplicatedIndex uint64
}
const indexStateCtxKey = "index_state"
// IndexStateContext returns a context with an added value holding the index
// state that should be populated on writes.
func IndexStateContext(ctx context.Context, state *WALState) context.Context {
return context.WithValue(ctx, indexStateCtxKey, state)
}
// IndexStateFromContext is a helper to look up if the provided context contains
// an index state pointer.
func IndexStateFromContext(ctx context.Context) *WALState {
s, ok := ctx.Value(indexStateCtxKey).(*WALState)
if !ok {
return nil
}
return s
}
// AuthorizationDetail stores one enterprise token authorization detail object.
// The "type" field is required.
type AuthorizationDetail map[string]any
// Request is a struct that stores the parameters and context of a request
// being made to Vault. It is used to abstract the details of the higher level
// request protocol from the handlers.
//
// Note: Many of these have Sentinel disabled because they are values populated
// by the router after policy checks; the token namespace would be the right
// place to access them via Sentinel
type Request struct {
// Id is the uuid associated with each request
ID string `json:"id" structs:"id" mapstructure:"id" sentinel:""`
// If set, the name given to the replication secondary where this request
// originated
ReplicationCluster string `json:"replication_cluster" structs:"replication_cluster" mapstructure:"replication_cluster" sentinel:""`
// Operation is the requested operation type
Operation Operation `json:"operation" structs:"operation" mapstructure:"operation"`
// Path is the full path of the request
Path string `json:"path" structs:"path" mapstructure:"path" sentinel:""`
// Request data is an opaque map that must have string keys.
Data map[string]interface{} `json:"map" structs:"data" mapstructure:"data"`
// Storage can be used to durably store and retrieve state.
Storage Storage `json:"-" sentinel:""`
// Secret will be non-nil only for Revoke and Renew operations
// to represent the secret that was returned prior.
Secret *Secret `json:"secret" structs:"secret" mapstructure:"secret" sentinel:""`
// Auth will be non-nil only for Renew operations
// to represent the auth that was returned prior.
Auth *Auth `json:"auth" structs:"auth" mapstructure:"auth" sentinel:""`
// Headers will contain the http headers from the request. This value will
// be used in the audit broker to ensure we are auditing only the allowed
// headers.
Headers map[string][]string `json:"headers" structs:"headers" mapstructure:"headers" sentinel:""`
// Connection will be non-nil only for credential providers to
// inspect the connection information and potentially use it for
// authentication/protection.
Connection *Connection `json:"connection" structs:"connection" mapstructure:"connection"`
// ClientToken is provided to the core so that the identity
// can be verified and ACLs applied. This value is passed
// through to the logical backends but after being salted and
// hashed.
ClientToken string `json:"client_token" structs:"client_token" mapstructure:"client_token" sentinel:""`
// EnterpriseTokenMetadata stores enterprise token metadata.
EnterpriseTokenMetadata string `json:"enterprise_token_metadata" structs:"enterprise_token_metadata" mapstructure:"enterprise_token_metadata" sentinel:""`
// EnterpriseTokenIssuer stores the enterprise token issuer.
EnterpriseTokenIssuer string `json:"enterprise_token_issuer,omitempty" structs:"enterprise_token_issuer" mapstructure:"enterprise_token_issuer"`
// EnterpriseTokenAudience stores enterprise token audience values.
EnterpriseTokenAudience []string `json:"enterprise_token_audience,omitempty" structs:"enterprise_token_audience" mapstructure:"enterprise_token_audience"`
// EnterpriseTokenAuthorizationDetails stores enterprise token authorization details.
EnterpriseTokenAuthorizationDetails []AuthorizationDetail `json:"enterprise_token_authorization_details,omitempty" structs:"enterprise_token_authorization_details" mapstructure:"enterprise_token_authorization_details"`
// ClientTokenAccessor is provided to the core so that the it can get
// logged as part of request audit logging.
ClientTokenAccessor string `json:"client_token_accessor" structs:"client_token_accessor" mapstructure:"client_token_accessor" sentinel:""`
// DisplayName is provided to the logical backend to help associate
// dynamic secrets with the source entity. This is not a sensitive
// name, but is useful for operators.
DisplayName string `json:"display_name" structs:"display_name" mapstructure:"display_name" sentinel:""`
// MountPoint is provided so that a logical backend can generate
// paths relative to itself. The `Path` is effectively the client
// request path with the MountPoint trimmed off.
MountPoint string `json:"mount_point" structs:"mount_point" mapstructure:"mount_point" sentinel:""`
// MountType is provided so that a logical backend can make decisions
// based on the specific mount type (e.g., if a mount type has different
// aliases, generating different defaults depending on the alias)
MountType string `json:"mount_type" structs:"mount_type" mapstructure:"mount_type" sentinel:""`
// MountAccessor is provided so that identities returned by the authentication
// backends can be tied to the mount it belongs to.
MountAccessor string `json:"mount_accessor" structs:"mount_accessor" mapstructure:"mount_accessor" sentinel:""`
// mountRunningVersion is used internally to propagate the semantic version
// of the mounted plugin as reported by its vault.MountEntry to audit logging
mountRunningVersion string
// mountRunningSha256 is used internally to propagate the encoded sha256
// of the mounted plugin as reported its vault.MountEntry to audit logging
mountRunningSha256 string
// mountIsExternalPlugin is used internally to propagate whether
// the backend of the mounted plugin is running externally (i.e., over GRPC)
// to audit logging
mountIsExternalPlugin bool
// mountClass is used internally to propagate the mount class of the mounted plugin to audit logging
mountClass string
// WrapInfo contains requested response wrapping parameters
WrapInfo *RequestWrapInfo `json:"wrap_info" structs:"wrap_info" mapstructure:"wrap_info" sentinel:""`
// ClientTokenRemainingUses represents the allowed number of uses left on the
// token supplied
ClientTokenRemainingUses int `json:"client_token_remaining_uses" structs:"client_token_remaining_uses" mapstructure:"client_token_remaining_uses"`
// EntityID is the identity of the caller extracted out of the token used
// to make this request
EntityID string `json:"entity_id" structs:"entity_id" mapstructure:"entity_id" sentinel:""`
// ActorEntityID is the entity ID of the actor in a request.
ActorEntityID string `json:"actor_entity_id" structs:"actor_entity_id" mapstructure:"actor_entity_id" sentinel:""`
// ActorEntityName is the name of the actor entity in a request.
ActorEntityName string `json:"actor_entity_name" structs:"actor_entity_name" mapstructure:"actor_entity_name" sentinel:""`
// PolicyOverride indicates that the requestor wishes to override
// soft-mandatory Sentinel policies
PolicyOverride bool `json:"policy_override" structs:"policy_override" mapstructure:"policy_override"`
// Whether the request is unauthenticated, as in, had no client token
// attached. Useful in some situations where the client token is not made
// accessible.
Unauthenticated bool `json:"unauthenticated" structs:"unauthenticated" mapstructure:"unauthenticated"`
// PathLimited indicates that the request path is marked for special-case
// request limiting.
PathLimited bool `json:"path_limited" structs:"path_limited" mapstructure:"path_limited"`
// MFACreds holds the parsed MFA information supplied over the API as part of
// X-Vault-MFA header
MFACreds MFACreds `json:"mfa_creds" structs:"mfa_creds" mapstructure:"mfa_creds" sentinel:""`
// Deprecated: use RotationID in RotationInfo instead
// RotationID is set by the Rotation Manager
// when making rotate requests to plugin backends
RotationID string
// RotationInfo is set by the Rotation Manager
// when making rotate requests to plugin backends
RotationInfo *rotation.RotationInfo
// Cached token entry. This avoids another lookup in request handling when
// we've already looked it up at http handling time. Note that this token
// has not been "used", as in it will not properly take into account use
// count limitations. As a result this field should only ever be used for
// transport to a function that would otherwise do a lookup and then
// properly use the token.
tokenEntry *TokenEntry
// For replication, contains the last WAL on the remote side after handling
// the request, used for best-effort avoidance of stale read-after-write
lastRemoteWAL uint64
// ControlGroup holds the authorizations that have happened on this
// request
ControlGroup *ControlGroup `json:"control_group" structs:"control_group" mapstructure:"control_group" sentinel:""`
// ClientTokenSource tells us where the client token was sourced from, so
// we can delete it before sending off to plugins
ClientTokenSource ClientTokenSource
// HTTPRequest, if set, can be used to access fields from the HTTP request
// that generated this logical.Request object, such as the request body.
HTTPRequest *http.Request `json:"-" sentinel:""`
// ResponseWriter if set can be used to stream a response value to the http
// request that generated this logical.Request object.
ResponseWriter *HTTPResponseWriter `json:"-" sentinel:""`
// requiredState is used internally to propagate the X-Vault-Index request
// header to later levels of request processing that operate only on
// logical.Request.
requiredState []string
// responseState is used internally to propagate the state that should appear
// in response headers; it's attached to the request rather than the response
// because not all requests yields non-nil responses.
responseState *WALState
// ClientID is the identity of the caller. If the token is associated with an
// entity, it will be the same as the EntityID . If the token has no entity,
// this will be the sha256(sorted policies + namespace) associated with the
// client token.
ClientID string `json:"client_id" structs:"client_id" mapstructure:"client_id" sentinel:""`
// InboundSSCToken is the token that arrives on an inbound request, supplied
// by the vault user.
InboundSSCToken string
// When a request has been forwarded, contains information of the host the request was forwarded 'from'
ForwardedFrom string `json:"forwarded_from,omitempty"`
// Name of the chroot namespace for the listener that the request was made against
ChrootNamespace string `json:"chroot_namespace,omitempty"`
// RequestLimiterDisabled tells whether the request context has Request Limiter applied.
RequestLimiterDisabled bool `json:"request_limiter_disabled,omitempty"`
// RequiresSnapshotID holds a loaded snapshot ID that the request will use,
// for either a read, list, or recover operation
RequiresSnapshotID string `json:"snapshot_id,omitempty"`
// RecoverSourcePath is the path where a recover request should read the data
// from. This can be empty if the request is not a recover request, or if the
// request is a recover request where the source path is the same as the
// destination path
RecoverSourcePath string `json:"recover_source_path,omitempty"`
}
// Clone returns a deep copy (almost) of the request.
// It will set unexported fields which were only previously accessible outside
// the package via receiver methods.
// NOTE: Request.Connection is NOT deep-copied, due to issues with the results
// of copystructure on serial numbers within the x509.Certificate objects.
func (r *Request) Clone() (*Request, error) {
cpy, err := copystructure.Copy(r)
if err != nil {
return nil, err
}
req := cpy.(*Request)
// Add the unexported values that were only retrievable via receivers.
// copystructure isn't able to do this, which is why we're doing it manually.
req.mountClass = r.MountClass()
req.mountRunningVersion = r.MountRunningVersion()
req.mountRunningSha256 = r.MountRunningSha256()
req.mountIsExternalPlugin = r.MountIsExternalPlugin()
// This needs to be overwritten as the internal connection state is not cloned properly
// mainly the big.Int serial numbers within the x509.Certificate objects get mangled.
req.Connection = r.Connection
return req, nil
}
// Get returns a data field and guards for nil Data
func (r *Request) Get(key string) interface{} {
if r.Data == nil {
return nil
}
return r.Data[key]
}
// GetString returns a data field as a string
func (r *Request) GetString(key string) string {
raw := r.Get(key)
s, _ := raw.(string)
return s
}
func (r *Request) GoString() string {
return fmt.Sprintf("*%#v", *r)
}
func (r *Request) SentinelGet(key string) (interface{}, error) {
switch key {
case "path":
// Sanitize it here so that it's consistent in policies
return strings.TrimPrefix(r.Path, "/"), nil
case "wrapping", "wrap_info":
// If the pointer is nil accessing the wrap info is considered
// "undefined" so this allows us to instead discover a TTL of zero
if r.WrapInfo == nil {
return &RequestWrapInfo{}, nil
}
return r.WrapInfo, nil
}
return nil, nil
}
func (r *Request) SentinelKeys() []string {
return []string{
"path",
"wrapping",
"wrap_info",
}
}
func (r *Request) MountRunningVersion() string {
return r.mountRunningVersion
}
func (r *Request) SetMountRunningVersion(mountRunningVersion string) {
r.mountRunningVersion = mountRunningVersion
}
func (r *Request) MountRunningSha256() string {
return r.mountRunningSha256
}
func (r *Request) SetMountRunningSha256(mountRunningSha256 string) {
r.mountRunningSha256 = mountRunningSha256
}
func (r *Request) MountIsExternalPlugin() bool {
return r.mountIsExternalPlugin
}
func (r *Request) SetMountIsExternalPlugin(mountIsExternalPlugin bool) {
r.mountIsExternalPlugin = mountIsExternalPlugin
}
func (r *Request) MountClass() string {
return r.mountClass
}
func (r *Request) SetMountClass(mountClass string) {
r.mountClass = mountClass
}
func (r *Request) LastRemoteWAL() uint64 {
return r.lastRemoteWAL
}
func (r *Request) SetLastRemoteWAL(last uint64) {
r.lastRemoteWAL = last
}
func (r *Request) RequiredState() []string {
return r.requiredState
}
func (r *Request) SetRequiredState(state []string) {
r.requiredState = state
}
func (r *Request) ResponseState() *WALState {
return r.responseState
}
func (r *Request) SetResponseState(w *WALState) {
r.responseState = w
}
func (r *Request) TokenEntry() *TokenEntry {
return r.tokenEntry
}
func (r *Request) SetTokenEntry(te *TokenEntry) {
r.tokenEntry = te
}
// IsSnapshotReadOrList checks whether the request reads or lists from a
// snapshot. When this method returns true, handling the request should not
// modify any internal caches or state
func (r *Request) IsSnapshotReadOrList() bool {
return (r.Operation == ReadOperation || r.Operation == ListOperation) && r.RequiresSnapshotID != ""
}
// RenewRequest creates the structure of the renew request.
func RenewRequest(path string, secret *Secret, data map[string]interface{}) *Request {
return &Request{
Operation: RenewOperation,
Path: path,
Data: data,
Secret: secret,
}
}
// RenewAuthRequest creates the structure of the renew request for an auth.
func RenewAuthRequest(path string, auth *Auth, data map[string]interface{}) *Request {
return &Request{
Operation: RenewOperation,
Path: path,
Data: data,
Auth: auth,
}
}
// RevokeRequest creates the structure of the revoke request.
func RevokeRequest(path string, secret *Secret, data map[string]interface{}) *Request {
return &Request{
Operation: RevokeOperation,
Path: path,
Data: data,
Secret: secret,
}
}
// RollbackRequest creates the structure of the revoke request.
func RollbackRequest(path string) *Request {
return &Request{
Operation: RollbackOperation,
Path: path,
Data: make(map[string]interface{}),
}
}
// Operation is an enum that is used to specify the type
// of request being made
type Operation string
const (
// The operations below are called per path
CreateOperation Operation = "create"
ReadOperation = "read"
UpdateOperation = "update"
PatchOperation = "patch"
DeleteOperation = "delete"
ListOperation = "list"
HelpOperation = "help"
AliasLookaheadOperation = "alias-lookahead"
ResolveRoleOperation = "resolve-role"
HeaderOperation = "header"
RecoverOperation = "recover"
// The operations below are called globally, the path is less relevant.
RevokeOperation Operation = "revoke"
RenewOperation = "renew"
RollbackOperation = "rollback"
RotationOperation = "rotate"
)
type MFACreds map[string][]string
// InitializationRequest stores the parameters and context of an Initialize()
// call being made to a logical.Backend.
type InitializationRequest struct {
// Storage can be used to durably store and retrieve state.
Storage Storage
}
type CustomHeader struct {
Name string
Value string
}
type CtxKeyInFlightRequestID struct{}
func (c CtxKeyInFlightRequestID) String() string {
return "in-flight-request-ID"
}
type CtxKeyInFlightRequestPriority struct{}
func (c CtxKeyInFlightRequestPriority) String() string {
return "in-flight-request-priority"
}
// CtxKeyInFlightTraceID is used for passing a trace ID through request
// forwarding. The CtxKeyInFlightRequestID created at the HTTP layer is
// propagated on through any forwarded requests using this key.
//
// Note that this applies to replication service RPCs (including
// ForwardingRequest from perf standbys or secondaries). The Forwarding RPC
// service may propagate the context but the handling on the active node runs
// back through the `http` package handler which builds a new context from HTTP
// request properties and creates a fresh request ID. Forwarding RPC is used
// exclusively in Community Edition but also in some special cases in Enterprise
// such as when forwarding is forced by an HTTP header.
type CtxKeyInFlightTraceID struct{}
func (c CtxKeyInFlightTraceID) String() string {
return "in-flight-trace-ID"
}
type CtxKeyRequestRole struct{}
func (c CtxKeyRequestRole) String() string {
return "request-role"
}
// ctxKeyDisableReplicationStatusEndpoints is a custom type used as a key in
// context.Context to store the value `true` when the
// disable_replication_status_endpoints configuration parameter is set to true
// for the listener through which a request was received.
type ctxKeyDisableReplicationStatusEndpoints struct{}
// String returns a string representation of the receiver type.
func (c ctxKeyDisableReplicationStatusEndpoints) String() string {
return "disable-replication-status-endpoints"
}
// ContextDisableReplicationStatusEndpointsValue examines the provided
// context.Context for the disable replication status endpoints value and
// returns it as a bool value if it's found along with the ok return value set
// to true; otherwise the ok return value is false.
func ContextDisableReplicationStatusEndpointsValue(ctx context.Context) (value, ok bool) {
value, ok = ctx.Value(ctxKeyDisableReplicationStatusEndpoints{}).(bool)
return
}
// CreateContextDisableReplicationStatusEndpoints creates a new context.Context
// based on the provided parent that also includes the provided value for the
// ctxKeyDisableReplicationStatusEndpoints key.
func CreateContextDisableReplicationStatusEndpoints(parent context.Context, value bool) context.Context {
return context.WithValue(parent, ctxKeyDisableReplicationStatusEndpoints{}, value)
}
// CtxKeyOriginalRequestPath is a custom type used as a key in context.Context
// to store the original request path.
type ctxKeyOriginalRequestPath struct{}
// String returns a string representation of the receiver type.
func (c ctxKeyOriginalRequestPath) String() string {
return "original_request_path"
}
// ContextOriginalRequestPathValue examines the provided context.Context for the
// original request path value and returns it as a string value if it's found
// along with the ok value set to true; otherwise the ok return value is false.
func ContextOriginalRequestPathValue(ctx context.Context) (value string, ok bool) {
value, ok = ctx.Value(ctxKeyOriginalRequestPath{}).(string)
return
}
// CreateContextOriginalRequestPath creates a new context.Context based on the
// provided parent that also includes the provided original request path value
// for the ctxKeyOriginalRequestPath key.
func CreateContextOriginalRequestPath(parent context.Context, value string) context.Context {
return context.WithValue(parent, ctxKeyOriginalRequestPath{}, value)
}
type ctxKeyOriginalBody struct{}
func ContextOriginalBodyValue(ctx context.Context) (io.ReadCloser, bool) {
value, ok := ctx.Value(ctxKeyOriginalBody{}).(io.ReadCloser)
return value, ok
}
func CreateContextOriginalBody(parent context.Context, body io.ReadCloser) context.Context {
return context.WithValue(parent, ctxKeyOriginalBody{}, body)
}
type CtxKeyDisableRequestLimiter struct{}
func (c CtxKeyDisableRequestLimiter) String() string {
return "disable_request_limiter"
}
// ctxKeyRedactionSettings is a custom type used as a key in context.Context to
// store the value the redaction settings for the listener that received the
// request.
type ctxKeyRedactionSettings struct{}
// String returns a string representation of the receiver type.
func (c ctxKeyRedactionSettings) String() string {
return "redaction-settings"
}
// CtxRedactionSettingsValue examines the provided context.Context for the
// redaction settings value and returns them as a tuple of bool values if they
// are found along with the ok return value set to true; otherwise the ok return
// value is false.
func CtxRedactionSettingsValue(ctx context.Context) (redactVersion, redactAddresses, redactClusterName, ok bool) {
value, ok := ctx.Value(ctxKeyRedactionSettings{}).([]bool)
if !ok {
return false, false, false, false
}
return value[0], value[1], value[2], true
}
// CreatecontextRedactionSettings creates a new context.Context based on the
// provided parent that also includes the provided redaction settings values for
// the ctxKeyRedactionSettings key.
func CreateContextRedactionSettings(parent context.Context, redactVersion, redactAddresses, redactClusterName bool) context.Context {
return context.WithValue(parent, ctxKeyRedactionSettings{}, []bool{redactVersion, redactAddresses, redactClusterName})
}
type ctxKeySnapshotID struct{}
func (c ctxKeySnapshotID) String() string {
return "snapshot-id"
}
// CreateContextWithSnapshotID creates a new context indicating that any storage
// operations should be done using the given snapshot ID. If the value is empty,
// it means that the request should use the normal storage.
func CreateContextWithSnapshotID(parent context.Context, value string) context.Context {
return context.WithValue(parent, ctxKeySnapshotID{}, value)
}
// ContextSnapshotIDValue retrieves the snapshot ID value stored in the context.
// This value can be empty, indicating that the request should use the normal
// storage.
func ContextSnapshotIDValue(ctx context.Context) (value string, ok bool) {
value, ok = ctx.Value(ctxKeySnapshotID{}).(string)
return
}