Support custom s3 credentials #515

Merged
alexvanin merged 7 commits from dkirillov/frostfs-s3-gw:poc/custom_aws_creds into master 2024-11-02 14:21:44 +00:00
28 changed files with 1098 additions and 437 deletions

View file

@ -9,6 +9,7 @@ This document outlines major changes between releases.
- Support new param `frostfs.graceful_close_on_switch_timeout` (#475)
- Support patch object method (#479)
- Add `sign` command to `frostfs-s3-authmate` (#467)
- Support custom aws credentials (#509)
### Changed
- Update go version to go1.19 (#470)

View file

@ -18,6 +18,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"github.com/aws/aws-sdk-go/aws/credentials"
)
@ -34,6 +35,11 @@ type (
postReg *RegexpSubmatcher
cli tokens.Credentials
allowedAccessKeyIDPrefixes []string // empty slice means all access key ids are allowed
settings CenterSettings
}
CenterSettings interface {
AccessBoxContainer() (cid.ID, bool)
}
//nolint:revive
@ -50,7 +56,6 @@ type (
)
const (
accessKeyPartsNum = 2
authHeaderPartsNum = 6
maxFormSizeMemory = 50 * 1048576 // 50 MB
@ -82,12 +87,13 @@ var ContentSHA256HeaderStandardValue = map[string]struct{}{
}
// New creates an instance of AuthCenter.
func New(creds tokens.Credentials, prefixes []string) *Center {
func New(creds tokens.Credentials, prefixes []string, settings CenterSettings) *Center {
return &Center{
cli: creds,
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
allowedAccessKeyIDPrefixes: prefixes,
settings: settings,
}
}
@ -97,11 +103,6 @@ func (c *Center) parseAuthHeader(header string) (*AuthHeader, error) {
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), header)
}
accessKey := strings.Split(submatches["access_key_id"], "0")
if len(accessKey) != accessKeyPartsNum {
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrInvalidAccessKeyID), accessKey)
}
signedFields := strings.Split(submatches["signed_header_fields"], ";")
return &AuthHeader{
@ -114,15 +115,6 @@ func (c *Center) parseAuthHeader(header string) (*AuthHeader, error) {
}, nil
}
func getAddress(accessKeyID string) (oid.Address, error) {
var addr oid.Address
if err := addr.DecodeString(strings.ReplaceAll(accessKeyID, "0", "/")); err != nil {
return addr, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrInvalidAccessKeyID), accessKeyID)
}
return addr, nil
}
func IsStandardContentSHA256(key string) bool {
_, ok := ContentSHA256HeaderStandardValue[key]
return ok
@ -181,14 +173,14 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
return nil, err
}
addr, err := getAddress(authHdr.AccessKeyID)
cnrID, err := c.getAccessBoxContainer(authHdr.AccessKeyID)
if err != nil {
return nil, err
}
box, attrs, err := c.cli.GetBox(r.Context(), addr)
box, attrs, err := c.cli.GetBox(r.Context(), cnrID, authHdr.AccessKeyID)
if err != nil {
return nil, fmt.Errorf("get box '%s': %w", addr, err)
return nil, fmt.Errorf("get box by access key '%s': %w", authHdr.AccessKeyID, err)
}
if err = checkFormatHashContentSHA256(r.Header.Get(AmzContentSHA256)); err != nil {
@ -216,6 +208,20 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
return result, nil
}
func (c *Center) getAccessBoxContainer(accessKeyID string) (cid.ID, error) {
var addr oid.Address
if err := addr.DecodeString(strings.ReplaceAll(accessKeyID, "0", "/")); err == nil {
return addr.Container(), nil
}
cnrID, ok := c.settings.AccessBoxContainer()
if ok {
return cnrID, nil
}
return cid.ID{}, fmt.Errorf("%w: unknown container for creds '%s'", apierr.GetAPIError(apierr.ErrInvalidAccessKeyID), accessKeyID)
}
func checkFormatHashContentSHA256(hash string) error {
if !IsStandardContentSHA256(hash) {
hashBinary, err := hex.DecodeString(hash)
@ -272,14 +278,14 @@ func (c *Center) checkFormData(r *http.Request) (*middleware.Box, error) {
accessKeyID := submatches["access_key_id"]
addr, err := getAddress(accessKeyID)
cnrID, err := c.getAccessBoxContainer(accessKeyID)
if err != nil {
return nil, err
}
box, attrs, err := c.cli.GetBox(r.Context(), addr)
box, attrs, err := c.cli.GetBox(r.Context(), cnrID, accessKeyID)
if err != nil {
return nil, fmt.Errorf("get box '%s': %w", addr, err)
return nil, fmt.Errorf("get box by accessKeyID '%s': %w", accessKeyID, err)
}
secret := box.Gate.SecretKey

View file

@ -19,6 +19,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
frosterr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
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"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
@ -28,11 +29,23 @@ import (
"go.uber.org/zap/zaptest"
)
type centerSettingsMock struct {
accessBoxContainer *cid.ID
}
func (c *centerSettingsMock) AccessBoxContainer() (cid.ID, bool) {
if c.accessBoxContainer == nil {
return cid.ID{}, false
}
return *c.accessBoxContainer, true
}
func TestAuthHeaderParse(t *testing.T) {
defaultHeader := "AWS4-HMAC-SHA256 Credential=oid0cid/20210809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=2811ccb9e242f41426738fb1f"
center := &Center{
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
settings: &centerSettingsMock{},
}
for _, tc := range []struct {
@ -57,11 +70,6 @@ func TestAuthHeaderParse(t *testing.T) {
err: errors.GetAPIError(errors.ErrAuthorizationHeaderMalformed),
expected: nil,
},
{
header: strings.ReplaceAll(defaultHeader, "oid0cid", "oidcid"),
err: errors.GetAPIError(errors.ErrInvalidAccessKeyID),
expected: nil,
},
} {
authHeader, err := center.parseAuthHeader(tc.header)
require.ErrorIs(t, err, tc.err, tc.header)
@ -69,43 +77,6 @@ func TestAuthHeaderParse(t *testing.T) {
}
}
func TestAuthHeaderGetAddress(t *testing.T) {
defaulErr := errors.GetAPIError(errors.ErrInvalidAccessKeyID)
for _, tc := range []struct {
authHeader *AuthHeader
err error
}{
{
authHeader: &AuthHeader{
AccessKeyID: "vWqF8cMDRbJcvnPLALoQGnABPPhw8NyYMcGsfDPfZJM0HrgjonN8CgFvCZ3kh9BUXw4W2tJ5E7EAGhueSF122HB",
},
err: nil,
},
{
authHeader: &AuthHeader{
AccessKeyID: "vWqF8cMDRbJcvnPLALoQGnABPPhw8NyYMcGsfDPfZJMHrgjonN8CgFvCZ3kh9BUXw4W2tJ5E7EAGhueSF122HB",
},
err: defaulErr,
},
{
authHeader: &AuthHeader{
AccessKeyID: "oid0cid",
},
err: defaulErr,
},
{
authHeader: &AuthHeader{
AccessKeyID: "oidcid",
},
err: defaulErr,
},
} {
_, err := getAddress(tc.authHeader.AccessKeyID)
require.ErrorIs(t, err, tc.err, tc.authHeader.AccessKeyID)
}
}
func TestSignature(t *testing.T) {
secret := "66be461c3cd429941c55daf42fad2b8153e5a2016ba89c9494d97677cc9d3872"
strToSign := "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAiYWNsIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwKICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDg0L2FjbCJ9LAogICAgWyJzdGFydHMtd2l0aCIsICIkQ29udGVudC1UeXBlIiwgImltYWdlLyJdLAogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwKICAgIFsic3RhcnRzLXdpdGgiLCAiJHgtYW16LW1ldGEtdGFnIiwgIiJdLAoKICAgIHsiWC1BbXotQ3JlZGVudGlhbCI6ICI4Vmk0MVBIbjVGMXNzY2J4OUhqMXdmMUU2aERUYURpNndxOGhxTU05NllKdTA1QzVDeUVkVlFoV1E2aVZGekFpTkxXaTlFc3BiUTE5ZDRuR3pTYnZVZm10TS8yMDE1MTIyOS91cy1lYXN0LTEvczMvYXdzNF9yZXF1ZXN0In0sCiAgICB7IngtYW16LWFsZ29yaXRobSI6ICJBV1M0LUhNQUMtU0hBMjU2In0sCiAgICB7IlgtQW16LURhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfSwKICAgIHsieC1pZ25vcmUtdG1wIjogInNvbWV0aGluZyIgfQogIF0KfQ=="
@ -171,17 +142,17 @@ func TestCheckFormatContentSHA256(t *testing.T) {
}
type frostFSMock struct {
objects map[oid.Address]*object.Object
objects map[string]*object.Object
}
func newFrostFSMock() *frostFSMock {
return &frostFSMock{
objects: map[oid.Address]*object.Object{},
objects: map[string]*object.Object{},
}
}
func (f *frostFSMock) GetCredsObject(_ context.Context, address oid.Address) (*object.Object, error) {
obj, ok := f.objects[address]
func (f *frostFSMock) GetCredsObject(_ context.Context, prm tokens.PrmGetCredsObject) (*object.Object, error) {
obj, ok := f.objects[prm.AccessKeyID]
if !ok {
return nil, fmt.Errorf("not found")
}
@ -208,7 +179,7 @@ func TestAuthenticate(t *testing.T) {
GateKey: key.PublicKey(),
}}
accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret"))
accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret"), false)
require.NoError(t, err)
data, err := accessBox.Marshal()
require.NoError(t, err)
@ -219,10 +190,10 @@ func TestAuthenticate(t *testing.T) {
obj.SetContainerID(addr.Container())
obj.SetID(addr.Object())
frostfs := newFrostFSMock()
frostfs.objects[addr] = &obj
accessKeyID := getAccessKeyID(addr)
accessKeyID := addr.Container().String() + "0" + addr.Object().String()
frostfs := newFrostFSMock()
frostfs.objects[accessKeyID] = &obj
awsCreds := credentials.NewStaticCredentials(accessKeyID, secret.SecretKey, "")
defaultSigner := v4.NewSigner(awsCreds)
@ -413,7 +384,7 @@ func TestAuthenticate(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
creds := tokens.New(bigConfig)
cntr := New(creds, tc.prefixes)
cntr := New(creds, tc.prefixes, &centerSettingsMock{})
box, err := cntr.Authenticate(tc.request)
if tc.err {
@ -455,7 +426,7 @@ func TestHTTPPostAuthenticate(t *testing.T) {
GateKey: key.PublicKey(),
}}
accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret"))
accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret"), false)
require.NoError(t, err)
data, err := accessBox.Marshal()
require.NoError(t, err)
@ -466,10 +437,11 @@ func TestHTTPPostAuthenticate(t *testing.T) {
obj.SetContainerID(addr.Container())
obj.SetID(addr.Object())
frostfs := newFrostFSMock()
frostfs.objects[addr] = &obj
accessKeyID := getAccessKeyID(addr)
frostfs := newFrostFSMock()
frostfs.objects[accessKeyID] = &obj
accessKeyID := addr.Container().String() + "0" + addr.Object().String()
invalidAccessKeyID := oidtest.Address().String() + "0" + oidtest.Address().Object().String()
timeToSign := time.Now()
@ -590,7 +562,7 @@ func TestHTTPPostAuthenticate(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
creds := tokens.New(bigConfig)
cntr := New(creds, tc.prefixes)
cntr := New(creds, tc.prefixes, &centerSettingsMock{})
box, err := cntr.Authenticate(tc.request)
if tc.err {
@ -633,3 +605,7 @@ func getRequestWithMultipartForm(t *testing.T, policy, creds, date, sign, fieldN
return req
}
func getAccessKeyID(addr oid.Address) string {
return strings.ReplaceAll(addr.EncodeToString(), "/", "0")
}

View file

@ -29,11 +29,11 @@ func newTokensFrostfsMock() *credentialsMock {
}
func (m credentialsMock) addBox(addr oid.Address, box *accessbox.Box) {
m.boxes[addr.String()] = box
m.boxes[getAccessKeyID(addr)] = box
}
func (m credentialsMock) GetBox(_ context.Context, addr oid.Address) (*accessbox.Box, []object.Attribute, error) {
box, ok := m.boxes[addr.String()]
func (m credentialsMock) GetBox(_ context.Context, _ cid.ID, accessKeyID string) (*accessbox.Box, []object.Attribute, error) {
box, ok := m.boxes[accessKeyID]
if !ok {
return nil, nil, &apistatus.ObjectNotFound{}
}
@ -41,11 +41,11 @@ func (m credentialsMock) GetBox(_ context.Context, addr oid.Address) (*accessbox
return box, nil, nil
}
func (m credentialsMock) Put(context.Context, cid.ID, tokens.CredentialsParam) (oid.Address, error) {
func (m credentialsMock) Put(context.Context, tokens.CredentialsParam) (oid.Address, error) {
return oid.Address{}, nil
}
func (m credentialsMock) Update(context.Context, oid.Address, tokens.CredentialsParam) (oid.Address, error) {
func (m credentialsMock) Update(context.Context, tokens.CredentialsParam) (oid.Address, error) {
return oid.Address{}, nil
}
@ -84,9 +84,10 @@ func TestCheckSign(t *testing.T) {
mock.addBox(accessKeyAddr, expBox)
c := &Center{
cli: mock,
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
cli: mock,
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
settings: &centerSettingsMock{},
}
box, err := c.Authenticate(req)
require.NoError(t, err)

View file

@ -30,6 +30,7 @@ type (
Box *accessbox.Box
Attributes []object.Attribute
PutTime time.Time
Address *oid.Address
}
)
@ -57,8 +58,8 @@ func NewAccessBoxCache(config *Config) *AccessBoxCache {
}
// Get returns a cached accessbox.
func (o *AccessBoxCache) Get(address oid.Address) *AccessBoxCacheValue {
entry, err := o.cache.Get(address)
func (o *AccessBoxCache) Get(accessKeyID string) *AccessBoxCacheValue {
entry, err := o.cache.Get(accessKeyID)
if err != nil {
return nil
}
@ -74,16 +75,11 @@ func (o *AccessBoxCache) Get(address oid.Address) *AccessBoxCacheValue {
}
// Put stores an accessbox to cache.
func (o *AccessBoxCache) Put(address oid.Address, box *accessbox.Box, attrs []object.Attribute) error {
val := &AccessBoxCacheValue{
Box: box,
Attributes: attrs,
PutTime: time.Now(),
}
return o.cache.Set(address, val)
func (o *AccessBoxCache) Put(accessKeyID string, val *AccessBoxCacheValue) error {
return o.cache.Set(accessKeyID, val)
}
// Delete removes an accessbox from cache.
func (o *AccessBoxCache) Delete(address oid.Address) {
o.cache.Remove(address)
func (o *AccessBoxCache) Delete(accessKeyID string) {
o.cache.Remove(accessKeyID)
}

View file

@ -1,13 +1,14 @@
package cache
import (
"strings"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -22,18 +23,21 @@ func TestAccessBoxCacheType(t *testing.T) {
addr := oidtest.Address()
box := &accessbox.Box{}
var attrs []object.Attribute
val := &AccessBoxCacheValue{
Box: box,
}
err := cache.Put(addr, box, attrs)
accessKeyID := getAccessKeyID(addr)
err := cache.Put(accessKeyID, val)
require.NoError(t, err)
val := cache.Get(addr)
require.Equal(t, box, val.Box)
require.Equal(t, attrs, val.Attributes)
resVal := cache.Get(accessKeyID)
require.Equal(t, box, resVal.Box)
require.Equal(t, 0, observedLog.Len())
err = cache.cache.Set(addr, "tmp")
err = cache.cache.Set(accessKeyID, "tmp")
require.NoError(t, err)
assertInvalidCacheEntry(t, cache.Get(addr), observedLog)
assertInvalidCacheEntry(t, cache.Get(accessKeyID), observedLog)
}
func TestBucketsCacheType(t *testing.T) {
@ -230,3 +234,7 @@ func getObservedLogger() (*zap.Logger, *observer.ObservedLogs) {
loggerCore, observedLog := observer.New(zap.WarnLevel)
return zap.New(loggerCore), observedLog
}
func getAccessKeyID(addr oid.Address) string {
return strings.ReplaceAll(addr.EncodeToString(), "/", "0")
}

View file

@ -98,7 +98,9 @@ type (
// IssueSecretOptions contains options for passing to Agent.IssueSecret method.
IssueSecretOptions struct {
Container ContainerOptions
Container cid.ID
AccessKeyID string
SecretAccessKey string
FrostFSKey *keys.PrivateKey
GatesPublicKeys []*keys.PublicKey
Impersonate bool
@ -114,7 +116,9 @@ type (
UpdateSecretOptions struct {
FrostFSKey *keys.PrivateKey
GatesPublicKeys []*keys.PublicKey
Address oid.Address
IsCustom bool
AccessKeyID string
ContainerID cid.ID
GatePrivateKey *keys.PrivateKey
CustomAttributes []object.Attribute
}
@ -141,7 +145,8 @@ type (
// ObtainSecretOptions contains options for passing to Agent.ObtainSecret method.
ObtainSecretOptions struct {
SecretAddress string
Container cid.ID
AccessKeyID string
GatePrivateKey *keys.PrivateKey
}
)
@ -168,32 +173,9 @@ type (
}
)
func (a *Agent) checkContainer(ctx context.Context, opts ContainerOptions, idOwner user.ID) (cid.ID, error) {
if !opts.ID.Equals(cid.ID{}) {
a.log.Info(logs.CheckContainer, zap.Stringer("cid", opts.ID))
return opts.ID, a.frostFS.ContainerExists(ctx, opts.ID)
}
a.log.Info(logs.CreateContainer,
zap.String("friendly_name", opts.FriendlyName),
zap.String("placement_policy", opts.PlacementPolicy))
var prm PrmContainerCreate
err := prm.Policy.DecodeString(opts.PlacementPolicy)
if err != nil {
return cid.ID{}, fmt.Errorf("failed to build placement policy: %w", err)
}
prm.Owner = idOwner
prm.FriendlyName = opts.FriendlyName
cnrID, err := a.frostFS.CreateContainer(ctx, prm)
if err != nil {
return cid.ID{}, fmt.Errorf("create container in FrostFS: %w", err)
}
return cnrID, nil
func (a *Agent) checkContainer(ctx context.Context, cnrID cid.ID) error {
a.log.Info(logs.CheckContainer, zap.Stringer("cid", cnrID))
return a.frostFS.ContainerExists(ctx, cnrID)
}
func checkPolicy(policyString string) (*netmap.PlacementPolicy, error) {
@ -255,20 +237,24 @@ func (a *Agent) IssueSecret(ctx context.Context, w io.Writer, options *IssueSecr
return fmt.Errorf("create tokens: %w", err)
}
box, secrets, err := accessbox.PackTokens(gatesData, nil)
var secret []byte
isCustom := options.AccessKeyID != ""
if isCustom {
secret = []byte(options.SecretAccessKey)
}
box, secrets, err := accessbox.PackTokens(gatesData, secret, isCustom)
if err != nil {
return fmt.Errorf("pack tokens: %w", err)
}
box.ContainerPolicy = policies
var idOwner user.ID
user.IDFromKey(&idOwner, options.FrostFSKey.PrivateKey.PublicKey)
id, err := a.checkContainer(ctx, options.Container, idOwner)
if err != nil {
if err = a.checkContainer(ctx, options.Container); err != nil {
return fmt.Errorf("check container: %w", err)
}
var idOwner user.ID
user.IDFromKey(&idOwner, options.FrostFSKey.PrivateKey.PublicKey)
a.log.Info(logs.StoreBearerTokenIntoFrostFS,
zap.Stringer("owner_tkn", idOwner))
@ -281,26 +267,31 @@ func (a *Agent) IssueSecret(ctx context.Context, w io.Writer, options *IssueSecr
creds := tokens.New(cfg)
prm := tokens.CredentialsParam{
OwnerID: idOwner,
Container: options.Container,
AccessKeyID: options.AccessKeyID,
AccessBox: box,
Expiration: lifetime.Exp,
Keys: options.GatesPublicKeys,
CustomAttributes: options.CustomAttributes,
}
addr, err := creds.Put(ctx, id, prm)
addr, err := creds.Put(ctx, prm)
if err != nil {
return fmt.Errorf("failed to put creds: %w", err)
}
accessKeyID := accessKeyIDFromAddr(addr)
accessKeyID := options.AccessKeyID
if accessKeyID == "" {
accessKeyID = accessKeyIDFromAddr(addr)
}
ir := &issuingResult{
InitialAccessKeyID: accessKeyID,
AccessKeyID: accessKeyID,
SecretAccessKey: secrets.SecretKey,
OwnerPrivateKey: hex.EncodeToString(secrets.EphemeralKey.Bytes()),
WalletPublicKey: hex.EncodeToString(options.FrostFSKey.PublicKey().Bytes()),
ContainerID: id.EncodeToString(),
ContainerID: options.Container.EncodeToString(),
}
enc := json.NewEncoder(w)
@ -337,13 +328,15 @@ func (a *Agent) UpdateSecret(ctx context.Context, w io.Writer, options *UpdateSe
creds := tokens.New(cfg)
box, _, err := creds.GetBox(ctx, options.Address)
box, _, err := creds.GetBox(ctx, options.ContainerID, options.AccessKeyID)
if err != nil {
return fmt.Errorf("get accessbox: %w", err)
}
secret, err := hex.DecodeString(box.Gate.SecretKey)
if err != nil {
var secret []byte
if options.IsCustom {
secret = []byte(box.Gate.SecretKey)
} else if secret, err = hex.DecodeString(box.Gate.SecretKey); err != nil {
return fmt.Errorf("failed to decode secret key access box: %w", err)
}
@ -360,7 +353,7 @@ func (a *Agent) UpdateSecret(ctx context.Context, w io.Writer, options *UpdateSe
return fmt.Errorf("create tokens: %w", err)
}
updatedBox, secrets, err := accessbox.PackTokens(gatesData, secret)
updatedBox, secrets, err := accessbox.PackTokens(gatesData, secret, options.IsCustom)
if err != nil {
return fmt.Errorf("pack tokens: %w", err)
}
@ -371,22 +364,26 @@ func (a *Agent) UpdateSecret(ctx context.Context, w io.Writer, options *UpdateSe
zap.Stringer("owner_tkn", idOwner))
prm := tokens.CredentialsParam{
OwnerID: idOwner,
Container: options.ContainerID,
AccessBox: updatedBox,
Expiration: lifetime.Exp,
Keys: options.GatesPublicKeys,
CustomAttributes: options.CustomAttributes,
}
oldAddr := options.Address
addr, err := creds.Update(ctx, oldAddr, prm)
addr, err := creds.Update(ctx, prm)
if err != nil {
return fmt.Errorf("failed to update creds: %w", err)
}
accessKeyID := options.AccessKeyID
if !options.IsCustom {
accessKeyID = accessKeyIDFromAddr(addr)
}
ir := &issuingResult{
AccessKeyID: accessKeyIDFromAddr(addr),
InitialAccessKeyID: accessKeyIDFromAddr(oldAddr),
AccessKeyID: accessKeyID,
InitialAccessKeyID: options.AccessKeyID,
SecretAccessKey: secrets.SecretKey,
OwnerPrivateKey: hex.EncodeToString(secrets.EphemeralKey.Bytes()),
WalletPublicKey: hex.EncodeToString(options.FrostFSKey.PublicKey().Bytes()),
@ -419,12 +416,7 @@ func (a *Agent) ObtainSecret(ctx context.Context, w io.Writer, options *ObtainSe
bearerCreds := tokens.New(cfg)
var addr oid.Address
if err := addr.DecodeString(options.SecretAddress); err != nil {
return fmt.Errorf("failed to parse secret address: %w", err)
}
box, _, err := bearerCreds.GetBox(ctx, addr)
box, _, err := bearerCreds.GetBox(ctx, options.Container, options.AccessKeyID)
if err != nil {
return fmt.Errorf("failed to get tokens: %w", err)
}

View file

@ -4,22 +4,34 @@ import (
"context"
"fmt"
"os"
"strings"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
)
var issueSecretCmd = &cobra.Command{
Use: "issue-secret",
Short: "Issue a secret in FrostFS network",
Long: "Creates new s3 credentials to use with frostfs-s3-gw",
Example: `frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a
frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a --attributes LOGIN=NUUb82KR2JrVByHs2YSKgtK29gKnF5q6Vt`,
Example: `To create new s3 credentials use:
frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a
frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a --attributes LOGIN=NUUb82KR2JrVByHs2YSKgtK29gKnF5q6Vt
To create new s3 credentials using specific access key id and secret access key use:
frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a --access-key-id my-access-key-id --secret-access-key my-secret-key --container-id BpExV76416Vo7GrkJsGwXGoLM35xsBwup8voedDZR3c6
`,
RunE: runIssueSecretCmd,
}
@ -54,6 +66,9 @@ const (
poolHealthcheckTimeoutFlag = "pool-healthcheck-timeout"
poolRebalanceIntervalFlag = "pool-rebalance-interval"
poolStreamTimeoutFlag = "pool-stream-timeout"
accessKeyIDFlag = "access-key-id"
secretAccessKeyFlag = "secret-access-key"
)
func initIssueSecretCmd() {
@ -73,6 +88,9 @@ func initIssueSecretCmd() {
issueSecretCmd.Flags().Duration(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status")
issueSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC")
issueSecretCmd.Flags().String(attributesFlag, "", "User attributes in form of Key1=Value1,Key2=Value2 (note: you cannot override system attributes)")
issueSecretCmd.Flags().String(accessKeyIDFlag, "", "Access key id of s3 credential that must be created")
issueSecretCmd.Flags().String(secretAccessKeyFlag, "", "Secret access key of s3 credential that must be used")
issueSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address (must be provided if container-id is nns name)")
_ = issueSecretCmd.MarkFlagRequired(walletFlag)
_ = issueSecretCmd.MarkFlagRequired(peerFlag)
@ -91,14 +109,6 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error {
return wrapPreparationError(fmt.Errorf("failed to load frostfs private key: %s", err))
}
var cnrID cid.ID
containerID := viper.GetString(containerIDFlag)
if len(containerID) > 0 {
if err = cnrID.DecodeString(containerID); err != nil {
return wrapPreparationError(fmt.Errorf("failed to parse auth container id: %s", err))
}
}
var gatesPublicKeys []*keys.PublicKey
for _, keyStr := range viper.GetStringSlice(gatePublicKeyFlag) {
gpk, err := keys.NewPublicKeyFromString(keyStr)
@ -137,17 +147,29 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error {
return wrapFrostFSInitError(fmt.Errorf("failed to create FrostFS component: %s", err))
}
var accessBox cid.ID
if viper.IsSet(containerIDFlag) {
if accessBox, err = util.ResolveContainerID(viper.GetString(containerIDFlag), viper.GetString(rpcEndpointFlag)); err != nil {
return wrapPreparationError(fmt.Errorf("resolve accessbox container id (make sure you provided %s): %w", rpcEndpointFlag, err))
}
} else if accessBox, err = createAccessBox(ctx, frostFS, key, log); err != nil {
return wrapPreparationError(err)
}
accessKeyID, secretAccessKey, err := parseAccessKeys()
if err != nil {
return wrapPreparationError(err)
}
customAttrs, err := parseObjectAttrs(viper.GetString(attributesFlag))
if err != nil {
return wrapPreparationError(fmt.Errorf("failed to parse attributes: %s", err))
}
issueSecretOptions := &authmate.IssueSecretOptions{
Container: authmate.ContainerOptions{
ID: cnrID,
FriendlyName: viper.GetString(containerFriendlyNameFlag),
PlacementPolicy: viper.GetString(containerPlacementPolicyFlag),
},
Container: accessBox,
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
FrostFSKey: key,
GatesPublicKeys: gatesPublicKeys,
Impersonate: true,
@ -164,3 +186,59 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error {
}
return nil
}
func parseAccessKeys() (accessKeyID, secretAccessKey string, err error) {
accessKeyID = viper.GetString(accessKeyIDFlag)
secretAccessKey = viper.GetString(secretAccessKeyFlag)
if accessKeyID == "" && secretAccessKey != "" || accessKeyID != "" && secretAccessKey == "" {
return "", "", fmt.Errorf("flags %s and %s must be both provided or not", accessKeyIDFlag, secretAccessKeyFlag)
}
if accessKeyID != "" {
if !isCustomCreds(accessKeyID) {
return "", "", fmt.Errorf("invalid custom AccessKeyID format: %s", accessKeyID)
}
if !checkAccessKeyLength(accessKeyID) {
return "", "", fmt.Errorf("invalid custom AccessKeyID length: %s", accessKeyID)
}
if !checkAccessKeyLength(secretAccessKey) {
return "", "", fmt.Errorf("invalid custom SecretAccessKey length: %s", secretAccessKey)
}
}
return accessKeyID, secretAccessKey, nil
}
func isCustomCreds(accessKeyID string) bool {
var addr oid.Address
return addr.DecodeString(strings.ReplaceAll(accessKeyID, "0", "/")) != nil
}
func checkAccessKeyLength(key string) bool {
return 4 <= len(key) && len(key) <= 128
}
func createAccessBox(ctx context.Context, frostFS *frostfs.AuthmateFrostFS, key *keys.PrivateKey, log *zap.Logger) (cid.ID, error) {
friendlyName := viper.GetString(containerFriendlyNameFlag)
placementPolicy := viper.GetString(containerPlacementPolicyFlag)
log.Info(logs.CreateContainer, zap.String("friendly_name", friendlyName), zap.String("placement_policy", placementPolicy))
prm := authmate.PrmContainerCreate{
FriendlyName: friendlyName,
}
user.IDFromKey(&prm.Owner, key.PrivateKey.PublicKey)
if err := prm.Policy.DecodeString(placementPolicy); err != nil {
return cid.ID{}, fmt.Errorf("failed to build placement policy: %w", err)
}
accessBox, err := frostFS.CreateContainer(ctx, prm)
if err != nil {
return cid.ID{}, fmt.Errorf("create container in FrostFS: %w", err)
}
return accessBox, nil
}

View file

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"os"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
@ -24,7 +23,6 @@ var obtainSecretCmd = &cobra.Command{
const (
gateWalletFlag = "gate-wallet"
gateAddressFlag = "gate-address"
accessKeyIDFlag = "access-key-id"
)
const (
@ -38,10 +36,12 @@ func initObtainSecretCmd() {
obtainSecretCmd.Flags().String(gateWalletFlag, "", "Path to the s3 gateway wallet to decrypt accessbox")
obtainSecretCmd.Flags().String(gateAddressFlag, "", "Address of the s3 gateway wallet account")
obtainSecretCmd.Flags().String(accessKeyIDFlag, "", "Access key id of s3 credential for which secret must be obtained")
obtainSecretCmd.Flags().String(containerIDFlag, "", "CID or NNS name of auth container that contains provided credential (must be provided if custom access key id is used)")
obtainSecretCmd.Flags().Duration(poolDialTimeoutFlag, defaultPoolDialTimeout, "Timeout for connection to the node in pool to be established")
obtainSecretCmd.Flags().Duration(poolHealthcheckTimeoutFlag, defaultPoolHealthcheckTimeout, "Timeout for request to node to decide if it is alive")
obtainSecretCmd.Flags().Duration(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status")
obtainSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC")
obtainSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address (must be provided if container-id is nns name)")
_ = obtainSecretCmd.MarkFlagRequired(walletFlag)
_ = obtainSecretCmd.MarkFlagRequired(peerFlag)
@ -81,8 +81,14 @@ func runObtainSecretCmd(cmd *cobra.Command, _ []string) error {
return wrapFrostFSInitError(cli.Exit(fmt.Sprintf("failed to create FrostFS component: %s", err), 2))
}
accessBox, accessKeyID, _, err := getAccessBoxID()
if err != nil {
return wrapPreparationError(err)
}
obtainSecretOptions := &authmate.ObtainSecretOptions{
SecretAddress: strings.Replace(viper.GetString(accessKeyIDFlag), "0", "/", 1),
Container: accessBox,
AccessKeyID: accessKeyID,
GatePrivateKey: gateKey,
}

View file

@ -4,11 +4,9 @@ import (
"context"
"fmt"
"os"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -33,13 +31,15 @@ func initUpdateSecretCmd() {
updateSecretCmd.Flags().String(peerFlag, "", "Address of a frostfs peer to connect to")
updateSecretCmd.Flags().String(gateWalletFlag, "", "Path to the s3 gateway wallet to decrypt accessbox")
updateSecretCmd.Flags().String(gateAddressFlag, "", "Address of the s3 gateway wallet account")
updateSecretCmd.Flags().String(accessKeyIDFlag, "", "Access key id of s3 credential for which secret must be obtained")
updateSecretCmd.Flags().String(accessKeyIDFlag, "", "Access key id of s3 credential for which secret must be updatedd")
updateSecretCmd.Flags().String(containerIDFlag, "", "CID or NNS name of auth container that contains provided credential (must be provided if custom access key id is used)")
updateSecretCmd.Flags().StringSlice(gatePublicKeyFlag, nil, "Public 256r1 key of a gate (use flags repeatedly for multiple gates or separate them by comma)")
updateSecretCmd.Flags().Duration(poolDialTimeoutFlag, defaultPoolDialTimeout, "Timeout for connection to the node in pool to be established")
updateSecretCmd.Flags().Duration(poolHealthcheckTimeoutFlag, defaultPoolHealthcheckTimeout, "Timeout for request to node to decide if it is alive")
updateSecretCmd.Flags().Duration(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status")
updateSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC")
updateSecretCmd.Flags().String(attributesFlag, "", "User attributes in form of Key1=Value1,Key2=Value2 (note: you cannot override system attributes)")
updateSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address (must be provided if container-id is nns name)")
_ = updateSecretCmd.MarkFlagRequired(walletFlag)
_ = updateSecretCmd.MarkFlagRequired(peerFlag)
@ -66,10 +66,9 @@ func runUpdateSecretCmd(cmd *cobra.Command, _ []string) error {
return wrapPreparationError(fmt.Errorf("failed to load s3 gate private key: %s", err))
}
var accessBoxAddress oid.Address
credAddr := strings.Replace(viper.GetString(accessKeyIDFlag), "0", "/", 1)
if err = accessBoxAddress.DecodeString(credAddr); err != nil {
return wrapPreparationError(fmt.Errorf("failed to parse creds address: %w", err))
accessBox, accessKeyID, isCustom, err := getAccessBoxID()
if err != nil {
return wrapPreparationError(err)
}
var gatesPublicKeys []*keys.PublicKey
@ -101,7 +100,9 @@ func runUpdateSecretCmd(cmd *cobra.Command, _ []string) error {
}
updateSecretOptions := &authmate.UpdateSecretOptions{
Address: accessBoxAddress,
ContainerID: accessBox,
AccessKeyID: accessKeyID,
IsCustom: isCustom,
FrostFSKey: key,
GatesPublicKeys: gatesPublicKeys,
GatePrivateKey: gateKey,

View file

@ -3,6 +3,7 @@ package modules
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
@ -11,8 +12,11 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
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"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/spf13/viper"
@ -163,3 +167,23 @@ func parseObjectAttrs(attributes string) ([]object.Attribute, error) {
return attrs, nil
}
func getAccessBoxID() (cid.ID, string, bool, error) {
accessKeyID := viper.GetString(accessKeyIDFlag)
var accessBoxAddress oid.Address
if err := accessBoxAddress.DecodeString(strings.Replace(accessKeyID, "0", "/", 1)); err == nil {
return accessBoxAddress.Container(), accessKeyID, false, nil
}
if !viper.IsSet(containerIDFlag) {
return cid.ID{}, "", false, errors.New("accessbox parameter must be set when custom access key id is used")
}
accessBox, err := util.ResolveContainerID(viper.GetString(containerIDFlag), viper.GetString(rpcEndpointFlag))
if err != nil {
return cid.ID{}, "", false, fmt.Errorf("resolve accessbox container id (make sure you provided %s): %w", rpcEndpointFlag, err)
}
return accessBox, accessKeyID, true, nil
}

View file

@ -96,6 +96,7 @@ type (
resolveZoneList []string
isResolveListAllow bool // True if ResolveZoneList contains allowed zones
frostfsidValidation bool
accessbox *cid.ID
mu sync.RWMutex
namespaces Namespaces
@ -132,18 +133,7 @@ type (
func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App {
objPool, treePool, key := getPools(ctx, log.logger, v)
cfg := tokens.Config{
FrostFS: frostfs.NewAuthmateFrostFS(frostfs.NewFrostFS(objPool, key), log.logger),
Key: key,
CacheConfig: getAccessBoxCacheConfig(v, log.logger),
RemovingCheckAfterDurations: fetchRemovingCheckInterval(v, log.logger),
}
// prepare auth center
ctr := auth.New(tokens.New(cfg), v.GetStringSlice(cfgAllowedAccessKeyIDPrefixes))
app := &App{
ctr: ctr,
log: log.logger,
cfg: v,
pool: objPool,
@ -162,6 +152,8 @@ func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App {
}
func (a *App) init(ctx context.Context) {
a.initResolver()
a.initAuthCenter(ctx)
a.setRuntimeParameters()
a.initFrostfsID(ctx)
a.initPolicyStorage(ctx)
@ -171,9 +163,26 @@ func (a *App) init(ctx context.Context) {
a.initTracing(ctx)
}
func (a *App) initLayer(ctx context.Context) {
a.initResolver()
func (a *App) initAuthCenter(ctx context.Context) {
if a.cfg.IsSet(cfgContainersAccessBox) {
cnrID, err := a.resolveContainerID(ctx, cfgContainersAccessBox)
if err != nil {
a.log.Fatal(logs.CouldNotFetchAccessBoxContainerInfo, zap.Error(err))
}
a.settings.accessbox = &cnrID
}
cfg := tokens.Config{
FrostFS: frostfs.NewAuthmateFrostFS(frostfs.NewFrostFS(a.pool, a.key), a.log),
Key: a.key,
CacheConfig: getAccessBoxCacheConfig(a.cfg, a.log),
RemovingCheckAfterDurations: fetchRemovingCheckInterval(a.cfg, a.log),
}
a.ctr = auth.New(tokens.New(cfg), a.cfg.GetStringSlice(cfgAllowedAccessKeyIDPrefixes), a.settings)
}
func (a *App) initLayer(ctx context.Context) {
// prepare random key for anonymous requests
randomKey, err := keys.NewPrivateKey()
if err != nil {
@ -484,6 +493,14 @@ func (s *appSettings) RetryStrategy() handler.RetryStrategy {
return s.retryStrategy
}
func (s *appSettings) AccessBoxContainer() (cid.ID, bool) {
if s.accessbox != nil {
return *s.accessbox, true
}
return cid.ID{}, false
}
func (a *App) initAPI(ctx context.Context) {
a.initLayer(ctx)
a.initHandler()
@ -1104,21 +1121,30 @@ func (a *App) tryReconnect(ctx context.Context, sr *http.Server) bool {
}
func (a *App) fetchContainerInfo(ctx context.Context, cfgKey string) (info *data.BucketInfo, err error) {
cnrID, err := a.resolveContainerID(ctx, cfgKey)
if err != nil {
return nil, err
}
return getContainerInfo(ctx, cnrID, a.pool)
}
func (a *App) resolveContainerID(ctx context.Context, cfgKey string) (cid.ID, error) {
containerString := a.cfg.GetString(cfgKey)
var id cid.ID
if err = id.DecodeString(containerString); err != nil {
if err := id.DecodeString(containerString); err != nil {
i := strings.Index(containerString, ".")
if i < 0 {
return nil, fmt.Errorf("invalid container address: %s", containerString)
return cid.ID{}, fmt.Errorf("invalid container address: %s", containerString)
}
if id, err = a.bucketResolver.Resolve(ctx, containerString[i+1:], containerString[:i]); err != nil {
return nil, fmt.Errorf("resolve container address %s: %w", containerString, err)
return cid.ID{}, fmt.Errorf("resolve container address %s: %w", containerString, err)
}
}
return getContainerInfo(ctx, id, a.pool)
return id, nil
}
func getContainerInfo(ctx context.Context, id cid.ID, frostFSPool *pool.Pool) (*data.BucketInfo, error) {

View file

@ -208,6 +208,7 @@ const ( // Settings.
// Containers.
cfgContainersCORS = "containers.cors"
cfgContainersLifecycle = "containers.lifecycle"
cfgContainersAccessBox = "containers.accessbox"
// Command line args.
cmdHelp = "help"

View file

@ -99,13 +99,14 @@ func (x *AccessBox) Unmarshal(data []byte) error {
// PackTokens adds bearer and session tokens to BearerTokens and SessionToken lists respectively.
// Session token can be nil.
// Secret can be nil. In such case secret will be generated.
func PackTokens(gatesData []*GateData, secret []byte) (*AccessBox, *Secrets, error) {
func PackTokens(gatesData []*GateData, secret []byte, isCustomSecret bool) (*AccessBox, *Secrets, error) {
box := &AccessBox{}
ephemeralKey, err := keys.NewPrivateKey()
if err != nil {
return nil, nil, fmt.Errorf("create ephemeral key: %w", err)
}
box.SeedKey = ephemeralKey.PublicKey().Bytes()
box.IsCustom = isCustomSecret
if secret == nil {
secret, err = generateSecret()
@ -118,7 +119,12 @@ func PackTokens(gatesData []*GateData, secret []byte) (*AccessBox, *Secrets, err
return nil, nil, fmt.Errorf("failed to add tokens to accessbox: %w", err)
}
return box, &Secrets{hex.EncodeToString(secret), ephemeralKey}, err
secretKey := string(secret)
if !isCustomSecret {
secretKey = hex.EncodeToString(secret)
}
return box, &Secrets{SecretKey: secretKey, EphemeralKey: ephemeralKey}, err
}
// GetTokens returns gate tokens from AccessBox.
@ -133,7 +139,7 @@ func (x *AccessBox) GetTokens(owner *keys.PrivateKey) (*GateData, error) {
continue
}
gateData, err := decodeGate(gate, owner, seedKey)
gateData, err := x.decodeGate(gate, owner, seedKey)
if err != nil {
return nil, fmt.Errorf("failed to decode gate: %w", err)
}
@ -217,7 +223,7 @@ func encodeGate(ephemeralKey *keys.PrivateKey, seedKey *keys.PublicKey, tokens *
return gate, nil
}
func decodeGate(gate *AccessBox_Gate, owner *keys.PrivateKey, seedKey *keys.PublicKey) (*GateData, error) {
func (x *AccessBox) decodeGate(gate *AccessBox_Gate, owner *keys.PrivateKey, seedKey *keys.PublicKey) (*GateData, error) {
data, err := decrypt(owner, seedKey, gate.Tokens)
if err != nil {
return nil, fmt.Errorf("decrypt tokens: %w", err)
@ -243,7 +249,11 @@ func decodeGate(gate *AccessBox_Gate, owner *keys.PrivateKey, seedKey *keys.Publ
gateData := NewGateData(owner.PublicKey(), &bearerTkn)
gateData.SessionTokens = sessionTkns
gateData.SecretKey = hex.EncodeToString(tokens.SecretKey)
if x.IsCustom {
gateData.SecretKey = string(tokens.SecretKey)
} else {
gateData.SecretKey = hex.EncodeToString(tokens.SecretKey)
}
return gateData, nil
}

View file

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.30.0
// protoc v3.12.4
// protoc-gen-go v1.34.2
// protoc v3.21.9
// source: creds/accessbox/accessbox.proto
package accessbox
@ -28,6 +28,7 @@ type AccessBox struct {
SeedKey []byte `protobuf:"bytes,1,opt,name=seedKey,proto3" json:"seedKey,omitempty"`
Gates []*AccessBox_Gate `protobuf:"bytes,2,rep,name=gates,proto3" json:"gates,omitempty"`
ContainerPolicy []*AccessBox_ContainerPolicy `protobuf:"bytes,3,rep,name=containerPolicy,proto3" json:"containerPolicy,omitempty"`
IsCustom bool `protobuf:"varint,4,opt,name=isCustom,proto3" json:"isCustom,omitempty"`
}
func (x *AccessBox) Reset() {
@ -83,6 +84,13 @@ func (x *AccessBox) GetContainerPolicy() []*AccessBox_ContainerPolicy {
return nil
}
func (x *AccessBox) GetIsCustom() bool {
if x != nil {
return x.IsCustom
}
return false
}
type Tokens struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -261,7 +269,7 @@ var File_creds_accessbox_accessbox_proto protoreflect.FileDescriptor
var file_creds_accessbox_accessbox_proto_rawDesc = []byte{
0x0a, 0x1f, 0x63, 0x72, 0x65, 0x64, 0x73, 0x2f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f,
0x78, 0x2f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x12, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x22, 0xc7, 0x02, 0x0a,
0x6f, 0x12, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x22, 0xe3, 0x02, 0x0a,
0x09, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x6f, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65,
0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x73, 0x65, 0x65,
0x64, 0x4b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x67, 0x61, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20,
@ -272,29 +280,31 @@ var file_creds_accessbox_accessbox_proto_rawDesc = []byte{
0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73,
0x73, 0x42, 0x6f, 0x78, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f,
0x6c, 0x69, 0x63, 0x79, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x50,
0x6f, 0x6c, 0x69, 0x63, 0x79, 0x1a, 0x44, 0x0a, 0x04, 0x47, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a,
0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x74,
0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x67, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62,
0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x67, 0x61,
0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x1a, 0x59, 0x0a, 0x0f, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x2e,
0x0a, 0x12, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72,
0x61, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6c, 0x6f, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x12, 0x16,
0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06,
0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x6e, 0x0a, 0x06, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73,
0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x20,
0x0a, 0x0b, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x0b, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
0x12, 0x24, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x2e, 0x66, 0x72,
0x6f, 0x73, 0x74, 0x66, 0x73, 0x2e, 0x69, 0x6e, 0x66, 0x6f, 0x2f, 0x54, 0x72, 0x75, 0x65, 0x43,
0x6c, 0x6f, 0x75, 0x64, 0x4c, 0x61, 0x62, 0x2f, 0x66, 0x72, 0x6f, 0x73, 0x74, 0x66, 0x73, 0x2d,
0x73, 0x33, 0x2d, 0x67, 0x77, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x73, 0x2f, 0x74, 0x6f, 0x6b, 0x65,
0x6e, 0x62, 0x6f, 0x78, 0x3b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x73, 0x43, 0x75, 0x73, 0x74, 0x6f,
0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x43, 0x75, 0x73, 0x74, 0x6f,
0x6d, 0x1a, 0x44, 0x0a, 0x04, 0x47, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x6f, 0x6b,
0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
0x73, 0x12, 0x24, 0x0a, 0x0d, 0x67, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b,
0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x67, 0x61, 0x74, 0x65, 0x50, 0x75,
0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x1a, 0x59, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x2e, 0x0a, 0x12, 0x6c, 0x6f,
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6f,
0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69,
0x63, 0x79, 0x22, 0x6e, 0x0a, 0x06, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09,
0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x62, 0x65,
0x61, 0x72, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x0b, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0d,
0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0c, 0x52, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65,
0x6e, 0x73, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x2e, 0x66, 0x72, 0x6f, 0x73, 0x74, 0x66,
0x73, 0x2e, 0x69, 0x6e, 0x66, 0x6f, 0x2f, 0x54, 0x72, 0x75, 0x65, 0x43, 0x6c, 0x6f, 0x75, 0x64,
0x4c, 0x61, 0x62, 0x2f, 0x66, 0x72, 0x6f, 0x73, 0x74, 0x66, 0x73, 0x2d, 0x73, 0x33, 0x2d, 0x67,
0x77, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x73, 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x62, 0x6f, 0x78,
0x3b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
}
var (
@ -310,7 +320,7 @@ func file_creds_accessbox_accessbox_proto_rawDescGZIP() []byte {
}
var file_creds_accessbox_accessbox_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_creds_accessbox_accessbox_proto_goTypes = []interface{}{
var file_creds_accessbox_accessbox_proto_goTypes = []any{
(*AccessBox)(nil), // 0: accessbox.AccessBox
(*Tokens)(nil), // 1: accessbox.Tokens
(*AccessBox_Gate)(nil), // 2: accessbox.AccessBox.Gate
@ -332,7 +342,7 @@ func file_creds_accessbox_accessbox_proto_init() {
return
}
if !protoimpl.UnsafeEnabled {
file_creds_accessbox_accessbox_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
file_creds_accessbox_accessbox_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*AccessBox); i {
case 0:
return &v.state
@ -344,7 +354,7 @@ func file_creds_accessbox_accessbox_proto_init() {
return nil
}
}
file_creds_accessbox_accessbox_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
file_creds_accessbox_accessbox_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*Tokens); i {
case 0:
return &v.state
@ -356,7 +366,7 @@ func file_creds_accessbox_accessbox_proto_init() {
return nil
}
}
file_creds_accessbox_accessbox_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
file_creds_accessbox_accessbox_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*AccessBox_Gate); i {
case 0:
return &v.state
@ -368,7 +378,7 @@ func file_creds_accessbox_accessbox_proto_init() {
return nil
}
}
file_creds_accessbox_accessbox_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
file_creds_accessbox_accessbox_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*AccessBox_ContainerPolicy); i {
case 0:
return &v.state

View file

@ -20,6 +20,7 @@ message AccessBox {
bytes seedKey = 1 [json_name = "seedKey"];
repeated Gate gates = 2 [json_name = "gates"];
repeated ContainerPolicy containerPolicy = 3 [json_name = "containerPolicy"];
bool isCustom = 4 [json_name = "isCustom"];
}
message Tokens {

View file

@ -61,7 +61,7 @@ func TestBearerTokenInAccessBox(t *testing.T) {
require.NoError(t, tkn.Sign(sec.PrivateKey))
gate := NewGateData(cred.PublicKey(), &tkn)
box, _, err = PackTokens([]*GateData{gate}, nil)
box, _, err = PackTokens([]*GateData{gate}, nil, false)
require.NoError(t, err)
data, err := box.Marshal()
@ -96,7 +96,7 @@ func TestSessionTokenInAccessBox(t *testing.T) {
var newTkn bearer.Token
gate := NewGateData(cred.PublicKey(), &newTkn)
gate.SessionTokens = []*session.Container{tkn}
box, _, err = PackTokens([]*GateData{gate}, nil)
box, _, err = PackTokens([]*GateData{gate}, nil, false)
require.NoError(t, err)
data, err := box.Marshal()
@ -136,7 +136,7 @@ func TestAccessboxMultipleKeys(t *testing.T) {
}
}
box, _, err = PackTokens(gates, nil)
box, _, err = PackTokens(gates, nil, false)
require.NoError(t, err)
for i, k := range privateKeys {
@ -165,7 +165,7 @@ func TestUnknownKey(t *testing.T) {
require.NoError(t, tkn.Sign(sec.PrivateKey))
gate := NewGateData(cred.PublicKey(), &tkn)
box, _, err = PackTokens([]*GateData{gate}, nil)
box, _, err = PackTokens([]*GateData{gate}, nil, false)
require.NoError(t, err)
_, err = box.GetTokens(wrongCred)
@ -224,14 +224,27 @@ func TestGetBox(t *testing.T) {
var tkn bearer.Token
gate := NewGateData(cred.PublicKey(), &tkn)
secret := []byte("secret")
accessBox, _, err := PackTokens([]*GateData{gate}, secret)
require.NoError(t, err)
box, err := accessBox.GetBox(cred)
require.NoError(t, err)
require.Equal(t, hex.EncodeToString(secret), box.Gate.SecretKey)
t.Run("regular secret", func(t *testing.T) {
accessBox, secrets, err := PackTokens([]*GateData{gate}, secret, false)
require.NoError(t, err)
require.Equal(t, hex.EncodeToString(secret), secrets.SecretKey)
box, err := accessBox.GetBox(cred)
require.NoError(t, err)
require.Equal(t, hex.EncodeToString(secret), box.Gate.SecretKey)
})
t.Run("custom secret", func(t *testing.T) {
accessBox, secrets, err := PackTokens([]*GateData{gate}, secret, true)
require.NoError(t, err)
require.Equal(t, string(secret), secrets.SecretKey)
box, err := accessBox.GetBox(cred)
require.NoError(t, err)
require.Equal(t, string(secret), box.Gate.SecretKey)
})
}
func TestAccessBox(t *testing.T) {
@ -241,7 +254,7 @@ func TestAccessBox(t *testing.T) {
var tkn bearer.Token
gate := NewGateData(cred.PublicKey(), &tkn)
accessBox, _, err := PackTokens([]*GateData{gate}, nil)
accessBox, _, err := PackTokens([]*GateData{gate}, nil, false)
require.NoError(t, err)
t.Run("invalid owner", func(t *testing.T) {
@ -300,7 +313,7 @@ func TestAccessBox(t *testing.T) {
BearerToken: &tkn,
GateKey: &keys.PublicKey{},
}
_, _, err = PackTokens([]*GateData{gate}, nil)
_, _, err = PackTokens([]*GateData{gate}, nil, false)
require.Error(t, err)
})
}

View file

@ -14,7 +14,6 @@ import (
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"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"go.uber.org/zap"
)
@ -22,13 +21,14 @@ import (
type (
// Credentials is a bearer token get/put interface.
Credentials interface {
GetBox(context.Context, oid.Address) (*accessbox.Box, []object.Attribute, error)
Put(context.Context, cid.ID, CredentialsParam) (oid.Address, error)
Update(context.Context, oid.Address, CredentialsParam) (oid.Address, error)
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 {
OwnerID user.ID
Container cid.ID
AccessKeyID string
AccessBox *accessbox.AccessBox
Expiration uint64
Keys keys.PublicKeys
@ -49,13 +49,16 @@ type (
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 identifier of the object creator.
Creator user.ID
// FrostFS container to store the object.
Container cid.ID
@ -64,7 +67,12 @@ type PrmObjectCreate struct {
// Optional.
// If provided cred object will be created using crdt approach.
NewVersionFor *oid.ID
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
@ -76,6 +84,21 @@ type PrmObjectCreate struct {
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
@ -92,8 +115,9 @@ type FrostFS interface {
//
// 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, oid.Address) (*object.Object, error)
GetCredsObject(context.Context, PrmGetCredsObject) (*object.Object, error)
}
var (
@ -116,84 +140,128 @@ func New(cfg Config) Credentials {
}
}
func (c *cred) GetBox(ctx context.Context, addr oid.Address) (*accessbox.Box, []object.Attribute, error) {
cachedBoxValue := c.cache.Get(addr)
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, addr, cachedBoxValue)
return c.checkIfCredentialsAreRemoved(ctx, cnrID, accessKeyID, cachedBoxValue)
}
box, attrs, err := c.getAccessBox(ctx, addr)
box, err := c.getAccessBox(ctx, cnrID, accessKeyID, nil)
if err != nil {
return nil, nil, fmt.Errorf("get access box: %w", err)
}
cachedBox, err := box.GetBox(c.key)
cachedBox, err := box.AccessBox.GetBox(c.key)
if err != nil {
return nil, nil, fmt.Errorf("get gate box: %w", err)
}
c.putBoxToCache(addr, cachedBox, attrs)
val := &cache.AccessBoxCacheValue{
Box: cachedBox,
Attributes: box.Attributes,
PutTime: time.Now(),
Address: box.Address,
}
return cachedBox, attrs, nil
c.putBoxToCache(accessKeyID, val)
return cachedBox, box.Attributes, nil
}
func (c *cred) checkIfCredentialsAreRemoved(ctx context.Context, addr oid.Address, cachedBoxValue *cache.AccessBoxCacheValue) (*accessbox.Box, []object.Attribute, error) {
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, attrs, err := c.getAccessBox(ctx, addr)
box, err := c.getAccessBox(ctx, cnrID, accessKeyID, cachedBoxValue.Address)
if err != nil {
if client.IsErrObjectAlreadyRemoved(err) {
c.cache.Delete(addr)
c.cache.Delete(accessKeyID)
return nil, nil, fmt.Errorf("get access box: %w", err)
}
return cachedBoxValue.Box, cachedBoxValue.Attributes, nil
}
cachedBox, err := box.GetBox(c.key)
cachedBox, err := box.AccessBox.GetBox(c.key)
if err != nil {
c.cache.Delete(addr)
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
c.putBoxToCache(addr, cachedBox, attrs)
val := &cache.AccessBoxCacheValue{
Box: cachedBox,
Attributes: box.Attributes,
PutTime: time.Now(),
Address: box.Address,
}
c.putBoxToCache(accessKeyID, val)
return cachedBoxValue.Box, attrs, nil
return cachedBoxValue.Box, box.Attributes, nil
}
func (c *cred) putBoxToCache(addr oid.Address, box *accessbox.Box, attrs []object.Attribute) {
if err := c.cache.Put(addr, box, attrs); err != nil {
c.log.Warn(logs.CouldntPutAccessBoxIntoCache, zap.String("address", addr.EncodeToString()))
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, addr oid.Address) (*accessbox.AccessBox, []object.Attribute, error) {
obj, err := c.frostFS.GetCredsObject(ctx, addr)
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, nil, fmt.Errorf("read payload and attributes: %w", err)
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, nil, fmt.Errorf("unmarhal access box: %w", err)
return nil, fmt.Errorf("unmarhal access box: %w", err)
}
return &box, obj.Attributes(), nil
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, idCnr cid.ID, prm CredentialsParam) (oid.Address, error) {
return c.createObject(ctx, idCnr, nil, prm)
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, addr oid.Address, prm CredentialsParam) (oid.Address, error) {
objID := addr.Object()
return c.createObject(ctx, addr.Container(), &objID, prm)
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, cnrID cid.ID, newVersionFor *oid.ID, prm CredentialsParam) (oid.Address, error) {
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 {
@ -204,14 +272,19 @@ func (c *cred) createObject(ctx context.Context, cnrID cid.ID, newVersionFor *oi
return oid.Address{}, fmt.Errorf("marshall box: %w", err)
}
var newVersionFor string
if update {
newVersionFor = prm.AccessKeyID
}
idObj, err := c.frostFS.CreateObject(ctx, PrmObjectCreate{
Creator: prm.OwnerID,
Container: cnrID,
Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box",
ExpirationEpoch: prm.Expiration,
NewVersionFor: newVersionFor,
Payload: data,
CustomAttributes: prm.CustomAttributes,
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)
@ -219,7 +292,7 @@ func (c *cred) createObject(ctx context.Context, cnrID cid.ID, newVersionFor *oi
var addr oid.Address
addr.SetObject(idObj)
addr.SetContainer(cnrID)
addr.SetContainer(prm.Container)
return addr, nil
}

View file

@ -4,6 +4,7 @@ import (
"context"
"encoding/hex"
"errors"
"strings"
"testing"
"time"
@ -15,27 +16,40 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
type frostfsMock struct {
objects map[oid.Address][]*object.Object
errors map[oid.Address]error
key *keys.PrivateKey
objects map[string][]*object.Object
errors map[string]error
}
func newFrostfsMock() *frostfsMock {
func newFrostfsMock(key *keys.PrivateKey) *frostfsMock {
return &frostfsMock{
objects: map[oid.Address][]*object.Object{},
errors: map[oid.Address]error{},
objects: map[string][]*object.Object{},
errors: map[string]error{},
key: key,
}
}
func (f *frostfsMock) ownerID() user.ID {
if f.key == nil {
return user.ID{}
}
var ownerID user.ID
user.IDFromKey(&ownerID, f.key.PrivateKey.PublicKey)
return ownerID
}
func (f *frostfsMock) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.ID, error) {
var obj object.Object
obj.SetPayload(prm.Payload)
obj.SetOwnerID(prm.Creator)
obj.SetOwnerID(f.ownerID())
obj.SetContainerID(prm.Container)
a := object.NewAttribute()
@ -44,19 +58,15 @@ func (f *frostfsMock) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.
prm.CustomAttributes = append(prm.CustomAttributes, *a)
obj.SetAttributes(prm.CustomAttributes...)
if prm.NewVersionFor != nil {
var addr oid.Address
addr.SetObject(*prm.NewVersionFor)
addr.SetContainer(prm.Container)
_, ok := f.objects[addr]
if prm.NewVersionForAccessKeyID != "" {
_, ok := f.objects[prm.NewVersionForAccessKeyID]
if !ok {
return oid.ID{}, errors.New("not found")
}
objID := oidtest.ID()
obj.SetID(objID)
f.objects[addr] = append(f.objects[addr], &obj)
f.objects[prm.NewVersionForAccessKeyID] = append(f.objects[prm.NewVersionForAccessKeyID], &obj)
return objID, nil
}
@ -64,22 +74,27 @@ func (f *frostfsMock) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.
objID := oidtest.ID()
obj.SetID(objID)
accessKeyID := prm.CustomAccessKey
if accessKeyID == "" {
accessKeyID = prm.Container.EncodeToString() + "0" + objID.EncodeToString()
}
var addr oid.Address
addr.SetObject(objID)
addr.SetContainer(prm.Container)
f.objects[addr] = []*object.Object{&obj}
f.objects[accessKeyID] = []*object.Object{&obj}
return objID, nil
}
func (f *frostfsMock) GetCredsObject(_ context.Context, address oid.Address) (*object.Object, error) {
if err := f.errors[address]; err != nil {
func (f *frostfsMock) GetCredsObject(_ context.Context, prm PrmGetCredsObject) (*object.Object, error) {
if err := f.errors[prm.AccessKeyID]; err != nil {
return nil, err
}
objects, ok := f.objects[address]
objects, ok := f.objects[prm.AccessKeyID]
if !ok {
return nil, errors.New("not found")
return nil, ErrCustomAccessKeyIDNotFound
}
return objects[len(objects)-1], nil
@ -100,7 +115,7 @@ func TestRemovingAccessBox(t *testing.T) {
sk, err := hex.DecodeString(secretKey)
require.NoError(t, err)
accessBox, _, err := accessbox.PackTokens(gateData, sk)
accessBox, _, err := accessbox.PackTokens(gateData, sk, false)
require.NoError(t, err)
data, err := accessBox.Marshal()
require.NoError(t, err)
@ -111,9 +126,24 @@ func TestRemovingAccessBox(t *testing.T) {
obj.SetID(addr.Object())
obj.SetContainerID(addr.Container())
accessKeyID := getAccessKeyID(addr)
accessBoxCustom, _, err := accessbox.PackTokens(gateData, []byte("secret"), true)
require.NoError(t, err)
dataCustom, err := accessBoxCustom.Marshal()
require.NoError(t, err)
var objCustom object.Object
objCustom.SetPayload(dataCustom)
addrCustom := oidtest.Address()
objCustom.SetID(addrCustom.Object())
objCustom.SetContainerID(addrCustom.Container())
accessKeyIDCustom := "accessKeyID"
frostfs := &frostfsMock{
objects: map[oid.Address][]*object.Object{addr: {&obj}},
errors: map[oid.Address]error{},
objects: map[string][]*object.Object{accessKeyID: {&obj}, accessKeyIDCustom: {&objCustom}},
errors: map[string]error{},
}
cfg := Config{
@ -129,15 +159,30 @@ func TestRemovingAccessBox(t *testing.T) {
creds := New(cfg)
_, _, err = creds.GetBox(ctx, addr)
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err)
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyIDCustom)
require.NoError(t, err)
frostfs.errors[addr] = errors.New("network error")
_, _, err = creds.GetBox(ctx, addr)
frostfs.errors[accessKeyID] = errors.New("network error")
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err)
frostfs.errors[accessKeyIDCustom] = errors.New("network error")
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyIDCustom)
require.NoError(t, err)
frostfs.errors[addr] = &apistatus.ObjectAlreadyRemoved{}
_, _, err = creds.GetBox(ctx, addr)
frostfs.errors[accessKeyID] = &apistatus.ObjectAlreadyRemoved{}
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.Error(t, err)
frostfs.errors[accessKeyIDCustom] = &apistatus.ObjectAlreadyRemoved{}
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyIDCustom)
require.Error(t, err)
frostfs.errors[accessKeyID] = &apistatus.ObjectAlreadyRemoved{}
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.Error(t, err)
frostfs.errors[accessKeyIDCustom] = &apistatus.ObjectAlreadyRemoved{}
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyIDCustom)
require.Error(t, err)
}
@ -153,8 +198,9 @@ func TestGetBox(t *testing.T) {
}}
secret := []byte("secret")
accessBox, _, err := accessbox.PackTokens(gateData, secret)
accessBox, secrets, err := accessbox.PackTokens(gateData, secret, false)
require.NoError(t, err)
require.Equal(t, hex.EncodeToString(secret), secrets.SecretKey)
data, err := accessBox.Marshal()
require.NoError(t, err)
@ -172,108 +218,107 @@ func TestGetBox(t *testing.T) {
}
t.Run("no removing check, accessbox from cache", func(t *testing.T) {
frostfs := newFrostfsMock()
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = time.Hour
cfg.Key = key
creds := New(cfg)
creds := newCreds(key, cfg, time.Hour)
cnrID := cidtest.ID()
addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
addr, err := creds.Put(ctx, CredentialsParam{Container: cnrID, Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
require.NoError(t, err)
_, _, err = creds.GetBox(ctx, addr)
accessKeyID := getAccessKeyID(addr)
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err)
frostfs.errors[addr] = &apistatus.ObjectAlreadyRemoved{}
_, _, err = creds.GetBox(ctx, addr)
creds.(*cred).frostFS.(*frostfsMock).errors[accessKeyID] = &apistatus.ObjectAlreadyRemoved{}
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err)
})
t.Run("error while getting box from frostfs", func(t *testing.T) {
frostfs := newFrostfsMock()
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
creds := newCreds(key, cfg, 0)
cnrID := cidtest.ID()
addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
addr, err := creds.Put(ctx, CredentialsParam{Container: cnrID, Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
require.NoError(t, err)
frostfs.errors[addr] = errors.New("network error")
_, _, err = creds.GetBox(ctx, addr)
accessKeyID := getAccessKeyID(addr)
creds.(*cred).frostFS.(*frostfsMock).errors[accessKeyID] = errors.New("network error")
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.Error(t, err)
})
t.Run("invalid key", func(t *testing.T) {
frostfs := newFrostfsMock()
frostfs := newFrostfsMock(key)
var obj object.Object
obj.SetPayload(data)
addr := oidtest.Address()
frostfs.objects[addr] = []*object.Object{&obj}
accessKeyID := getAccessKeyID(addr)
frostfs.objects[accessKeyID] = []*object.Object{&obj}
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = &keys.PrivateKey{}
creds := New(cfg)
_, _, err = creds.GetBox(ctx, addr)
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.Error(t, err)
})
t.Run("invalid payload", func(t *testing.T) {
frostfs := newFrostfsMock()
frostfs := newFrostfsMock(key)
var obj object.Object
obj.SetPayload([]byte("invalid"))
addr := oidtest.Address()
frostfs.objects[addr] = []*object.Object{&obj}
accessKeyID := getAccessKeyID(addr)
frostfs.objects[accessKeyID] = []*object.Object{&obj}
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
_, _, err = creds.GetBox(ctx, addr)
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.Error(t, err)
})
t.Run("check attributes update", func(t *testing.T) {
frostfs := newFrostfsMock()
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
creds := newCreds(key, cfg, 0)
cnrID := cidtest.ID()
addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
addr, err := creds.Put(ctx, CredentialsParam{Container: cnrID, Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
require.NoError(t, err)
_, boxAttrs, err := creds.GetBox(ctx, addr)
accessKeyID := getAccessKeyID(addr)
_, boxAttrs, err := creds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err)
_, err = creds.Update(ctx, addr, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox, CustomAttributes: attrs})
prm := CredentialsParam{
Container: addr.Container(),
AccessKeyID: accessKeyID,
Keys: keys.PublicKeys{key.PublicKey()},
AccessBox: accessBox,
CustomAttributes: attrs,
}
_, err = creds.Update(ctx, prm)
require.NoError(t, err)
_, newBoxAttrs, err := creds.GetBox(ctx, addr)
_, newBoxAttrs, err := creds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err)
require.Equal(t, len(boxAttrs)+1, len(newBoxAttrs))
})
t.Run("check accessbox update", func(t *testing.T) {
frostfs := newFrostfsMock()
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
creds := newCreds(key, cfg, 0)
cnrID := cidtest.ID()
addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
addr, err := creds.Put(ctx, CredentialsParam{Container: cnrID, Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
require.NoError(t, err)
box, _, err := creds.GetBox(ctx, addr)
accessKeyID := getAccessKeyID(addr)
box, _, err := creds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err)
require.Equal(t, hex.EncodeToString(secret), box.Gate.SecretKey)
@ -286,44 +331,134 @@ func TestGetBox(t *testing.T) {
}}
newSecret := []byte("new-secret")
newAccessBox, _, err := accessbox.PackTokens(newGateData, newSecret)
newAccessBox, _, err := accessbox.PackTokens(newGateData, newSecret, false)
require.NoError(t, err)
_, err = creds.Update(ctx, addr, CredentialsParam{Keys: keys.PublicKeys{newKey.PublicKey()}, AccessBox: newAccessBox})
prm := CredentialsParam{
Container: addr.Container(),
AccessKeyID: accessKeyID,
Keys: keys.PublicKeys{newKey.PublicKey()},
AccessBox: newAccessBox,
}
_, err = creds.Update(ctx, prm)
require.NoError(t, err)
_, _, err = creds.GetBox(ctx, addr)
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.Error(t, err)
cfg.Key = newKey
newCreds := New(cfg)
newCfg := Config{
FrostFS: creds.(*cred).frostFS,
Key: newKey,
CacheConfig: cfg.CacheConfig,
}
newCreds := New(newCfg)
box, _, err = newCreds.GetBox(ctx, addr)
box, _, err = newCreds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err)
require.Equal(t, hex.EncodeToString(newSecret), box.Gate.SecretKey)
})
t.Run("check access key id uniqueness", func(t *testing.T) {
creds := newCreds(key, cfg, 0)
prm := CredentialsParam{
Container: cidtest.ID(),
AccessBox: accessBox,
Keys: keys.PublicKeys{key.PublicKey()},
}
_, err = creds.Put(ctx, prm)
require.NoError(t, err)
_, err = creds.Put(ctx, prm)
require.NoError(t, err)
})
t.Run("empty keys", func(t *testing.T) {
frostfs := newFrostfsMock()
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
creds := newCreds(key, cfg, 0)
cnrID := cidtest.ID()
_, err = creds.Put(ctx, cnrID, CredentialsParam{AccessBox: accessBox})
_, err = creds.Put(ctx, CredentialsParam{Container: cnrID, AccessBox: accessBox})
require.ErrorIs(t, err, ErrEmptyPublicKeys)
})
t.Run("empty accessbox", func(t *testing.T) {
frostfs := newFrostfsMock()
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
creds := newCreds(key, cfg, 0)
cnrID := cidtest.ID()
_, err = creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}})
_, err = creds.Put(ctx, CredentialsParam{Container: cnrID, Keys: keys.PublicKeys{key.PublicKey()}})
require.ErrorIs(t, err, ErrEmptyBearerToken)
})
}
func TestBoxWithCustomAccessKeyID(t *testing.T) {
ctx := context.Background()
key, err := keys.NewPrivateKey()
require.NoError(t, err)
gateData := []*accessbox.GateData{{
BearerToken: &bearer.Token{},
GateKey: key.PublicKey(),
}}
secret := []byte("secret")
accessBox, secrets, err := accessbox.PackTokens(gateData, secret, true)
require.NoError(t, err)
require.Equal(t, string(secret), secrets.SecretKey)
cfg := Config{
CacheConfig: &cache.Config{
Size: 10,
Lifetime: 24 * time.Hour,
Logger: zaptest.NewLogger(t),
},
}
t.Run("check secret format", func(t *testing.T) {
creds := newCreds(key, cfg, 0)
prm := CredentialsParam{
Container: cidtest.ID(),
AccessKeyID: "custom-access-key-id",
AccessBox: accessBox,
Keys: keys.PublicKeys{key.PublicKey()},
}
_, err = creds.Put(ctx, prm)
require.NoError(t, err)
box, _, err := creds.GetBox(ctx, prm.Container, prm.AccessKeyID)
require.NoError(t, err)
require.Equal(t, string(secret), box.Gate.SecretKey)
})
t.Run("check custom access key id uniqueness", func(t *testing.T) {
creds := newCreds(key, cfg, 0)
prm := CredentialsParam{
Container: cidtest.ID(),
AccessKeyID: "custom-access-key-id",
AccessBox: accessBox,
Keys: keys.PublicKeys{key.PublicKey()},
}
_, err = creds.Put(ctx, prm)
require.NoError(t, err)
_, err = creds.Put(ctx, prm)
require.Error(t, err)
})
}
func newCreds(key *keys.PrivateKey, cfg Config, removingCheckDuration time.Duration) Credentials {
frostfs := newFrostfsMock(key)
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = removingCheckDuration
cfg.Key = key
return New(cfg)
}
func getAccessKeyID(addr oid.Address) string {
return strings.ReplaceAll(addr.EncodeToString(), "/", "0")
}

View file

@ -159,8 +159,10 @@ storage node.
Object s3 credentials are formed based on:
* `AccessKeyId` - is concatenated container id and object id (`<cid>0<oid>`) of `AccessBox` (
e.g. `2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf`)
* `SecretAccessKey` - hex-encoded random generated 32 bytes (that is encrypted and stored in object payload)
e.g. `2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf`).
Or it can be arbitrary user-provided unique string with min length 4 and max length 128.
* `SecretAccessKey` - hex-encoded random generated 32 bytes (that is encrypted and stored in object payload).
Or it can be arbitrary user-provided unique string with min length 4 and max length 128.
> **Note**: sensitive info in `AccessBox` is [encrypted](#encryption), so only someone who posses specific private key
> can decrypt such info.
@ -192,7 +194,7 @@ It contains:
* List of gate data:
* Gate public key (so that gate (when it will decrypt data later) know which item from the list it should process)
* Encrypted tokens:
* `SecretAccessKey` - hex-encoded random generated 32 bytes
* `SecretAccessKey` - hex-encoded random generated 32 bytes (or arbitrary user-provided string)
* Marshaled bearer token - more detail
in [spec](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/commit/4c68d92468503b10282c8a92af83a56f170c8a3a/acl/types.proto#L189)
* Marshaled session token - more detail
@ -229,10 +231,12 @@ relevant data) the following sequence is used:
</a>
* Search all object whose attribute `S3-Access-Box-CRDT-Name` is equal to `AccessKeyId` (extract container id
from `AccessKeyId` that has format: `<cid>0<oid>`).
from `AccessKeyId` that has format: `<cid>0<oid>` if `AccessBox` was created with default parameters, or it can also
be arbitrary user-defined string).
* Get metadata for these object using `HEAD` requests (not `Get` to reduce network traffic)
* Sort all these objects by creation epoch and object id
* Pick last object id (If no object is found then extract object id from `AccessKeyId` that has format: `<cid>0<oid>`.
* Pick last object id (If no object is found then extract object id from `AccessKeyId` that has format: `<cid>0<oid>`
(if `AccessBox` was created with default parameters, or it can also be arbitrary user-defined string).
We need to do this because versions of `AccessBox` can miss the `S3-Access-Box-CRDT-Name` attribute.)
* Get appropriate object from FrostFS storage
* Decrypt `AccessBox` (see [encryption](#encryption))
@ -253,7 +257,7 @@ secp256r1 or prime256v1) is used (unless otherwise stated).
* Create ephemeral key (`SeedKey`), it's need to generate shared secret
* Generate random 32-byte (that after hex-encoded be `SecretAccessKey`) or use existing secret access key
(if `AccessBox` is being updated rather than creating brand new)
(if `AccessBox` is being updated rather than creating brand new) or use arbitrary user-provided string
* Generate shared secret as [ECDH](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman)
* Derive 32-byte key using shared secret from previous step with key derivation function based on
HMAC with SHA256 [HKDF](https://en.wikipedia.org/wiki/HKDF)

View file

@ -146,6 +146,32 @@ the secret. Format of `access_key_id`: `%cid0%oid`, where 0(zero) is a delimiter
24h). Default value is `720h` (30 days). It will be ceil rounded to the nearest amount of epoch
* `--aws-cli-credentials` - path to the aws cli credentials file, where authmate will write `access_key_id` and
`secret_access_key` to
* `--rpc-endpoint` - NEO node RPC address (must be provided if `--container-id` is NNS name)
* `--access-key-id` - access key id of s3 credential that must be created (must be unique)
* `--secret-access-key` - secret access key of s3 credential that must be used
You also can specify `AccessKeyID`/`SecretAccessKey` pair that should be created:
```shell
$ frostfs-s3-authmate issue-secret --wallet wallet.json \
--peer 192.168.130.71:8080 \
--gate-public-key 0313b1ac3a8076e155a7e797b24f0b650cccad5941ea59d7cfd51a024a8b2a06bf \
--gate-public-key 0317585fa8274f7afdf1fc5f2a2e7bece549d5175c4e5182e37924f30229aef967 \
--access-key-id my-access-key \
--secret-access-key my-secret-key \
--container-id BpExV76416Vo7GrkJsGwXGoLM35xsBwup8voedDZR3c6
Enter password for wallet.json >
{
"initial_access_key_id": "my-access-key-3",
"access_key_id": "my-access-key",
"secret_access_key": "my-secret-key",
"owner_private_key": "d9972cc4f21b07a90f4b347c72c33c1d1611c2b9a2cfd0cc28cee8cb221e8e55",
"wallet_public_key": "031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a",
"container_id": "BpExV76416Vo7GrkJsGwXGoLM35xsBwup8voedDZR3c6"
}
```
### Bearer tokens

View file

@ -761,12 +761,14 @@ Section for well-known containers to store s3-related data and settings.
containers:
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
accessbox: ExnA1gSY3kzgomi2wJxNyWo1ytWv9VAKXRE55fNXEPL2
```
| Parameter | Type | SIGHUP reload | Default value | Description |
|-------------|----------|---------------|---------------|-------------------------------------------------------------------------------------------|
| `cors` | `string` | no | | Container name for CORS configurations. If not set, container of the bucket is used. |
| `lifecycle` | `string` | no | | Container name for lifecycle configurations. If not set, container of the bucket is used. |
| Parameter | Type | SIGHUP reload | Default value | Description |
|-------------|----------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------|
| `cors` | `string` | no | | Container name for CORS configurations. If not set, container of the bucket is used. |
| `lifecycle` | `string` | no | | Container name for lifecycle configurations. If not set, container of the bucket is used. |
| `accessbox` | `string` | no | | Container name to lookup accessbox if custom aws credentials is used. If not set, custom credentials are not supported. |
# `vhs` section

View file

@ -21,6 +21,7 @@ package AccessBox {
SeedKey => Encoded public seed key
List of Gates *--> Gate
List of container policies *--> ContainerPolicy
IsCustom => True if SecretKey was imported and must be treated as it is
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -6,6 +6,7 @@ import (
"fmt"
"io"
"strconv"
"strings"
"time"
objectv2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
@ -73,20 +74,45 @@ func (x *AuthmateFrostFS) CreateContainer(ctx context.Context, prm authmate.PrmC
}
// GetCredsObject implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) GetCredsObject(ctx context.Context, addr oid.Address) (*object.Object, error) {
versions, err := x.getCredVersions(ctx, addr)
func (x *AuthmateFrostFS) GetCredsObject(ctx context.Context, prm tokens.PrmGetCredsObject) (obj *object.Object, err error) {
var readObjAddr *oid.Address
defer func() {
if prm.FallbackAddress == nil {
return
}
if err != nil && (readObjAddr == nil || !readObjAddr.Equals(*prm.FallbackAddress)) {
obj, err = x.readObject(ctx, *prm.FallbackAddress)
}
}()
versions, err := x.getCredVersions(ctx, prm.Container, prm.AccessKeyID)
if err != nil {
return nil, err
}
credObjID := addr.Object()
var addr oid.Address
isCustom := addr.DecodeString(strings.ReplaceAll(prm.AccessKeyID, "0", "/")) != nil
var credObjID oid.ID
if last := versions.GetLast(); last != nil {
credObjID = last.ObjID
} else if !isCustom {
credObjID = addr.Object()
} else {
return nil, fmt.Errorf("%w: '%s'", tokens.ErrCustomAccessKeyIDNotFound, prm.AccessKeyID)
}
readObjAddr = &oid.Address{}
readObjAddr.SetContainer(prm.Container)
readObjAddr.SetObject(credObjID)
return x.readObject(ctx, *readObjAddr)
}
func (x *AuthmateFrostFS) readObject(ctx context.Context, addr oid.Address) (*object.Object, error) {
res, err := x.frostFS.GetObject(ctx, frostfs.PrmObjectGet{
Container: addr.Container(),
Object: credObjID,
Object: addr.Object(),
})
if err != nil {
return nil, err
@ -111,17 +137,20 @@ func (x *AuthmateFrostFS) GetCredsObject(ctx context.Context, addr oid.Address)
func (x *AuthmateFrostFS) CreateObject(ctx context.Context, prm tokens.PrmObjectCreate) (oid.ID, error) {
attributes := [][2]string{{objectv2.SysAttributeExpEpoch, strconv.FormatUint(prm.ExpirationEpoch, 10)}}
if prm.NewVersionFor != nil {
var addr oid.Address
addr.SetContainer(prm.Container)
addr.SetObject(*prm.NewVersionFor)
versions, err := x.getCredVersions(ctx, addr)
if prm.NewVersionForAccessKeyID != "" {
versions, err := x.getCredVersions(ctx, prm.Container, prm.NewVersionForAccessKeyID)
if err != nil {
return oid.ID{}, err
}
if versions.GetLast() == nil {
var addr oid.Address
isCustom := addr.DecodeString(strings.ReplaceAll(prm.NewVersionForAccessKeyID, "0", "/")) != nil
if isCustom {
return oid.ID{}, fmt.Errorf("creds object for accessKeyId '%s' not found", prm.NewVersionForAccessKeyID)
}
versions.AppendVersion(&crdt.ObjectVersion{ObjID: addr.Object()})
}
@ -130,6 +159,8 @@ func (x *AuthmateFrostFS) CreateObject(ctx context.Context, prm tokens.PrmObject
}
attributes = append(attributes, [2]string{accessBoxCRDTNameAttr, versions.Name()})
} else if prm.CustomAccessKey != "" {
attributes = append(attributes, [2]string{accessBoxCRDTNameAttr, prm.CustomAccessKey})
}
for _, attr := range prm.CustomAttributes {
@ -150,21 +181,20 @@ func (x *AuthmateFrostFS) CreateObject(ctx context.Context, prm tokens.PrmObject
return res.ObjectID, nil
}
func (x *AuthmateFrostFS) getCredVersions(ctx context.Context, addr oid.Address) (*crdt.ObjectVersions, error) {
objCredSystemName := credVersionSysName(addr.Container(), addr.Object())
func (x *AuthmateFrostFS) getCredVersions(ctx context.Context, cnrID cid.ID, accessKeyID string) (*crdt.ObjectVersions, error) {
credVersions, err := x.frostFS.SearchObjects(ctx, frostfs.PrmObjectSearch{
Container: addr.Container(),
ExactAttribute: [2]string{accessBoxCRDTNameAttr, objCredSystemName},
Container: cnrID,
ExactAttribute: [2]string{accessBoxCRDTNameAttr, accessKeyID},
})
if err != nil {
return nil, fmt.Errorf("search s3 access boxes: %w", err)
}
versions := crdt.NewObjectVersions(objCredSystemName)
versions := crdt.NewObjectVersions(accessKeyID)
for _, id := range credVersions {
objVersion, err := x.frostFS.HeadObject(ctx, frostfs.PrmObjectHead{
Container: addr.Container(),
Container: cnrID,
Object: id,
})
if err != nil {
@ -184,7 +214,3 @@ func (x *AuthmateFrostFS) reqLogger(ctx context.Context) *zap.Logger {
}
return x.log
}
func credVersionSysName(cnrID cid.ID, objID oid.ID) string {
return cnrID.EncodeToString() + "0" + objID.EncodeToString()
}

View file

@ -2,22 +2,29 @@ package frostfs
import (
"context"
"errors"
"strconv"
"strings"
"testing"
objectv2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
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"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
func TestGetCredsObject(t *testing.T) {
func TestCredsObject(t *testing.T) {
ctx, bktName, payload, newPayload := context.Background(), "bucket", []byte("payload"), []byte("new-payload")
key, err := keys.NewPrivateKey()
@ -38,34 +45,242 @@ func TestGetCredsObject(t *testing.T) {
frostfs := NewAuthmateFrostFS(layer.NewTestFrostFS(key), zaptest.NewLogger(t))
cid, err := frostfs.CreateContainer(ctx, authmate.PrmContainerCreate{
cnrID, err := frostfs.CreateContainer(ctx, authmate.PrmContainerCreate{
FriendlyName: bktName,
Owner: userID,
})
require.NoError(t, err)
objID, err := frostfs.CreateObject(ctx, tokens.PrmObjectCreate{
Container: cid,
Payload: payload,
t.Run("regular access key", func(t *testing.T) {
attr1 := object.NewAttribute()
attr1.SetKey("attr1")
attr1.SetValue("val1")
prm := tokens.PrmObjectCreate{
Container: cnrID,
Filepath: "regular-obj",
ExpirationEpoch: 10,
Payload: payload,
CustomAttributes: []object.Attribute{*attr1},
}
objID, err := frostfs.CreateObject(ctx, prm)
require.NoError(t, err)
accessKeyID := cnrID.EncodeToString() + "0" + objID.EncodeToString()
obj, err := frostfs.GetCredsObject(ctx, tokens.PrmGetCredsObject{Container: cnrID, AccessKeyID: accessKeyID})
require.NoError(t, err)
assertParamsSet(t, prm, obj, userID)
t.Run("update existing", func(t *testing.T) {
attr2 := object.NewAttribute()
attr2.SetKey("attr2")
attr2.SetValue("val2")
prmNew := tokens.PrmObjectCreate{
Container: cnrID,
Filepath: "regular-obj-new",
ExpirationEpoch: 11,
Payload: newPayload,
CustomAttributes: []object.Attribute{*attr2},
NewVersionForAccessKeyID: accessKeyID,
}
_, err = frostfs.CreateObject(ctx, prmNew)
require.NoError(t, err)
obj, err = frostfs.GetCredsObject(ctx, tokens.PrmGetCredsObject{Container: cnrID, AccessKeyID: accessKeyID})
require.NoError(t, err)
assertParamsSet(t, prmNew, obj, userID)
})
t.Run("update not existing", func(t *testing.T) {
attr2 := object.NewAttribute()
attr2.SetKey("attr2")
attr2.SetValue("val2")
addr := oidtest.Address()
accessKeyIDNotExisting := getAccessKeyID(addr)
prmNew := tokens.PrmObjectCreate{
Container: cnrID,
Filepath: "regular-obj-new",
ExpirationEpoch: 11,
Payload: newPayload,
CustomAttributes: []object.Attribute{*attr2},
NewVersionForAccessKeyID: accessKeyIDNotExisting,
}
_, err = frostfs.CreateObject(ctx, prmNew)
require.NoError(t, err)
obj, err = frostfs.GetCredsObject(ctx, tokens.PrmGetCredsObject{Container: cnrID, AccessKeyID: accessKeyIDNotExisting})
require.NoError(t, err)
assertParamsSet(t, prmNew, obj, userID)
})
})
require.NoError(t, err)
var addr oid.Address
addr.SetContainer(cid)
addr.SetObject(objID)
t.Run("custom access key", func(t *testing.T) {
attr1 := object.NewAttribute()
attr1.SetKey("attr1")
attr1.SetValue("val1")
obj, err := frostfs.GetCredsObject(ctx, addr)
require.NoError(t, err)
require.Equal(t, payload, obj.Payload())
accessKeyID := "custom-access-key-id"
_, err = frostfs.CreateObject(ctx, tokens.PrmObjectCreate{
Container: cid,
Payload: newPayload,
NewVersionFor: &objID,
prm := tokens.PrmObjectCreate{
Container: cnrID,
Filepath: "custom-obj",
ExpirationEpoch: 10,
Payload: payload,
CustomAccessKey: accessKeyID,
CustomAttributes: []object.Attribute{*attr1},
}
_, err = frostfs.CreateObject(ctx, prm)
require.NoError(t, err)
obj, err := frostfs.GetCredsObject(ctx, tokens.PrmGetCredsObject{Container: cnrID, AccessKeyID: accessKeyID})
require.NoError(t, err)
assertParamsSet(t, prm, obj, userID)
t.Run("update", func(t *testing.T) {
attr2 := object.NewAttribute()
attr2.SetKey("attr2")
attr2.SetValue("val2")
prmNew := tokens.PrmObjectCreate{
Container: cnrID,
Filepath: "custom-obj-new",
ExpirationEpoch: 11,
Payload: newPayload,
CustomAttributes: []object.Attribute{*attr2},
NewVersionForAccessKeyID: accessKeyID,
}
_, err = frostfs.CreateObject(ctx, prmNew)
require.NoError(t, err)
obj, err = frostfs.GetCredsObject(ctx, tokens.PrmGetCredsObject{Container: cnrID, AccessKeyID: accessKeyID})
require.NoError(t, err)
assertParamsSet(t, prmNew, obj, userID)
})
t.Run("update not existing", func(t *testing.T) {
accessKeyIDNotExisting := "unknown"
prmNew := tokens.PrmObjectCreate{
Container: cnrID,
Payload: newPayload,
NewVersionForAccessKeyID: accessKeyIDNotExisting,
}
_, err = frostfs.CreateObject(ctx, prmNew)
require.Error(t, err)
})
})
require.NoError(t, err)
obj, err = frostfs.GetCredsObject(ctx, addr)
require.NoError(t, err)
require.Equal(t, newPayload, obj.Payload())
t.Run("fallback", func(t *testing.T) {
t.Run("regular", func(t *testing.T) {
prm := tokens.PrmObjectCreate{
Container: cnrID,
Filepath: "regular-obj",
Payload: payload,
}
objID, err := frostfs.CreateObject(ctx, prm)
require.NoError(t, err)
accessKeyID := cnrID.EncodeToString() + "0" + objID.EncodeToString()
prmNew := tokens.PrmObjectCreate{
Container: cnrID,
Filepath: "regular-obj-new",
Payload: newPayload,
NewVersionForAccessKeyID: accessKeyID,
}
objIDNew, err := frostfs.CreateObject(ctx, prmNew)
require.NoError(t, err)
addr := newAddress(cnrID, objID)
prmFallback := tokens.PrmGetCredsObject{
Container: cnrID,
AccessKeyID: accessKeyID,
FallbackAddress: &addr,
}
frostfs.frostFS.(*layer.TestFrostFS).SetObjectError(newAddress(cnrID, objIDNew), errors.New("error"))
obj, err := frostfs.GetCredsObject(ctx, prmFallback)
require.NoError(t, err)
assertParamsSet(t, prm, obj, userID)
})
t.Run("custom", func(t *testing.T) {
prm := tokens.PrmObjectCreate{
Container: cnrID,
Filepath: "custom-obj",
ExpirationEpoch: 10,
Payload: payload,
CustomAccessKey: "custom-access-key-id",
}
objID, err := frostfs.CreateObject(ctx, prm)
require.NoError(t, err)
addr := newAddress(cnrID, objID)
prmFallback := tokens.PrmGetCredsObject{
Container: cnrID,
AccessKeyID: "unknown",
FallbackAddress: &addr,
}
obj, err := frostfs.GetCredsObject(ctx, prmFallback)
require.NoError(t, err)
assertParamsSet(t, prm, obj, userID)
})
})
}
func newAddress(cnr cid.ID, obj oid.ID) oid.Address {
var addr oid.Address
addr.SetContainer(cnr)
addr.SetObject(obj)
return addr
}
func assertParamsSet(t *testing.T, prm tokens.PrmObjectCreate, obj *object.Object, userID user.ID) {
require.Equal(t, prm.Payload, obj.Payload())
require.True(t, userID.Equals(obj.OwnerID()), "owners not matched")
require.True(t, containerAttribute(obj.Attributes(), object.AttributeFilePath, prm.Filepath), "missing FilePath")
require.True(t, containerAttribute(obj.Attributes(), objectv2.SysAttributeExpEpoch, strconv.FormatUint(prm.ExpirationEpoch, 10)), "missing expiration epoch")
var crdtName string
if prm.CustomAccessKey != "" {
crdtName = prm.CustomAccessKey
} else if prm.NewVersionForAccessKeyID != "" {
crdtName = prm.NewVersionForAccessKeyID
}
if crdtName != "" {
require.Truef(t, containerAttribute(obj.Attributes(), accessBoxCRDTNameAttr, crdtName), "wrong crdt name '%s'", crdtName)
}
for _, attr := range prm.CustomAttributes {
require.True(t, containerAttribute(obj.Attributes(), attr.Key(), attr.Value()), "missing custom attribute")
}
}
func containerAttribute(attrs []object.Attribute, key, val string) bool {
for _, attr := range attrs {
if attr.Key() == key && attr.Value() == val {
return true
}
}
return false
}
func getAccessKeyID(addr oid.Address) string {
return strings.ReplaceAll(addr.EncodeToString(), "/", "0")
}

View file

@ -7,6 +7,7 @@ import (
"time"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ns"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -36,6 +37,31 @@ func ResolveContractHash(contractHash, rpcAddress string) (util.Uint160, error)
return nns.ResolveContractHash(domain)
}
// ResolveContainerID determine container id by resolving NNS name.
func ResolveContainerID(containerID, rpcAddress string) (cid.ID, error) {
var cnrID cid.ID
if err := cnrID.DecodeString(containerID); err == nil {
return cnrID, nil
}
splitName := strings.Split(containerID, ".")
if len(splitName) != 2 {
return cid.ID{}, fmt.Errorf("invalid container name: '%s'", containerID)
}
var domain container.Domain
domain.SetName(splitName[0])
domain.SetZone(splitName[1])
var nns ns.NNS
if err := nns.Dial(rpcAddress); err != nil {
return cid.ID{}, fmt.Errorf("dial nns '%s': %w", rpcAddress, err)
}
defer nns.Close()
return nns.ResolveContainerDomain(domain)
}
func TimeToEpoch(ni *netmap.NetworkInfo, now, t time.Time) (uint64, error) {
duration := t.Sub(now)
durationAbs := duration.Abs()

View file

@ -159,6 +159,7 @@ const (
FoundSeveralSystemNodes = "found several system nodes"
FailedToParsePartInfo = "failed to parse part info"
CouldNotFetchCORSContainerInfo = "couldn't fetch CORS container info"
CouldNotFetchAccessBoxContainerInfo = "couldn't fetch AccessBox container info"
CloseCredsObjectPayload = "close creds object payload"
CouldntDeleteLifecycleObject = "couldn't delete lifecycle configuration object"
CouldntCacheLifecycleConfiguration = "couldn't cache lifecycle configuration"
@ -171,4 +172,5 @@ const (
FailedToRemoveOldPartNode = "failed to remove old part node"
CouldntCacheNetworkInfo = "couldn't cache network info"
NotSupported = "not supported"
CheckCustomAccessKeyIDUniqueness = "check custom access key id uniqueness"
)