[#135] authmate: Support CRDT GSet for credentials

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2023-06-13 12:35:40 +03:00
parent 7a380fa46c
commit 84358f6742
7 changed files with 274 additions and 38 deletions

View file

@ -46,6 +46,10 @@ func (m credentialsMock) Put(context.Context, cid.ID, user.ID, *accessbox.Access
return oid.Address{}, nil
}
func (m credentialsMock) Update(context.Context, oid.Address, user.ID, *accessbox.AccessBox, uint64, ...*keys.PublicKey) (oid.Address, error) {
return oid.Address{}, nil
}
func TestCheckSign(t *testing.T) {
var accessKeyAddr oid.Address
err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto")

View file

@ -105,6 +105,7 @@ type (
Lifetime time.Duration
AwsCliCredentialsFile string
ContainerPolicies ContainerPolicies
UpdateCreds *UpdateOptions
}
// ContainerOptions groups parameters of auth container to put the secret into.
@ -114,6 +115,12 @@ type (
PlacementPolicy string
}
// UpdateOptions groups parameters to update existing the secret into.
UpdateOptions struct {
Address oid.Address
SecretAccessKey []byte
}
// ObtainSecretOptions contains options for passing to Agent.ObtainSecret method.
ObtainSecretOptions struct {
SecretAddress string
@ -129,11 +136,12 @@ type lifetimeOptions struct {
type (
issuingResult struct {
AccessKeyID string `json:"access_key_id"`
SecretAccessKey string `json:"secret_access_key"`
OwnerPrivateKey string `json:"owner_private_key"`
WalletPublicKey string `json:"wallet_public_key"`
ContainerID string `json:"container_id"`
AccessKeyID string `json:"access_key_id"`
InitialAccessKeyID string `json:"initial_access_key_id"`
SecretAccessKey string `json:"secret_access_key"`
OwnerPrivateKey string `json:"owner_private_key"`
WalletPublicKey string `json:"wallet_public_key"`
ContainerID string `json:"container_id"`
}
obtainingResult struct {
@ -144,9 +152,14 @@ 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("check container", zap.Stringer("cid", opts.ID))
return opts.ID, a.frostFS.ContainerExists(ctx, opts.ID)
}
a.log.Info("create container",
zap.String("friendly_name", opts.FriendlyName),
zap.String("placement_policy", opts.PlacementPolicy))
var prm PrmContainerCreate
err := prm.Policy.DecodeString(opts.PlacementPolicy)
@ -224,7 +237,12 @@ 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)
var secret []byte
if options.UpdateCreds != nil {
secret = options.UpdateCreds.SecretAccessKey
}
box, secrets, err := accessbox.PackTokens(gatesData, secret)
if err != nil {
return fmt.Errorf("pack tokens: %w", err)
}
@ -233,10 +251,6 @@ func (a *Agent) IssueSecret(ctx context.Context, w io.Writer, options *IssueSecr
var idOwner user.ID
user.IDFromKey(&idOwner, options.FrostFSKey.PrivateKey.PublicKey)
a.log.Info("check container or create", zap.Stringer("cid", options.Container.ID),
zap.String("friendly_name", options.Container.FriendlyName),
zap.String("placement_policy", options.Container.PlacementPolicy))
id, err := a.checkContainer(ctx, options.Container, idOwner)
if err != nil {
return fmt.Errorf("check container: %w", err)
@ -245,24 +259,30 @@ func (a *Agent) IssueSecret(ctx context.Context, w io.Writer, options *IssueSecr
a.log.Info("store bearer token into FrostFS",
zap.Stringer("owner_tkn", idOwner))
addr, err := tokens.
New(a.frostFS, secrets.EphemeralKey, cache.DefaultAccessBoxConfig(a.log)).
Put(ctx, id, idOwner, box, lifetime.Exp, options.GatesPublicKeys...)
creds := tokens.New(a.frostFS, secrets.EphemeralKey, cache.DefaultAccessBoxConfig(a.log))
var addr oid.Address
var oldAddr oid.Address
if options.UpdateCreds != nil {
oldAddr = options.UpdateCreds.Address
addr, err = creds.Update(ctx, oldAddr, idOwner, box, lifetime.Exp, options.GatesPublicKeys...)
} else {
addr, err = creds.Put(ctx, id, idOwner, box, lifetime.Exp, options.GatesPublicKeys...)
oldAddr = addr
}
if err != nil {
return fmt.Errorf("failed to put bearer token: %w", err)
return fmt.Errorf("failed to put creds: %w", err)
}
objID := addr.Object()
strIDObj := objID.EncodeToString()
accessKeyID := addr.Container().EncodeToString() + "0" + strIDObj
accessKeyID := addr.Container().EncodeToString() + "0" + addr.Object().EncodeToString()
ir := &issuingResult{
AccessKeyID: accessKeyID,
SecretAccessKey: secrets.AccessKey,
OwnerPrivateKey: hex.EncodeToString(secrets.EphemeralKey.Bytes()),
WalletPublicKey: hex.EncodeToString(options.FrostFSKey.PublicKey().Bytes()),
ContainerID: id.EncodeToString(),
AccessKeyID: accessKeyID,
InitialAccessKeyID: oldAddr.Container().EncodeToString() + "0" + oldAddr.Object().EncodeToString(),
SecretAccessKey: secrets.AccessKey,
OwnerPrivateKey: hex.EncodeToString(secrets.EphemeralKey.Bytes()),
WalletPublicKey: hex.EncodeToString(options.FrostFSKey.PublicKey().Bytes()),
ContainerID: id.EncodeToString(),
}
enc := json.NewEncoder(w)
@ -272,7 +292,7 @@ func (a *Agent) IssueSecret(ctx context.Context, w io.Writer, options *IssueSecr
}
if options.AwsCliCredentialsFile != "" {
profileName := "authmate_cred_" + strIDObj
profileName := "authmate_cred_" + addr.Object().EncodeToString()
if _, err = os.Stat(options.AwsCliCredentialsFile); os.IsNotExist(err) {
profileName = "default"
}

View file

@ -3,6 +3,7 @@ package main
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"os"
@ -19,6 +20,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
"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/pool"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
@ -88,6 +90,7 @@ var (
const (
envWalletPassphrase = "wallet.passphrase"
envWalletGatePassphrase = "wallet.gate.passphrase"
envSecretAccessKey = "secret.access.key"
)
var zapConfig = zap.Config{
@ -229,6 +232,12 @@ func issueSecret() *cli.Command {
Required: false,
Destination: &containerIDFlag,
},
&cli.StringFlag{
Name: "access-key-id",
Usage: "access key id for s3 (use this flag to update existing creds, if this flag is provided '--container-id', '--container-friendly-name' and '--container-placement-policy' are ineffective)",
Required: false,
Destination: &accessKeyIDFlag,
},
&cli.StringFlag{
Name: "container-friendly-name",
Usage: "friendly name of auth container to put the secret into",
@ -333,6 +342,32 @@ It will be ceil rounded to the nearest amount of epoch.`,
}
}
var credsToUpdate *authmate.UpdateOptions
if len(accessKeyIDFlag) > 0 {
secretAccessKeyStr := wallet.GetPassword(viper.GetViper(), envSecretAccessKey)
if secretAccessKeyStr == nil {
return fmt.Errorf("you must provide AUTHMATE_SECRET_ACCESS_KEY env to update existing creds")
}
secretAccessKey, err := hex.DecodeString(*secretAccessKeyStr)
if err != nil {
return fmt.Errorf("access key must be hex encoded")
}
var addr oid.Address
credAddr := strings.Replace(accessKeyIDFlag, "0", "/", 1)
if err = addr.DecodeString(credAddr); err != nil {
return fmt.Errorf("failed to parse creds address: %w", err)
}
// we can create new creds version only in the same container
containerID = addr.Container()
credsToUpdate = &authmate.UpdateOptions{
Address: addr,
SecretAccessKey: secretAccessKey,
}
}
var gatesPublicKeys []*keys.PublicKey
for _, key := range gatesPublicKeysFlag.Value() {
gpk, err := keys.NewPublicKeyFromString(key)
@ -380,6 +415,7 @@ It will be ceil rounded to the nearest amount of epoch.`,
ContainerPolicies: policies,
Lifetime: lifetimeFlag,
AwsCliCredentialsFile: awcCliCredFile,
UpdateCreds: credsToUpdate,
}
var tcancel context.CancelFunc

View file

@ -95,7 +95,8 @@ func (x *AccessBox) Unmarshal(data []byte) error {
// PackTokens adds bearer and session tokens to BearerTokens and SessionToken lists respectively.
// Session token can be nil.
func PackTokens(gatesData []*GateData) (*AccessBox, *Secrets, error) {
// Secret can be nil. In such case secret will be generated.
func PackTokens(gatesData []*GateData, secret []byte) (*AccessBox, *Secrets, error) {
box := &AccessBox{}
ephemeralKey, err := keys.NewPrivateKey()
if err != nil {
@ -103,9 +104,11 @@ func PackTokens(gatesData []*GateData) (*AccessBox, *Secrets, error) {
}
box.OwnerPublicKey = ephemeralKey.PublicKey().Bytes()
secret, err := generateSecret()
if err != nil {
return nil, nil, fmt.Errorf("failed to generate accessKey as hex: %w", err)
if secret == nil {
secret, err = generateSecret()
if err != nil {
return nil, nil, fmt.Errorf("failed to generate accessKey as hex: %w", err)
}
}
if err := box.addTokens(gatesData, ephemeralKey, secret); err != nil {

View file

@ -60,7 +60,7 @@ func TestBearerTokenInAccessBox(t *testing.T) {
require.NoError(t, tkn.Sign(sec.PrivateKey))
gate := NewGateData(cred.PublicKey(), &tkn)
box, _, err = PackTokens([]*GateData{gate})
box, _, err = PackTokens([]*GateData{gate}, nil)
require.NoError(t, err)
data, err := box.Marshal()
@ -95,7 +95,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})
box, _, err = PackTokens([]*GateData{gate}, nil)
require.NoError(t, err)
data, err := box.Marshal()
@ -135,7 +135,7 @@ func TestAccessboxMultipleKeys(t *testing.T) {
}
}
box, _, err = PackTokens(gates)
box, _, err = PackTokens(gates, nil)
require.NoError(t, err)
for i, k := range privateKeys {
@ -164,7 +164,7 @@ func TestUnknownKey(t *testing.T) {
require.NoError(t, tkn.Sign(sec.PrivateKey))
gate := NewGateData(cred.PublicKey(), &tkn)
box, _, err = PackTokens([]*GateData{gate})
box, _, err = PackTokens([]*GateData{gate}, nil)
require.NoError(t, err)
_, err = box.GetTokens(wrongCred)

View file

@ -20,6 +20,7 @@ type (
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)
}
cred struct {
@ -40,6 +41,10 @@ type PrmObjectCreate struct {
// File path.
Filepath string
// Optional.
// If provided cred object will be created using crdt approach.
NewVersionFor *oid.ID
// Last FrostFS epoch of the object lifetime.
ExpirationEpoch uint64
@ -57,12 +62,13 @@ type FrostFS interface {
// prevented the object from being created.
CreateObject(context.Context, PrmObjectCreate) (oid.ID, error)
// ReadObjectPayload reads payload of the object from FrostFS network by address
// into memory.
// GetCredsPayload gets payload of the credential object from FrostFS network.
// It uses search by system name and select using CRDT 2PSet. In case of absence CRDT header
// it heads object by address.
//
// It returns exactly one non-nil value. It returns any error encountered which
// prevented the object payload from being read.
ReadObjectPayload(context.Context, oid.Address) ([]byte, error)
GetCredsPayload(context.Context, oid.Address) ([]byte, error)
}
var (
@ -103,7 +109,7 @@ func (c *cred) GetBox(ctx context.Context, addr oid.Address) (*accessbox.Box, er
}
func (c *cred) getAccessBox(ctx context.Context, addr oid.Address) (*accessbox.AccessBox, error) {
data, err := c.frostFS.ReadObjectPayload(ctx, addr)
data, err := c.frostFS.GetCredsPayload(ctx, addr)
if err != nil {
return nil, fmt.Errorf("read payload: %w", err)
}
@ -118,6 +124,15 @@ func (c *cred) getAccessBox(ctx context.Context, addr oid.Address) (*accessbox.A
}
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) Update(ctx context.Context, addr oid.Address, issuer user.ID, box *accessbox.AccessBox, expiration uint64, keys ...*keys.PublicKey) (oid.Address, error) {
objID := addr.Object()
return c.createObject(ctx, addr.Container(), &objID, issuer, box, expiration, keys...)
}
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 {
return oid.Address{}, ErrEmptyPublicKeys
} else if box == nil {
@ -130,9 +145,10 @@ func (c *cred) Put(ctx context.Context, idCnr cid.ID, issuer user.ID, box *acces
idObj, err := c.frostFS.CreateObject(ctx, PrmObjectCreate{
Creator: issuer,
Container: idCnr,
Container: cnrID,
Filepath: strconv.FormatInt(time.Now().Unix(), 10) + "_access.box",
ExpirationEpoch: expiration,
NewVersionFor: newVersionFor,
Payload: data,
})
if err != nil {
@ -141,7 +157,7 @@ func (c *cred) Put(ctx context.Context, idCnr cid.ID, issuer user.ID, box *acces
var addr oid.Address
addr.SetObject(idObj)
addr.SetContainer(idCnr)
addr.SetContainer(cnrID)
return addr, nil
}

View file

@ -0,0 +1,157 @@
package frostfs
import (
"bytes"
"context"
"fmt"
"io"
"strconv"
"time"
objectv2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/crdt"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
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/pool"
)
const (
accessBoxCRDTNameAttr = "S3-Access-Box-CRDT-Name"
)
// AuthmateFrostFS is a mediator which implements authmate.FrostFS through pool.Pool.
type AuthmateFrostFS struct {
frostFS *FrostFS
}
// NewAuthmateFrostFS creates new AuthmateFrostFS using provided pool.Pool.
func NewAuthmateFrostFS(p *pool.Pool) *AuthmateFrostFS {
return &AuthmateFrostFS{frostFS: NewFrostFS(p)}
}
// ContainerExists implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) ContainerExists(ctx context.Context, idCnr cid.ID) error {
_, err := x.frostFS.Container(ctx, idCnr)
if err != nil {
return fmt.Errorf("get container via connection pool: %w", err)
}
return nil
}
// TimeToEpoch implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) TimeToEpoch(ctx context.Context, futureTime time.Time) (uint64, uint64, error) {
return x.frostFS.TimeToEpoch(ctx, time.Now(), futureTime)
}
// CreateContainer implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) CreateContainer(ctx context.Context, prm authmate.PrmContainerCreate) (cid.ID, error) {
basicACL := acl.Private
// allow reading objects to OTHERS in order to provide read access to S3 gateways
basicACL.AllowOp(acl.OpObjectGet, acl.RoleOthers)
basicACL.AllowOp(acl.OpObjectHead, acl.RoleOthers)
basicACL.AllowOp(acl.OpObjectSearch, acl.RoleOthers)
return x.frostFS.CreateContainer(ctx, layer.PrmContainerCreate{
Creator: prm.Owner,
Policy: prm.Policy,
Name: prm.FriendlyName,
BasicACL: basicACL,
})
}
// GetCredsPayload implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) GetCredsPayload(ctx context.Context, addr oid.Address) ([]byte, error) {
versions, err := x.getCredVersions(ctx, addr)
if err != nil {
return nil, err
}
credObjID := addr.Object()
if last := versions.GetLast(); last != nil {
credObjID = last.OjbID
}
res, err := x.frostFS.ReadObject(ctx, layer.PrmObjectRead{
Container: addr.Container(),
Object: credObjID,
WithPayload: true,
})
if err != nil {
return nil, err
}
defer res.Payload.Close()
return io.ReadAll(res.Payload)
}
// CreateObject implements authmate.FrostFS interface method.
func (x *AuthmateFrostFS) CreateObject(ctx context.Context, prm tokens.PrmObjectCreate) (oid.ID, error) {
attributes := [][2]string{{objectv2.SysAttributeExpEpoch, strconv.FormatUint(prm.ExpirationEpoch, 10)}}
if prm.NewVersionFor != nil {
var addr oid.Address
addr.SetContainer(prm.Container)
addr.SetObject(*prm.NewVersionFor)
versions, err := x.getCredVersions(ctx, addr)
if err != nil {
return oid.ID{}, err
}
if versions.GetLast() == nil {
versions.AppendVersion(&crdt.ObjectVersion{OjbID: addr.Object()})
}
for key, val := range versions.GetCRDTHeaders() {
attributes = append(attributes, [2]string{key, val})
}
attributes = append(attributes, [2]string{accessBoxCRDTNameAttr, versions.Name()})
}
return x.frostFS.CreateObject(ctx, layer.PrmObjectCreate{
Creator: prm.Creator,
Container: prm.Container,
Filepath: prm.Filepath,
Attributes: attributes,
Payload: bytes.NewReader(prm.Payload),
})
}
func (x *AuthmateFrostFS) getCredVersions(ctx context.Context, addr oid.Address) (*crdt.ObjectVersions, error) {
objCredSystemName := credVersionSysName(addr.Container(), addr.Object())
credVersions, err := x.frostFS.SearchObjects(ctx, layer.PrmObjectSearch{
Container: addr.Container(),
ExactAttribute: [2]string{accessBoxCRDTNameAttr, objCredSystemName},
})
if err != nil {
return nil, fmt.Errorf("search s3 access boxes: %w", err)
}
versions := crdt.NewObjectVersions(objCredSystemName)
for _, id := range credVersions {
objVersion, err := x.frostFS.ReadObject(ctx, layer.PrmObjectRead{
Container: addr.Container(),
Object: id,
WithHeader: true,
})
if err != nil {
return nil, fmt.Errorf("head crdt access box '%s': %w", id.EncodeToString(), err)
}
versions.AppendVersion(crdt.NewObjectVersion(objVersion.Head))
}
return versions, nil
}
func credVersionSysName(cnrID cid.ID, objID oid.ID) string {
return cnrID.EncodeToString() + "0" + objID.EncodeToString()
}