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 new param `frostfs.graceful_close_on_switch_timeout` (#475)
- Support patch object method (#479) - Support patch object method (#479)
- Add `sign` command to `frostfs-s3-authmate` (#467) - Add `sign` command to `frostfs-s3-authmate` (#467)
- Support custom aws credentials (#509)
### Changed ### Changed
- Update go version to go1.19 (#470) - 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/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens" "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" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
) )
@ -34,6 +35,11 @@ type (
postReg *RegexpSubmatcher postReg *RegexpSubmatcher
cli tokens.Credentials cli tokens.Credentials
allowedAccessKeyIDPrefixes []string // empty slice means all access key ids are allowed allowedAccessKeyIDPrefixes []string // empty slice means all access key ids are allowed
settings CenterSettings
}
CenterSettings interface {
AccessBoxContainer() (cid.ID, bool)
} }
//nolint:revive //nolint:revive
@ -50,7 +56,6 @@ type (
) )
const ( const (
accessKeyPartsNum = 2
authHeaderPartsNum = 6 authHeaderPartsNum = 6
maxFormSizeMemory = 50 * 1048576 // 50 MB maxFormSizeMemory = 50 * 1048576 // 50 MB
@ -82,12 +87,13 @@ var ContentSHA256HeaderStandardValue = map[string]struct{}{
} }
// New creates an instance of AuthCenter. // 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{ return &Center{
cli: creds, cli: creds,
reg: NewRegexpMatcher(AuthorizationFieldRegexp), reg: NewRegexpMatcher(AuthorizationFieldRegexp),
postReg: NewRegexpMatcher(postPolicyCredentialRegexp), postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
allowedAccessKeyIDPrefixes: prefixes, 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) 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"], ";") signedFields := strings.Split(submatches["signed_header_fields"], ";")
return &AuthHeader{ return &AuthHeader{
@ -114,15 +115,6 @@ func (c *Center) parseAuthHeader(header string) (*AuthHeader, error) {
}, nil }, 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 { func IsStandardContentSHA256(key string) bool {
_, ok := ContentSHA256HeaderStandardValue[key] _, ok := ContentSHA256HeaderStandardValue[key]
return ok return ok
@ -181,14 +173,14 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
return nil, err return nil, err
} }
addr, err := getAddress(authHdr.AccessKeyID) cnrID, err := c.getAccessBoxContainer(authHdr.AccessKeyID)
if err != nil { if err != nil {
return nil, err 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 { 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 { 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 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 { func checkFormatHashContentSHA256(hash string) error {
if !IsStandardContentSHA256(hash) { if !IsStandardContentSHA256(hash) {
hashBinary, err := hex.DecodeString(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"] accessKeyID := submatches["access_key_id"]
addr, err := getAddress(accessKeyID) cnrID, err := c.getAccessBoxContainer(accessKeyID)
if err != nil { if err != nil {
return nil, err 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 { 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 secret := box.Gate.SecretKey

View file

@ -19,6 +19,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
frosterr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors" frosterr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "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" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
@ -28,11 +29,23 @@ import (
"go.uber.org/zap/zaptest" "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) { 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" 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{ center := &Center{
reg: NewRegexpMatcher(AuthorizationFieldRegexp), reg: NewRegexpMatcher(AuthorizationFieldRegexp),
settings: &centerSettingsMock{},
} }
for _, tc := range []struct { for _, tc := range []struct {
@ -57,11 +70,6 @@ func TestAuthHeaderParse(t *testing.T) {
err: errors.GetAPIError(errors.ErrAuthorizationHeaderMalformed), err: errors.GetAPIError(errors.ErrAuthorizationHeaderMalformed),
expected: nil, expected: nil,
}, },
{
header: strings.ReplaceAll(defaultHeader, "oid0cid", "oidcid"),
err: errors.GetAPIError(errors.ErrInvalidAccessKeyID),
expected: nil,
},
} { } {
authHeader, err := center.parseAuthHeader(tc.header) authHeader, err := center.parseAuthHeader(tc.header)
require.ErrorIs(t, err, tc.err, 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) { func TestSignature(t *testing.T) {
secret := "66be461c3cd429941c55daf42fad2b8153e5a2016ba89c9494d97677cc9d3872" secret := "66be461c3cd429941c55daf42fad2b8153e5a2016ba89c9494d97677cc9d3872"
strToSign := "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAiYWNsIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwKICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDg0L2FjbCJ9LAogICAgWyJzdGFydHMtd2l0aCIsICIkQ29udGVudC1UeXBlIiwgImltYWdlLyJdLAogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwKICAgIFsic3RhcnRzLXdpdGgiLCAiJHgtYW16LW1ldGEtdGFnIiwgIiJdLAoKICAgIHsiWC1BbXotQ3JlZGVudGlhbCI6ICI4Vmk0MVBIbjVGMXNzY2J4OUhqMXdmMUU2aERUYURpNndxOGhxTU05NllKdTA1QzVDeUVkVlFoV1E2aVZGekFpTkxXaTlFc3BiUTE5ZDRuR3pTYnZVZm10TS8yMDE1MTIyOS91cy1lYXN0LTEvczMvYXdzNF9yZXF1ZXN0In0sCiAgICB7IngtYW16LWFsZ29yaXRobSI6ICJBV1M0LUhNQUMtU0hBMjU2In0sCiAgICB7IlgtQW16LURhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfSwKICAgIHsieC1pZ25vcmUtdG1wIjogInNvbWV0aGluZyIgfQogIF0KfQ==" strToSign := "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAiYWNsIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwKICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDg0L2FjbCJ9LAogICAgWyJzdGFydHMtd2l0aCIsICIkQ29udGVudC1UeXBlIiwgImltYWdlLyJdLAogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwKICAgIFsic3RhcnRzLXdpdGgiLCAiJHgtYW16LW1ldGEtdGFnIiwgIiJdLAoKICAgIHsiWC1BbXotQ3JlZGVudGlhbCI6ICI4Vmk0MVBIbjVGMXNzY2J4OUhqMXdmMUU2aERUYURpNndxOGhxTU05NllKdTA1QzVDeUVkVlFoV1E2aVZGekFpTkxXaTlFc3BiUTE5ZDRuR3pTYnZVZm10TS8yMDE1MTIyOS91cy1lYXN0LTEvczMvYXdzNF9yZXF1ZXN0In0sCiAgICB7IngtYW16LWFsZ29yaXRobSI6ICJBV1M0LUhNQUMtU0hBMjU2In0sCiAgICB7IlgtQW16LURhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfSwKICAgIHsieC1pZ25vcmUtdG1wIjogInNvbWV0aGluZyIgfQogIF0KfQ=="
@ -171,17 +142,17 @@ func TestCheckFormatContentSHA256(t *testing.T) {
} }
type frostFSMock struct { type frostFSMock struct {
objects map[oid.Address]*object.Object objects map[string]*object.Object
} }
func newFrostFSMock() *frostFSMock { func newFrostFSMock() *frostFSMock {
return &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) { func (f *frostFSMock) GetCredsObject(_ context.Context, prm tokens.PrmGetCredsObject) (*object.Object, error) {
obj, ok := f.objects[address] obj, ok := f.objects[prm.AccessKeyID]
if !ok { if !ok {
return nil, fmt.Errorf("not found") return nil, fmt.Errorf("not found")
} }
@ -208,7 +179,7 @@ func TestAuthenticate(t *testing.T) {
GateKey: key.PublicKey(), GateKey: key.PublicKey(),
}} }}
accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret")) accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret"), false)
require.NoError(t, err) require.NoError(t, err)
data, err := accessBox.Marshal() data, err := accessBox.Marshal()
require.NoError(t, err) require.NoError(t, err)
@ -219,10 +190,10 @@ func TestAuthenticate(t *testing.T) {
obj.SetContainerID(addr.Container()) obj.SetContainerID(addr.Container())
obj.SetID(addr.Object()) obj.SetID(addr.Object())
frostfs := newFrostFSMock() accessKeyID := getAccessKeyID(addr)
frostfs.objects[addr] = &obj
accessKeyID := addr.Container().String() + "0" + addr.Object().String() frostfs := newFrostFSMock()
frostfs.objects[accessKeyID] = &obj
awsCreds := credentials.NewStaticCredentials(accessKeyID, secret.SecretKey, "") awsCreds := credentials.NewStaticCredentials(accessKeyID, secret.SecretKey, "")
defaultSigner := v4.NewSigner(awsCreds) defaultSigner := v4.NewSigner(awsCreds)
@ -413,7 +384,7 @@ func TestAuthenticate(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
creds := tokens.New(bigConfig) creds := tokens.New(bigConfig)
cntr := New(creds, tc.prefixes) cntr := New(creds, tc.prefixes, &centerSettingsMock{})
box, err := cntr.Authenticate(tc.request) box, err := cntr.Authenticate(tc.request)
if tc.err { if tc.err {
@ -455,7 +426,7 @@ func TestHTTPPostAuthenticate(t *testing.T) {
GateKey: key.PublicKey(), GateKey: key.PublicKey(),
}} }}
accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret")) accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret"), false)
require.NoError(t, err) require.NoError(t, err)
data, err := accessBox.Marshal() data, err := accessBox.Marshal()
require.NoError(t, err) require.NoError(t, err)
@ -466,10 +437,11 @@ func TestHTTPPostAuthenticate(t *testing.T) {
obj.SetContainerID(addr.Container()) obj.SetContainerID(addr.Container())
obj.SetID(addr.Object()) obj.SetID(addr.Object())
frostfs := newFrostFSMock() accessKeyID := getAccessKeyID(addr)
frostfs.objects[addr] = &obj
frostfs := newFrostFSMock()
frostfs.objects[accessKeyID] = &obj
accessKeyID := addr.Container().String() + "0" + addr.Object().String()
invalidAccessKeyID := oidtest.Address().String() + "0" + oidtest.Address().Object().String() invalidAccessKeyID := oidtest.Address().String() + "0" + oidtest.Address().Object().String()
timeToSign := time.Now() timeToSign := time.Now()
@ -590,7 +562,7 @@ func TestHTTPPostAuthenticate(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
creds := tokens.New(bigConfig) creds := tokens.New(bigConfig)
cntr := New(creds, tc.prefixes) cntr := New(creds, tc.prefixes, &centerSettingsMock{})
box, err := cntr.Authenticate(tc.request) box, err := cntr.Authenticate(tc.request)
if tc.err { if tc.err {
@ -633,3 +605,7 @@ func getRequestWithMultipartForm(t *testing.T, policy, creds, date, sign, fieldN
return req 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) { 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) { func (m credentialsMock) GetBox(_ context.Context, _ cid.ID, accessKeyID string) (*accessbox.Box, []object.Attribute, error) {
box, ok := m.boxes[addr.String()] box, ok := m.boxes[accessKeyID]
if !ok { if !ok {
return nil, nil, &apistatus.ObjectNotFound{} return nil, nil, &apistatus.ObjectNotFound{}
} }
@ -41,11 +41,11 @@ func (m credentialsMock) GetBox(_ context.Context, addr oid.Address) (*accessbox
return box, nil, nil 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 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 return oid.Address{}, nil
} }
@ -84,9 +84,10 @@ func TestCheckSign(t *testing.T) {
mock.addBox(accessKeyAddr, expBox) mock.addBox(accessKeyAddr, expBox)
c := &Center{ c := &Center{
cli: mock, cli: mock,
reg: NewRegexpMatcher(AuthorizationFieldRegexp), reg: NewRegexpMatcher(AuthorizationFieldRegexp),
postReg: NewRegexpMatcher(postPolicyCredentialRegexp), postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
settings: &centerSettingsMock{},
} }
box, err := c.Authenticate(req) box, err := c.Authenticate(req)
require.NoError(t, err) require.NoError(t, err)

View file

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

View file

@ -1,13 +1,14 @@
package cache package cache
import ( import (
"strings"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" 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" 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/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
@ -22,18 +23,21 @@ func TestAccessBoxCacheType(t *testing.T) {
addr := oidtest.Address() addr := oidtest.Address()
box := &accessbox.Box{} 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) require.NoError(t, err)
val := cache.Get(addr) resVal := cache.Get(accessKeyID)
require.Equal(t, box, val.Box) require.Equal(t, box, resVal.Box)
require.Equal(t, attrs, val.Attributes)
require.Equal(t, 0, observedLog.Len()) require.Equal(t, 0, observedLog.Len())
err = cache.cache.Set(addr, "tmp") err = cache.cache.Set(accessKeyID, "tmp")
require.NoError(t, err) require.NoError(t, err)
assertInvalidCacheEntry(t, cache.Get(addr), observedLog) assertInvalidCacheEntry(t, cache.Get(accessKeyID), observedLog)
} }
func TestBucketsCacheType(t *testing.T) { func TestBucketsCacheType(t *testing.T) {
@ -230,3 +234,7 @@ func getObservedLogger() (*zap.Logger, *observer.ObservedLogs) {
loggerCore, observedLog := observer.New(zap.WarnLevel) loggerCore, observedLog := observer.New(zap.WarnLevel)
return zap.New(loggerCore), observedLog 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 contains options for passing to Agent.IssueSecret method.
IssueSecretOptions struct { IssueSecretOptions struct {
Container ContainerOptions Container cid.ID
AccessKeyID string
SecretAccessKey string
FrostFSKey *keys.PrivateKey FrostFSKey *keys.PrivateKey
GatesPublicKeys []*keys.PublicKey GatesPublicKeys []*keys.PublicKey
Impersonate bool Impersonate bool
@ -114,7 +116,9 @@ type (
UpdateSecretOptions struct { UpdateSecretOptions struct {
FrostFSKey *keys.PrivateKey FrostFSKey *keys.PrivateKey
GatesPublicKeys []*keys.PublicKey GatesPublicKeys []*keys.PublicKey
Address oid.Address IsCustom bool
AccessKeyID string
ContainerID cid.ID
GatePrivateKey *keys.PrivateKey GatePrivateKey *keys.PrivateKey
CustomAttributes []object.Attribute CustomAttributes []object.Attribute
} }
@ -141,7 +145,8 @@ type (
// ObtainSecretOptions contains options for passing to Agent.ObtainSecret method. // ObtainSecretOptions contains options for passing to Agent.ObtainSecret method.
ObtainSecretOptions struct { ObtainSecretOptions struct {
SecretAddress string Container cid.ID
AccessKeyID string
GatePrivateKey *keys.PrivateKey GatePrivateKey *keys.PrivateKey
} }
) )
@ -168,32 +173,9 @@ type (
} }
) )
func (a *Agent) checkContainer(ctx context.Context, opts ContainerOptions, idOwner user.ID) (cid.ID, error) { func (a *Agent) checkContainer(ctx context.Context, cnrID cid.ID) error {
if !opts.ID.Equals(cid.ID{}) { a.log.Info(logs.CheckContainer, zap.Stringer("cid", cnrID))
a.log.Info(logs.CheckContainer, zap.Stringer("cid", opts.ID)) return a.frostFS.ContainerExists(ctx, cnrID)
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 checkPolicy(policyString string) (*netmap.PlacementPolicy, error) { 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) 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 { if err != nil {
return fmt.Errorf("pack tokens: %w", err) return fmt.Errorf("pack tokens: %w", err)
} }
box.ContainerPolicy = policies box.ContainerPolicy = policies
var idOwner user.ID if err = a.checkContainer(ctx, options.Container); err != nil {
user.IDFromKey(&idOwner, options.FrostFSKey.PrivateKey.PublicKey)
id, err := a.checkContainer(ctx, options.Container, idOwner)
if err != nil {
return fmt.Errorf("check container: %w", err) return fmt.Errorf("check container: %w", err)
} }
var idOwner user.ID
user.IDFromKey(&idOwner, options.FrostFSKey.PrivateKey.PublicKey)
a.log.Info(logs.StoreBearerTokenIntoFrostFS, a.log.Info(logs.StoreBearerTokenIntoFrostFS,
zap.Stringer("owner_tkn", idOwner)) 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) creds := tokens.New(cfg)
prm := tokens.CredentialsParam{ prm := tokens.CredentialsParam{
OwnerID: idOwner, Container: options.Container,
AccessKeyID: options.AccessKeyID,
AccessBox: box, AccessBox: box,
Expiration: lifetime.Exp, Expiration: lifetime.Exp,
Keys: options.GatesPublicKeys, Keys: options.GatesPublicKeys,
CustomAttributes: options.CustomAttributes, CustomAttributes: options.CustomAttributes,
} }
addr, err := creds.Put(ctx, id, prm) addr, err := creds.Put(ctx, prm)
if err != nil { if err != nil {
return fmt.Errorf("failed to put creds: %w", err) return fmt.Errorf("failed to put creds: %w", err)
} }
accessKeyID := accessKeyIDFromAddr(addr) accessKeyID := options.AccessKeyID
if accessKeyID == "" {
accessKeyID = accessKeyIDFromAddr(addr)
}
ir := &issuingResult{ ir := &issuingResult{
InitialAccessKeyID: accessKeyID, InitialAccessKeyID: accessKeyID,
AccessKeyID: accessKeyID, AccessKeyID: accessKeyID,
SecretAccessKey: secrets.SecretKey, SecretAccessKey: secrets.SecretKey,
OwnerPrivateKey: hex.EncodeToString(secrets.EphemeralKey.Bytes()), OwnerPrivateKey: hex.EncodeToString(secrets.EphemeralKey.Bytes()),
WalletPublicKey: hex.EncodeToString(options.FrostFSKey.PublicKey().Bytes()), WalletPublicKey: hex.EncodeToString(options.FrostFSKey.PublicKey().Bytes()),
ContainerID: id.EncodeToString(), ContainerID: options.Container.EncodeToString(),
} }
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
@ -337,13 +328,15 @@ func (a *Agent) UpdateSecret(ctx context.Context, w io.Writer, options *UpdateSe
creds := tokens.New(cfg) creds := tokens.New(cfg)
box, _, err := creds.GetBox(ctx, options.Address) box, _, err := creds.GetBox(ctx, options.ContainerID, options.AccessKeyID)
if err != nil { if err != nil {
return fmt.Errorf("get accessbox: %w", err) return fmt.Errorf("get accessbox: %w", err)
} }
secret, err := hex.DecodeString(box.Gate.SecretKey) var secret []byte
if err != nil { 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) 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) 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 { if err != nil {
return fmt.Errorf("pack tokens: %w", err) 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)) zap.Stringer("owner_tkn", idOwner))
prm := tokens.CredentialsParam{ prm := tokens.CredentialsParam{
OwnerID: idOwner, Container: options.ContainerID,
AccessBox: updatedBox, AccessBox: updatedBox,
Expiration: lifetime.Exp, Expiration: lifetime.Exp,
Keys: options.GatesPublicKeys, Keys: options.GatesPublicKeys,
CustomAttributes: options.CustomAttributes, CustomAttributes: options.CustomAttributes,
} }
oldAddr := options.Address addr, err := creds.Update(ctx, prm)
addr, err := creds.Update(ctx, oldAddr, prm)
if err != nil { if err != nil {
return fmt.Errorf("failed to update creds: %w", err) return fmt.Errorf("failed to update creds: %w", err)
} }
accessKeyID := options.AccessKeyID
if !options.IsCustom {
accessKeyID = accessKeyIDFromAddr(addr)
}
ir := &issuingResult{ ir := &issuingResult{
AccessKeyID: accessKeyIDFromAddr(addr), AccessKeyID: accessKeyID,
InitialAccessKeyID: accessKeyIDFromAddr(oldAddr), InitialAccessKeyID: options.AccessKeyID,
SecretAccessKey: secrets.SecretKey, SecretAccessKey: secrets.SecretKey,
OwnerPrivateKey: hex.EncodeToString(secrets.EphemeralKey.Bytes()), OwnerPrivateKey: hex.EncodeToString(secrets.EphemeralKey.Bytes()),
WalletPublicKey: hex.EncodeToString(options.FrostFSKey.PublicKey().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) bearerCreds := tokens.New(cfg)
var addr oid.Address box, _, err := bearerCreds.GetBox(ctx, options.Container, options.AccessKeyID)
if err := addr.DecodeString(options.SecretAddress); err != nil {
return fmt.Errorf("failed to parse secret address: %w", err)
}
box, _, err := bearerCreds.GetBox(ctx, addr)
if err != nil { if err != nil {
return fmt.Errorf("failed to get tokens: %w", err) return fmt.Errorf("failed to get tokens: %w", err)
} }

View file

@ -4,22 +4,34 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"strings"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" "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" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" 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/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.uber.org/zap"
) )
var issueSecretCmd = &cobra.Command{ var issueSecretCmd = &cobra.Command{
Use: "issue-secret", Use: "issue-secret",
Short: "Issue a secret in FrostFS network", Short: "Issue a secret in FrostFS network",
Long: "Creates new s3 credentials to use with frostfs-s3-gw", 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 Example: `To create new s3 credentials use:
frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a --attributes LOGIN=NUUb82KR2JrVByHs2YSKgtK29gKnF5q6Vt`, 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, RunE: runIssueSecretCmd,
} }
@ -54,6 +66,9 @@ const (
poolHealthcheckTimeoutFlag = "pool-healthcheck-timeout" poolHealthcheckTimeoutFlag = "pool-healthcheck-timeout"
poolRebalanceIntervalFlag = "pool-rebalance-interval" poolRebalanceIntervalFlag = "pool-rebalance-interval"
poolStreamTimeoutFlag = "pool-stream-timeout" poolStreamTimeoutFlag = "pool-stream-timeout"
accessKeyIDFlag = "access-key-id"
secretAccessKeyFlag = "secret-access-key"
) )
func initIssueSecretCmd() { func initIssueSecretCmd() {
@ -73,6 +88,9 @@ func initIssueSecretCmd() {
issueSecretCmd.Flags().Duration(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status") 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().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(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(walletFlag)
_ = issueSecretCmd.MarkFlagRequired(peerFlag) _ = 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)) 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 var gatesPublicKeys []*keys.PublicKey
for _, keyStr := range viper.GetStringSlice(gatePublicKeyFlag) { for _, keyStr := range viper.GetStringSlice(gatePublicKeyFlag) {
gpk, err := keys.NewPublicKeyFromString(keyStr) 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)) 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)) customAttrs, err := parseObjectAttrs(viper.GetString(attributesFlag))
if err != nil { if err != nil {
return wrapPreparationError(fmt.Errorf("failed to parse attributes: %s", err)) return wrapPreparationError(fmt.Errorf("failed to parse attributes: %s", err))
} }
issueSecretOptions := &authmate.IssueSecretOptions{ issueSecretOptions := &authmate.IssueSecretOptions{
Container: authmate.ContainerOptions{ Container: accessBox,
ID: cnrID, AccessKeyID: accessKeyID,
FriendlyName: viper.GetString(containerFriendlyNameFlag), SecretAccessKey: secretAccessKey,
PlacementPolicy: viper.GetString(containerPlacementPolicyFlag),
},
FrostFSKey: key, FrostFSKey: key,
GatesPublicKeys: gatesPublicKeys, GatesPublicKeys: gatesPublicKeys,
Impersonate: true, Impersonate: true,
@ -164,3 +186,59 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error {
} }
return nil 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" "context"
"fmt" "fmt"
"os" "os"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
@ -24,7 +23,6 @@ var obtainSecretCmd = &cobra.Command{
const ( const (
gateWalletFlag = "gate-wallet" gateWalletFlag = "gate-wallet"
gateAddressFlag = "gate-address" gateAddressFlag = "gate-address"
accessKeyIDFlag = "access-key-id"
) )
const ( const (
@ -38,10 +36,12 @@ func initObtainSecretCmd() {
obtainSecretCmd.Flags().String(gateWalletFlag, "", "Path to the s3 gateway wallet to decrypt accessbox") 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(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(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(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(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(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status")
obtainSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC") 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(walletFlag)
_ = obtainSecretCmd.MarkFlagRequired(peerFlag) _ = 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)) 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{ obtainSecretOptions := &authmate.ObtainSecretOptions{
SecretAddress: strings.Replace(viper.GetString(accessKeyIDFlag), "0", "/", 1), Container: accessBox,
AccessKeyID: accessKeyID,
GatePrivateKey: gateKey, GatePrivateKey: gateKey,
} }

View file

@ -4,11 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet" "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/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "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(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(gateWalletFlag, "", "Path to the s3 gateway wallet to decrypt accessbox")
updateSecretCmd.Flags().String(gateAddressFlag, "", "Address of the s3 gateway wallet account") 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().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(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(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(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status")
updateSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC") 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(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(walletFlag)
_ = updateSecretCmd.MarkFlagRequired(peerFlag) _ = 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)) return wrapPreparationError(fmt.Errorf("failed to load s3 gate private key: %s", err))
} }
var accessBoxAddress oid.Address accessBox, accessKeyID, isCustom, err := getAccessBoxID()
credAddr := strings.Replace(viper.GetString(accessKeyIDFlag), "0", "/", 1) if err != nil {
if err = accessBoxAddress.DecodeString(credAddr); err != nil { return wrapPreparationError(err)
return wrapPreparationError(fmt.Errorf("failed to parse creds address: %w", err))
} }
var gatesPublicKeys []*keys.PublicKey var gatesPublicKeys []*keys.PublicKey
@ -101,7 +100,9 @@ func runUpdateSecretCmd(cmd *cobra.Command, _ []string) error {
} }
updateSecretOptions := &authmate.UpdateSecretOptions{ updateSecretOptions := &authmate.UpdateSecretOptions{
Address: accessBoxAddress, ContainerID: accessBox,
AccessKeyID: accessKeyID,
IsCustom: isCustom,
FrostFSKey: key, FrostFSKey: key,
GatesPublicKeys: gatesPublicKeys, GatesPublicKeys: gatesPublicKeys,
GatePrivateKey: gateKey, GatePrivateKey: gateKey,

View file

@ -3,6 +3,7 @@ package modules
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@ -11,8 +12,11 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" "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"
"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/logs"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" "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" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -163,3 +167,23 @@ func parseObjectAttrs(attributes string) ([]object.Attribute, error) {
return attrs, nil 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 resolveZoneList []string
isResolveListAllow bool // True if ResolveZoneList contains allowed zones isResolveListAllow bool // True if ResolveZoneList contains allowed zones
frostfsidValidation bool frostfsidValidation bool
accessbox *cid.ID
mu sync.RWMutex mu sync.RWMutex
namespaces Namespaces namespaces Namespaces
@ -132,18 +133,7 @@ type (
func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App { func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App {
objPool, treePool, key := getPools(ctx, log.logger, v) 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{ app := &App{
ctr: ctr,
log: log.logger, log: log.logger,
cfg: v, cfg: v,
pool: objPool, pool: objPool,
@ -162,6 +152,8 @@ func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App {
} }
func (a *App) init(ctx context.Context) { func (a *App) init(ctx context.Context) {
a.initResolver()
a.initAuthCenter(ctx)
a.setRuntimeParameters() a.setRuntimeParameters()
a.initFrostfsID(ctx) a.initFrostfsID(ctx)
a.initPolicyStorage(ctx) a.initPolicyStorage(ctx)
@ -171,9 +163,26 @@ func (a *App) init(ctx context.Context) {
a.initTracing(ctx) a.initTracing(ctx)
} }
func (a *App) initLayer(ctx context.Context) { func (a *App) initAuthCenter(ctx context.Context) {
a.initResolver() 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 // prepare random key for anonymous requests
randomKey, err := keys.NewPrivateKey() randomKey, err := keys.NewPrivateKey()
if err != nil { if err != nil {
@ -484,6 +493,14 @@ func (s *appSettings) RetryStrategy() handler.RetryStrategy {
return s.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) { func (a *App) initAPI(ctx context.Context) {
a.initLayer(ctx) a.initLayer(ctx)
a.initHandler() 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) { 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) containerString := a.cfg.GetString(cfgKey)
var id cid.ID var id cid.ID
if err = id.DecodeString(containerString); err != nil { if err := id.DecodeString(containerString); err != nil {
i := strings.Index(containerString, ".") i := strings.Index(containerString, ".")
if i < 0 { 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 { 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) { func getContainerInfo(ctx context.Context, id cid.ID, frostFSPool *pool.Pool) (*data.BucketInfo, error) {

View file

@ -208,6 +208,7 @@ const ( // Settings.
// Containers. // Containers.
cfgContainersCORS = "containers.cors" cfgContainersCORS = "containers.cors"
cfgContainersLifecycle = "containers.lifecycle" cfgContainersLifecycle = "containers.lifecycle"
cfgContainersAccessBox = "containers.accessbox"
// Command line args. // Command line args.
cmdHelp = "help" 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. // PackTokens adds bearer and session tokens to BearerTokens and SessionToken lists respectively.
// Session token can be nil. // Session token can be nil.
// Secret can be nil. In such case secret will be generated. // 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{} box := &AccessBox{}
ephemeralKey, err := keys.NewPrivateKey() ephemeralKey, err := keys.NewPrivateKey()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("create ephemeral key: %w", err) return nil, nil, fmt.Errorf("create ephemeral key: %w", err)
} }
box.SeedKey = ephemeralKey.PublicKey().Bytes() box.SeedKey = ephemeralKey.PublicKey().Bytes()
box.IsCustom = isCustomSecret
if secret == nil { if secret == nil {
secret, err = generateSecret() 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 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. // GetTokens returns gate tokens from AccessBox.
@ -133,7 +139,7 @@ func (x *AccessBox) GetTokens(owner *keys.PrivateKey) (*GateData, error) {
continue continue
} }
gateData, err := decodeGate(gate, owner, seedKey) gateData, err := x.decodeGate(gate, owner, seedKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decode gate: %w", err) 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 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) data, err := decrypt(owner, seedKey, gate.Tokens)
if err != nil { if err != nil {
return nil, fmt.Errorf("decrypt tokens: %w", err) 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 := NewGateData(owner.PublicKey(), &bearerTkn)
gateData.SessionTokens = sessionTkns 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 return gateData, nil
} }

View file

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

View file

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

View file

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

View file

@ -14,7 +14,6 @@ import (
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/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/nspcc-dev/neo-go/pkg/crypto/keys"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -22,13 +21,14 @@ import (
type ( type (
// Credentials is a bearer token get/put interface. // Credentials is a bearer token get/put interface.
Credentials interface { Credentials interface {
GetBox(context.Context, oid.Address) (*accessbox.Box, []object.Attribute, error) GetBox(context.Context, cid.ID, string) (*accessbox.Box, []object.Attribute, error)
Put(context.Context, cid.ID, CredentialsParam) (oid.Address, error) Put(context.Context, CredentialsParam) (oid.Address, error)
Update(context.Context, oid.Address, CredentialsParam) (oid.Address, error) Update(context.Context, CredentialsParam) (oid.Address, error)
} }
CredentialsParam struct { CredentialsParam struct {
OwnerID user.ID Container cid.ID
AccessKeyID string
AccessBox *accessbox.AccessBox AccessBox *accessbox.AccessBox
Expiration uint64 Expiration uint64
Keys keys.PublicKeys Keys keys.PublicKeys
@ -49,13 +49,16 @@ type (
CacheConfig *cache.Config CacheConfig *cache.Config
RemovingCheckAfterDurations time.Duration RemovingCheckAfterDurations time.Duration
} }
Box struct {
AccessBox *accessbox.AccessBox
Attributes []object.Attribute
Address *oid.Address
}
) )
// PrmObjectCreate groups parameters of objects created by credential tool. // PrmObjectCreate groups parameters of objects created by credential tool.
type PrmObjectCreate struct { type PrmObjectCreate struct {
// FrostFS identifier of the object creator.
Creator user.ID
// FrostFS container to store the object. // FrostFS container to store the object.
Container cid.ID Container cid.ID
@ -64,7 +67,12 @@ type PrmObjectCreate struct {
// Optional. // Optional.
// If provided cred object will be created using crdt approach. // 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. // Last FrostFS epoch of the object lifetime.
ExpirationEpoch uint64 ExpirationEpoch uint64
@ -76,6 +84,21 @@ type PrmObjectCreate struct {
CustomAttributes []object.Attribute 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. // FrostFS represents virtual connection to FrostFS network.
type FrostFS interface { type FrostFS interface {
// CreateObject creates and saves a parameterized object in the specified // 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 // It returns exactly one non-nil value. It returns any error encountered which
// prevented the object payload from being read. // prevented the object payload from being read.
// Returns ErrCustomAccessKeyIDNotFound if provided AccessKey is custom, and it was not found.
// Object must contain full payload. // Object must contain full payload.
GetCredsObject(context.Context, oid.Address) (*object.Object, error) GetCredsObject(context.Context, PrmGetCredsObject) (*object.Object, error)
} }
var ( 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) { func (c *cred) GetBox(ctx context.Context, cnrID cid.ID, accessKeyID string) (*accessbox.Box, []object.Attribute, error) {
cachedBoxValue := c.cache.Get(addr) cachedBoxValue := c.cache.Get(accessKeyID)
if cachedBoxValue != nil { 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 { if err != nil {
return nil, nil, fmt.Errorf("get access box: %w", err) 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 { if err != nil {
return nil, nil, fmt.Errorf("get gate box: %w", err) 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 { if time.Since(cachedBoxValue.PutTime) < c.removingCheckDuration {
return cachedBoxValue.Box, cachedBoxValue.Attributes, nil 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 err != nil {
if client.IsErrObjectAlreadyRemoved(err) { if client.IsErrObjectAlreadyRemoved(err) {
c.cache.Delete(addr) c.cache.Delete(accessKeyID)
return nil, nil, fmt.Errorf("get access box: %w", err) return nil, nil, fmt.Errorf("get access box: %w", err)
} }
return cachedBoxValue.Box, cachedBoxValue.Attributes, nil return cachedBoxValue.Box, cachedBoxValue.Attributes, nil
} }
cachedBox, err := box.GetBox(c.key) cachedBox, err := box.AccessBox.GetBox(c.key)
if err != nil { if err != nil {
c.cache.Delete(addr) c.cache.Delete(accessKeyID)
return nil, nil, fmt.Errorf("get gate box: %w", err) return nil, nil, fmt.Errorf("get gate box: %w", err)
} }
// we need this to reset PutTime // we need this to reset PutTime
// to don't check for removing each time after removingCheckDuration interval // 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) { func (c *cred) putBoxToCache(accessKeyID string, val *cache.AccessBoxCacheValue) {
if err := c.cache.Put(addr, box, attrs); err != nil { if err := c.cache.Put(accessKeyID, val); err != nil {
c.log.Warn(logs.CouldntPutAccessBoxIntoCache, zap.String("address", addr.EncodeToString())) c.log.Warn(logs.CouldntPutAccessBoxIntoCache, zap.String("accessKeyID", accessKeyID))
} }
} }
func (c *cred) getAccessBox(ctx context.Context, addr oid.Address) (*accessbox.AccessBox, []object.Attribute, error) { func (c *cred) getAccessBox(ctx context.Context, cnrID cid.ID, accessKeyID string, fallbackAddr *oid.Address) (*Box, error) {
obj, err := c.frostFS.GetCredsObject(ctx, addr) prm := PrmGetCredsObject{
Container: cnrID,
AccessKeyID: accessKeyID,
FallbackAddress: fallbackAddr,
}
obj, err := c.frostFS.GetCredsObject(ctx, prm)
if err != nil { 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 // decode access box
var box accessbox.AccessBox var box accessbox.AccessBox
if err = box.Unmarshal(obj.Payload()); err != nil { 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) { func (c *cred) Put(ctx context.Context, prm CredentialsParam) (oid.Address, error) {
return c.createObject(ctx, idCnr, nil, prm) 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) { func (c *cred) Update(ctx context.Context, prm CredentialsParam) (oid.Address, error) {
objID := addr.Object() return c.createObject(ctx, prm, true)
return c.createObject(ctx, addr.Container(), &objID, prm)
} }
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 { if len(prm.Keys) == 0 {
return oid.Address{}, ErrEmptyPublicKeys return oid.Address{}, ErrEmptyPublicKeys
} else if prm.AccessBox == nil { } 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) return oid.Address{}, fmt.Errorf("marshall box: %w", err)
} }
var newVersionFor string
if update {
newVersionFor = prm.AccessKeyID
}
idObj, err := c.frostFS.CreateObject(ctx, PrmObjectCreate{ idObj, err := c.frostFS.CreateObject(ctx, PrmObjectCreate{
Creator: prm.OwnerID, Container: prm.Container,
Container: cnrID, Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box",
Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box", ExpirationEpoch: prm.Expiration,
ExpirationEpoch: prm.Expiration, CustomAccessKey: prm.AccessKeyID,
NewVersionFor: newVersionFor, NewVersionForAccessKeyID: newVersionFor,
Payload: data, Payload: data,
CustomAttributes: prm.CustomAttributes, CustomAttributes: prm.CustomAttributes,
}) })
if err != nil { if err != nil {
return oid.Address{}, fmt.Errorf("create object: %w", err) 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 var addr oid.Address
addr.SetObject(idObj) addr.SetObject(idObj)
addr.SetContainer(cnrID) addr.SetContainer(prm.Container)
return addr, nil return addr, nil
} }

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"errors" "errors"
"strings"
"testing" "testing"
"time" "time"
@ -15,27 +16,40 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" 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/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest" "go.uber.org/zap/zaptest"
) )
type frostfsMock struct { type frostfsMock struct {
objects map[oid.Address][]*object.Object key *keys.PrivateKey
errors map[oid.Address]error objects map[string][]*object.Object
errors map[string]error
} }
func newFrostfsMock() *frostfsMock { func newFrostfsMock(key *keys.PrivateKey) *frostfsMock {
return &frostfsMock{ return &frostfsMock{
objects: map[oid.Address][]*object.Object{}, objects: map[string][]*object.Object{},
errors: map[oid.Address]error{}, 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) { func (f *frostfsMock) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.ID, error) {
var obj object.Object var obj object.Object
obj.SetPayload(prm.Payload) obj.SetPayload(prm.Payload)
obj.SetOwnerID(prm.Creator) obj.SetOwnerID(f.ownerID())
obj.SetContainerID(prm.Container) obj.SetContainerID(prm.Container)
a := object.NewAttribute() a := object.NewAttribute()
@ -44,19 +58,15 @@ func (f *frostfsMock) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.
prm.CustomAttributes = append(prm.CustomAttributes, *a) prm.CustomAttributes = append(prm.CustomAttributes, *a)
obj.SetAttributes(prm.CustomAttributes...) obj.SetAttributes(prm.CustomAttributes...)
if prm.NewVersionFor != nil { if prm.NewVersionForAccessKeyID != "" {
var addr oid.Address _, ok := f.objects[prm.NewVersionForAccessKeyID]
addr.SetObject(*prm.NewVersionFor)
addr.SetContainer(prm.Container)
_, ok := f.objects[addr]
if !ok { if !ok {
return oid.ID{}, errors.New("not found") return oid.ID{}, errors.New("not found")
} }
objID := oidtest.ID() objID := oidtest.ID()
obj.SetID(objID) obj.SetID(objID)
f.objects[addr] = append(f.objects[addr], &obj) f.objects[prm.NewVersionForAccessKeyID] = append(f.objects[prm.NewVersionForAccessKeyID], &obj)
return objID, nil return objID, nil
} }
@ -64,22 +74,27 @@ func (f *frostfsMock) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.
objID := oidtest.ID() objID := oidtest.ID()
obj.SetID(objID) obj.SetID(objID)
accessKeyID := prm.CustomAccessKey
if accessKeyID == "" {
accessKeyID = prm.Container.EncodeToString() + "0" + objID.EncodeToString()
}
var addr oid.Address var addr oid.Address
addr.SetObject(objID) addr.SetObject(objID)
addr.SetContainer(prm.Container) addr.SetContainer(prm.Container)
f.objects[addr] = []*object.Object{&obj} f.objects[accessKeyID] = []*object.Object{&obj}
return objID, nil return objID, nil
} }
func (f *frostfsMock) GetCredsObject(_ context.Context, address oid.Address) (*object.Object, error) { func (f *frostfsMock) GetCredsObject(_ context.Context, prm PrmGetCredsObject) (*object.Object, error) {
if err := f.errors[address]; err != nil { if err := f.errors[prm.AccessKeyID]; err != nil {
return nil, err return nil, err
} }
objects, ok := f.objects[address] objects, ok := f.objects[prm.AccessKeyID]
if !ok { if !ok {
return nil, errors.New("not found") return nil, ErrCustomAccessKeyIDNotFound
} }
return objects[len(objects)-1], nil return objects[len(objects)-1], nil
@ -100,7 +115,7 @@ func TestRemovingAccessBox(t *testing.T) {
sk, err := hex.DecodeString(secretKey) sk, err := hex.DecodeString(secretKey)
require.NoError(t, err) require.NoError(t, err)
accessBox, _, err := accessbox.PackTokens(gateData, sk) accessBox, _, err := accessbox.PackTokens(gateData, sk, false)
require.NoError(t, err) require.NoError(t, err)
data, err := accessBox.Marshal() data, err := accessBox.Marshal()
require.NoError(t, err) require.NoError(t, err)
@ -111,9 +126,24 @@ func TestRemovingAccessBox(t *testing.T) {
obj.SetID(addr.Object()) obj.SetID(addr.Object())
obj.SetContainerID(addr.Container()) 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{ frostfs := &frostfsMock{
objects: map[oid.Address][]*object.Object{addr: {&obj}}, objects: map[string][]*object.Object{accessKeyID: {&obj}, accessKeyIDCustom: {&objCustom}},
errors: map[oid.Address]error{}, errors: map[string]error{},
} }
cfg := Config{ cfg := Config{
@ -129,15 +159,30 @@ func TestRemovingAccessBox(t *testing.T) {
creds := New(cfg) 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) require.NoError(t, err)
frostfs.errors[addr] = errors.New("network error") frostfs.errors[accessKeyID] = errors.New("network error")
_, _, err = creds.GetBox(ctx, addr) _, _, 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) require.NoError(t, err)
frostfs.errors[addr] = &apistatus.ObjectAlreadyRemoved{} frostfs.errors[accessKeyID] = &apistatus.ObjectAlreadyRemoved{}
_, _, err = creds.GetBox(ctx, addr) _, _, 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) require.Error(t, err)
} }
@ -153,8 +198,9 @@ func TestGetBox(t *testing.T) {
}} }}
secret := []byte("secret") secret := []byte("secret")
accessBox, _, err := accessbox.PackTokens(gateData, secret) accessBox, secrets, err := accessbox.PackTokens(gateData, secret, false)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, hex.EncodeToString(secret), secrets.SecretKey)
data, err := accessBox.Marshal() data, err := accessBox.Marshal()
require.NoError(t, err) 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) { t.Run("no removing check, accessbox from cache", func(t *testing.T) {
frostfs := newFrostfsMock() creds := newCreds(key, cfg, time.Hour)
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = time.Hour
cfg.Key = key
creds := New(cfg)
cnrID := cidtest.ID() 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) require.NoError(t, err)
_, _, err = creds.GetBox(ctx, addr) accessKeyID := getAccessKeyID(addr)
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err) require.NoError(t, err)
frostfs.errors[addr] = &apistatus.ObjectAlreadyRemoved{} creds.(*cred).frostFS.(*frostfsMock).errors[accessKeyID] = &apistatus.ObjectAlreadyRemoved{}
_, _, err = creds.GetBox(ctx, addr) _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err) require.NoError(t, err)
}) })
t.Run("error while getting box from frostfs", func(t *testing.T) { t.Run("error while getting box from frostfs", func(t *testing.T) {
frostfs := newFrostfsMock() creds := newCreds(key, cfg, 0)
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
cnrID := cidtest.ID() 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) require.NoError(t, err)
frostfs.errors[addr] = errors.New("network error") accessKeyID := getAccessKeyID(addr)
_, _, err = creds.GetBox(ctx, addr) creds.(*cred).frostFS.(*frostfsMock).errors[accessKeyID] = errors.New("network error")
_, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.Error(t, err) require.Error(t, err)
}) })
t.Run("invalid key", func(t *testing.T) { t.Run("invalid key", func(t *testing.T) {
frostfs := newFrostfsMock() frostfs := newFrostfsMock(key)
var obj object.Object var obj object.Object
obj.SetPayload(data) obj.SetPayload(data)
addr := oidtest.Address() addr := oidtest.Address()
frostfs.objects[addr] = []*object.Object{&obj} accessKeyID := getAccessKeyID(addr)
frostfs.objects[accessKeyID] = []*object.Object{&obj}
cfg.FrostFS = frostfs cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0 cfg.RemovingCheckAfterDurations = 0
cfg.Key = &keys.PrivateKey{} cfg.Key = &keys.PrivateKey{}
creds := New(cfg) creds := New(cfg)
_, _, err = creds.GetBox(ctx, addr) _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.Error(t, err) require.Error(t, err)
}) })
t.Run("invalid payload", func(t *testing.T) { t.Run("invalid payload", func(t *testing.T) {
frostfs := newFrostfsMock() frostfs := newFrostfsMock(key)
var obj object.Object var obj object.Object
obj.SetPayload([]byte("invalid")) obj.SetPayload([]byte("invalid"))
addr := oidtest.Address() addr := oidtest.Address()
frostfs.objects[addr] = []*object.Object{&obj} accessKeyID := getAccessKeyID(addr)
frostfs.objects[accessKeyID] = []*object.Object{&obj}
cfg.FrostFS = frostfs cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0 cfg.RemovingCheckAfterDurations = 0
cfg.Key = key cfg.Key = key
creds := New(cfg) creds := New(cfg)
_, _, err = creds.GetBox(ctx, addr) _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.Error(t, err) require.Error(t, err)
}) })
t.Run("check attributes update", func(t *testing.T) { t.Run("check attributes update", func(t *testing.T) {
frostfs := newFrostfsMock() creds := newCreds(key, cfg, 0)
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
cnrID := cidtest.ID() 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) 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) 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) require.NoError(t, err)
_, newBoxAttrs, err := creds.GetBox(ctx, addr) _, newBoxAttrs, err := creds.GetBox(ctx, addr.Container(), accessKeyID)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, len(boxAttrs)+1, len(newBoxAttrs)) require.Equal(t, len(boxAttrs)+1, len(newBoxAttrs))
}) })
t.Run("check accessbox update", func(t *testing.T) { t.Run("check accessbox update", func(t *testing.T) {
frostfs := newFrostfsMock() creds := newCreds(key, cfg, 0)
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
cnrID := cidtest.ID() 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) 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.NoError(t, err)
require.Equal(t, hex.EncodeToString(secret), box.Gate.SecretKey) require.Equal(t, hex.EncodeToString(secret), box.Gate.SecretKey)
@ -286,44 +331,134 @@ func TestGetBox(t *testing.T) {
}} }}
newSecret := []byte("new-secret") newSecret := []byte("new-secret")
newAccessBox, _, err := accessbox.PackTokens(newGateData, newSecret) newAccessBox, _, err := accessbox.PackTokens(newGateData, newSecret, false)
require.NoError(t, err) 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) require.NoError(t, err)
_, _, err = creds.GetBox(ctx, addr) _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID)
require.Error(t, err) require.Error(t, err)
cfg.Key = newKey newCfg := Config{
newCreds := New(cfg) 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.NoError(t, err)
require.Equal(t, hex.EncodeToString(newSecret), box.Gate.SecretKey) 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) { t.Run("empty keys", func(t *testing.T) {
frostfs := newFrostfsMock() creds := newCreds(key, cfg, 0)
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
cnrID := cidtest.ID() 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) require.ErrorIs(t, err, ErrEmptyPublicKeys)
}) })
t.Run("empty accessbox", func(t *testing.T) { t.Run("empty accessbox", func(t *testing.T) {
frostfs := newFrostfsMock() creds := newCreds(key, cfg, 0)
cfg.FrostFS = frostfs
cfg.RemovingCheckAfterDurations = 0
cfg.Key = key
creds := New(cfg)
cnrID := cidtest.ID() 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) 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: Object s3 credentials are formed based on:
* `AccessKeyId` - is concatenated container id and object id (`<cid>0<oid>`) of `AccessBox` ( * `AccessKeyId` - is concatenated container id and object id (`<cid>0<oid>`) of `AccessBox` (
e.g. `2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf`) e.g. `2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf`).
* `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.
* `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 > **Note**: sensitive info in `AccessBox` is [encrypted](#encryption), so only someone who posses specific private key
> can decrypt such info. > can decrypt such info.
@ -192,7 +194,7 @@ It contains:
* List of gate data: * 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) * Gate public key (so that gate (when it will decrypt data later) know which item from the list it should process)
* Encrypted tokens: * 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 * Marshaled bearer token - more detail
in [spec](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/commit/4c68d92468503b10282c8a92af83a56f170c8a3a/acl/types.proto#L189) in [spec](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/commit/4c68d92468503b10282c8a92af83a56f170c8a3a/acl/types.proto#L189)
* Marshaled session token - more detail * Marshaled session token - more detail
@ -229,10 +231,12 @@ relevant data) the following sequence is used:
</a> </a>
* Search all object whose attribute `S3-Access-Box-CRDT-Name` is equal to `AccessKeyId` (extract container id * 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) * Get metadata for these object using `HEAD` requests (not `Get` to reduce network traffic)
* Sort all these objects by creation epoch and object id * 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.) We need to do this because versions of `AccessBox` can miss the `S3-Access-Box-CRDT-Name` attribute.)
* Get appropriate object from FrostFS storage * Get appropriate object from FrostFS storage
* Decrypt `AccessBox` (see [encryption](#encryption)) * 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 * 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 * 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) * 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 * 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) 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 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 * `--aws-cli-credentials` - path to the aws cli credentials file, where authmate will write `access_key_id` and
`secret_access_key` to `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 ### Bearer tokens

View file

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

View file

@ -21,6 +21,7 @@ package AccessBox {
SeedKey => Encoded public seed key SeedKey => Encoded public seed key
List of Gates *--> Gate List of Gates *--> Gate
List of container policies *--> ContainerPolicy 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" "fmt"
"io" "io"
"strconv" "strconv"
"strings"
"time" "time"
objectv2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" 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. // GetCredsObject implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) GetCredsObject(ctx context.Context, addr oid.Address) (*object.Object, error) { func (x *AuthmateFrostFS) GetCredsObject(ctx context.Context, prm tokens.PrmGetCredsObject) (obj *object.Object, err error) {
versions, err := x.getCredVersions(ctx, addr) 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 { if err != nil {
return nil, err 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 { if last := versions.GetLast(); last != nil {
credObjID = last.ObjID 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{ res, err := x.frostFS.GetObject(ctx, frostfs.PrmObjectGet{
Container: addr.Container(), Container: addr.Container(),
Object: credObjID, Object: addr.Object(),
}) })
if err != nil { if err != nil {
return nil, err 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) { func (x *AuthmateFrostFS) CreateObject(ctx context.Context, prm tokens.PrmObjectCreate) (oid.ID, error) {
attributes := [][2]string{{objectv2.SysAttributeExpEpoch, strconv.FormatUint(prm.ExpirationEpoch, 10)}} attributes := [][2]string{{objectv2.SysAttributeExpEpoch, strconv.FormatUint(prm.ExpirationEpoch, 10)}}
if prm.NewVersionFor != nil { if prm.NewVersionForAccessKeyID != "" {
var addr oid.Address versions, err := x.getCredVersions(ctx, prm.Container, prm.NewVersionForAccessKeyID)
addr.SetContainer(prm.Container)
addr.SetObject(*prm.NewVersionFor)
versions, err := x.getCredVersions(ctx, addr)
if err != nil { if err != nil {
return oid.ID{}, err return oid.ID{}, err
} }
if versions.GetLast() == nil { 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()}) 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()}) 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 { for _, attr := range prm.CustomAttributes {
@ -150,21 +181,20 @@ func (x *AuthmateFrostFS) CreateObject(ctx context.Context, prm tokens.PrmObject
return res.ObjectID, nil return res.ObjectID, nil
} }
func (x *AuthmateFrostFS) getCredVersions(ctx context.Context, addr oid.Address) (*crdt.ObjectVersions, error) { func (x *AuthmateFrostFS) getCredVersions(ctx context.Context, cnrID cid.ID, accessKeyID string) (*crdt.ObjectVersions, error) {
objCredSystemName := credVersionSysName(addr.Container(), addr.Object())
credVersions, err := x.frostFS.SearchObjects(ctx, frostfs.PrmObjectSearch{ credVersions, err := x.frostFS.SearchObjects(ctx, frostfs.PrmObjectSearch{
Container: addr.Container(), Container: cnrID,
ExactAttribute: [2]string{accessBoxCRDTNameAttr, objCredSystemName}, ExactAttribute: [2]string{accessBoxCRDTNameAttr, accessKeyID},
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("search s3 access boxes: %w", err) return nil, fmt.Errorf("search s3 access boxes: %w", err)
} }
versions := crdt.NewObjectVersions(objCredSystemName) versions := crdt.NewObjectVersions(accessKeyID)
for _, id := range credVersions { for _, id := range credVersions {
objVersion, err := x.frostFS.HeadObject(ctx, frostfs.PrmObjectHead{ objVersion, err := x.frostFS.HeadObject(ctx, frostfs.PrmObjectHead{
Container: addr.Container(), Container: cnrID,
Object: id, Object: id,
}) })
if err != nil { if err != nil {
@ -184,7 +214,3 @@ func (x *AuthmateFrostFS) reqLogger(ctx context.Context) *zap.Logger {
} }
return x.log 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 ( import (
"context" "context"
"errors"
"strconv"
"strings"
"testing" "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/layer"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" "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/accessbox"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "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" 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" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest" "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") ctx, bktName, payload, newPayload := context.Background(), "bucket", []byte("payload"), []byte("new-payload")
key, err := keys.NewPrivateKey() key, err := keys.NewPrivateKey()
@ -38,34 +45,242 @@ func TestGetCredsObject(t *testing.T) {
frostfs := NewAuthmateFrostFS(layer.NewTestFrostFS(key), zaptest.NewLogger(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, FriendlyName: bktName,
Owner: userID, Owner: userID,
}) })
require.NoError(t, err) require.NoError(t, err)
objID, err := frostfs.CreateObject(ctx, tokens.PrmObjectCreate{ t.Run("regular access key", func(t *testing.T) {
Container: cid, attr1 := object.NewAttribute()
Payload: payload, 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 t.Run("custom access key", func(t *testing.T) {
addr.SetContainer(cid) attr1 := object.NewAttribute()
addr.SetObject(objID) attr1.SetKey("attr1")
attr1.SetValue("val1")
obj, err := frostfs.GetCredsObject(ctx, addr) accessKeyID := "custom-access-key-id"
require.NoError(t, err)
require.Equal(t, payload, obj.Payload())
_, err = frostfs.CreateObject(ctx, tokens.PrmObjectCreate{ prm := tokens.PrmObjectCreate{
Container: cid, Container: cnrID,
Payload: newPayload, Filepath: "custom-obj",
NewVersionFor: &objID, 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) t.Run("fallback", func(t *testing.T) {
require.NoError(t, err) t.Run("regular", func(t *testing.T) {
require.Equal(t, newPayload, obj.Payload()) 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" "time"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" "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/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ns" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ns"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
@ -36,6 +37,31 @@ func ResolveContractHash(contractHash, rpcAddress string) (util.Uint160, error)
return nns.ResolveContractHash(domain) 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) { func TimeToEpoch(ni *netmap.NetworkInfo, now, t time.Time) (uint64, error) {
duration := t.Sub(now) duration := t.Sub(now)
durationAbs := duration.Abs() durationAbs := duration.Abs()

View file

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