From eae49908da137766151b25ca97995122dd2f8548 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Fri, 19 Jan 2024 10:00:04 +0300 Subject: [PATCH] [#292] authmate: Support custom attributes Signed-off-by: Denis Kirillov --- CHANGELOG.md | 1 + api/auth/presign_test.go | 6 ++-- authmate/authmate.go | 31 ++++++++++++---- cmd/s3-authmate/modules/issue-secret.go | 19 +++++++--- cmd/s3-authmate/modules/update-secret.go | 15 +++++--- cmd/s3-authmate/modules/utils.go | 22 ++++++++++++ creds/tokens/credentials.go | 45 +++++++++++++++--------- internal/frostfs/authmate.go | 5 +++ 8 files changed, 109 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07ae8836..5ea2498e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This document outlines major changes between releases. - Support policy-engine (#257) - Support `policy` contract (#259) - Support `proxy` contract (#287) +- Authmate: support custom attributes (#292) ### Changed - Generalise config param `use_default_xmlns_for_complete_multipart` to `use_default_xmlns` so that use default xmlns for all requests (#221) diff --git a/api/auth/presign_test.go b/api/auth/presign_test.go index 36eb97cb..9cebb817 100644 --- a/api/auth/presign_test.go +++ b/api/auth/presign_test.go @@ -11,9 +11,7 @@ import ( apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" 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/aws/aws-sdk-go/aws/credentials" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/stretchr/testify/require" ) @@ -42,11 +40,11 @@ func (m credentialsMock) GetBox(_ context.Context, addr oid.Address) (*accessbox return box, nil } -func (m credentialsMock) Put(context.Context, cid.ID, user.ID, *accessbox.AccessBox, uint64, ...*keys.PublicKey) (oid.Address, error) { +func (m credentialsMock) Put(context.Context, cid.ID, tokens.CredentialsParam) (oid.Address, error) { return oid.Address{}, nil } -func (m credentialsMock) Update(context.Context, oid.Address, user.ID, *accessbox.AccessBox, uint64, ...*keys.PublicKey) (oid.Address, error) { +func (m credentialsMock) Update(context.Context, oid.Address, tokens.CredentialsParam) (oid.Address, error) { return oid.Address{}, nil } diff --git a/authmate/authmate.go b/authmate/authmate.go index 5b02f996..97ac0e94 100644 --- a/authmate/authmate.go +++ b/authmate/authmate.go @@ -22,6 +22,7 @@ import ( frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" + "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/session" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" @@ -113,14 +114,16 @@ type ( Lifetime time.Duration AwsCliCredentialsFile string ContainerPolicies ContainerPolicies + CustomAttributes []object.Attribute } // UpdateSecretOptions contains options for passing to Agent.UpdateSecret method. UpdateSecretOptions struct { - FrostFSKey *keys.PrivateKey - GatesPublicKeys []*keys.PublicKey - Address oid.Address - GatePrivateKey *keys.PrivateKey + FrostFSKey *keys.PrivateKey + GatesPublicKeys []*keys.PublicKey + Address oid.Address + GatePrivateKey *keys.PrivateKey + CustomAttributes []object.Attribute } tokenUpdateOptions struct { @@ -278,7 +281,15 @@ func (a *Agent) IssueSecret(ctx context.Context, w io.Writer, options *IssueSecr creds := tokens.New(a.frostFS, secrets.EphemeralKey, cache.DefaultAccessBoxConfig(a.log)) - addr, err := creds.Put(ctx, id, idOwner, box, lifetime.Exp, options.GatesPublicKeys...) + prm := tokens.CredentialsParam{ + OwnerID: idOwner, + AccessBox: box, + Expiration: lifetime.Exp, + Keys: options.GatesPublicKeys, + CustomAttributes: options.CustomAttributes, + } + + addr, err := creds.Put(ctx, id, prm) if err != nil { return fmt.Errorf("failed to put creds: %w", err) } @@ -354,8 +365,16 @@ func (a *Agent) UpdateSecret(ctx context.Context, w io.Writer, options *UpdateSe a.log.Info(logs.UpdateAccessCredObjectIntoFrostFS, zap.Stringer("owner_tkn", idOwner)) + prm := tokens.CredentialsParam{ + OwnerID: idOwner, + AccessBox: updatedBox, + Expiration: lifetime.Exp, + Keys: options.GatesPublicKeys, + CustomAttributes: options.CustomAttributes, + } + oldAddr := options.Address - addr, err := creds.Update(ctx, oldAddr, idOwner, updatedBox, lifetime.Exp, options.GatesPublicKeys...) + addr, err := creds.Update(ctx, oldAddr, prm) if err != nil { return fmt.Errorf("failed to update creds: %w", err) } diff --git a/cmd/s3-authmate/modules/issue-secret.go b/cmd/s3-authmate/modules/issue-secret.go index dbeefaf8..fe494e4f 100644 --- a/cmd/s3-authmate/modules/issue-secret.go +++ b/cmd/s3-authmate/modules/issue-secret.go @@ -17,11 +17,12 @@ import ( ) 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`, - RunE: runIssueSecretCmd, + 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`, + RunE: runIssueSecretCmd, } const ( @@ -42,6 +43,7 @@ const ( frostfsIDProxyFlag = "frostfsid-proxy" frostfsIDNamespaceFlag = "frostfsid-namespace" rpcEndpointFlag = "rpc-endpoint" + attributesFlag = "attributes" ) const ( @@ -86,6 +88,7 @@ func initIssueSecretCmd() { issueSecretCmd.Flags().String(frostfsIDProxyFlag, "", "Proxy contract hash (LE) or name in NNS to use when interact with frostfsid contract") issueSecretCmd.Flags().String(frostfsIDNamespaceFlag, "", "Namespace to register public key in frostfsid contract") issueSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address") + issueSecretCmd.Flags().String(attributesFlag, "", "User attributes in form of Key1=Value1,Key2=Value2 (note: you cannot override system attributes)") _ = issueSecretCmd.MarkFlagRequired(walletFlag) _ = issueSecretCmd.MarkFlagRequired(peerFlag) @@ -184,6 +187,11 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error { } } + 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, @@ -199,6 +207,7 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error { ContainerPolicies: policies, Lifetime: lifetime, AwsCliCredentialsFile: viper.GetString(awsCLICredentialFlag), + CustomAttributes: customAttrs, } if err = authmate.New(log, frostFS).IssueSecret(ctx, os.Stdout, issueSecretOptions); err != nil { diff --git a/cmd/s3-authmate/modules/update-secret.go b/cmd/s3-authmate/modules/update-secret.go index 69bd09a7..2f0bb130 100644 --- a/cmd/s3-authmate/modules/update-secret.go +++ b/cmd/s3-authmate/modules/update-secret.go @@ -44,6 +44,7 @@ func initUpdateSecretCmd() { updateSecretCmd.Flags().String(frostfsIDProxyFlag, "", "Proxy contract hash (LE) or name in NNS to use when interact with frostfsid contract") updateSecretCmd.Flags().String(frostfsIDNamespaceFlag, "", "Namespace to register public key in frostfsid contract") updateSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address") + updateSecretCmd.Flags().String(attributesFlag, "", "User attributes in form of Key1=Value1,Key2=Value2 (note: you cannot override system attributes)") _ = updateSecretCmd.MarkFlagRequired(walletFlag) _ = updateSecretCmd.MarkFlagRequired(peerFlag) @@ -122,11 +123,17 @@ func runUpdateSecretCmd(cmd *cobra.Command, _ []string) error { } } + customAttrs, err := parseObjectAttrs(viper.GetString(attributesFlag)) + if err != nil { + return wrapPreparationError(fmt.Errorf("failed to parse attributes: %s", err)) + } + updateSecretOptions := &authmate.UpdateSecretOptions{ - Address: accessBoxAddress, - FrostFSKey: key, - GatesPublicKeys: gatesPublicKeys, - GatePrivateKey: gateKey, + Address: accessBoxAddress, + FrostFSKey: key, + GatesPublicKeys: gatesPublicKeys, + GatePrivateKey: gateKey, + CustomAttributes: customAttrs, } if err = authmate.New(log, frostFS).UpdateSecret(ctx, os.Stdout, updateSecretOptions); err != nil { diff --git a/cmd/s3-authmate/modules/utils.go b/cmd/s3-authmate/modules/utils.go index e43ef273..a72d2a10 100644 --- a/cmd/s3-authmate/modules/utils.go +++ b/cmd/s3-authmate/modules/utils.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "time" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" @@ -12,6 +13,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/spf13/viper" @@ -153,3 +155,23 @@ func createFrostFSID(ctx context.Context, log *zap.Logger, cfg frostfsid.Config) return cli, nil } + +func parseObjectAttrs(attributes string) ([]object.Attribute, error) { + if len(attributes) == 0 { + return nil, nil + } + + rawAttrs := strings.Split(attributes, ",") + + attrs := make([]object.Attribute, len(rawAttrs)) + for i := range rawAttrs { + k, v, found := strings.Cut(rawAttrs[i], "=") + if !found { + return nil, fmt.Errorf("invalid attribute format: %s", rawAttrs[i]) + } + attrs[i].SetKey(k) + attrs[i].SetValue(v) + } + + return attrs, nil +} diff --git a/creds/tokens/credentials.go b/creds/tokens/credentials.go index e850e348..741e641f 100644 --- a/creds/tokens/credentials.go +++ b/creds/tokens/credentials.go @@ -10,6 +10,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" 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" @@ -19,8 +20,16 @@ type ( // Credentials is a bearer token get/put interface. Credentials interface { GetBox(context.Context, oid.Address) (*accessbox.Box, error) - Put(context.Context, cid.ID, user.ID, *accessbox.AccessBox, uint64, ...*keys.PublicKey) (oid.Address, error) - Update(context.Context, oid.Address, user.ID, *accessbox.AccessBox, uint64, ...*keys.PublicKey) (oid.Address, error) + Put(context.Context, cid.ID, CredentialsParam) (oid.Address, error) + Update(context.Context, oid.Address, CredentialsParam) (oid.Address, error) + } + + CredentialsParam struct { + OwnerID user.ID + AccessBox *accessbox.AccessBox + Expiration uint64 + Keys keys.PublicKeys + CustomAttributes []object.Attribute } cred struct { @@ -50,6 +59,9 @@ type PrmObjectCreate struct { // Object payload. Payload []byte + + // CustomAttributes are additional user provided attributes for box object. + CustomAttributes []object.Attribute } // FrostFS represents virtual connection to FrostFS network. @@ -123,33 +135,34 @@ func (c *cred) getAccessBox(ctx context.Context, addr oid.Address) (*accessbox.A return &box, nil } -func (c *cred) Put(ctx context.Context, idCnr cid.ID, issuer user.ID, box *accessbox.AccessBox, expiration uint64, keys ...*keys.PublicKey) (oid.Address, error) { - return c.createObject(ctx, idCnr, nil, issuer, box, expiration, keys...) +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) Update(ctx context.Context, addr oid.Address, issuer user.ID, box *accessbox.AccessBox, expiration uint64, keys ...*keys.PublicKey) (oid.Address, error) { +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, issuer, box, expiration, keys...) + return c.createObject(ctx, addr.Container(), &objID, prm) } -func (c *cred) createObject(ctx context.Context, cnrID cid.ID, newVersionFor *oid.ID, issuer user.ID, box *accessbox.AccessBox, expiration uint64, keys ...*keys.PublicKey) (oid.Address, error) { - if len(keys) == 0 { +func (c *cred) createObject(ctx context.Context, cnrID cid.ID, newVersionFor *oid.ID, prm CredentialsParam) (oid.Address, error) { + if len(prm.Keys) == 0 { return oid.Address{}, ErrEmptyPublicKeys - } else if box == nil { + } else if prm.AccessBox == nil { return oid.Address{}, ErrEmptyBearerToken } - data, err := box.Marshal() + data, err := prm.AccessBox.Marshal() if err != nil { return oid.Address{}, fmt.Errorf("marshall box: %w", err) } idObj, err := c.frostFS.CreateObject(ctx, PrmObjectCreate{ - Creator: issuer, - Container: cnrID, - Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box", - ExpirationEpoch: expiration, - NewVersionFor: newVersionFor, - Payload: data, + Creator: prm.OwnerID, + Container: cnrID, + Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box", + ExpirationEpoch: prm.Expiration, + NewVersionFor: newVersionFor, + Payload: data, + CustomAttributes: prm.CustomAttributes, }) if err != nil { return oid.Address{}, fmt.Errorf("create object: %w", err) diff --git a/internal/frostfs/authmate.go b/internal/frostfs/authmate.go index 2e9e4234..398e080f 100644 --- a/internal/frostfs/authmate.go +++ b/internal/frostfs/authmate.go @@ -120,6 +120,11 @@ func (x *AuthmateFrostFS) CreateObject(ctx context.Context, prm tokens.PrmObject attributes = append(attributes, [2]string{accessBoxCRDTNameAttr, versions.Name()}) } + for _, attr := range prm.CustomAttributes { + // we don't check attribute duplication since storage node does this + attributes = append(attributes, [2]string{attr.Key(), attr.Value()}) + } + return x.frostFS.CreateObject(ctx, layer.PrmObjectCreate{ Container: prm.Container, Filepath: prm.Filepath,