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 }