diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cfd4075..3926633f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This document outlines major changes between releases. - Support new param `frostfs.graceful_close_on_switch_timeout` (#475) - Support patch object method (#479) - Add `sign` command to `frostfs-s3-authmate` (#467) +- Support custom aws credentials (#509) ### Changed - Update go version to go1.19 (#470) diff --git a/api/auth/center.go b/api/auth/center.go index 5b27e643..e654c254 100644 --- a/api/auth/center.go +++ b/api/auth/center.go @@ -18,6 +18,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "github.com/aws/aws-sdk-go/aws/credentials" ) @@ -34,6 +35,11 @@ type ( postReg *RegexpSubmatcher cli tokens.Credentials allowedAccessKeyIDPrefixes []string // empty slice means all access key ids are allowed + settings CenterSettings + } + + CenterSettings interface { + AccessBoxContainer() (cid.ID, bool) } //nolint:revive @@ -50,7 +56,6 @@ type ( ) const ( - accessKeyPartsNum = 2 authHeaderPartsNum = 6 maxFormSizeMemory = 50 * 1048576 // 50 MB @@ -82,12 +87,13 @@ var ContentSHA256HeaderStandardValue = map[string]struct{}{ } // New creates an instance of AuthCenter. -func New(creds tokens.Credentials, prefixes []string) *Center { +func New(creds tokens.Credentials, prefixes []string, settings CenterSettings) *Center { return &Center{ cli: creds, reg: NewRegexpMatcher(AuthorizationFieldRegexp), postReg: NewRegexpMatcher(postPolicyCredentialRegexp), allowedAccessKeyIDPrefixes: prefixes, + settings: settings, } } @@ -97,11 +103,6 @@ func (c *Center) parseAuthHeader(header string) (*AuthHeader, error) { return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), header) } - accessKey := strings.Split(submatches["access_key_id"], "0") - if len(accessKey) != accessKeyPartsNum { - return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrInvalidAccessKeyID), accessKey) - } - signedFields := strings.Split(submatches["signed_header_fields"], ";") return &AuthHeader{ @@ -114,15 +115,6 @@ func (c *Center) parseAuthHeader(header string) (*AuthHeader, error) { }, nil } -func getAddress(accessKeyID string) (oid.Address, error) { - var addr oid.Address - if err := addr.DecodeString(strings.ReplaceAll(accessKeyID, "0", "/")); err != nil { - return addr, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrInvalidAccessKeyID), accessKeyID) - } - - return addr, nil -} - func IsStandardContentSHA256(key string) bool { _, ok := ContentSHA256HeaderStandardValue[key] return ok @@ -181,14 +173,14 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) { return nil, err } - addr, err := getAddress(authHdr.AccessKeyID) + cnrID, err := c.getAccessBoxContainer(authHdr.AccessKeyID) if err != nil { return nil, err } - box, attrs, err := c.cli.GetBox(r.Context(), addr) + box, attrs, err := c.cli.GetBox(r.Context(), cnrID, authHdr.AccessKeyID) if err != nil { - return nil, fmt.Errorf("get box '%s': %w", addr, err) + return nil, fmt.Errorf("get box by access key '%s': %w", authHdr.AccessKeyID, err) } if err = checkFormatHashContentSHA256(r.Header.Get(AmzContentSHA256)); err != nil { @@ -216,6 +208,20 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) { return result, nil } +func (c *Center) getAccessBoxContainer(accessKeyID string) (cid.ID, error) { + var addr oid.Address + if err := addr.DecodeString(strings.ReplaceAll(accessKeyID, "0", "/")); err == nil { + return addr.Container(), nil + } + + cnrID, ok := c.settings.AccessBoxContainer() + if ok { + return cnrID, nil + } + + return cid.ID{}, fmt.Errorf("%w: unknown container for creds '%s'", apierr.GetAPIError(apierr.ErrInvalidAccessKeyID), accessKeyID) +} + func checkFormatHashContentSHA256(hash string) error { if !IsStandardContentSHA256(hash) { hashBinary, err := hex.DecodeString(hash) @@ -272,14 +278,14 @@ func (c *Center) checkFormData(r *http.Request) (*middleware.Box, error) { accessKeyID := submatches["access_key_id"] - addr, err := getAddress(accessKeyID) + cnrID, err := c.getAccessBoxContainer(accessKeyID) if err != nil { return nil, err } - box, attrs, err := c.cli.GetBox(r.Context(), addr) + box, attrs, err := c.cli.GetBox(r.Context(), cnrID, accessKeyID) if err != nil { - return nil, fmt.Errorf("get box '%s': %w", addr, err) + return nil, fmt.Errorf("get box by accessKeyID '%s': %w", accessKeyID, err) } secret := box.Gate.SecretKey diff --git a/api/auth/center_test.go b/api/auth/center_test.go index 8066dc8b..c8c82616 100644 --- a/api/auth/center_test.go +++ b/api/auth/center_test.go @@ -19,6 +19,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens" frosterr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" @@ -28,11 +29,23 @@ import ( "go.uber.org/zap/zaptest" ) +type centerSettingsMock struct { + accessBoxContainer *cid.ID +} + +func (c *centerSettingsMock) AccessBoxContainer() (cid.ID, bool) { + if c.accessBoxContainer == nil { + return cid.ID{}, false + } + return *c.accessBoxContainer, true +} + func TestAuthHeaderParse(t *testing.T) { defaultHeader := "AWS4-HMAC-SHA256 Credential=oid0cid/20210809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=2811ccb9e242f41426738fb1f" center := &Center{ - reg: NewRegexpMatcher(AuthorizationFieldRegexp), + reg: NewRegexpMatcher(AuthorizationFieldRegexp), + settings: ¢erSettingsMock{}, } for _, tc := range []struct { @@ -57,11 +70,6 @@ func TestAuthHeaderParse(t *testing.T) { err: errors.GetAPIError(errors.ErrAuthorizationHeaderMalformed), expected: nil, }, - { - header: strings.ReplaceAll(defaultHeader, "oid0cid", "oidcid"), - err: errors.GetAPIError(errors.ErrInvalidAccessKeyID), - expected: nil, - }, } { authHeader, err := center.parseAuthHeader(tc.header) require.ErrorIs(t, err, tc.err, tc.header) @@ -69,43 +77,6 @@ func TestAuthHeaderParse(t *testing.T) { } } -func TestAuthHeaderGetAddress(t *testing.T) { - defaulErr := errors.GetAPIError(errors.ErrInvalidAccessKeyID) - - for _, tc := range []struct { - authHeader *AuthHeader - err error - }{ - { - authHeader: &AuthHeader{ - AccessKeyID: "vWqF8cMDRbJcvnPLALoQGnABPPhw8NyYMcGsfDPfZJM0HrgjonN8CgFvCZ3kh9BUXw4W2tJ5E7EAGhueSF122HB", - }, - err: nil, - }, - { - authHeader: &AuthHeader{ - AccessKeyID: "vWqF8cMDRbJcvnPLALoQGnABPPhw8NyYMcGsfDPfZJMHrgjonN8CgFvCZ3kh9BUXw4W2tJ5E7EAGhueSF122HB", - }, - err: defaulErr, - }, - { - authHeader: &AuthHeader{ - AccessKeyID: "oid0cid", - }, - err: defaulErr, - }, - { - authHeader: &AuthHeader{ - AccessKeyID: "oidcid", - }, - err: defaulErr, - }, - } { - _, err := getAddress(tc.authHeader.AccessKeyID) - require.ErrorIs(t, err, tc.err, tc.authHeader.AccessKeyID) - } -} - func TestSignature(t *testing.T) { secret := "66be461c3cd429941c55daf42fad2b8153e5a2016ba89c9494d97677cc9d3872" strToSign := "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAiYWNsIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwKICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDg0L2FjbCJ9LAogICAgWyJzdGFydHMtd2l0aCIsICIkQ29udGVudC1UeXBlIiwgImltYWdlLyJdLAogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwKICAgIFsic3RhcnRzLXdpdGgiLCAiJHgtYW16LW1ldGEtdGFnIiwgIiJdLAoKICAgIHsiWC1BbXotQ3JlZGVudGlhbCI6ICI4Vmk0MVBIbjVGMXNzY2J4OUhqMXdmMUU2aERUYURpNndxOGhxTU05NllKdTA1QzVDeUVkVlFoV1E2aVZGekFpTkxXaTlFc3BiUTE5ZDRuR3pTYnZVZm10TS8yMDE1MTIyOS91cy1lYXN0LTEvczMvYXdzNF9yZXF1ZXN0In0sCiAgICB7IngtYW16LWFsZ29yaXRobSI6ICJBV1M0LUhNQUMtU0hBMjU2In0sCiAgICB7IlgtQW16LURhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfSwKICAgIHsieC1pZ25vcmUtdG1wIjogInNvbWV0aGluZyIgfQogIF0KfQ==" @@ -171,17 +142,17 @@ func TestCheckFormatContentSHA256(t *testing.T) { } type frostFSMock struct { - objects map[oid.Address]*object.Object + objects map[string]*object.Object } func newFrostFSMock() *frostFSMock { return &frostFSMock{ - objects: map[oid.Address]*object.Object{}, + objects: map[string]*object.Object{}, } } -func (f *frostFSMock) GetCredsObject(_ context.Context, address oid.Address) (*object.Object, error) { - obj, ok := f.objects[address] +func (f *frostFSMock) GetCredsObject(_ context.Context, prm tokens.PrmGetCredsObject) (*object.Object, error) { + obj, ok := f.objects[prm.AccessKeyID] if !ok { return nil, fmt.Errorf("not found") } @@ -208,7 +179,7 @@ func TestAuthenticate(t *testing.T) { GateKey: key.PublicKey(), }} - accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret")) + accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret"), false) require.NoError(t, err) data, err := accessBox.Marshal() require.NoError(t, err) @@ -219,10 +190,10 @@ func TestAuthenticate(t *testing.T) { obj.SetContainerID(addr.Container()) obj.SetID(addr.Object()) - frostfs := newFrostFSMock() - frostfs.objects[addr] = &obj + accessKeyID := getAccessKeyID(addr) - accessKeyID := addr.Container().String() + "0" + addr.Object().String() + frostfs := newFrostFSMock() + frostfs.objects[accessKeyID] = &obj awsCreds := credentials.NewStaticCredentials(accessKeyID, secret.SecretKey, "") defaultSigner := v4.NewSigner(awsCreds) @@ -413,7 +384,7 @@ func TestAuthenticate(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { creds := tokens.New(bigConfig) - cntr := New(creds, tc.prefixes) + cntr := New(creds, tc.prefixes, ¢erSettingsMock{}) box, err := cntr.Authenticate(tc.request) if tc.err { @@ -455,7 +426,7 @@ func TestHTTPPostAuthenticate(t *testing.T) { GateKey: key.PublicKey(), }} - accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret")) + accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret"), false) require.NoError(t, err) data, err := accessBox.Marshal() require.NoError(t, err) @@ -466,10 +437,11 @@ func TestHTTPPostAuthenticate(t *testing.T) { obj.SetContainerID(addr.Container()) obj.SetID(addr.Object()) - frostfs := newFrostFSMock() - frostfs.objects[addr] = &obj + accessKeyID := getAccessKeyID(addr) + + frostfs := newFrostFSMock() + frostfs.objects[accessKeyID] = &obj - accessKeyID := addr.Container().String() + "0" + addr.Object().String() invalidAccessKeyID := oidtest.Address().String() + "0" + oidtest.Address().Object().String() timeToSign := time.Now() @@ -590,7 +562,7 @@ func TestHTTPPostAuthenticate(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { creds := tokens.New(bigConfig) - cntr := New(creds, tc.prefixes) + cntr := New(creds, tc.prefixes, ¢erSettingsMock{}) box, err := cntr.Authenticate(tc.request) if tc.err { @@ -633,3 +605,7 @@ func getRequestWithMultipartForm(t *testing.T, policy, creds, date, sign, fieldN return req } + +func getAccessKeyID(addr oid.Address) string { + return strings.ReplaceAll(addr.EncodeToString(), "/", "0") +} diff --git a/api/auth/presign_test.go b/api/auth/presign_test.go index 8efca52e..f669b1b4 100644 --- a/api/auth/presign_test.go +++ b/api/auth/presign_test.go @@ -29,11 +29,11 @@ func newTokensFrostfsMock() *credentialsMock { } func (m credentialsMock) addBox(addr oid.Address, box *accessbox.Box) { - m.boxes[addr.String()] = box + m.boxes[getAccessKeyID(addr)] = box } -func (m credentialsMock) GetBox(_ context.Context, addr oid.Address) (*accessbox.Box, []object.Attribute, error) { - box, ok := m.boxes[addr.String()] +func (m credentialsMock) GetBox(_ context.Context, _ cid.ID, accessKeyID string) (*accessbox.Box, []object.Attribute, error) { + box, ok := m.boxes[accessKeyID] if !ok { return nil, nil, &apistatus.ObjectNotFound{} } @@ -41,11 +41,11 @@ func (m credentialsMock) GetBox(_ context.Context, addr oid.Address) (*accessbox return box, nil, nil } -func (m credentialsMock) Put(context.Context, cid.ID, tokens.CredentialsParam) (oid.Address, error) { +func (m credentialsMock) Put(context.Context, tokens.CredentialsParam) (oid.Address, error) { return oid.Address{}, nil } -func (m credentialsMock) Update(context.Context, oid.Address, tokens.CredentialsParam) (oid.Address, error) { +func (m credentialsMock) Update(context.Context, tokens.CredentialsParam) (oid.Address, error) { return oid.Address{}, nil } @@ -84,9 +84,10 @@ func TestCheckSign(t *testing.T) { mock.addBox(accessKeyAddr, expBox) c := &Center{ - cli: mock, - reg: NewRegexpMatcher(AuthorizationFieldRegexp), - postReg: NewRegexpMatcher(postPolicyCredentialRegexp), + cli: mock, + reg: NewRegexpMatcher(AuthorizationFieldRegexp), + postReg: NewRegexpMatcher(postPolicyCredentialRegexp), + settings: ¢erSettingsMock{}, } box, err := c.Authenticate(req) require.NoError(t, err) diff --git a/api/cache/accessbox.go b/api/cache/accessbox.go index 94018042..7da325cc 100644 --- a/api/cache/accessbox.go +++ b/api/cache/accessbox.go @@ -30,6 +30,7 @@ type ( Box *accessbox.Box Attributes []object.Attribute PutTime time.Time + Address *oid.Address } ) @@ -57,8 +58,8 @@ func NewAccessBoxCache(config *Config) *AccessBoxCache { } // Get returns a cached accessbox. -func (o *AccessBoxCache) Get(address oid.Address) *AccessBoxCacheValue { - entry, err := o.cache.Get(address) +func (o *AccessBoxCache) Get(accessKeyID string) *AccessBoxCacheValue { + entry, err := o.cache.Get(accessKeyID) if err != nil { return nil } @@ -74,16 +75,11 @@ func (o *AccessBoxCache) Get(address oid.Address) *AccessBoxCacheValue { } // Put stores an accessbox to cache. -func (o *AccessBoxCache) Put(address oid.Address, box *accessbox.Box, attrs []object.Attribute) error { - val := &AccessBoxCacheValue{ - Box: box, - Attributes: attrs, - PutTime: time.Now(), - } - return o.cache.Set(address, val) +func (o *AccessBoxCache) Put(accessKeyID string, val *AccessBoxCacheValue) error { + return o.cache.Set(accessKeyID, val) } // Delete removes an accessbox from cache. -func (o *AccessBoxCache) Delete(address oid.Address) { - o.cache.Remove(address) +func (o *AccessBoxCache) Delete(accessKeyID string) { + o.cache.Remove(accessKeyID) } diff --git a/api/cache/cache_test.go b/api/cache/cache_test.go index 31aceba4..61bb64cf 100644 --- a/api/cache/cache_test.go +++ b/api/cache/cache_test.go @@ -1,13 +1,14 @@ package cache import ( + "strings" "testing" "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" + oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/util" @@ -22,18 +23,21 @@ func TestAccessBoxCacheType(t *testing.T) { addr := oidtest.Address() box := &accessbox.Box{} - var attrs []object.Attribute + val := &AccessBoxCacheValue{ + Box: box, + } - err := cache.Put(addr, box, attrs) + accessKeyID := getAccessKeyID(addr) + + err := cache.Put(accessKeyID, val) require.NoError(t, err) - val := cache.Get(addr) - require.Equal(t, box, val.Box) - require.Equal(t, attrs, val.Attributes) + resVal := cache.Get(accessKeyID) + require.Equal(t, box, resVal.Box) require.Equal(t, 0, observedLog.Len()) - err = cache.cache.Set(addr, "tmp") + err = cache.cache.Set(accessKeyID, "tmp") require.NoError(t, err) - assertInvalidCacheEntry(t, cache.Get(addr), observedLog) + assertInvalidCacheEntry(t, cache.Get(accessKeyID), observedLog) } func TestBucketsCacheType(t *testing.T) { @@ -230,3 +234,7 @@ func getObservedLogger() (*zap.Logger, *observer.ObservedLogs) { loggerCore, observedLog := observer.New(zap.WarnLevel) return zap.New(loggerCore), observedLog } + +func getAccessKeyID(addr oid.Address) string { + return strings.ReplaceAll(addr.EncodeToString(), "/", "0") +} diff --git a/authmate/authmate.go b/authmate/authmate.go index 401b26ff..ef48d8c2 100644 --- a/authmate/authmate.go +++ b/authmate/authmate.go @@ -98,7 +98,9 @@ type ( // IssueSecretOptions contains options for passing to Agent.IssueSecret method. IssueSecretOptions struct { - Container ContainerOptions + Container cid.ID + AccessKeyID string + SecretAccessKey string FrostFSKey *keys.PrivateKey GatesPublicKeys []*keys.PublicKey Impersonate bool @@ -114,7 +116,9 @@ type ( UpdateSecretOptions struct { FrostFSKey *keys.PrivateKey GatesPublicKeys []*keys.PublicKey - Address oid.Address + IsCustom bool + AccessKeyID string + ContainerID cid.ID GatePrivateKey *keys.PrivateKey CustomAttributes []object.Attribute } @@ -141,7 +145,8 @@ type ( // ObtainSecretOptions contains options for passing to Agent.ObtainSecret method. ObtainSecretOptions struct { - SecretAddress string + Container cid.ID + AccessKeyID string GatePrivateKey *keys.PrivateKey } ) @@ -168,32 +173,9 @@ type ( } ) -func (a *Agent) checkContainer(ctx context.Context, opts ContainerOptions, idOwner user.ID) (cid.ID, error) { - if !opts.ID.Equals(cid.ID{}) { - a.log.Info(logs.CheckContainer, zap.Stringer("cid", opts.ID)) - return opts.ID, a.frostFS.ContainerExists(ctx, opts.ID) - } - - a.log.Info(logs.CreateContainer, - zap.String("friendly_name", opts.FriendlyName), - zap.String("placement_policy", opts.PlacementPolicy)) - - var prm PrmContainerCreate - - err := prm.Policy.DecodeString(opts.PlacementPolicy) - if err != nil { - return cid.ID{}, fmt.Errorf("failed to build placement policy: %w", err) - } - - prm.Owner = idOwner - prm.FriendlyName = opts.FriendlyName - - cnrID, err := a.frostFS.CreateContainer(ctx, prm) - if err != nil { - return cid.ID{}, fmt.Errorf("create container in FrostFS: %w", err) - } - - return cnrID, nil +func (a *Agent) checkContainer(ctx context.Context, cnrID cid.ID) error { + a.log.Info(logs.CheckContainer, zap.Stringer("cid", cnrID)) + return a.frostFS.ContainerExists(ctx, cnrID) } func checkPolicy(policyString string) (*netmap.PlacementPolicy, error) { @@ -255,20 +237,24 @@ func (a *Agent) IssueSecret(ctx context.Context, w io.Writer, options *IssueSecr return fmt.Errorf("create tokens: %w", err) } - box, secrets, err := accessbox.PackTokens(gatesData, nil) + var secret []byte + isCustom := options.AccessKeyID != "" + if isCustom { + secret = []byte(options.SecretAccessKey) + } + box, secrets, err := accessbox.PackTokens(gatesData, secret, isCustom) if err != nil { return fmt.Errorf("pack tokens: %w", err) } box.ContainerPolicy = policies - var idOwner user.ID - user.IDFromKey(&idOwner, options.FrostFSKey.PrivateKey.PublicKey) - id, err := a.checkContainer(ctx, options.Container, idOwner) - if err != nil { + if err = a.checkContainer(ctx, options.Container); err != nil { return fmt.Errorf("check container: %w", err) } + var idOwner user.ID + user.IDFromKey(&idOwner, options.FrostFSKey.PrivateKey.PublicKey) a.log.Info(logs.StoreBearerTokenIntoFrostFS, zap.Stringer("owner_tkn", idOwner)) @@ -281,26 +267,31 @@ func (a *Agent) IssueSecret(ctx context.Context, w io.Writer, options *IssueSecr creds := tokens.New(cfg) prm := tokens.CredentialsParam{ - OwnerID: idOwner, + Container: options.Container, + AccessKeyID: options.AccessKeyID, AccessBox: box, Expiration: lifetime.Exp, Keys: options.GatesPublicKeys, CustomAttributes: options.CustomAttributes, } - addr, err := creds.Put(ctx, id, prm) + addr, err := creds.Put(ctx, prm) if err != nil { return fmt.Errorf("failed to put creds: %w", err) } - accessKeyID := accessKeyIDFromAddr(addr) + accessKeyID := options.AccessKeyID + if accessKeyID == "" { + accessKeyID = accessKeyIDFromAddr(addr) + } + ir := &issuingResult{ InitialAccessKeyID: accessKeyID, AccessKeyID: accessKeyID, SecretAccessKey: secrets.SecretKey, OwnerPrivateKey: hex.EncodeToString(secrets.EphemeralKey.Bytes()), WalletPublicKey: hex.EncodeToString(options.FrostFSKey.PublicKey().Bytes()), - ContainerID: id.EncodeToString(), + ContainerID: options.Container.EncodeToString(), } enc := json.NewEncoder(w) @@ -337,13 +328,15 @@ func (a *Agent) UpdateSecret(ctx context.Context, w io.Writer, options *UpdateSe creds := tokens.New(cfg) - box, _, err := creds.GetBox(ctx, options.Address) + box, _, err := creds.GetBox(ctx, options.ContainerID, options.AccessKeyID) if err != nil { return fmt.Errorf("get accessbox: %w", err) } - secret, err := hex.DecodeString(box.Gate.SecretKey) - if err != nil { + var secret []byte + if options.IsCustom { + secret = []byte(box.Gate.SecretKey) + } else if secret, err = hex.DecodeString(box.Gate.SecretKey); err != nil { return fmt.Errorf("failed to decode secret key access box: %w", err) } @@ -360,7 +353,7 @@ func (a *Agent) UpdateSecret(ctx context.Context, w io.Writer, options *UpdateSe return fmt.Errorf("create tokens: %w", err) } - updatedBox, secrets, err := accessbox.PackTokens(gatesData, secret) + updatedBox, secrets, err := accessbox.PackTokens(gatesData, secret, options.IsCustom) if err != nil { return fmt.Errorf("pack tokens: %w", err) } @@ -371,22 +364,26 @@ func (a *Agent) UpdateSecret(ctx context.Context, w io.Writer, options *UpdateSe zap.Stringer("owner_tkn", idOwner)) prm := tokens.CredentialsParam{ - OwnerID: idOwner, + Container: options.ContainerID, AccessBox: updatedBox, Expiration: lifetime.Exp, Keys: options.GatesPublicKeys, CustomAttributes: options.CustomAttributes, } - oldAddr := options.Address - addr, err := creds.Update(ctx, oldAddr, prm) + addr, err := creds.Update(ctx, prm) if err != nil { return fmt.Errorf("failed to update creds: %w", err) } + accessKeyID := options.AccessKeyID + if !options.IsCustom { + accessKeyID = accessKeyIDFromAddr(addr) + } + ir := &issuingResult{ - AccessKeyID: accessKeyIDFromAddr(addr), - InitialAccessKeyID: accessKeyIDFromAddr(oldAddr), + AccessKeyID: accessKeyID, + InitialAccessKeyID: options.AccessKeyID, SecretAccessKey: secrets.SecretKey, OwnerPrivateKey: hex.EncodeToString(secrets.EphemeralKey.Bytes()), WalletPublicKey: hex.EncodeToString(options.FrostFSKey.PublicKey().Bytes()), @@ -419,12 +416,7 @@ func (a *Agent) ObtainSecret(ctx context.Context, w io.Writer, options *ObtainSe bearerCreds := tokens.New(cfg) - var addr oid.Address - if err := addr.DecodeString(options.SecretAddress); err != nil { - return fmt.Errorf("failed to parse secret address: %w", err) - } - - box, _, err := bearerCreds.GetBox(ctx, addr) + box, _, err := bearerCreds.GetBox(ctx, options.Container, options.AccessKeyID) if err != nil { return fmt.Errorf("failed to get tokens: %w", err) } diff --git a/cmd/s3-authmate/modules/issue-secret.go b/cmd/s3-authmate/modules/issue-secret.go index 6895a193..b9df1ebe 100644 --- a/cmd/s3-authmate/modules/issue-secret.go +++ b/cmd/s3-authmate/modules/issue-secret.go @@ -4,22 +4,34 @@ import ( "context" "fmt" "os" + "strings" "time" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" + oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/spf13/cobra" "github.com/spf13/viper" + "go.uber.org/zap" ) var issueSecretCmd = &cobra.Command{ Use: "issue-secret", Short: "Issue a secret in FrostFS network", Long: "Creates new s3 credentials to use with frostfs-s3-gw", - Example: `frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a -frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a --attributes LOGIN=NUUb82KR2JrVByHs2YSKgtK29gKnF5q6Vt`, + Example: `To create new s3 credentials use: +frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a +frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a --attributes LOGIN=NUUb82KR2JrVByHs2YSKgtK29gKnF5q6Vt + +To create new s3 credentials using specific access key id and secret access key use: +frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a --access-key-id my-access-key-id --secret-access-key my-secret-key --container-id BpExV76416Vo7GrkJsGwXGoLM35xsBwup8voedDZR3c6 +`, RunE: runIssueSecretCmd, } @@ -54,6 +66,9 @@ const ( poolHealthcheckTimeoutFlag = "pool-healthcheck-timeout" poolRebalanceIntervalFlag = "pool-rebalance-interval" poolStreamTimeoutFlag = "pool-stream-timeout" + + accessKeyIDFlag = "access-key-id" + secretAccessKeyFlag = "secret-access-key" ) func initIssueSecretCmd() { @@ -73,6 +88,9 @@ func initIssueSecretCmd() { issueSecretCmd.Flags().Duration(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status") issueSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC") issueSecretCmd.Flags().String(attributesFlag, "", "User attributes in form of Key1=Value1,Key2=Value2 (note: you cannot override system attributes)") + issueSecretCmd.Flags().String(accessKeyIDFlag, "", "Access key id of s3 credential that must be created") + issueSecretCmd.Flags().String(secretAccessKeyFlag, "", "Secret access key of s3 credential that must be used") + issueSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address (must be provided if container-id is nns name)") _ = issueSecretCmd.MarkFlagRequired(walletFlag) _ = issueSecretCmd.MarkFlagRequired(peerFlag) @@ -91,14 +109,6 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error { return wrapPreparationError(fmt.Errorf("failed to load frostfs private key: %s", err)) } - var cnrID cid.ID - containerID := viper.GetString(containerIDFlag) - if len(containerID) > 0 { - if err = cnrID.DecodeString(containerID); err != nil { - return wrapPreparationError(fmt.Errorf("failed to parse auth container id: %s", err)) - } - } - var gatesPublicKeys []*keys.PublicKey for _, keyStr := range viper.GetStringSlice(gatePublicKeyFlag) { gpk, err := keys.NewPublicKeyFromString(keyStr) @@ -137,17 +147,29 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error { return wrapFrostFSInitError(fmt.Errorf("failed to create FrostFS component: %s", err)) } + var accessBox cid.ID + if viper.IsSet(containerIDFlag) { + if accessBox, err = util.ResolveContainerID(viper.GetString(containerIDFlag), viper.GetString(rpcEndpointFlag)); err != nil { + return wrapPreparationError(fmt.Errorf("resolve accessbox container id (make sure you provided %s): %w", rpcEndpointFlag, err)) + } + } else if accessBox, err = createAccessBox(ctx, frostFS, key, log); err != nil { + return wrapPreparationError(err) + } + + accessKeyID, secretAccessKey, err := parseAccessKeys() + if err != nil { + return wrapPreparationError(err) + } + customAttrs, err := parseObjectAttrs(viper.GetString(attributesFlag)) if err != nil { return wrapPreparationError(fmt.Errorf("failed to parse attributes: %s", err)) } issueSecretOptions := &authmate.IssueSecretOptions{ - Container: authmate.ContainerOptions{ - ID: cnrID, - FriendlyName: viper.GetString(containerFriendlyNameFlag), - PlacementPolicy: viper.GetString(containerPlacementPolicyFlag), - }, + Container: accessBox, + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, FrostFSKey: key, GatesPublicKeys: gatesPublicKeys, Impersonate: true, @@ -164,3 +186,59 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error { } return nil } + +func parseAccessKeys() (accessKeyID, secretAccessKey string, err error) { + accessKeyID = viper.GetString(accessKeyIDFlag) + secretAccessKey = viper.GetString(secretAccessKeyFlag) + + if accessKeyID == "" && secretAccessKey != "" || accessKeyID != "" && secretAccessKey == "" { + return "", "", fmt.Errorf("flags %s and %s must be both provided or not", accessKeyIDFlag, secretAccessKeyFlag) + } + + if accessKeyID != "" { + if !isCustomCreds(accessKeyID) { + return "", "", fmt.Errorf("invalid custom AccessKeyID format: %s", accessKeyID) + } + if !checkAccessKeyLength(accessKeyID) { + return "", "", fmt.Errorf("invalid custom AccessKeyID length: %s", accessKeyID) + } + if !checkAccessKeyLength(secretAccessKey) { + return "", "", fmt.Errorf("invalid custom SecretAccessKey length: %s", secretAccessKey) + } + } + + return accessKeyID, secretAccessKey, nil +} + +func isCustomCreds(accessKeyID string) bool { + var addr oid.Address + return addr.DecodeString(strings.ReplaceAll(accessKeyID, "0", "/")) != nil +} + +func checkAccessKeyLength(key string) bool { + return 4 <= len(key) && len(key) <= 128 +} + +func createAccessBox(ctx context.Context, frostFS *frostfs.AuthmateFrostFS, key *keys.PrivateKey, log *zap.Logger) (cid.ID, error) { + friendlyName := viper.GetString(containerFriendlyNameFlag) + placementPolicy := viper.GetString(containerPlacementPolicyFlag) + + log.Info(logs.CreateContainer, zap.String("friendly_name", friendlyName), zap.String("placement_policy", placementPolicy)) + + prm := authmate.PrmContainerCreate{ + FriendlyName: friendlyName, + } + + user.IDFromKey(&prm.Owner, key.PrivateKey.PublicKey) + + if err := prm.Policy.DecodeString(placementPolicy); err != nil { + return cid.ID{}, fmt.Errorf("failed to build placement policy: %w", err) + } + + accessBox, err := frostFS.CreateContainer(ctx, prm) + if err != nil { + return cid.ID{}, fmt.Errorf("create container in FrostFS: %w", err) + } + + return accessBox, nil +} diff --git a/cmd/s3-authmate/modules/obtain-secret.go b/cmd/s3-authmate/modules/obtain-secret.go index b7d51301..ab0b9ce3 100644 --- a/cmd/s3-authmate/modules/obtain-secret.go +++ b/cmd/s3-authmate/modules/obtain-secret.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "strings" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet" @@ -24,7 +23,6 @@ var obtainSecretCmd = &cobra.Command{ const ( gateWalletFlag = "gate-wallet" gateAddressFlag = "gate-address" - accessKeyIDFlag = "access-key-id" ) const ( @@ -38,10 +36,12 @@ func initObtainSecretCmd() { obtainSecretCmd.Flags().String(gateWalletFlag, "", "Path to the s3 gateway wallet to decrypt accessbox") obtainSecretCmd.Flags().String(gateAddressFlag, "", "Address of the s3 gateway wallet account") obtainSecretCmd.Flags().String(accessKeyIDFlag, "", "Access key id of s3 credential for which secret must be obtained") + obtainSecretCmd.Flags().String(containerIDFlag, "", "CID or NNS name of auth container that contains provided credential (must be provided if custom access key id is used)") obtainSecretCmd.Flags().Duration(poolDialTimeoutFlag, defaultPoolDialTimeout, "Timeout for connection to the node in pool to be established") obtainSecretCmd.Flags().Duration(poolHealthcheckTimeoutFlag, defaultPoolHealthcheckTimeout, "Timeout for request to node to decide if it is alive") obtainSecretCmd.Flags().Duration(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status") obtainSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC") + obtainSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address (must be provided if container-id is nns name)") _ = obtainSecretCmd.MarkFlagRequired(walletFlag) _ = obtainSecretCmd.MarkFlagRequired(peerFlag) @@ -81,8 +81,14 @@ func runObtainSecretCmd(cmd *cobra.Command, _ []string) error { return wrapFrostFSInitError(cli.Exit(fmt.Sprintf("failed to create FrostFS component: %s", err), 2)) } + accessBox, accessKeyID, _, err := getAccessBoxID() + if err != nil { + return wrapPreparationError(err) + } + obtainSecretOptions := &authmate.ObtainSecretOptions{ - SecretAddress: strings.Replace(viper.GetString(accessKeyIDFlag), "0", "/", 1), + Container: accessBox, + AccessKeyID: accessKeyID, GatePrivateKey: gateKey, } diff --git a/cmd/s3-authmate/modules/update-secret.go b/cmd/s3-authmate/modules/update-secret.go index 7250691b..8bc6fd05 100644 --- a/cmd/s3-authmate/modules/update-secret.go +++ b/cmd/s3-authmate/modules/update-secret.go @@ -4,11 +4,9 @@ import ( "context" "fmt" "os" - "strings" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet" - oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -33,13 +31,15 @@ func initUpdateSecretCmd() { updateSecretCmd.Flags().String(peerFlag, "", "Address of a frostfs peer to connect to") updateSecretCmd.Flags().String(gateWalletFlag, "", "Path to the s3 gateway wallet to decrypt accessbox") updateSecretCmd.Flags().String(gateAddressFlag, "", "Address of the s3 gateway wallet account") - updateSecretCmd.Flags().String(accessKeyIDFlag, "", "Access key id of s3 credential for which secret must be obtained") + updateSecretCmd.Flags().String(accessKeyIDFlag, "", "Access key id of s3 credential for which secret must be updatedd") + updateSecretCmd.Flags().String(containerIDFlag, "", "CID or NNS name of auth container that contains provided credential (must be provided if custom access key id is used)") updateSecretCmd.Flags().StringSlice(gatePublicKeyFlag, nil, "Public 256r1 key of a gate (use flags repeatedly for multiple gates or separate them by comma)") updateSecretCmd.Flags().Duration(poolDialTimeoutFlag, defaultPoolDialTimeout, "Timeout for connection to the node in pool to be established") updateSecretCmd.Flags().Duration(poolHealthcheckTimeoutFlag, defaultPoolHealthcheckTimeout, "Timeout for request to node to decide if it is alive") updateSecretCmd.Flags().Duration(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status") updateSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC") updateSecretCmd.Flags().String(attributesFlag, "", "User attributes in form of Key1=Value1,Key2=Value2 (note: you cannot override system attributes)") + updateSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address (must be provided if container-id is nns name)") _ = updateSecretCmd.MarkFlagRequired(walletFlag) _ = updateSecretCmd.MarkFlagRequired(peerFlag) @@ -66,10 +66,9 @@ func runUpdateSecretCmd(cmd *cobra.Command, _ []string) error { return wrapPreparationError(fmt.Errorf("failed to load s3 gate private key: %s", err)) } - var accessBoxAddress oid.Address - credAddr := strings.Replace(viper.GetString(accessKeyIDFlag), "0", "/", 1) - if err = accessBoxAddress.DecodeString(credAddr); err != nil { - return wrapPreparationError(fmt.Errorf("failed to parse creds address: %w", err)) + accessBox, accessKeyID, isCustom, err := getAccessBoxID() + if err != nil { + return wrapPreparationError(err) } var gatesPublicKeys []*keys.PublicKey @@ -101,7 +100,9 @@ func runUpdateSecretCmd(cmd *cobra.Command, _ []string) error { } updateSecretOptions := &authmate.UpdateSecretOptions{ - Address: accessBoxAddress, + ContainerID: accessBox, + AccessKeyID: accessKeyID, + IsCustom: isCustom, FrostFSKey: key, GatesPublicKeys: gatesPublicKeys, GatePrivateKey: gateKey, diff --git a/cmd/s3-authmate/modules/utils.go b/cmd/s3-authmate/modules/utils.go index b7924e8e..d88db356 100644 --- a/cmd/s3-authmate/modules/utils.go +++ b/cmd/s3-authmate/modules/utils.go @@ -3,6 +3,7 @@ package modules import ( "context" "encoding/json" + "errors" "fmt" "os" "strings" @@ -11,8 +12,11 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" + oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/spf13/viper" @@ -163,3 +167,23 @@ func parseObjectAttrs(attributes string) ([]object.Attribute, error) { return attrs, nil } + +func getAccessBoxID() (cid.ID, string, bool, error) { + accessKeyID := viper.GetString(accessKeyIDFlag) + + var accessBoxAddress oid.Address + if err := accessBoxAddress.DecodeString(strings.Replace(accessKeyID, "0", "/", 1)); err == nil { + return accessBoxAddress.Container(), accessKeyID, false, nil + } + + if !viper.IsSet(containerIDFlag) { + return cid.ID{}, "", false, errors.New("accessbox parameter must be set when custom access key id is used") + } + + accessBox, err := util.ResolveContainerID(viper.GetString(containerIDFlag), viper.GetString(rpcEndpointFlag)) + if err != nil { + return cid.ID{}, "", false, fmt.Errorf("resolve accessbox container id (make sure you provided %s): %w", rpcEndpointFlag, err) + } + + return accessBox, accessKeyID, true, nil +} diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go index d3fcca23..8a1746a5 100644 --- a/cmd/s3-gw/app.go +++ b/cmd/s3-gw/app.go @@ -96,6 +96,7 @@ type ( resolveZoneList []string isResolveListAllow bool // True if ResolveZoneList contains allowed zones frostfsidValidation bool + accessbox *cid.ID mu sync.RWMutex namespaces Namespaces @@ -132,18 +133,7 @@ type ( func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App { objPool, treePool, key := getPools(ctx, log.logger, v) - cfg := tokens.Config{ - FrostFS: frostfs.NewAuthmateFrostFS(frostfs.NewFrostFS(objPool, key), log.logger), - Key: key, - CacheConfig: getAccessBoxCacheConfig(v, log.logger), - RemovingCheckAfterDurations: fetchRemovingCheckInterval(v, log.logger), - } - - // prepare auth center - ctr := auth.New(tokens.New(cfg), v.GetStringSlice(cfgAllowedAccessKeyIDPrefixes)) - app := &App{ - ctr: ctr, log: log.logger, cfg: v, pool: objPool, @@ -162,6 +152,8 @@ func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App { } func (a *App) init(ctx context.Context) { + a.initResolver() + a.initAuthCenter(ctx) a.setRuntimeParameters() a.initFrostfsID(ctx) a.initPolicyStorage(ctx) @@ -171,9 +163,26 @@ func (a *App) init(ctx context.Context) { a.initTracing(ctx) } -func (a *App) initLayer(ctx context.Context) { - a.initResolver() +func (a *App) initAuthCenter(ctx context.Context) { + if a.cfg.IsSet(cfgContainersAccessBox) { + cnrID, err := a.resolveContainerID(ctx, cfgContainersAccessBox) + if err != nil { + a.log.Fatal(logs.CouldNotFetchAccessBoxContainerInfo, zap.Error(err)) + } + a.settings.accessbox = &cnrID + } + cfg := tokens.Config{ + FrostFS: frostfs.NewAuthmateFrostFS(frostfs.NewFrostFS(a.pool, a.key), a.log), + Key: a.key, + CacheConfig: getAccessBoxCacheConfig(a.cfg, a.log), + RemovingCheckAfterDurations: fetchRemovingCheckInterval(a.cfg, a.log), + } + + a.ctr = auth.New(tokens.New(cfg), a.cfg.GetStringSlice(cfgAllowedAccessKeyIDPrefixes), a.settings) +} + +func (a *App) initLayer(ctx context.Context) { // prepare random key for anonymous requests randomKey, err := keys.NewPrivateKey() if err != nil { @@ -484,6 +493,14 @@ func (s *appSettings) RetryStrategy() handler.RetryStrategy { return s.retryStrategy } +func (s *appSettings) AccessBoxContainer() (cid.ID, bool) { + if s.accessbox != nil { + return *s.accessbox, true + } + + return cid.ID{}, false +} + func (a *App) initAPI(ctx context.Context) { a.initLayer(ctx) a.initHandler() @@ -1104,21 +1121,30 @@ func (a *App) tryReconnect(ctx context.Context, sr *http.Server) bool { } func (a *App) fetchContainerInfo(ctx context.Context, cfgKey string) (info *data.BucketInfo, err error) { + cnrID, err := a.resolveContainerID(ctx, cfgKey) + if err != nil { + return nil, err + } + + return getContainerInfo(ctx, cnrID, a.pool) +} + +func (a *App) resolveContainerID(ctx context.Context, cfgKey string) (cid.ID, error) { containerString := a.cfg.GetString(cfgKey) var id cid.ID - if err = id.DecodeString(containerString); err != nil { + if err := id.DecodeString(containerString); err != nil { i := strings.Index(containerString, ".") if i < 0 { - return nil, fmt.Errorf("invalid container address: %s", containerString) + return cid.ID{}, fmt.Errorf("invalid container address: %s", containerString) } if id, err = a.bucketResolver.Resolve(ctx, containerString[i+1:], containerString[:i]); err != nil { - return nil, fmt.Errorf("resolve container address %s: %w", containerString, err) + return cid.ID{}, fmt.Errorf("resolve container address %s: %w", containerString, err) } } - return getContainerInfo(ctx, id, a.pool) + return id, nil } func getContainerInfo(ctx context.Context, id cid.ID, frostFSPool *pool.Pool) (*data.BucketInfo, error) { diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go index 94db37df..70716237 100644 --- a/cmd/s3-gw/app_settings.go +++ b/cmd/s3-gw/app_settings.go @@ -208,6 +208,7 @@ const ( // Settings. // Containers. cfgContainersCORS = "containers.cors" cfgContainersLifecycle = "containers.lifecycle" + cfgContainersAccessBox = "containers.accessbox" // Command line args. cmdHelp = "help" diff --git a/creds/accessbox/accessbox.go b/creds/accessbox/accessbox.go index 690c1717..6d580f49 100644 --- a/creds/accessbox/accessbox.go +++ b/creds/accessbox/accessbox.go @@ -99,13 +99,14 @@ func (x *AccessBox) Unmarshal(data []byte) error { // PackTokens adds bearer and session tokens to BearerTokens and SessionToken lists respectively. // Session token can be nil. // Secret can be nil. In such case secret will be generated. -func PackTokens(gatesData []*GateData, secret []byte) (*AccessBox, *Secrets, error) { +func PackTokens(gatesData []*GateData, secret []byte, isCustomSecret bool) (*AccessBox, *Secrets, error) { box := &AccessBox{} ephemeralKey, err := keys.NewPrivateKey() if err != nil { return nil, nil, fmt.Errorf("create ephemeral key: %w", err) } box.SeedKey = ephemeralKey.PublicKey().Bytes() + box.IsCustom = isCustomSecret if secret == nil { secret, err = generateSecret() @@ -118,7 +119,12 @@ func PackTokens(gatesData []*GateData, secret []byte) (*AccessBox, *Secrets, err return nil, nil, fmt.Errorf("failed to add tokens to accessbox: %w", err) } - return box, &Secrets{hex.EncodeToString(secret), ephemeralKey}, err + secretKey := string(secret) + if !isCustomSecret { + secretKey = hex.EncodeToString(secret) + } + + return box, &Secrets{SecretKey: secretKey, EphemeralKey: ephemeralKey}, err } // GetTokens returns gate tokens from AccessBox. @@ -133,7 +139,7 @@ func (x *AccessBox) GetTokens(owner *keys.PrivateKey) (*GateData, error) { continue } - gateData, err := decodeGate(gate, owner, seedKey) + gateData, err := x.decodeGate(gate, owner, seedKey) if err != nil { return nil, fmt.Errorf("failed to decode gate: %w", err) } @@ -217,7 +223,7 @@ func encodeGate(ephemeralKey *keys.PrivateKey, seedKey *keys.PublicKey, tokens * return gate, nil } -func decodeGate(gate *AccessBox_Gate, owner *keys.PrivateKey, seedKey *keys.PublicKey) (*GateData, error) { +func (x *AccessBox) decodeGate(gate *AccessBox_Gate, owner *keys.PrivateKey, seedKey *keys.PublicKey) (*GateData, error) { data, err := decrypt(owner, seedKey, gate.Tokens) if err != nil { return nil, fmt.Errorf("decrypt tokens: %w", err) @@ -243,7 +249,11 @@ func decodeGate(gate *AccessBox_Gate, owner *keys.PrivateKey, seedKey *keys.Publ gateData := NewGateData(owner.PublicKey(), &bearerTkn) gateData.SessionTokens = sessionTkns - gateData.SecretKey = hex.EncodeToString(tokens.SecretKey) + if x.IsCustom { + gateData.SecretKey = string(tokens.SecretKey) + } else { + gateData.SecretKey = hex.EncodeToString(tokens.SecretKey) + } return gateData, nil } diff --git a/creds/accessbox/accessbox.pb.go b/creds/accessbox/accessbox.pb.go index 714f2e81..dbe9517c 100644 --- a/creds/accessbox/accessbox.pb.go +++ b/creds/accessbox/accessbox.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.30.0 -// protoc v3.12.4 +// protoc-gen-go v1.34.2 +// protoc v3.21.9 // source: creds/accessbox/accessbox.proto package accessbox @@ -28,6 +28,7 @@ type AccessBox struct { SeedKey []byte `protobuf:"bytes,1,opt,name=seedKey,proto3" json:"seedKey,omitempty"` Gates []*AccessBox_Gate `protobuf:"bytes,2,rep,name=gates,proto3" json:"gates,omitempty"` ContainerPolicy []*AccessBox_ContainerPolicy `protobuf:"bytes,3,rep,name=containerPolicy,proto3" json:"containerPolicy,omitempty"` + IsCustom bool `protobuf:"varint,4,opt,name=isCustom,proto3" json:"isCustom,omitempty"` } func (x *AccessBox) Reset() { @@ -83,6 +84,13 @@ func (x *AccessBox) GetContainerPolicy() []*AccessBox_ContainerPolicy { return nil } +func (x *AccessBox) GetIsCustom() bool { + if x != nil { + return x.IsCustom + } + return false +} + type Tokens struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -261,7 +269,7 @@ var File_creds_accessbox_accessbox_proto protoreflect.FileDescriptor var file_creds_accessbox_accessbox_proto_rawDesc = []byte{ 0x0a, 0x1f, 0x63, 0x72, 0x65, 0x64, 0x73, 0x2f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x2f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x22, 0xc7, 0x02, 0x0a, + 0x6f, 0x12, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x22, 0xe3, 0x02, 0x0a, 0x09, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x6f, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x73, 0x65, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x67, 0x61, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, @@ -272,29 +280,31 @@ var file_creds_accessbox_accessbox_proto_rawDesc = []byte{ 0x2e, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x6f, 0x78, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x1a, 0x44, 0x0a, 0x04, 0x47, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x67, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x67, 0x61, - 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x1a, 0x59, 0x0a, 0x0f, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x2e, - 0x0a, 0x12, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, - 0x61, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6c, 0x6f, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x12, 0x16, - 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x6e, 0x0a, 0x06, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, - 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x20, - 0x0a, 0x0b, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x2e, 0x66, 0x72, - 0x6f, 0x73, 0x74, 0x66, 0x73, 0x2e, 0x69, 0x6e, 0x66, 0x6f, 0x2f, 0x54, 0x72, 0x75, 0x65, 0x43, - 0x6c, 0x6f, 0x75, 0x64, 0x4c, 0x61, 0x62, 0x2f, 0x66, 0x72, 0x6f, 0x73, 0x74, 0x66, 0x73, 0x2d, - 0x73, 0x33, 0x2d, 0x67, 0x77, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x73, 0x2f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x62, 0x6f, 0x78, 0x3b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x73, 0x43, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x43, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x1a, 0x44, 0x0a, 0x04, 0x47, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x67, 0x61, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x67, 0x61, 0x74, 0x65, 0x50, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x1a, 0x59, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x2e, 0x0a, 0x12, 0x6c, 0x6f, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x22, 0x6e, 0x0a, 0x06, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x62, 0x65, + 0x61, 0x72, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x0b, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0d, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x73, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x2e, 0x66, 0x72, 0x6f, 0x73, 0x74, 0x66, + 0x73, 0x2e, 0x69, 0x6e, 0x66, 0x6f, 0x2f, 0x54, 0x72, 0x75, 0x65, 0x43, 0x6c, 0x6f, 0x75, 0x64, + 0x4c, 0x61, 0x62, 0x2f, 0x66, 0x72, 0x6f, 0x73, 0x74, 0x66, 0x73, 0x2d, 0x73, 0x33, 0x2d, 0x67, + 0x77, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x73, 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x62, 0x6f, 0x78, + 0x3b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x62, 0x6f, 0x78, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -310,7 +320,7 @@ func file_creds_accessbox_accessbox_proto_rawDescGZIP() []byte { } var file_creds_accessbox_accessbox_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_creds_accessbox_accessbox_proto_goTypes = []interface{}{ +var file_creds_accessbox_accessbox_proto_goTypes = []any{ (*AccessBox)(nil), // 0: accessbox.AccessBox (*Tokens)(nil), // 1: accessbox.Tokens (*AccessBox_Gate)(nil), // 2: accessbox.AccessBox.Gate @@ -332,7 +342,7 @@ func file_creds_accessbox_accessbox_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_creds_accessbox_accessbox_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_creds_accessbox_accessbox_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*AccessBox); i { case 0: return &v.state @@ -344,7 +354,7 @@ func file_creds_accessbox_accessbox_proto_init() { return nil } } - file_creds_accessbox_accessbox_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_creds_accessbox_accessbox_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*Tokens); i { case 0: return &v.state @@ -356,7 +366,7 @@ func file_creds_accessbox_accessbox_proto_init() { return nil } } - file_creds_accessbox_accessbox_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_creds_accessbox_accessbox_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*AccessBox_Gate); i { case 0: return &v.state @@ -368,7 +378,7 @@ func file_creds_accessbox_accessbox_proto_init() { return nil } } - file_creds_accessbox_accessbox_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_creds_accessbox_accessbox_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*AccessBox_ContainerPolicy); i { case 0: return &v.state diff --git a/creds/accessbox/accessbox.proto b/creds/accessbox/accessbox.proto index ffac6b28..2b0b1db5 100644 --- a/creds/accessbox/accessbox.proto +++ b/creds/accessbox/accessbox.proto @@ -20,6 +20,7 @@ message AccessBox { bytes seedKey = 1 [json_name = "seedKey"]; repeated Gate gates = 2 [json_name = "gates"]; repeated ContainerPolicy containerPolicy = 3 [json_name = "containerPolicy"]; + bool isCustom = 4 [json_name = "isCustom"]; } message Tokens { diff --git a/creds/accessbox/bearer_token_test.go b/creds/accessbox/accessbox_test.go similarity index 87% rename from creds/accessbox/bearer_token_test.go rename to creds/accessbox/accessbox_test.go index b6ee0e05..5cd1693f 100644 --- a/creds/accessbox/bearer_token_test.go +++ b/creds/accessbox/accessbox_test.go @@ -61,7 +61,7 @@ func TestBearerTokenInAccessBox(t *testing.T) { require.NoError(t, tkn.Sign(sec.PrivateKey)) gate := NewGateData(cred.PublicKey(), &tkn) - box, _, err = PackTokens([]*GateData{gate}, nil) + box, _, err = PackTokens([]*GateData{gate}, nil, false) require.NoError(t, err) data, err := box.Marshal() @@ -96,7 +96,7 @@ func TestSessionTokenInAccessBox(t *testing.T) { var newTkn bearer.Token gate := NewGateData(cred.PublicKey(), &newTkn) gate.SessionTokens = []*session.Container{tkn} - box, _, err = PackTokens([]*GateData{gate}, nil) + box, _, err = PackTokens([]*GateData{gate}, nil, false) require.NoError(t, err) data, err := box.Marshal() @@ -136,7 +136,7 @@ func TestAccessboxMultipleKeys(t *testing.T) { } } - box, _, err = PackTokens(gates, nil) + box, _, err = PackTokens(gates, nil, false) require.NoError(t, err) for i, k := range privateKeys { @@ -165,7 +165,7 @@ func TestUnknownKey(t *testing.T) { require.NoError(t, tkn.Sign(sec.PrivateKey)) gate := NewGateData(cred.PublicKey(), &tkn) - box, _, err = PackTokens([]*GateData{gate}, nil) + box, _, err = PackTokens([]*GateData{gate}, nil, false) require.NoError(t, err) _, err = box.GetTokens(wrongCred) @@ -224,14 +224,27 @@ func TestGetBox(t *testing.T) { var tkn bearer.Token gate := NewGateData(cred.PublicKey(), &tkn) - secret := []byte("secret") - accessBox, _, err := PackTokens([]*GateData{gate}, secret) - require.NoError(t, err) - box, err := accessBox.GetBox(cred) - require.NoError(t, err) - require.Equal(t, hex.EncodeToString(secret), box.Gate.SecretKey) + t.Run("regular secret", func(t *testing.T) { + accessBox, secrets, err := PackTokens([]*GateData{gate}, secret, false) + require.NoError(t, err) + require.Equal(t, hex.EncodeToString(secret), secrets.SecretKey) + + box, err := accessBox.GetBox(cred) + require.NoError(t, err) + require.Equal(t, hex.EncodeToString(secret), box.Gate.SecretKey) + }) + + t.Run("custom secret", func(t *testing.T) { + accessBox, secrets, err := PackTokens([]*GateData{gate}, secret, true) + require.NoError(t, err) + require.Equal(t, string(secret), secrets.SecretKey) + + box, err := accessBox.GetBox(cred) + require.NoError(t, err) + require.Equal(t, string(secret), box.Gate.SecretKey) + }) } func TestAccessBox(t *testing.T) { @@ -241,7 +254,7 @@ func TestAccessBox(t *testing.T) { var tkn bearer.Token gate := NewGateData(cred.PublicKey(), &tkn) - accessBox, _, err := PackTokens([]*GateData{gate}, nil) + accessBox, _, err := PackTokens([]*GateData{gate}, nil, false) require.NoError(t, err) t.Run("invalid owner", func(t *testing.T) { @@ -300,7 +313,7 @@ func TestAccessBox(t *testing.T) { BearerToken: &tkn, GateKey: &keys.PublicKey{}, } - _, _, err = PackTokens([]*GateData{gate}, nil) + _, _, err = PackTokens([]*GateData{gate}, nil, false) require.Error(t, err) }) } diff --git a/creds/tokens/credentials.go b/creds/tokens/credentials.go index 573f3622..a8bd597d 100644 --- a/creds/tokens/credentials.go +++ b/creds/tokens/credentials.go @@ -14,7 +14,6 @@ import ( cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "go.uber.org/zap" ) @@ -22,13 +21,14 @@ import ( type ( // Credentials is a bearer token get/put interface. Credentials interface { - GetBox(context.Context, oid.Address) (*accessbox.Box, []object.Attribute, error) - Put(context.Context, cid.ID, CredentialsParam) (oid.Address, error) - Update(context.Context, oid.Address, CredentialsParam) (oid.Address, error) + GetBox(context.Context, cid.ID, string) (*accessbox.Box, []object.Attribute, error) + Put(context.Context, CredentialsParam) (oid.Address, error) + Update(context.Context, CredentialsParam) (oid.Address, error) } CredentialsParam struct { - OwnerID user.ID + Container cid.ID + AccessKeyID string AccessBox *accessbox.AccessBox Expiration uint64 Keys keys.PublicKeys @@ -49,13 +49,16 @@ type ( CacheConfig *cache.Config RemovingCheckAfterDurations time.Duration } + + Box struct { + AccessBox *accessbox.AccessBox + Attributes []object.Attribute + Address *oid.Address + } ) // PrmObjectCreate groups parameters of objects created by credential tool. type PrmObjectCreate struct { - // FrostFS identifier of the object creator. - Creator user.ID - // FrostFS container to store the object. Container cid.ID @@ -64,7 +67,12 @@ type PrmObjectCreate struct { // Optional. // If provided cred object will be created using crdt approach. - NewVersionFor *oid.ID + NewVersionForAccessKeyID string + + // Optional. + // If provided cred object will contain specific crdt name attribute for first accessbox object version. + // If NewVersionForAccessKeyID is provided this field isn't used. + CustomAccessKey string // Last FrostFS epoch of the object lifetime. ExpirationEpoch uint64 @@ -76,6 +84,21 @@ type PrmObjectCreate struct { CustomAttributes []object.Attribute } +// PrmGetCredsObject groups parameters of getting credential object. +type PrmGetCredsObject struct { + // FrostFS container to get the object. + Container cid.ID + + // S3 access key id. + AccessKeyID string + + // FallbackAddress is an address that should be used to get creds if we couldn't find it by AccessKeyID. + // Optional. + FallbackAddress *oid.Address +} + +var ErrCustomAccessKeyIDNotFound = errors.New("custom AccessKeyId not found") + // FrostFS represents virtual connection to FrostFS network. type FrostFS interface { // CreateObject creates and saves a parameterized object in the specified @@ -92,8 +115,9 @@ type FrostFS interface { // // It returns exactly one non-nil value. It returns any error encountered which // prevented the object payload from being read. + // Returns ErrCustomAccessKeyIDNotFound if provided AccessKey is custom, and it was not found. // Object must contain full payload. - GetCredsObject(context.Context, oid.Address) (*object.Object, error) + GetCredsObject(context.Context, PrmGetCredsObject) (*object.Object, error) } var ( @@ -116,84 +140,128 @@ func New(cfg Config) Credentials { } } -func (c *cred) GetBox(ctx context.Context, addr oid.Address) (*accessbox.Box, []object.Attribute, error) { - cachedBoxValue := c.cache.Get(addr) +func (c *cred) GetBox(ctx context.Context, cnrID cid.ID, accessKeyID string) (*accessbox.Box, []object.Attribute, error) { + cachedBoxValue := c.cache.Get(accessKeyID) if cachedBoxValue != nil { - return c.checkIfCredentialsAreRemoved(ctx, addr, cachedBoxValue) + return c.checkIfCredentialsAreRemoved(ctx, cnrID, accessKeyID, cachedBoxValue) } - box, attrs, err := c.getAccessBox(ctx, addr) + box, err := c.getAccessBox(ctx, cnrID, accessKeyID, nil) if err != nil { return nil, nil, fmt.Errorf("get access box: %w", err) } - cachedBox, err := box.GetBox(c.key) + cachedBox, err := box.AccessBox.GetBox(c.key) if err != nil { return nil, nil, fmt.Errorf("get gate box: %w", err) } - c.putBoxToCache(addr, cachedBox, attrs) + val := &cache.AccessBoxCacheValue{ + Box: cachedBox, + Attributes: box.Attributes, + PutTime: time.Now(), + Address: box.Address, + } - return cachedBox, attrs, nil + c.putBoxToCache(accessKeyID, val) + + return cachedBox, box.Attributes, nil } -func (c *cred) checkIfCredentialsAreRemoved(ctx context.Context, addr oid.Address, cachedBoxValue *cache.AccessBoxCacheValue) (*accessbox.Box, []object.Attribute, error) { +func (c *cred) checkIfCredentialsAreRemoved(ctx context.Context, cnrID cid.ID, accessKeyID string, cachedBoxValue *cache.AccessBoxCacheValue) (*accessbox.Box, []object.Attribute, error) { if time.Since(cachedBoxValue.PutTime) < c.removingCheckDuration { return cachedBoxValue.Box, cachedBoxValue.Attributes, nil } - box, attrs, err := c.getAccessBox(ctx, addr) + box, err := c.getAccessBox(ctx, cnrID, accessKeyID, cachedBoxValue.Address) if err != nil { if client.IsErrObjectAlreadyRemoved(err) { - c.cache.Delete(addr) + c.cache.Delete(accessKeyID) return nil, nil, fmt.Errorf("get access box: %w", err) } return cachedBoxValue.Box, cachedBoxValue.Attributes, nil } - cachedBox, err := box.GetBox(c.key) + cachedBox, err := box.AccessBox.GetBox(c.key) if err != nil { - c.cache.Delete(addr) + c.cache.Delete(accessKeyID) return nil, nil, fmt.Errorf("get gate box: %w", err) } // we need this to reset PutTime // to don't check for removing each time after removingCheckDuration interval - c.putBoxToCache(addr, cachedBox, attrs) + val := &cache.AccessBoxCacheValue{ + Box: cachedBox, + Attributes: box.Attributes, + PutTime: time.Now(), + Address: box.Address, + } + c.putBoxToCache(accessKeyID, val) - return cachedBoxValue.Box, attrs, nil + return cachedBoxValue.Box, box.Attributes, nil } -func (c *cred) putBoxToCache(addr oid.Address, box *accessbox.Box, attrs []object.Attribute) { - if err := c.cache.Put(addr, box, attrs); err != nil { - c.log.Warn(logs.CouldntPutAccessBoxIntoCache, zap.String("address", addr.EncodeToString())) +func (c *cred) putBoxToCache(accessKeyID string, val *cache.AccessBoxCacheValue) { + if err := c.cache.Put(accessKeyID, val); err != nil { + c.log.Warn(logs.CouldntPutAccessBoxIntoCache, zap.String("accessKeyID", accessKeyID)) } } -func (c *cred) getAccessBox(ctx context.Context, addr oid.Address) (*accessbox.AccessBox, []object.Attribute, error) { - obj, err := c.frostFS.GetCredsObject(ctx, addr) +func (c *cred) getAccessBox(ctx context.Context, cnrID cid.ID, accessKeyID string, fallbackAddr *oid.Address) (*Box, error) { + prm := PrmGetCredsObject{ + Container: cnrID, + AccessKeyID: accessKeyID, + FallbackAddress: fallbackAddr, + } + obj, err := c.frostFS.GetCredsObject(ctx, prm) if err != nil { - return nil, nil, fmt.Errorf("read payload and attributes: %w", err) + return nil, fmt.Errorf("read payload and attributes: %w", err) } // decode access box var box accessbox.AccessBox if err = box.Unmarshal(obj.Payload()); err != nil { - return nil, nil, fmt.Errorf("unmarhal access box: %w", err) + return nil, fmt.Errorf("unmarhal access box: %w", err) } - return &box, obj.Attributes(), nil + addr := &oid.Address{} + boxCnrID, cnrIDOk := obj.ContainerID() + boxObjID, objIDOk := obj.ID() + addr.SetContainer(boxCnrID) + addr.SetObject(boxObjID) + if !cnrIDOk || !objIDOk { + addr = nil + } + + return &Box{ + AccessBox: &box, + Attributes: obj.Attributes(), + Address: addr, + }, nil } -func (c *cred) Put(ctx context.Context, idCnr cid.ID, prm CredentialsParam) (oid.Address, error) { - return c.createObject(ctx, idCnr, nil, prm) +func (c *cred) Put(ctx context.Context, prm CredentialsParam) (oid.Address, error) { + if prm.AccessKeyID != "" { + c.log.Info(logs.CheckCustomAccessKeyIDUniqueness, zap.String("access_key_id", prm.AccessKeyID)) + credsPrm := PrmGetCredsObject{ + Container: prm.Container, + AccessKeyID: prm.AccessKeyID, + } + + if _, err := c.frostFS.GetCredsObject(ctx, credsPrm); err == nil { + return oid.Address{}, fmt.Errorf("access key id '%s' already exists", prm.AccessKeyID) + } else if !errors.Is(err, ErrCustomAccessKeyIDNotFound) { + return oid.Address{}, fmt.Errorf("check AccessKeyID uniqueness: %w", err) + } + } + + return c.createObject(ctx, prm, false) } -func (c *cred) Update(ctx context.Context, addr oid.Address, prm CredentialsParam) (oid.Address, error) { - objID := addr.Object() - return c.createObject(ctx, addr.Container(), &objID, prm) +func (c *cred) Update(ctx context.Context, prm CredentialsParam) (oid.Address, error) { + return c.createObject(ctx, prm, true) } -func (c *cred) createObject(ctx context.Context, cnrID cid.ID, newVersionFor *oid.ID, prm CredentialsParam) (oid.Address, error) { +func (c *cred) createObject(ctx context.Context, prm CredentialsParam, update bool) (oid.Address, error) { if len(prm.Keys) == 0 { return oid.Address{}, ErrEmptyPublicKeys } else if prm.AccessBox == nil { @@ -204,14 +272,19 @@ func (c *cred) createObject(ctx context.Context, cnrID cid.ID, newVersionFor *oi return oid.Address{}, fmt.Errorf("marshall box: %w", err) } + var newVersionFor string + if update { + newVersionFor = prm.AccessKeyID + } + idObj, err := c.frostFS.CreateObject(ctx, PrmObjectCreate{ - Creator: prm.OwnerID, - Container: cnrID, - Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box", - ExpirationEpoch: prm.Expiration, - NewVersionFor: newVersionFor, - Payload: data, - CustomAttributes: prm.CustomAttributes, + Container: prm.Container, + Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box", + ExpirationEpoch: prm.Expiration, + CustomAccessKey: prm.AccessKeyID, + NewVersionForAccessKeyID: newVersionFor, + Payload: data, + CustomAttributes: prm.CustomAttributes, }) if err != nil { return oid.Address{}, fmt.Errorf("create object: %w", err) @@ -219,7 +292,7 @@ func (c *cred) createObject(ctx context.Context, cnrID cid.ID, newVersionFor *oi var addr oid.Address addr.SetObject(idObj) - addr.SetContainer(cnrID) + addr.SetContainer(prm.Container) return addr, nil } diff --git a/creds/tokens/credentials_test.go b/creds/tokens/credentials_test.go index 35240454..86c6696a 100644 --- a/creds/tokens/credentials_test.go +++ b/creds/tokens/credentials_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "errors" + "strings" "testing" "time" @@ -15,27 +16,40 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) type frostfsMock struct { - objects map[oid.Address][]*object.Object - errors map[oid.Address]error + key *keys.PrivateKey + objects map[string][]*object.Object + errors map[string]error } -func newFrostfsMock() *frostfsMock { +func newFrostfsMock(key *keys.PrivateKey) *frostfsMock { return &frostfsMock{ - objects: map[oid.Address][]*object.Object{}, - errors: map[oid.Address]error{}, + objects: map[string][]*object.Object{}, + errors: map[string]error{}, + key: key, } } +func (f *frostfsMock) ownerID() user.ID { + if f.key == nil { + return user.ID{} + } + + var ownerID user.ID + user.IDFromKey(&ownerID, f.key.PrivateKey.PublicKey) + return ownerID +} + func (f *frostfsMock) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.ID, error) { var obj object.Object obj.SetPayload(prm.Payload) - obj.SetOwnerID(prm.Creator) + obj.SetOwnerID(f.ownerID()) obj.SetContainerID(prm.Container) a := object.NewAttribute() @@ -44,19 +58,15 @@ func (f *frostfsMock) CreateObject(_ context.Context, prm PrmObjectCreate) (oid. prm.CustomAttributes = append(prm.CustomAttributes, *a) obj.SetAttributes(prm.CustomAttributes...) - if prm.NewVersionFor != nil { - var addr oid.Address - addr.SetObject(*prm.NewVersionFor) - addr.SetContainer(prm.Container) - - _, ok := f.objects[addr] + if prm.NewVersionForAccessKeyID != "" { + _, ok := f.objects[prm.NewVersionForAccessKeyID] if !ok { return oid.ID{}, errors.New("not found") } objID := oidtest.ID() obj.SetID(objID) - f.objects[addr] = append(f.objects[addr], &obj) + f.objects[prm.NewVersionForAccessKeyID] = append(f.objects[prm.NewVersionForAccessKeyID], &obj) return objID, nil } @@ -64,22 +74,27 @@ func (f *frostfsMock) CreateObject(_ context.Context, prm PrmObjectCreate) (oid. objID := oidtest.ID() obj.SetID(objID) + accessKeyID := prm.CustomAccessKey + if accessKeyID == "" { + accessKeyID = prm.Container.EncodeToString() + "0" + objID.EncodeToString() + } + var addr oid.Address addr.SetObject(objID) addr.SetContainer(prm.Container) - f.objects[addr] = []*object.Object{&obj} + f.objects[accessKeyID] = []*object.Object{&obj} return objID, nil } -func (f *frostfsMock) GetCredsObject(_ context.Context, address oid.Address) (*object.Object, error) { - if err := f.errors[address]; err != nil { +func (f *frostfsMock) GetCredsObject(_ context.Context, prm PrmGetCredsObject) (*object.Object, error) { + if err := f.errors[prm.AccessKeyID]; err != nil { return nil, err } - objects, ok := f.objects[address] + objects, ok := f.objects[prm.AccessKeyID] if !ok { - return nil, errors.New("not found") + return nil, ErrCustomAccessKeyIDNotFound } return objects[len(objects)-1], nil @@ -100,7 +115,7 @@ func TestRemovingAccessBox(t *testing.T) { sk, err := hex.DecodeString(secretKey) require.NoError(t, err) - accessBox, _, err := accessbox.PackTokens(gateData, sk) + accessBox, _, err := accessbox.PackTokens(gateData, sk, false) require.NoError(t, err) data, err := accessBox.Marshal() require.NoError(t, err) @@ -111,9 +126,24 @@ func TestRemovingAccessBox(t *testing.T) { obj.SetID(addr.Object()) obj.SetContainerID(addr.Container()) + accessKeyID := getAccessKeyID(addr) + + accessBoxCustom, _, err := accessbox.PackTokens(gateData, []byte("secret"), true) + require.NoError(t, err) + dataCustom, err := accessBoxCustom.Marshal() + require.NoError(t, err) + + var objCustom object.Object + objCustom.SetPayload(dataCustom) + addrCustom := oidtest.Address() + objCustom.SetID(addrCustom.Object()) + objCustom.SetContainerID(addrCustom.Container()) + + accessKeyIDCustom := "accessKeyID" + frostfs := &frostfsMock{ - objects: map[oid.Address][]*object.Object{addr: {&obj}}, - errors: map[oid.Address]error{}, + objects: map[string][]*object.Object{accessKeyID: {&obj}, accessKeyIDCustom: {&objCustom}}, + errors: map[string]error{}, } cfg := Config{ @@ -129,15 +159,30 @@ func TestRemovingAccessBox(t *testing.T) { creds := New(cfg) - _, _, err = creds.GetBox(ctx, addr) + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID) + require.NoError(t, err) + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyIDCustom) require.NoError(t, err) - frostfs.errors[addr] = errors.New("network error") - _, _, err = creds.GetBox(ctx, addr) + frostfs.errors[accessKeyID] = errors.New("network error") + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID) + require.NoError(t, err) + frostfs.errors[accessKeyIDCustom] = errors.New("network error") + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyIDCustom) require.NoError(t, err) - frostfs.errors[addr] = &apistatus.ObjectAlreadyRemoved{} - _, _, err = creds.GetBox(ctx, addr) + frostfs.errors[accessKeyID] = &apistatus.ObjectAlreadyRemoved{} + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID) + require.Error(t, err) + frostfs.errors[accessKeyIDCustom] = &apistatus.ObjectAlreadyRemoved{} + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyIDCustom) + require.Error(t, err) + + frostfs.errors[accessKeyID] = &apistatus.ObjectAlreadyRemoved{} + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID) + require.Error(t, err) + frostfs.errors[accessKeyIDCustom] = &apistatus.ObjectAlreadyRemoved{} + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyIDCustom) require.Error(t, err) } @@ -153,8 +198,9 @@ func TestGetBox(t *testing.T) { }} secret := []byte("secret") - accessBox, _, err := accessbox.PackTokens(gateData, secret) + accessBox, secrets, err := accessbox.PackTokens(gateData, secret, false) require.NoError(t, err) + require.Equal(t, hex.EncodeToString(secret), secrets.SecretKey) data, err := accessBox.Marshal() require.NoError(t, err) @@ -172,108 +218,107 @@ func TestGetBox(t *testing.T) { } t.Run("no removing check, accessbox from cache", func(t *testing.T) { - frostfs := newFrostfsMock() - cfg.FrostFS = frostfs - cfg.RemovingCheckAfterDurations = time.Hour - cfg.Key = key - creds := New(cfg) + creds := newCreds(key, cfg, time.Hour) cnrID := cidtest.ID() - addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox}) + addr, err := creds.Put(ctx, CredentialsParam{Container: cnrID, Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox}) require.NoError(t, err) - _, _, err = creds.GetBox(ctx, addr) + accessKeyID := getAccessKeyID(addr) + + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID) require.NoError(t, err) - frostfs.errors[addr] = &apistatus.ObjectAlreadyRemoved{} - _, _, err = creds.GetBox(ctx, addr) + creds.(*cred).frostFS.(*frostfsMock).errors[accessKeyID] = &apistatus.ObjectAlreadyRemoved{} + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID) require.NoError(t, err) }) t.Run("error while getting box from frostfs", func(t *testing.T) { - frostfs := newFrostfsMock() - cfg.FrostFS = frostfs - cfg.RemovingCheckAfterDurations = 0 - cfg.Key = key - creds := New(cfg) + creds := newCreds(key, cfg, 0) cnrID := cidtest.ID() - addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox}) + addr, err := creds.Put(ctx, CredentialsParam{Container: cnrID, Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox}) require.NoError(t, err) - frostfs.errors[addr] = errors.New("network error") - _, _, err = creds.GetBox(ctx, addr) + accessKeyID := getAccessKeyID(addr) + creds.(*cred).frostFS.(*frostfsMock).errors[accessKeyID] = errors.New("network error") + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID) require.Error(t, err) }) t.Run("invalid key", func(t *testing.T) { - frostfs := newFrostfsMock() + frostfs := newFrostfsMock(key) var obj object.Object obj.SetPayload(data) addr := oidtest.Address() - frostfs.objects[addr] = []*object.Object{&obj} + accessKeyID := getAccessKeyID(addr) + frostfs.objects[accessKeyID] = []*object.Object{&obj} cfg.FrostFS = frostfs cfg.RemovingCheckAfterDurations = 0 cfg.Key = &keys.PrivateKey{} creds := New(cfg) - _, _, err = creds.GetBox(ctx, addr) + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID) require.Error(t, err) }) t.Run("invalid payload", func(t *testing.T) { - frostfs := newFrostfsMock() + frostfs := newFrostfsMock(key) var obj object.Object obj.SetPayload([]byte("invalid")) addr := oidtest.Address() - frostfs.objects[addr] = []*object.Object{&obj} + accessKeyID := getAccessKeyID(addr) + frostfs.objects[accessKeyID] = []*object.Object{&obj} cfg.FrostFS = frostfs cfg.RemovingCheckAfterDurations = 0 cfg.Key = key creds := New(cfg) - _, _, err = creds.GetBox(ctx, addr) + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID) require.Error(t, err) }) t.Run("check attributes update", func(t *testing.T) { - frostfs := newFrostfsMock() - cfg.FrostFS = frostfs - cfg.RemovingCheckAfterDurations = 0 - cfg.Key = key - creds := New(cfg) + creds := newCreds(key, cfg, 0) cnrID := cidtest.ID() - addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox}) + addr, err := creds.Put(ctx, CredentialsParam{Container: cnrID, Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox}) require.NoError(t, err) - _, boxAttrs, err := creds.GetBox(ctx, addr) + accessKeyID := getAccessKeyID(addr) + _, boxAttrs, err := creds.GetBox(ctx, addr.Container(), accessKeyID) require.NoError(t, err) - _, err = creds.Update(ctx, addr, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox, CustomAttributes: attrs}) + prm := CredentialsParam{ + Container: addr.Container(), + AccessKeyID: accessKeyID, + Keys: keys.PublicKeys{key.PublicKey()}, + AccessBox: accessBox, + CustomAttributes: attrs, + } + _, err = creds.Update(ctx, prm) require.NoError(t, err) - _, newBoxAttrs, err := creds.GetBox(ctx, addr) + _, newBoxAttrs, err := creds.GetBox(ctx, addr.Container(), accessKeyID) require.NoError(t, err) require.Equal(t, len(boxAttrs)+1, len(newBoxAttrs)) }) t.Run("check accessbox update", func(t *testing.T) { - frostfs := newFrostfsMock() - cfg.FrostFS = frostfs - cfg.RemovingCheckAfterDurations = 0 - cfg.Key = key - creds := New(cfg) + creds := newCreds(key, cfg, 0) cnrID := cidtest.ID() - addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox}) + addr, err := creds.Put(ctx, CredentialsParam{Container: cnrID, Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox}) require.NoError(t, err) - box, _, err := creds.GetBox(ctx, addr) + accessKeyID := getAccessKeyID(addr) + + box, _, err := creds.GetBox(ctx, addr.Container(), accessKeyID) require.NoError(t, err) require.Equal(t, hex.EncodeToString(secret), box.Gate.SecretKey) @@ -286,44 +331,134 @@ func TestGetBox(t *testing.T) { }} newSecret := []byte("new-secret") - newAccessBox, _, err := accessbox.PackTokens(newGateData, newSecret) + newAccessBox, _, err := accessbox.PackTokens(newGateData, newSecret, false) require.NoError(t, err) - _, err = creds.Update(ctx, addr, CredentialsParam{Keys: keys.PublicKeys{newKey.PublicKey()}, AccessBox: newAccessBox}) + prm := CredentialsParam{ + Container: addr.Container(), + AccessKeyID: accessKeyID, + Keys: keys.PublicKeys{newKey.PublicKey()}, + AccessBox: newAccessBox, + } + + _, err = creds.Update(ctx, prm) require.NoError(t, err) - _, _, err = creds.GetBox(ctx, addr) + _, _, err = creds.GetBox(ctx, addr.Container(), accessKeyID) require.Error(t, err) - cfg.Key = newKey - newCreds := New(cfg) + newCfg := Config{ + FrostFS: creds.(*cred).frostFS, + Key: newKey, + CacheConfig: cfg.CacheConfig, + } + newCreds := New(newCfg) - box, _, err = newCreds.GetBox(ctx, addr) + box, _, err = newCreds.GetBox(ctx, addr.Container(), accessKeyID) require.NoError(t, err) require.Equal(t, hex.EncodeToString(newSecret), box.Gate.SecretKey) }) + t.Run("check access key id uniqueness", func(t *testing.T) { + creds := newCreds(key, cfg, 0) + + prm := CredentialsParam{ + Container: cidtest.ID(), + AccessBox: accessBox, + Keys: keys.PublicKeys{key.PublicKey()}, + } + + _, err = creds.Put(ctx, prm) + require.NoError(t, err) + + _, err = creds.Put(ctx, prm) + require.NoError(t, err) + }) + t.Run("empty keys", func(t *testing.T) { - frostfs := newFrostfsMock() - cfg.FrostFS = frostfs - cfg.RemovingCheckAfterDurations = 0 - cfg.Key = key - creds := New(cfg) + creds := newCreds(key, cfg, 0) cnrID := cidtest.ID() - _, err = creds.Put(ctx, cnrID, CredentialsParam{AccessBox: accessBox}) + _, err = creds.Put(ctx, CredentialsParam{Container: cnrID, AccessBox: accessBox}) require.ErrorIs(t, err, ErrEmptyPublicKeys) }) t.Run("empty accessbox", func(t *testing.T) { - frostfs := newFrostfsMock() - cfg.FrostFS = frostfs - cfg.RemovingCheckAfterDurations = 0 - cfg.Key = key - creds := New(cfg) + creds := newCreds(key, cfg, 0) cnrID := cidtest.ID() - _, err = creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}}) + _, err = creds.Put(ctx, CredentialsParam{Container: cnrID, Keys: keys.PublicKeys{key.PublicKey()}}) require.ErrorIs(t, err, ErrEmptyBearerToken) }) } + +func TestBoxWithCustomAccessKeyID(t *testing.T) { + ctx := context.Background() + key, err := keys.NewPrivateKey() + require.NoError(t, err) + + gateData := []*accessbox.GateData{{ + BearerToken: &bearer.Token{}, + GateKey: key.PublicKey(), + }} + + secret := []byte("secret") + accessBox, secrets, err := accessbox.PackTokens(gateData, secret, true) + require.NoError(t, err) + require.Equal(t, string(secret), secrets.SecretKey) + + cfg := Config{ + CacheConfig: &cache.Config{ + Size: 10, + Lifetime: 24 * time.Hour, + Logger: zaptest.NewLogger(t), + }, + } + + t.Run("check secret format", func(t *testing.T) { + creds := newCreds(key, cfg, 0) + + prm := CredentialsParam{ + Container: cidtest.ID(), + AccessKeyID: "custom-access-key-id", + AccessBox: accessBox, + Keys: keys.PublicKeys{key.PublicKey()}, + } + + _, err = creds.Put(ctx, prm) + require.NoError(t, err) + + box, _, err := creds.GetBox(ctx, prm.Container, prm.AccessKeyID) + require.NoError(t, err) + require.Equal(t, string(secret), box.Gate.SecretKey) + }) + + t.Run("check custom access key id uniqueness", func(t *testing.T) { + creds := newCreds(key, cfg, 0) + + prm := CredentialsParam{ + Container: cidtest.ID(), + AccessKeyID: "custom-access-key-id", + AccessBox: accessBox, + Keys: keys.PublicKeys{key.PublicKey()}, + } + + _, err = creds.Put(ctx, prm) + require.NoError(t, err) + + _, err = creds.Put(ctx, prm) + require.Error(t, err) + }) +} + +func newCreds(key *keys.PrivateKey, cfg Config, removingCheckDuration time.Duration) Credentials { + frostfs := newFrostfsMock(key) + cfg.FrostFS = frostfs + cfg.RemovingCheckAfterDurations = removingCheckDuration + cfg.Key = key + return New(cfg) +} + +func getAccessKeyID(addr oid.Address) string { + return strings.ReplaceAll(addr.EncodeToString(), "/", "0") +} diff --git a/docs/authentication.md b/docs/authentication.md index 60f2e4f1..caac9512 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -159,8 +159,10 @@ storage node. Object s3 credentials are formed based on: * `AccessKeyId` - is concatenated container id and object id (`0`) of `AccessBox` ( - e.g. `2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf`) -* `SecretAccessKey` - hex-encoded random generated 32 bytes (that is encrypted and stored in object payload) + e.g. `2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf`). + Or it can be arbitrary user-provided unique string with min length 4 and max length 128. +* `SecretAccessKey` - hex-encoded random generated 32 bytes (that is encrypted and stored in object payload). + Or it can be arbitrary user-provided unique string with min length 4 and max length 128. > **Note**: sensitive info in `AccessBox` is [encrypted](#encryption), so only someone who posses specific private key > can decrypt such info. @@ -192,7 +194,7 @@ It contains: * List of gate data: * Gate public key (so that gate (when it will decrypt data later) know which item from the list it should process) * Encrypted tokens: - * `SecretAccessKey` - hex-encoded random generated 32 bytes + * `SecretAccessKey` - hex-encoded random generated 32 bytes (or arbitrary user-provided string) * Marshaled bearer token - more detail in [spec](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/commit/4c68d92468503b10282c8a92af83a56f170c8a3a/acl/types.proto#L189) * Marshaled session token - more detail @@ -229,10 +231,12 @@ relevant data) the following sequence is used: * Search all object whose attribute `S3-Access-Box-CRDT-Name` is equal to `AccessKeyId` (extract container id - from `AccessKeyId` that has format: `0`). + from `AccessKeyId` that has format: `0` if `AccessBox` was created with default parameters, or it can also + be arbitrary user-defined string). * Get metadata for these object using `HEAD` requests (not `Get` to reduce network traffic) * Sort all these objects by creation epoch and object id -* Pick last object id (If no object is found then extract object id from `AccessKeyId` that has format: `0`. +* Pick last object id (If no object is found then extract object id from `AccessKeyId` that has format: `0` + (if `AccessBox` was created with default parameters, or it can also be arbitrary user-defined string). We need to do this because versions of `AccessBox` can miss the `S3-Access-Box-CRDT-Name` attribute.) * Get appropriate object from FrostFS storage * Decrypt `AccessBox` (see [encryption](#encryption)) @@ -253,7 +257,7 @@ secp256r1 or prime256v1) is used (unless otherwise stated). * Create ephemeral key (`SeedKey`), it's need to generate shared secret * Generate random 32-byte (that after hex-encoded be `SecretAccessKey`) or use existing secret access key - (if `AccessBox` is being updated rather than creating brand new) + (if `AccessBox` is being updated rather than creating brand new) or use arbitrary user-provided string * Generate shared secret as [ECDH](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman) * Derive 32-byte key using shared secret from previous step with key derivation function based on HMAC with SHA256 [HKDF](https://en.wikipedia.org/wiki/HKDF) diff --git a/docs/authmate.md b/docs/authmate.md index c0b1be5e..5c947ee7 100644 --- a/docs/authmate.md +++ b/docs/authmate.md @@ -146,6 +146,32 @@ the secret. Format of `access_key_id`: `%cid0%oid`, where 0(zero) is a delimiter 24h). Default value is `720h` (30 days). It will be ceil rounded to the nearest amount of epoch * `--aws-cli-credentials` - path to the aws cli credentials file, where authmate will write `access_key_id` and `secret_access_key` to +* `--rpc-endpoint` - NEO node RPC address (must be provided if `--container-id` is NNS name) +* `--access-key-id` - access key id of s3 credential that must be created (must be unique) +* `--secret-access-key` - secret access key of s3 credential that must be used + +You also can specify `AccessKeyID`/`SecretAccessKey` pair that should be created: + +```shell +$ frostfs-s3-authmate issue-secret --wallet wallet.json \ +--peer 192.168.130.71:8080 \ + --gate-public-key 0313b1ac3a8076e155a7e797b24f0b650cccad5941ea59d7cfd51a024a8b2a06bf \ + --gate-public-key 0317585fa8274f7afdf1fc5f2a2e7bece549d5175c4e5182e37924f30229aef967 \ + --access-key-id my-access-key \ + --secret-access-key my-secret-key \ + --container-id BpExV76416Vo7GrkJsGwXGoLM35xsBwup8voedDZR3c6 + + Enter password for wallet.json > + +{ + "initial_access_key_id": "my-access-key-3", + "access_key_id": "my-access-key", + "secret_access_key": "my-secret-key", + "owner_private_key": "d9972cc4f21b07a90f4b347c72c33c1d1611c2b9a2cfd0cc28cee8cb221e8e55", + "wallet_public_key": "031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a", + "container_id": "BpExV76416Vo7GrkJsGwXGoLM35xsBwup8voedDZR3c6" +} +``` ### Bearer tokens diff --git a/docs/configuration.md b/docs/configuration.md index 205a9017..6ff92e72 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -761,12 +761,14 @@ Section for well-known containers to store s3-related data and settings. containers: cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj + accessbox: ExnA1gSY3kzgomi2wJxNyWo1ytWv9VAKXRE55fNXEPL2 ``` -| Parameter | Type | SIGHUP reload | Default value | Description | -|-------------|----------|---------------|---------------|-------------------------------------------------------------------------------------------| -| `cors` | `string` | no | | Container name for CORS configurations. If not set, container of the bucket is used. | -| `lifecycle` | `string` | no | | Container name for lifecycle configurations. If not set, container of the bucket is used. | +| Parameter | Type | SIGHUP reload | Default value | Description | +|-------------|----------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------| +| `cors` | `string` | no | | Container name for CORS configurations. If not set, container of the bucket is used. | +| `lifecycle` | `string` | no | | Container name for lifecycle configurations. If not set, container of the bucket is used. | +| `accessbox` | `string` | no | | Container name to lookup accessbox if custom aws credentials is used. If not set, custom credentials are not supported. | # `vhs` section diff --git a/docs/images/authentication/accessbox-object.puml b/docs/images/authentication/accessbox-object.puml index d4bdb5da..28b9f249 100644 --- a/docs/images/authentication/accessbox-object.puml +++ b/docs/images/authentication/accessbox-object.puml @@ -21,6 +21,7 @@ package AccessBox { SeedKey => Encoded public seed key List of Gates *--> Gate List of container policies *--> ContainerPolicy + IsCustom => True if SecretKey was imported and must be treated as it is } diff --git a/docs/images/authentication/accessbox-object.svg b/docs/images/authentication/accessbox-object.svg index fe2f2efb..957efb75 100644 --- a/docs/images/authentication/accessbox-object.svg +++ b/docs/images/authentication/accessbox-object.svg @@ -1,10 +1,10 @@ -AccessBoxTokensSecretKeyPrivate keyBearerTokenEncoded bearer tokenSessionTokensList of encoded session tokensGateGateKeyEncoded public gate keyEncrypted tokensContainerPolicyLocationConstraintPolicy namePlacementPolicyEncoded placement policyBoxSeedKeyEncoded public seed keyList of GatesList of container policiesObjectAttributesTimestamp1710418478__SYSTEM__EXPIRATION_EPOCH10801S3-CRDT-Versions-Add5ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf,9bLtL1EsUpuSiqmHnqFf6RuT6x5QMLMNBqx7vCcCcNhyS3-Access-Box-CRDT-Name2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3SnxfFilePath1710418478_access.boxFrostFSObjectHeaderPayloadAccessBoxTokensSecretKeyPrivate keyBearerTokenEncoded bearer tokenSessionTokensList of encoded session tokensGateGateKeyEncoded public gate keyEncrypted tokensContainerPolicyLocationConstraintPolicy namePlacementPolicyEncoded placement policyBoxSeedKeyEncoded public seed keyList of GatesList of container policiesIsCustomTrue if SecretKey was imported and must be treated as it isObjectAttributesTimestamp1710418478__SYSTEM__EXPIRATION_EPOCH10801S3-CRDT-Versions-Add5ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf,9bLtL1EsUpuSiqmHnqFf6RuT6x5QMLMNBqx7vCcCcNhyS3-Access-Box-CRDT-Name2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3SnxfFilePath1710418478_access.boxFrostFSObjectHeaderPayload