forked from TrueCloudLab/frostfs-s3-gw
298 lines
9.1 KiB
Go
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
|
|
}
|