Compare commits

...

3 commits

Author SHA1 Message Date
ca46dc5ec1 [#XX] Don't reload rpc_endpoint for resolvers
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-10-12 18:08:11 +03:00
eccc3f8077 [#XX] Use contract resolver from sdk
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-10-12 18:04:22 +03:00
e3b6f534cc [#XX] Support frostfsid validation
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-10-05 17:37:34 +03:00
11 changed files with 902 additions and 45 deletions

View file

@ -486,7 +486,7 @@ the corresponding header to the upload request. Accessing the ACL protected data
works the same way.
##### Example
In order to generate a bearer token, you need to have wallet (which will be used to sign the token) and
In order to generate a bearer token, you need to have a wallet (which will be used to sign the token) and
the address of the sender who will do the request to FrostFS (in our case, it's a gateway wallet address).
Suppose we have:

View file

@ -2,6 +2,7 @@ package main
import (
"context"
"crypto/elliptic"
"fmt"
"net/http"
"os"
@ -11,6 +12,8 @@ import (
"syscall"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/frostfs/frostfsid"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/frostfs/services"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/handler"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/logs"
@ -21,6 +24,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/tree"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
"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"
@ -52,6 +56,7 @@ type (
services []*metrics.Service
settings *appSettings
servers []Server
frostfsid *frostfsid.FrostFSID
}
// App is an interface for the main gateway function.
@ -135,6 +140,7 @@ func newApp(ctx context.Context, opt ...Option) App {
a.initAppSettings()
a.initResolver()
a.initMetrics()
a.initIAM(ctx)
a.initTracing(ctx)
return a
@ -172,20 +178,22 @@ func (a *app) initAppSettings() {
func (a *app) initResolver() {
var err error
a.resolver, err = resolver.NewContainerResolver(a.getResolverConfig())
a.resolver, err = resolver.NewContainerResolver(a.getResolverOrder(), a.getResolverConfig())
if err != nil {
a.log.Fatal(logs.FailedToCreateResolver, zap.Error(err))
}
}
func (a *app) getResolverConfig() ([]string, *resolver.Config) {
resolveCfg := &resolver.Config{
func (a *app) getResolverConfig() *resolver.Config {
return &resolver.Config{
FrostFS: resolver.NewFrostFSResolver(a.pool),
RPCAddress: a.cfg.GetString(cfgRPCEndpoint),
}
}
func (a *app) getResolverOrder() []string {
order := a.cfg.GetStringSlice(cfgResolveOrder)
if resolveCfg.RPCAddress == "" {
if a.cfg.GetString(cfgRPCEndpoint) == "" {
order = remove(order, resolver.NNSResolver)
a.log.Warn(logs.ResolverNNSWontBeUsedSinceRPCEndpointIsntProvided)
}
@ -194,7 +202,7 @@ func (a *app) getResolverConfig() ([]string, *resolver.Config) {
a.log.Info(logs.ContainerResolverWillBeDisabledBecauseOfResolversResolverOrderIsEmpty)
}
return order, resolveCfg
return order
}
func (a *app) initMetrics() {
@ -203,6 +211,22 @@ func (a *app) initMetrics() {
a.metrics.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 newGateMetrics(logger *zap.Logger, provider *metrics.GateMetrics, enabled bool) *gateMetrics {
if !enabled {
logger.Warn(logs.MetricsAreDisabled)
@ -423,7 +447,7 @@ func (a *app) configReload(ctx context.Context) {
a.logLevel.SetLevel(lvl)
}
if err := a.resolver.UpdateResolvers(a.getResolverConfig()); err != nil {
if err := a.resolver.UpdateResolvers(a.getResolverOrder()); err != nil {
a.log.Warn(logs.FailedToUpdateResolvers, zap.Error(err))
}
@ -481,20 +505,75 @@ func (a *app) configureRouter(handler *handler.Handler) {
response.Error(r, "Method Not Allowed", fasthttp.StatusMethodNotAllowed)
}
r.POST("/upload/{cid}", a.logger(a.tokenizer(a.tracer(handler.Upload))))
r.POST("/upload/{cid}", a.addMiddlewares(handler.Upload))
a.log.Info(logs.AddedPathUploadCid)
r.GET("/get/{cid}/{oid:*}", a.logger(a.tokenizer(a.tracer(handler.DownloadByAddressOrBucketName))))
r.HEAD("/get/{cid}/{oid:*}", a.logger(a.tokenizer(a.tracer(handler.HeadByAddressOrBucketName))))
r.GET("/get/{cid}/{oid:*}", a.addMiddlewares(handler.DownloadByAddressOrBucketName))
r.HEAD("/get/{cid}/{oid:*}", a.addMiddlewares(handler.HeadByAddressOrBucketName))
a.log.Info(logs.AddedPathGetCidOid)
r.GET("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.logger(a.tokenizer(a.tracer(handler.DownloadByAttribute))))
r.HEAD("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.logger(a.tokenizer(a.tracer(handler.HeadByAttribute))))
r.GET("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.addMiddlewares(handler.DownloadByAttribute))
r.HEAD("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.addMiddlewares(handler.HeadByAttribute))
a.log.Info(logs.AddedPathGetByAttributeCidAttrKeyAttrVal)
r.GET("/zip/{cid}/{prefix:*}", a.logger(a.tokenizer(a.tracer(handler.DownloadZipped))))
r.GET("/zip/{cid}/{prefix:*}", a.addMiddlewares(handler.DownloadZipped))
a.log.Info(logs.AddedPathZipCidPrefix)
a.webServer.Handler = r.Handler
}
func (a *app) addMiddlewares(handler fasthttp.RequestHandler) fasthttp.RequestHandler {
list := []func(fasthttp.RequestHandler) fasthttp.RequestHandler{
a.logger,
a.tokenizer,
a.tracer,
}
if a.frostfsid != nil {
list = append(list, a.iam)
}
res := handler
for i := len(list) - 1; i >= 0; i-- {
res = list[i](res)
}
return res
}
func (a *app) iam(h fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(req *fasthttp.RequestCtx) {
ctx := utils.GetContextFromRequest(req)
tkn, err := tokens.LoadBearerToken(ctx)
if err != nil || tkn == nil {
a.log.Debug(logs.AnonRequestSkipIAMValidation, zap.Uint64("id", req.ID()))
h(req)
return
}
if err = validateBearerToken(a.frostfsid, tkn); err != nil {
a.log.Error(logs.IAMValidationFailed, zap.Uint64("id", req.ID()), zap.Error(err))
response.Error(req, "iam validation failed: "+err.Error(), fasthttp.StatusForbidden)
return
}
h(req)
}
}
func validateBearerToken(frostfsID *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
}
func (a *app) logger(h fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(req *fasthttp.RequestCtx) {
a.log.Info(logs.Request, zap.String("remote", req.RemoteAddr().String()),

View file

@ -96,6 +96,10 @@ const (
// Runtime.
cfgSoftMemoryLimit = "runtime.soft_memory_limit"
// FrostfsID.
cfgFrostfsIDEnabled = "frostfsid.enabled"
cfgFrostfsIDContract = "frostfsid.contract"
// Command line args.
cmdHelp = "help"
cmdVersion = "version"
@ -175,6 +179,9 @@ func settings() *viper.Viper {
v.SetDefault(cfgPprofAddress, "localhost:8083")
v.SetDefault(cfgPrometheusAddress, "localhost:8084")
// frostfsid
v.SetDefault(cfgFrostfsIDContract, "frostfsid.frostfs")
// Binding flags
if err := v.BindPFlag(cfgPprofEnabled, flags.Lookup(cmdPprof)); err != nil {
panic(err)

View file

@ -98,3 +98,9 @@ HTTP_GW_TRACING_ENDPOINT="localhost:4317"
HTTP_GW_TRACING_EXPORTER="otlp_grpc"
HTTP_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.
HTTP_GW_FROSTFSID_ENABLED=false
# FrostfsID contract hash (LE) or name in NNS.
HTTP_GW_FROSTFSID_CONTRACT=frostfsid.frostfs

View file

@ -104,3 +104,11 @@ zip:
runtime:
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

@ -54,6 +54,7 @@ $ cat http.log
| `prometheus` | [Prometheus configuration](#prometheus-section) |
| `tracing` | [Tracing configuration](#tracing-section) |
| `runtime` | [Runtime configuration](#runtime-section) |
| `frostfsid` | [FrostfsID configuration](#frostfsid-section) |
# General section
@ -72,14 +73,14 @@ pool_error_threshold: 100
```
| Parameter | Type | SIGHUP reload | Default value | Description |
|------------------------|------------|---------------|----------------|------------------------------------------------------------------------------------|
| `rpc_endpoint` | `string` | yes | | The address of the RPC host to which the gateway connects to resolve bucket names. |
| `resolve_order` | `[]string` | yes | `[nns, dns]` | Order of bucket name resolvers to use. |
| `connect_timeout` | `duration` | | `10s` | Timeout to connect to a node. |
| `stream_timeout` | `duration` | | `10s` | Timeout for individual operations in streaming RPC. |
| `request_timeout` | `duration` | | `15s` | Timeout to check node health during rebalance. |
| `rebalance_timer` | `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. |
|------------------------|------------|---------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `rpc_endpoint` | `string` | no | | 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 | `[nns, dns]` | Order of bucket name resolvers to use. Available resolvers: `dns`, `nns`. |
| `connect_timeout` | `duration` | no | `10s` | Timeout to connect to a node. |
| `stream_timeout` | `duration` | no | `10s` | Timeout for individual operations in streaming RPC. |
| `request_timeout` | `duration` | no | `15s` | Timeout to check node health during rebalance. |
| `rebalance_timer` | `duration` | no | `60s` | Interval to check node health. |
| `pool_error_threshold` | `uint32` | no | `100` | The number of errors on connection after which node is considered as unhealthy. |
# `wallet` section
@ -269,3 +270,18 @@ 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. |
# `frostfsid` section
FrostfsID contract configuration. To enable this functionality the `rpc_endpoint` param must be also set.
```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. |

10
go.mod
View file

@ -4,10 +4,11 @@ go 1.20
require (
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.15.1-0.20230802075510-964c3edb3f44
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231004065251-4194633db7bb
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20230531082742-c97d21411eb6
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20230802103237-363f153eafa6
github.com/fasthttp/router v1.4.1
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/prometheus/client_golang v1.15.1
github.com/prometheus/client_model v0.3.0
github.com/spf13/pflag v1.0.5
@ -17,12 +18,11 @@ require (
github.com/valyala/fasthttp v1.34.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
google.golang.org/grpc v1.55.0
)
require (
git.frostfs.info/TrueCloudLab/frostfs-contract v0.0.0-20230307110621-19a8ef2d02fb // 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
@ -37,7 +37,7 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/cgroups v1.0.3 // indirect
github.com/containerd/containerd v1.6.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
@ -67,7 +67,7 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // 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/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect

675
go.sum

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,87 @@
package frostfsid
import (
"context"
"fmt"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ns"
"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
}
// New creates new FrostfsID contract wrapper that implements auth.FrostFSID interface.
func New(ctx context.Context, cfg Config) (*FrostFSID, error) {
contractHash, err := fetchContractHash(cfg)
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)
}
}
rpcCli, err := rpcclient.New(ctx, cfg.RPCAddress, rpcclient.Options{})
if err != nil {
return nil, fmt.Errorf("init rpc client: %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(cfg Config) (util.Uint160, error) {
if hash, err := util.Uint160DecodeStringLE(cfg.Contract); err == nil {
return hash, nil
}
splitName := strings.Split(cfg.Contract, ".")
if len(splitName) != 2 {
return util.Uint160{}, fmt.Errorf("invalid contract name: '%s'", cfg.Contract)
}
var domain container.Domain
domain.SetName(splitName[0])
domain.SetZone(splitName[1])
var nns ns.NNS
if err := nns.Dial(cfg.RPCAddress); err != nil {
return util.Uint160{}, fmt.Errorf("dial nns %s: %w", cfg.RPCAddress, err)
}
return nns.ResolveContractHash(domain)
}

View file

@ -68,4 +68,6 @@ const (
FailedToCreateTreePool = "failed to create tree pool" // Fatal in ../../settings.go
FailedToDialTreePool = "failed to dial tree pool" // Fatal in ../../settings.go
AddedStoragePeer = "added storage peer" // Info in ../../settings.go
AnonRequestSkipIAMValidation = "anon request, skip IAM validation" // Debug in ../../app.go
IAMValidationFailed = "IAM validation failed" // Error in ../../app.go
)

View file

@ -34,6 +34,9 @@ type Config struct {
}
type ContainerResolver struct {
rpcAddress string
frostfs FrostFS
mu sync.RWMutex
resolvers []*Resolver
}
@ -58,6 +61,8 @@ func NewContainerResolver(resolverNames []string, cfg *Config) (*ContainerResolv
}
return &ContainerResolver{
rpcAddress: cfg.RPCAddress,
frostfs: cfg.FrostFS,
resolvers: resolvers,
}, nil
}
@ -101,7 +106,7 @@ func (r *ContainerResolver) Resolve(ctx context.Context, cnrName string) (*cid.I
return nil, ErrNoResolvers
}
func (r *ContainerResolver) UpdateResolvers(resolverNames []string, cfg *Config) error {
func (r *ContainerResolver) UpdateResolvers(resolverNames []string) error {
r.mu.Lock()
defer r.mu.Unlock()
@ -109,7 +114,7 @@ func (r *ContainerResolver) UpdateResolvers(resolverNames []string, cfg *Config)
return nil
}
resolvers, err := createResolvers(resolverNames, cfg)
resolvers, err := createResolvers(resolverNames, &Config{FrostFS: r.frostfs, RPCAddress: r.rpcAddress})
if err != nil {
return err
}