[#292] authmate: Support custom attributes

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2024-01-19 10:00:04 +03:00
parent c32220762f
commit eae49908da
8 changed files with 109 additions and 35 deletions

View file

@ -23,6 +23,7 @@ This document outlines major changes between releases.
- Support policy-engine (#257) - Support policy-engine (#257)
- Support `policy` contract (#259) - Support `policy` contract (#259)
- Support `proxy` contract (#287) - Support `proxy` contract (#287)
- Authmate: support custom attributes (#292)
### Changed ### Changed
- Generalise config param `use_default_xmlns_for_complete_multipart` to `use_default_xmlns` so that use default xmlns for all requests (#221) - Generalise config param `use_default_xmlns_for_complete_multipart` to `use_default_xmlns` so that use default xmlns for all requests (#221)

View file

@ -11,9 +11,7 @@ import (
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" 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/aws/aws-sdk-go/aws/credentials"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -42,11 +40,11 @@ func (m credentialsMock) GetBox(_ context.Context, addr oid.Address) (*accessbox
return box, nil 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 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 return oid.Address{}, nil
} }

View file

@ -22,6 +22,7 @@ import (
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa" 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/eacl"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" "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" 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/session"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
@ -113,14 +114,16 @@ type (
Lifetime time.Duration Lifetime time.Duration
AwsCliCredentialsFile string AwsCliCredentialsFile string
ContainerPolicies ContainerPolicies ContainerPolicies ContainerPolicies
CustomAttributes []object.Attribute
} }
// UpdateSecretOptions contains options for passing to Agent.UpdateSecret method. // UpdateSecretOptions contains options for passing to Agent.UpdateSecret method.
UpdateSecretOptions struct { UpdateSecretOptions struct {
FrostFSKey *keys.PrivateKey FrostFSKey *keys.PrivateKey
GatesPublicKeys []*keys.PublicKey GatesPublicKeys []*keys.PublicKey
Address oid.Address Address oid.Address
GatePrivateKey *keys.PrivateKey GatePrivateKey *keys.PrivateKey
CustomAttributes []object.Attribute
} }
tokenUpdateOptions struct { 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)) 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 { if err != nil {
return fmt.Errorf("failed to put creds: %w", err) 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, a.log.Info(logs.UpdateAccessCredObjectIntoFrostFS,
zap.Stringer("owner_tkn", idOwner)) zap.Stringer("owner_tkn", idOwner))
prm := tokens.CredentialsParam{
OwnerID: idOwner,
AccessBox: updatedBox,
Expiration: lifetime.Exp,
Keys: options.GatesPublicKeys,
CustomAttributes: options.CustomAttributes,
}
oldAddr := options.Address 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 { if err != nil {
return fmt.Errorf("failed to update creds: %w", err) return fmt.Errorf("failed to update creds: %w", err)
} }

View file

@ -17,11 +17,12 @@ import (
) )
var issueSecretCmd = &cobra.Command{ var issueSecretCmd = &cobra.Command{
Use: "issue-secret", Use: "issue-secret",
Short: "Issue a secret in FrostFS network", Short: "Issue a secret in FrostFS network",
Long: "Creates new s3 credentials to use with frostfs-s3-gw", Long: "Creates new s3 credentials to use with frostfs-s3-gw",
Example: `frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a`, Example: `frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a
RunE: runIssueSecretCmd, frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a --attributes LOGIN=NUUb82KR2JrVByHs2YSKgtK29gKnF5q6Vt`,
RunE: runIssueSecretCmd,
} }
const ( const (
@ -42,6 +43,7 @@ const (
frostfsIDProxyFlag = "frostfsid-proxy" frostfsIDProxyFlag = "frostfsid-proxy"
frostfsIDNamespaceFlag = "frostfsid-namespace" frostfsIDNamespaceFlag = "frostfsid-namespace"
rpcEndpointFlag = "rpc-endpoint" rpcEndpointFlag = "rpc-endpoint"
attributesFlag = "attributes"
) )
const ( 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(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(frostfsIDNamespaceFlag, "", "Namespace to register public key in frostfsid contract")
issueSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address") 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(walletFlag)
_ = issueSecretCmd.MarkFlagRequired(peerFlag) _ = 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{ issueSecretOptions := &authmate.IssueSecretOptions{
Container: authmate.ContainerOptions{ Container: authmate.ContainerOptions{
ID: cnrID, ID: cnrID,
@ -199,6 +207,7 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error {
ContainerPolicies: policies, ContainerPolicies: policies,
Lifetime: lifetime, Lifetime: lifetime,
AwsCliCredentialsFile: viper.GetString(awsCLICredentialFlag), AwsCliCredentialsFile: viper.GetString(awsCLICredentialFlag),
CustomAttributes: customAttrs,
} }
if err = authmate.New(log, frostFS).IssueSecret(ctx, os.Stdout, issueSecretOptions); err != nil { if err = authmate.New(log, frostFS).IssueSecret(ctx, os.Stdout, issueSecretOptions); err != nil {

View file

@ -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(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(frostfsIDNamespaceFlag, "", "Namespace to register public key in frostfsid contract")
updateSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address") 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(walletFlag)
_ = updateSecretCmd.MarkFlagRequired(peerFlag) _ = 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{ updateSecretOptions := &authmate.UpdateSecretOptions{
Address: accessBoxAddress, Address: accessBoxAddress,
FrostFSKey: key, FrostFSKey: key,
GatesPublicKeys: gatesPublicKeys, GatesPublicKeys: gatesPublicKeys,
GatePrivateKey: gateKey, GatePrivateKey: gateKey,
CustomAttributes: customAttrs,
} }
if err = authmate.New(log, frostFS).UpdateSecret(ctx, os.Stdout, updateSecretOptions); err != nil { if err = authmate.New(log, frostFS).UpdateSecret(ctx, os.Stdout, updateSecretOptions); err != nil {

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strings"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "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"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "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" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -153,3 +155,23 @@ func createFrostFSID(ctx context.Context, log *zap.Logger, cfg frostfsid.Config)
return cli, nil 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
}

View file

@ -10,6 +10,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
@ -19,8 +20,16 @@ type (
// Credentials is a bearer token get/put interface. // Credentials is a bearer token get/put interface.
Credentials interface { Credentials interface {
GetBox(context.Context, oid.Address) (*accessbox.Box, error) GetBox(context.Context, oid.Address) (*accessbox.Box, error)
Put(context.Context, cid.ID, user.ID, *accessbox.AccessBox, uint64, ...*keys.PublicKey) (oid.Address, error) Put(context.Context, cid.ID, CredentialsParam) (oid.Address, error)
Update(context.Context, oid.Address, user.ID, *accessbox.AccessBox, uint64, ...*keys.PublicKey) (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 { cred struct {
@ -50,6 +59,9 @@ type PrmObjectCreate struct {
// Object payload. // Object payload.
Payload []byte Payload []byte
// CustomAttributes are additional user provided attributes for box object.
CustomAttributes []object.Attribute
} }
// FrostFS represents virtual connection to FrostFS network. // 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 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) { func (c *cred) Put(ctx context.Context, idCnr cid.ID, prm CredentialsParam) (oid.Address, error) {
return c.createObject(ctx, idCnr, nil, issuer, box, expiration, keys...) 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() 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) { func (c *cred) createObject(ctx context.Context, cnrID cid.ID, newVersionFor *oid.ID, prm CredentialsParam) (oid.Address, error) {
if len(keys) == 0 { if len(prm.Keys) == 0 {
return oid.Address{}, ErrEmptyPublicKeys return oid.Address{}, ErrEmptyPublicKeys
} else if box == nil { } else if prm.AccessBox == nil {
return oid.Address{}, ErrEmptyBearerToken return oid.Address{}, ErrEmptyBearerToken
} }
data, err := box.Marshal() data, err := prm.AccessBox.Marshal()
if err != nil { if err != nil {
return oid.Address{}, fmt.Errorf("marshall box: %w", err) return oid.Address{}, fmt.Errorf("marshall box: %w", err)
} }
idObj, err := c.frostFS.CreateObject(ctx, PrmObjectCreate{ idObj, err := c.frostFS.CreateObject(ctx, PrmObjectCreate{
Creator: issuer, Creator: prm.OwnerID,
Container: cnrID, Container: cnrID,
Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box", Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box",
ExpirationEpoch: expiration, ExpirationEpoch: prm.Expiration,
NewVersionFor: newVersionFor, NewVersionFor: newVersionFor,
Payload: data, Payload: data,
CustomAttributes: prm.CustomAttributes,
}) })
if err != nil { if err != nil {
return oid.Address{}, fmt.Errorf("create object: %w", err) return oid.Address{}, fmt.Errorf("create object: %w", err)

View file

@ -120,6 +120,11 @@ func (x *AuthmateFrostFS) CreateObject(ctx context.Context, prm tokens.PrmObject
attributes = append(attributes, [2]string{accessBoxCRDTNameAttr, versions.Name()}) attributes = append(attributes, [2]string{accessBoxCRDTNameAttr, versions.Name()})
} }
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{ return x.frostFS.CreateObject(ctx, layer.PrmObjectCreate{
Container: prm.Container, Container: prm.Container,
Filepath: prm.Filepath, Filepath: prm.Filepath,