[#XX] Support frostfsid validation

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2023-10-05 16:25:25 +03:00
parent 80237822d1
commit c20194cc9d
13 changed files with 1001 additions and 65 deletions

View file

@ -1,13 +1,18 @@
package middleware
import (
"crypto/elliptic"
stderrors "errors"
"fmt"
"net/http"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"go.uber.org/zap"
)
@ -65,3 +70,45 @@ func Auth(center Center, log *zap.Logger) Func {
})
}
}
type FrostFSID interface {
ValidatePublicKey(key *keys.PublicKey) error
}
func IAM(frostfsID FrostFSID, log *zap.Logger) Func {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
bd, err := GetBoxData(ctx)
if err != nil || bd.Gate.BearerToken == nil {
reqLogOrDefault(ctx, log).Debug(logs.AnonRequestSkipIAMValidation)
h.ServeHTTP(w, r)
return
}
if err = validateBearerToken(frostfsID, bd.Gate.BearerToken); err != nil {
reqLogOrDefault(ctx, log).Error(logs.IAMValidationFailed, zap.Error(err))
WriteErrorResponse(w, GetReqInfo(r.Context()), err)
return
}
h.ServeHTTP(w, r)
})
}
}
func validateBearerToken(frostfsID FrostFSID, bt *bearer.Token) error {
m := new(acl.BearerToken)
bt.WriteToV2(m)
pk, err := keys.NewPublicKeyFromBytes(m.GetSignature().GetKey(), elliptic.P256())
if err != nil {
return fmt.Errorf("invalid bearer token public key: %w", err)
}
if err = frostfsID.ValidatePublicKey(pk); err != nil {
return fmt.Errorf("validation data user key failed: %w", err)
}
return nil
}

View file

@ -89,29 +89,50 @@ type (
}
)
func AttachChi(api *chi.Mux, domains []string, throttle middleware.ThrottleOpts, h Handler, center s3middleware.Center, log *zap.Logger, appMetrics *metrics.AppMetrics) {
type Config struct {
Throttle middleware.ThrottleOpts
Handler Handler
Center s3middleware.Center
Log *zap.Logger
Metrics *metrics.AppMetrics
// Domains optional. If empty no virtual hosted domains will be attached.
Domains []string
// FrostfsID optional. If nil middleware.IAM won't be attached.
FrostfsID s3middleware.FrostFSID
}
func NewRouter(cfg Config) *chi.Mux {
api := chi.NewRouter()
api.Use(
s3middleware.Request(log),
middleware.ThrottleWithOpts(throttle),
s3middleware.Request(cfg.Log),
middleware.ThrottleWithOpts(cfg.Throttle),
middleware.Recoverer,
s3middleware.Tracing(),
s3middleware.Metrics(log, h.ResolveBucket, appMetrics),
s3middleware.LogSuccessResponse(log),
s3middleware.Auth(center, log),
s3middleware.Metrics(cfg.Log, cfg.Handler.ResolveBucket, cfg.Metrics),
s3middleware.LogSuccessResponse(cfg.Log),
s3middleware.Auth(cfg.Center, cfg.Log),
)
if cfg.FrostfsID != nil {
api.Use(s3middleware.IAM(cfg.FrostfsID, cfg.Log))
}
defaultRouter := chi.NewRouter()
defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(h, log))
defaultRouter.Get("/", named("ListBuckets", h.ListBucketsHandler))
defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(cfg.Handler, cfg.Log))
defaultRouter.Get("/", named("ListBuckets", cfg.Handler.ListBucketsHandler))
hr := NewHostBucketRouter("bucket")
hr.Default(defaultRouter)
for _, domain := range domains {
hr.Map(domain, bucketRouter(h, log))
for _, domain := range cfg.Domains {
hr.Map(domain, bucketRouter(cfg.Handler, cfg.Log))
}
api.Mount("/", hr)
attachErrorHandler(api)
return api
}
func named(name string, handlerFunc http.HandlerFunc) http.HandlerFunc {

View file

@ -107,19 +107,17 @@ func TestRouterObjectEscaping(t *testing.T) {
}
func prepareRouter(t *testing.T) *chi.Mux {
throttleOps := middleware.ThrottleOpts{
Limit: 10,
BacklogTimeout: 30 * time.Second,
cfg := Config{
Throttle: middleware.ThrottleOpts{
Limit: 10,
BacklogTimeout: 30 * time.Second,
},
Handler: &handlerMock{t: t},
Center: &centerMock{},
Log: zaptest.NewLogger(t),
Metrics: &metrics.AppMetrics{},
}
handleMock := &handlerMock{t: t}
cntrMock := &centerMock{}
log := zaptest.NewLogger(t)
metric := &metrics.AppMetrics{}
chiRouter := chi.NewRouter()
AttachChi(chiRouter, nil, throttleOps, handleMock, cntrMock, log, metric)
return chiRouter
return NewRouter(cfg)
}
func readResponse(t *testing.T, w *httptest.ResponseRecorder) handlerResult {

View file

@ -25,6 +25,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/notifications"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/services"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
@ -35,7 +36,6 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/spf13/viper"
@ -58,6 +58,8 @@ type (
obj layer.Client
api api.Handler
frostfsid *frostfsid.FrostFSID
servers []Server
metrics *metrics.AppMetrics
@ -128,6 +130,7 @@ func (a *App) init(ctx context.Context) {
a.setRuntimeParameters()
a.initAPI(ctx)
a.initMetrics()
a.initIAM(ctx)
a.initServers(ctx)
a.initTracing(ctx)
}
@ -309,6 +312,22 @@ func (a *App) initMetrics() {
a.metrics.State().SetHealth(metrics.HealthStatusStarting)
}
func (a *App) initIAM(ctx context.Context) {
if !a.cfg.GetBool(cfgFrostfsIDEnabled) {
return
}
var err error
a.frostfsid, err = frostfsid.New(ctx, frostfsid.Config{
RPCAddress: a.cfg.GetString(cfgRPCEndpoint),
Contract: a.cfg.GetString(cfgFrostfsIDContract),
Key: a.key,
})
if err != nil {
a.log.Fatal("init frostfsid contract", zap.Error(err))
}
}
func (a *App) initResolver() {
var err error
a.bucketResolver, err = resolver.NewBucketResolver(a.getResolverConfig())
@ -489,13 +508,25 @@ func (a *App) Serve(ctx context.Context) {
domains := a.cfg.GetStringSlice(cfgListenDomains)
a.log.Info(logs.FetchDomainsPrepareToUseAPI, zap.Strings("domains", domains))
throttleOps := middleware.ThrottleOpts{
Limit: a.settings.maxClient.count,
BacklogTimeout: a.settings.maxClient.deadline,
cfg := api.Config{
Throttle: middleware.ThrottleOpts{
Limit: a.settings.maxClient.count,
BacklogTimeout: a.settings.maxClient.deadline,
},
Handler: a.api,
Center: a.ctr,
Log: a.log,
Metrics: a.metrics,
Domains: domains,
}
chiRouter := chi.NewRouter()
api.AttachChi(chiRouter, domains, throttleOps, a.api, a.ctr, a.log, a.metrics)
// We cannot make direct assignment if frostfsid.FrostFSID is nil
// because in that case the interface won't be nil, it will just contain nil value.
if a.frostfsid != nil {
cfg.FrostfsID = a.frostfsid
}
chiRouter := api.NewRouter(cfg)
// Use mux.Router as http.Handler
srv := new(http.Server)

View file

@ -160,6 +160,10 @@ const ( // Settings.
// Runtime.
cfgSoftMemoryLimit = "runtime.soft_memory_limit"
// FrostfsID.
cfgFrostfsIDEnabled = "frostfsid.enabled"
cfgFrostfsIDContract = "frostfsid.contract"
// envPrefix is an environment variables prefix used for configuration.
envPrefix = "S3_GW"
)
@ -533,6 +537,9 @@ func newSettings() *viper.Viper {
v.SetDefault(cfgKludgeCompleteMultipartUploadKeepalive, 10*time.Second)
v.SetDefault(cfgKludgeBypassContentEncodingCheckInChunks, false)
// frostfsid
v.SetDefault(cfgFrostfsIDContract, "frostfsid.frostfs")
// Bind flags
if err := bindFlags(v, flags); err != nil {
panic(fmt.Errorf("bind flags: %w", err))

View file

@ -147,4 +147,10 @@ S3_GW_TRACING_ENABLED=false
S3_GW_TRACING_ENDPOINT="localhost:4318"
S3_GW_TRACING_EXPORTER="otlp_grpc"
S3_GW_RUNTIME_SOFT_MEMORY_LIMIT=1073741824
S3_GW_RUNTIME_SOFT_MEMORY_LIMIT=1073741824
# FrostfsID contract configuration. To enable this functionality the `rpc_endpoint` param must be also set.
# Enables check that allow requests only users that is registered in FrostfsID contract.
S3_GW_FROSTFSID_ENABLED=false
# FrostfsID contract hash (LE) or name in NNS.
S3_GW_FROSTFSID_CONTRACT=frostfsid.frostfs

View file

@ -173,4 +173,11 @@ kludge:
bypass_content_encoding_check_in_chunks: false
runtime:
soft_memory_limit: 1gb
soft_memory_limit: 1gb
# FrostfsID contract configuration. To enable this functionality the `rpc_endpoint` param must be also set.
frostfsid:
# Enables check that allow requests only users that is registered in FrostfsID contract.
enabled: false
# FrostfsID contract hash (LE) or name in NNS.
contract: frostfsid.frostfs

View file

@ -186,6 +186,7 @@ There are some custom types used for brevity:
| `resolve_bucket` | [Bucket name resolving configuration](#resolve_bucket-section) |
| `kludge` | [Different kludge configuration](#kludge-section) |
| `runtime` | [Runtime configuration](#runtime-section) |
| `frostfsid` | [FrostfsID configuration](#frostfsid-section) |
### General section
@ -213,19 +214,19 @@ allowed_access_key_id_prefixes:
- 3stjWenX15YwYzczMr88gy3CQr4NYFBQ8P7keGzH5QFn
```
| Parameter | Type | SIGHUP reload | Default value | Description |
|----------------------------------|------------|---------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `listen_domains` | `[]string` | | | Domains to be able to use virtual-hosted-style access to bucket. |
| `rpc_endpoint` | `string` | yes | | The address of the RPC host to which the gateway connects to resolve bucket names (required to use the `nns` resolver). |
| `resolve_order` | `[]string` | yes | `[dns]` | Order of bucket name resolvers to use. Available resolvers: `dns`, `nns`. | |
| `connect_timeout` | `duration` | | `10s` | Timeout to connect to a node. |
| `stream_timeout` | `duration` | | `10s` | Timeout for individual operations in streaming RPC. |
| `healthcheck_timeout` | `duration` | | `15s` | Timeout to check node health during rebalance. |
| `rebalance_interval` | `duration` | | `60s` | Interval to check node health. |
| `pool_error_threshold` | `uint32` | | `100` | The number of errors on connection after which node is considered as unhealthy. |
| `max_clients_count` | `int` | | `100` | Limits for processing of clients' requests. |
| `max_clients_deadline` | `duration` | | `30s` | Deadline after which the gate sends error `RequestTimeout` to a client. |
| `allowed_access_key_id_prefixes` | `[]string` | | | List of allowed `AccessKeyID` prefixes which S3 GW serve. If the parameter is omitted, all `AccessKeyID` will be accepted. |
| Parameter | Type | SIGHUP reload | Default value | Description |
|----------------------------------|------------|-------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `listen_domains` | `[]string` | | | Domains to be able to use virtual-hosted-style access to bucket. |
| `rpc_endpoint` | `string` | depends on context (see related sections) | | The address of the RPC host to which the gateway connects to resolve bucket names and interact with frostfs contracts (required to use the `nns` resolver and `frostfsid` contract). |
| `resolve_order` | `[]string` | yes | `[dns]` | Order of bucket name resolvers to use. Available resolvers: `dns`, `nns`. For this resolvers `rpc_endpoint` supports SIGHUP reload. |
| `connect_timeout` | `duration` | | `10s` | Timeout to connect to a node. |
| `stream_timeout` | `duration` | | `10s` | Timeout for individual operations in streaming RPC. |
| `healthcheck_timeout` | `duration` | | `15s` | Timeout to check node health during rebalance. |
| `rebalance_interval` | `duration` | | `60s` | Interval to check node health. |
| `pool_error_threshold` | `uint32` | | `100` | The number of errors on connection after which node is considered as unhealthy. |
| `max_clients_count` | `int` | | `100` | Limits for processing of clients' requests. |
| `max_clients_deadline` | `duration` | | `30s` | Deadline after which the gate sends error `RequestTimeout` to a client. |
| `allowed_access_key_id_prefixes` | `[]string` | | | List of allowed `AccessKeyID` prefixes which S3 GW serve. If the parameter is omitted, all `AccessKeyID` will be accepted. |
### `wallet` section
@ -559,4 +560,19 @@ runtime:
| Parameter | Type | SIGHUP reload | Default value | Description |
|---------------------|--------|---------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `soft_memory_limit` | `size` | yes | maxint64 | Soft memory limit for the runtime. Zero or no value stands for no limit. If `GOMEMLIMIT` environment variable is set, the value from the configuration file will be ignored. |
| `soft_memory_limit` | `size` | yes | maxint64 | Soft memory limit for the runtime. Zero or no value stands for no limit. If `GOMEMLIMIT` environment variable is set, the value from the configuration file will be ignored. |
# `frostfsid` section
FrostfsID contract configuration. To enable this functionality the `rpc_endpoint` param must be also set (In this context `rpc_endpoint` does not support SIGHUP reload).
```yaml
frostfsid:
enabled: false
contract: frostfsid.frostfs
```
| Parameter | Type | SIGHUP reload | Default value | Description |
|------------|----------|---------------|-------------------|----------------------------------------------------------------------------------------|
| `enabled` | `bool` | no | false | Enables check that allow requests only users that is registered in FrostfsID contract. |
| `contract` | `string` | no | frostfsid.frostfs | FrostfsID contract hash (LE) or name in NNS. |

12
go.mod
View file

@ -12,7 +12,7 @@ require (
github.com/google/uuid v1.3.0
github.com/minio/sio v0.3.0
github.com/nats-io/nats.go v1.13.1-0.20220121202836-972a071d373d
github.com/nspcc-dev/neo-go v0.101.2-0.20230601131642-a0117042e8fc
github.com/nspcc-dev/neo-go v0.101.5-0.20230808195420-5fc61be5f6c5
github.com/panjf2000/ants/v2 v2.5.0
github.com/prometheus/client_golang v1.15.1
github.com/prometheus/client_model v0.3.0
@ -23,20 +23,19 @@ require (
github.com/urfave/cli/v2 v2.3.0
go.opentelemetry.io/otel v1.16.0
go.opentelemetry.io/otel/trace v1.16.0
go.uber.org/zap v1.24.0
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.9.0
google.golang.org/grpc v1.55.0
google.golang.org/protobuf v1.30.0
)
require (
git.frostfs.info/TrueCloudLab/frostfs-contract v0.0.0-20230307110621-19a8ef2d02fb // indirect
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231004065251-4194633db7bb // indirect
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 // indirect
git.frostfs.info/TrueCloudLab/hrw v1.2.1 // indirect
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 // indirect
git.frostfs.info/TrueCloudLab/tzhash v1.8.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/benbjohnson/clock v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
@ -47,9 +46,8 @@ require (
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
@ -63,7 +61,7 @@ require (
github.com/nats-io/nkeys v0.3.0 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20230615193820-9185820289ce // indirect
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20230808195420-5fc61be5f6c5 // indirect
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect

689
go.sum

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
package frostfsid
import (
"context"
"fmt"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/nns"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
type FrostFSID struct {
cli *client.Client
}
type Config struct {
// RPCAddress is an endpoint to connect to neo rpc.
RPCAddress string
// Contract is hash of contract or its name in NNS.
Contract string
// Key is used to interact with frostfsid contract.
// If this is nil than random key will be generated.
Key *keys.PrivateKey
}
var _ middleware.FrostFSID = (*FrostFSID)(nil)
// New creates new FrostfsID contract wrapper that implements auth.FrostFSID interface.
func New(ctx context.Context, cfg Config) (*FrostFSID, error) {
rpcCli, err := rpcclient.New(ctx, cfg.RPCAddress, rpcclient.Options{})
if err != nil {
return nil, fmt.Errorf("init rpc client: %w", err)
}
contractHash, err := fetchContractHash(rpcCli, cfg.Contract)
if err != nil {
return nil, fmt.Errorf("resolve frostfs contract hash: %w", err)
}
key := cfg.Key
if key == nil {
if key, err = keys.NewPrivateKey(); err != nil {
return nil, fmt.Errorf("generate anon private key for frostfsid: %w", err)
}
}
cli, err := client.New(rpcCli, wallet.NewAccountFromPrivateKey(key), contractHash, nil)
if err != nil {
return nil, fmt.Errorf("init frostfsid client: %w", err)
}
return &FrostFSID{
cli: cli,
}, nil
}
func (f *FrostFSID) ValidatePublicKey(key *keys.PublicKey) error {
_, err := f.cli.GetSubjectByKey(key)
return err
}
func fetchContractHash(rpcCli *rpcclient.Client, contractName string) (util.Uint160, error) {
if hash, err := util.Uint160DecodeStringLE(contractName); err == nil {
return hash, nil
}
splitName := strings.Split(contractName, ".")
if len(splitName) != 2 {
return util.Uint160{}, fmt.Errorf("invalid contract name: '%s'", contractName)
}
return nns.ResolveHash(rpcCli, contractName)
}

View file

@ -0,0 +1,62 @@
package nns
import (
"errors"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// ResolveHash resolves contract hash.
func ResolveHash(cli *rpcclient.Client, domain string) (util.Uint160, error) {
nnsCs, err := cli.GetContractStateByID(1)
if err != nil {
return util.Uint160{}, fmt.Errorf("get NNS contract by id: %w", err)
}
item, err := nnsResolve(invoker.New(cli, nil), nnsCs.Hash, domain)
if err != nil {
return util.Uint160{}, err
}
return parseNNSResolveResult(item)
}
func nnsResolve(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (stackitem.Item, error) {
return unwrap.Item(inv.Call(nnsHash, "resolve", domain, int64(nns.TXT)))
}
// parseNNSResolveResult parses the result of resolving NNS record.
// It works with multiple formats (corresponding to multiple NNS versions).
// If array of hashes is provided, it returns only the first one.
func parseNNSResolveResult(res stackitem.Item) (util.Uint160, error) {
arr, ok := res.Value().([]stackitem.Item)
if !ok {
arr = []stackitem.Item{res}
}
if _, ok := res.Value().(stackitem.Null); ok || len(arr) == 0 {
return util.Uint160{}, errors.New("NNS record is missing")
}
for i := range arr {
bs, err := arr[i].TryBytes()
if err != nil {
continue
}
h, err := address.StringToUint160(string(bs))
if err == nil {
return h, nil
}
h, err = util.Uint160DecodeStringLE(string(bs))
if err == nil {
return h, nil
}
}
return util.Uint160{}, errors.New("no valid hashes are found")
}

View file

@ -114,4 +114,6 @@ const (
CouldNotInitializeAPIHandler = "could not initialize API handler" // Fatal in ../../cmd/s3-gw/app.go
RuntimeSoftMemoryDefinedWithGOMEMLIMIT = "soft runtime memory defined with GOMEMLIMIT environment variable, config value skipped" // Warn in ../../cmd/s3-gw/app.go
RuntimeSoftMemoryLimitUpdated = "soft runtime memory limit value updated" // Info in ../../cmd/s3-gw/app.go
AnonRequestSkipIAMValidation = "anon request, skip IAM validation" // Debug in ../../api/middleware/auth.go
IAMValidationFailed = "IAM validation failed" // Error in ../../api/middleware/auth.go
)