frostfs-s3-gw/creds/tokens/credentials.go
Denis Kirillov 3cf27d281d [#509] Support fallback address when getting box
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-10-23 15:01:31 +03:00

298 lines
9.1 KiB
Go

package tokens
import (
"context"
"errors"
"fmt"
"strconv"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
"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/client"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"go.uber.org/zap"
)
type (
// Credentials is a bearer token get/put interface.
Credentials interface {
GetBox(context.Context, cid.ID, string) (*accessbox.Box, []object.Attribute, error)
Put(context.Context, CredentialsParam) (oid.Address, error)
Update(context.Context, CredentialsParam) (oid.Address, error)
}
CredentialsParam struct {
Container cid.ID
AccessKeyID string
AccessBox *accessbox.AccessBox
Expiration uint64
Keys keys.PublicKeys
CustomAttributes []object.Attribute
}
cred struct {
key *keys.PrivateKey
frostFS FrostFS
cache *cache.AccessBoxCache
removingCheckDuration time.Duration
log *zap.Logger
}
Config struct {
FrostFS FrostFS
Key *keys.PrivateKey
CacheConfig *cache.Config
RemovingCheckAfterDurations time.Duration
}
Box struct {
AccessBox *accessbox.AccessBox
Attributes []object.Attribute
Address *oid.Address
}
)
// PrmObjectCreate groups parameters of objects created by credential tool.
type PrmObjectCreate struct {
// FrostFS container to store the object.
Container cid.ID
// File path.
Filepath string
// Optional.
// If provided cred object will be created using crdt approach.
NewVersionForAccessKeyID string
// Optional.
// If provided cred object will contain specific crdt name attribute for first accessbox object version.
// If NewVersionForAccessKeyID is provided this field isn't used.
CustomAccessKey string
// Last FrostFS epoch of the object lifetime.
ExpirationEpoch uint64
// Object payload.
Payload []byte
// CustomAttributes are additional user provided attributes for box object.
CustomAttributes []object.Attribute
}
// PrmGetCredsObject groups parameters of getting credential object.
type PrmGetCredsObject struct {
// FrostFS container to get the object.
Container cid.ID
// S3 access key id.
AccessKeyID string
// FallbackAddress is an address that should be used to get creds if we couldn't find it by AccessKeyID.
// Optional.
FallbackAddress *oid.Address
}
var ErrCustomAccessKeyIDNotFound = errors.New("custom AccessKeyId not found")
// FrostFS represents virtual connection to FrostFS network.
type FrostFS interface {
// CreateObject creates and saves a parameterized object in the specified
// FrostFS container from a specific user. It sets 'Timestamp' attribute to the current time.
// It returns the ID of the saved object.
//
// It returns exactly one non-nil value. It returns any error encountered which
// prevented the object from being created.
CreateObject(context.Context, PrmObjectCreate) (oid.ID, error)
// GetCredsObject gets the credential object from FrostFS network.
// It uses search by system name and select using CRDT 2PSet. In case of absence CRDT header
// it heads object by address.
//
// It returns exactly one non-nil value. It returns any error encountered which
// prevented the object payload from being read.
// Returns ErrCustomAccessKeyIDNotFound if provided AccessKey is custom, and it was not found.
// Object must contain full payload.
GetCredsObject(context.Context, PrmGetCredsObject) (*object.Object, error)
}
var (
// ErrEmptyPublicKeys is returned when no HCS keys are provided.
ErrEmptyPublicKeys = errors.New("HCS public keys could not be empty")
// ErrEmptyBearerToken is returned when no bearer token is provided.
ErrEmptyBearerToken = errors.New("Bearer token could not be empty")
)
var _ Credentials = (*cred)(nil)
// New creates a new Credentials instance using the given cli and key.
func New(cfg Config) Credentials {
return &cred{
frostFS: cfg.FrostFS,
key: cfg.Key,
cache: cache.NewAccessBoxCache(cfg.CacheConfig),
removingCheckDuration: cfg.RemovingCheckAfterDurations,
log: cfg.CacheConfig.Logger,
}
}
func (c *cred) GetBox(ctx context.Context, cnrID cid.ID, accessKeyID string) (*accessbox.Box, []object.Attribute, error) {
cachedBoxValue := c.cache.Get(accessKeyID)
if cachedBoxValue != nil {
return c.checkIfCredentialsAreRemoved(ctx, cnrID, accessKeyID, cachedBoxValue)
}
box, err := c.getAccessBox(ctx, cnrID, accessKeyID, nil)
if err != nil {
return nil, nil, fmt.Errorf("get access box: %w", err)
}
cachedBox, err := box.AccessBox.GetBox(c.key)
if err != nil {
return nil, nil, fmt.Errorf("get gate box: %w", err)
}
val := &cache.AccessBoxCacheValue{
Box: cachedBox,
Attributes: box.Attributes,
PutTime: time.Now(),
Address: box.Address,
}
c.putBoxToCache(accessKeyID, val)
return cachedBox, box.Attributes, nil
}
func (c *cred) checkIfCredentialsAreRemoved(ctx context.Context, cnrID cid.ID, accessKeyID string, cachedBoxValue *cache.AccessBoxCacheValue) (*accessbox.Box, []object.Attribute, error) {
if time.Since(cachedBoxValue.PutTime) < c.removingCheckDuration {
return cachedBoxValue.Box, cachedBoxValue.Attributes, nil
}
box, err := c.getAccessBox(ctx, cnrID, accessKeyID, cachedBoxValue.Address)
if err != nil {
if client.IsErrObjectAlreadyRemoved(err) {
c.cache.Delete(accessKeyID)
return nil, nil, fmt.Errorf("get access box: %w", err)
}
return cachedBoxValue.Box, cachedBoxValue.Attributes, nil
}
cachedBox, err := box.AccessBox.GetBox(c.key)
if err != nil {
c.cache.Delete(accessKeyID)
return nil, nil, fmt.Errorf("get gate box: %w", err)
}
// we need this to reset PutTime
// to don't check for removing each time after removingCheckDuration interval
val := &cache.AccessBoxCacheValue{
Box: cachedBox,
Attributes: box.Attributes,
PutTime: time.Now(),
Address: box.Address,
}
c.putBoxToCache(accessKeyID, val)
return cachedBoxValue.Box, box.Attributes, nil
}
func (c *cred) putBoxToCache(accessKeyID string, val *cache.AccessBoxCacheValue) {
if err := c.cache.Put(accessKeyID, val); err != nil {
c.log.Warn(logs.CouldntPutAccessBoxIntoCache, zap.String("accessKeyID", accessKeyID))
}
}
func (c *cred) getAccessBox(ctx context.Context, cnrID cid.ID, accessKeyID string, fallbackAddr *oid.Address) (*Box, error) {
prm := PrmGetCredsObject{
Container: cnrID,
AccessKeyID: accessKeyID,
FallbackAddress: fallbackAddr,
}
obj, err := c.frostFS.GetCredsObject(ctx, prm)
if err != nil {
return nil, fmt.Errorf("read payload and attributes: %w", err)
}
// decode access box
var box accessbox.AccessBox
if err = box.Unmarshal(obj.Payload()); err != nil {
return nil, fmt.Errorf("unmarhal access box: %w", err)
}
addr := &oid.Address{}
boxCnrID, cnrIDOk := obj.ContainerID()
boxObjID, objIDOk := obj.ID()
addr.SetContainer(boxCnrID)
addr.SetObject(boxObjID)
if !cnrIDOk || !objIDOk {
addr = nil
}
return &Box{
AccessBox: &box,
Attributes: obj.Attributes(),
Address: addr,
}, nil
}
func (c *cred) Put(ctx context.Context, prm CredentialsParam) (oid.Address, error) {
if prm.AccessKeyID != "" {
c.log.Info(logs.CheckCustomAccessKeyIDUniqueness, zap.String("access_key_id", prm.AccessKeyID))
credsPrm := PrmGetCredsObject{
Container: prm.Container,
AccessKeyID: prm.AccessKeyID,
}
if _, err := c.frostFS.GetCredsObject(ctx, credsPrm); err == nil {
return oid.Address{}, fmt.Errorf("access key id '%s' already exists", prm.AccessKeyID)
} else if !errors.Is(err, ErrCustomAccessKeyIDNotFound) {
return oid.Address{}, fmt.Errorf("check AccessKeyID uniqueness: %w", err)
}
}
return c.createObject(ctx, prm, false)
}
func (c *cred) Update(ctx context.Context, prm CredentialsParam) (oid.Address, error) {
return c.createObject(ctx, prm, true)
}
func (c *cred) createObject(ctx context.Context, prm CredentialsParam, update bool) (oid.Address, error) {
if len(prm.Keys) == 0 {
return oid.Address{}, ErrEmptyPublicKeys
} else if prm.AccessBox == nil {
return oid.Address{}, ErrEmptyBearerToken
}
data, err := prm.AccessBox.Marshal()
if err != nil {
return oid.Address{}, fmt.Errorf("marshall box: %w", err)
}
var newVersionFor string
if update {
newVersionFor = prm.AccessKeyID
}
idObj, err := c.frostFS.CreateObject(ctx, PrmObjectCreate{
Container: prm.Container,
Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box",
ExpirationEpoch: prm.Expiration,
CustomAccessKey: prm.AccessKeyID,
NewVersionForAccessKeyID: newVersionFor,
Payload: data,
CustomAttributes: prm.CustomAttributes,
})
if err != nil {
return oid.Address{}, fmt.Errorf("create object: %w", err)
}
var addr oid.Address
addr.SetObject(idObj)
addr.SetContainer(prm.Container)
return addr, nil
}