diff --git a/api/auth/presign_test.go b/api/auth/presign_test.go index 898a6e9..8c4aa21 100644 --- a/api/auth/presign_test.go +++ b/api/auth/presign_test.go @@ -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") diff --git a/authmate/authmate.go b/authmate/authmate.go index 451b027..0faed2c 100644 --- a/authmate/authmate.go +++ b/authmate/authmate.go @@ -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" } diff --git a/cmd/s3-authmate/main.go b/cmd/s3-authmate/main.go index 26a0599..fded16e 100644 --- a/cmd/s3-authmate/main.go +++ b/cmd/s3-authmate/main.go @@ -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 diff --git a/creds/accessbox/accessbox.go b/creds/accessbox/accessbox.go index 19bf69f..a98025e 100644 --- a/creds/accessbox/accessbox.go +++ b/creds/accessbox/accessbox.go @@ -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 { diff --git a/creds/accessbox/bearer_token_test.go b/creds/accessbox/bearer_token_test.go index bf95fd5..57739db 100644 --- a/creds/accessbox/bearer_token_test.go +++ b/creds/accessbox/bearer_token_test.go @@ -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) diff --git a/creds/tokens/credentials.go b/creds/tokens/credentials.go index a9e0bef..e850e34 100644 --- a/creds/tokens/credentials.go +++ b/creds/tokens/credentials.go @@ -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 } diff --git a/internal/frostfs/authmate.go b/internal/frostfs/authmate.go new file mode 100644 index 0000000..7d497af --- /dev/null +++ b/internal/frostfs/authmate.go @@ -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() +}