frostfs-mfa/mfa/mfa.go

411 lines
11 KiB
Go
Raw Permalink Normal View History

package mfa
import (
"bytes"
"context"
"errors"
"fmt"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
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/pquerna/otp"
"go.uber.org/zap"
)
type (
// Manager provides interface to manage MFA devices in FrostFS container.
// It should be provided with Storage interface to manage FrostFS objects
// and KeyStore interface to encode and decode OTP keys inside FrostFS
// objects.
Manager struct {
storage Storage
unlocker KeyStore
container cid.ID
logger *zap.Logger
}
// KeyStore is an interface for Manager to provide keys to encode and decode
// OTP keys of MFA devices.
KeyStore interface {
// PrivateKey returns private key of this Manager.
PrivateKey() *keys.PrivateKey
// PublicKeys returns list of public keys for all managers, including
// this Manager.
PublicKeys() []*keys.PublicKey
}
// Config contains parameters for Manager constructor.
Config struct {
Storage Storage
Unlocker KeyStore
Container cid.ID
Logger *zap.Logger
}
)
// NewManager creates new instance of Manager.
func NewManager(cfg Config) (*Manager, error) {
if cfg.Storage == nil {
return nil, errors.New("mfa storage is nil")
}
if cfg.Logger == nil {
return nil, errors.New("mfa logger is nil")
}
if cfg.Unlocker == nil {
return nil, errors.New("mfa key store is nil")
}
return &Manager{
storage: cfg.Storage,
container: cfg.Container,
unlocker: cfg.Unlocker,
logger: cfg.Logger,
}, nil
}
// CreateMFADevice creates new FrostFS object with encoded MFA device inside and stores it in MFA container.
func (m *Manager) CreateMFADevice(ctx context.Context, device SecretDevice) error {
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.CreateMFADevice")
defer span.End()
filePath := getTreePath(device.Namespace, device.Name)
if _, err := m.storage.GetTreeNode(ctx, m.container, filePath); err == nil {
return ErrMFAEntityAlreadyExists // return well known error here
} else if !errors.Is(err, ErrTreeNodeNotFound) {
return fmt.Errorf("get mfa node '%s' to check: %w", filePath, err)
}
return m.putMFADevice(ctx, device)
}
// GetMFADevice returns decoded MFA device from MFA container.
func (m *Manager) GetMFADevice(ctx context.Context, ns, mfaName string) (*SecretDevice, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.GetMFADevice")
defer span.End()
node, err := m.storage.GetTreeNode(ctx, m.container, getTreePath(ns, mfaName))
if err != nil {
return nil, fmt.Errorf("get mfa nodes: %w", err)
}
var objID oid.ID
if err = objID.DecodeString(node.Current.Meta[OIDKey]); err != nil {
return nil, fmt.Errorf("decode oid '%s': %w", node.Current.Meta[OIDKey], err)
}
var addr oid.Address
addr.SetContainer(m.container)
addr.SetObject(objID)
boxData, err := m.storage.GetObject(ctx, addr)
if err != nil {
return nil, fmt.Errorf("get object '%s': %w", addr.EncodeToString(), err)
}
mfaBox := new(MFABox)
if err = mfaBox.Unmarshal(boxData); err != nil {
return nil, fmt.Errorf("unmarshal box data: %w", err)
}
secrets, err := UnpackMFABox(mfaBox, m.unlocker.PrivateKey())
if err != nil {
return nil, fmt.Errorf("unpack mfa box: %w", err)
}
key, err := otp.NewKeyFromURL(secrets.URL())
if err != nil {
return nil, err
}
dev, err := newDevice(&node.Current)
if err != nil {
return nil, err
}
return &SecretDevice{
Device: *dev,
Key: key,
}, nil
}
// GetTinyMFADevice returns MFA device metadata without OTP key from the tree of MFA container.
func (m *Manager) GetTinyMFADevice(ctx context.Context, ns, mfaName string) (*Device, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.GetTinyMFADevice")
defer span.End()
node, err := m.storage.GetTreeNode(ctx, m.container, getTreePath(ns, mfaName))
if err != nil {
return nil, fmt.Errorf("get mfa nodes: %w", err)
}
dev, err := newDevice(&node.Current)
if err != nil {
return nil, err
}
return dev, nil
}
// ListMFADevices lists all available MFA device metadata with specified device namespace from the tree of MFA container.
func (m *Manager) ListMFADevices(ctx context.Context, ns string) ([]*Device, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.ListMFADevices")
defer span.End()
list, err := m.storage.GetTreeNodes(ctx, m.container, ns)
if err != nil {
return nil, err
}
return m.formDevices(list)
}
// ListAllMFADevices lists all available MFA device metadata from the tree of MFA container.
func (m *Manager) ListAllMFADevices(ctx context.Context) ([]*Device, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.ListAllMFADevices")
defer span.End()
list, err := m.storage.GetTreeNodes(ctx, m.container, "")
if err != nil {
return nil, err
}
return m.formDevices(list)
}
// UpdateMFADevice updates MFA device metadata in the tree of MFA container.
func (m *Manager) UpdateMFADevice(ctx context.Context, device *Device) error {
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.UpdateMFADevice")
defer span.End()
node, err := m.storage.SetTreeNode(ctx, m.container, getTreePath(device.Namespace, device.Name), device.Meta)
if err != nil {
return fmt.Errorf("set mfa tree node : %w", err)
}
m.deleteObjects(ctx, node.Old)
return nil
}
// DeleteMFADevice removes FrostFS object and metadata from the tree of MFA container related to device.
func (m *Manager) DeleteMFADevice(ctx context.Context, device *Device) error {
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.DeleteMFADevice")
defer span.End()
meta := device.Meta
var objID oid.ID
if err := objID.DecodeString(meta[OIDKey]); err != nil {
return fmt.Errorf("decode oid '%s': %w", meta[OIDKey], err)
}
address := oid.Address{}
address.SetObject(objID)
address.SetContainer(m.container)
if err := m.storage.DeleteObject(ctx, address); err != nil {
return fmt.Errorf("failed to delete from storage: %w", err)
}
old, err := m.storage.DeleteTreeNode(ctx, m.container, getTreePath(device.Namespace, device.Name))
if err != nil {
m.deleteObjects(ctx, old)
return fmt.Errorf("failed to delete from tree: %w", err)
}
return nil
}
// RecipherDevice creates new FrostFS object with encoded OTP key for new set
// of public keys from KeyManager. If existing FrostFS object already contains
// all public keys from KeyManager, then does nothing.
func (m *Manager) RecipherDevice(ctx context.Context, dev *Device) error {
ctx, span := tracing.StartSpanFromContext(ctx, "mfa.RecipherDevice")
defer span.End()
node, err := m.storage.GetTreeNode(ctx, m.container, getTreePath(dev.Namespace, dev.Name))
if err != nil {
return fmt.Errorf("get mfa nodes: %w", err)
}
if dev.OID.EncodeToString() != node.Current.Meta[OIDKey] {
// This can happen in the following cases:
// * device was reciphered by other Manager
// * device was removed and newly created with the same name
// in both cases it must be already encrypted for new list of keys
return nil
}
var addr oid.Address
addr.SetContainer(m.container)
addr.SetObject(dev.OID)
boxData, err := m.storage.GetObject(ctx, addr)
if err != nil {
return fmt.Errorf("get object '%s': %w", addr.EncodeToString(), err)
}
mfaBox := new(MFABox)
if err = mfaBox.Unmarshal(boxData); err != nil {
return fmt.Errorf("unmarshal box data: %w", err)
}
newKeys := m.unlocker.PublicKeys()
oldKeys := mfaBox.GetUnlockers()
if equalKeys(newKeys, oldKeys) {
m.logger.Info("mfabox has already been reciphered",
zap.String("device", dev.String()),
zap.String("OID", dev.OID.EncodeToString()))
return nil
}
secrets, err := UnpackMFABox(mfaBox, m.unlocker.PrivateKey())
if err != nil {
return fmt.Errorf("unpack mfa box: %w", err)
}
key, err := otp.NewKeyFromURL(secrets.URL())
if err != nil {
return err
}
return m.putMFADevice(ctx, SecretDevice{
Device: *dev,
Key: key,
})
}
func (m *Manager) putMFADevice(ctx context.Context, device SecretDevice) error {
filePath := "/" + device.Namespace + "/" + device.Name
box, err := PackMFABox(device.Key, m.unlocker.PublicKeys())
if err != nil {
return fmt.Errorf("pack mfa box: %w", err)
}
boxData, err := box.Marshal()
if err != nil {
return fmt.Errorf("marshal mfa box: %w", err)
}
var owner user.ID
user.IDFromKey(&owner, m.unlocker.PrivateKey().PrivateKey.PublicKey)
objID, err := m.storage.CreateObject(ctx, PrmObjectCreate{
Container: m.container,
Owner: owner,
FilePath: filePath,
Payload: boxData,
})
if err != nil {
return fmt.Errorf("put mfa box object: %w", err)
}
meta := map[string]string{
FilePathKey: filePath,
OIDKey: objID.EncodeToString(),
}
for k, v := range device.Meta {
if k != FilePathKey && k != OIDKey {
meta[k] = v
}
}
node, err := m.storage.SetTreeNode(ctx, m.container, filePath, meta)
if err != nil {
return fmt.Errorf("set mfa tree node : %w", err)
}
m.deleteObjects(ctx, node.Old)
m.deleteObject(ctx, device.OID)
return nil
}
func (m *Manager) formDevices(list []*TreeNode) ([]*Device, error) {
res := make([]*Device, 0, len(list))
for _, item := range list {
dev, err := newDevice(item)
if err != nil {
m.logger.Warn("invalid mfa device tree node", zap.Error(err))
continue
}
res = append(res, dev)
}
return res, nil
}
func (m *Manager) deleteObjects(ctx context.Context, nodes []*TreeNode) {
var addr oid.Address
addr.SetContainer(m.container)
for _, node := range nodes {
var objID oid.ID
if err := objID.DecodeString(node.Meta[OIDKey]); err != nil {
m.logger.Warn("failed to decode old multinode oid", zap.String("oid", node.Meta[OIDKey]), zap.Error(err))
}
addr.SetObject(objID)
if err := m.storage.DeleteObject(ctx, addr); err != nil {
m.logger.Warn("failed to delete old multinode object", zap.String("address", addr.EncodeToString()), zap.Error(err))
}
}
}
func (m *Manager) deleteObject(ctx context.Context, objID oid.ID) {
if objID.Equals(oid.ID{}) {
return
}
var addr oid.Address
addr.SetContainer(m.container)
addr.SetObject(objID)
if err := m.storage.DeleteObject(ctx, addr); err != nil {
m.logger.Warn("failed to delete object", zap.String("address", addr.EncodeToString()), zap.Error(err))
}
}
func newDevice(node *TreeNode) (*Device, error) {
meta := node.Meta
filepathArr := strings.Split(meta[FilePathKey], "/")
if len(filepathArr) != 2 {
return nil, fmt.Errorf("invalid device filepath: '%s'", meta[FilePathKey])
}
var objID oid.ID
if err := objID.DecodeString(meta[OIDKey]); err != nil {
return nil, fmt.Errorf("decode oid '%s': %w", meta[OIDKey], err)
}
return &Device{
Namespace: filepathArr[0],
Name: filepathArr[1],
OID: objID,
Meta: meta,
}, nil
}
// equalKeys returns true if 'got' contains all public keys from 'expected' slice.
func equalKeys(expected []*keys.PublicKey, got []*Unlocker) bool {
loop:
for _, newKey := range expected {
for _, svcKey := range got {
if bytes.Equal(newKey.Bytes(), svcKey.GetPublicKey()) {
continue loop
}
}
return false
}
return true
}
func getTreePath(ns, mfaName string) string {
return ns + "/" + mfaName
}