[#604] Add support of MFADelete argument and x-amz-mfa header

Signed-off-by: Pavel Pogodaev <p.pogodaev@yadro.com>
This commit is contained in:
Pavel Pogodaev 2025-01-21 15:08:34 +03:00
parent 0064e7ab07
commit 552bd7b932
29 changed files with 1212 additions and 36 deletions

View file

@ -173,7 +173,10 @@ func TestSettingsCacheType(t *testing.T) {
cache := NewSystemCache(DefaultSystemConfig(logger))
key := "key"
settings := &data.BucketSettings{Versioning: data.VersioningEnabled}
settings := &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningEnabled,
MFADeleteStatus: data.MFADeleteEnabled,
}}
err := cache.PutSettings(key, settings)
require.NoError(t, err)

View file

@ -20,6 +20,9 @@ const (
VersioningUnversioned = "Unversioned"
VersioningEnabled = "Enabled"
VersioningSuspended = "Suspended"
MFADeleteDisabled = "Disabled"
MFADeleteEnabled = "Enabled"
)
type (
@ -55,12 +58,19 @@ type (
// BucketSettings stores settings such as versioning.
BucketSettings struct {
Versioning string
Versioning Versioning
LockConfiguration *ObjectLockConfiguration
CannedACL string
OwnerKey *keys.PublicKey
}
// Versioning stores bucket versioning settings.
Versioning struct {
VersioningStatus string
MFADeleteStatus string
MFASerialNumber string
}
// CORSConfiguration stores CORS configuration of a request.
CORSConfiguration struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CORSConfiguration" json:"-"`
@ -130,15 +140,19 @@ func (o *ObjectInfo) ETag(md5Enabled bool) string {
}
func (b BucketSettings) Unversioned() bool {
return b.Versioning == VersioningUnversioned
return b.Versioning.VersioningStatus == VersioningUnversioned
}
func (b BucketSettings) VersioningEnabled() bool {
return b.Versioning == VersioningEnabled
return b.Versioning.VersioningStatus == VersioningEnabled
}
func (b BucketSettings) VersioningSuspended() bool {
return b.Versioning == VersioningSuspended
return b.Versioning.VersioningStatus == VersioningSuspended
}
func (b BucketSettings) MFADeleteEnabled() bool {
return b.Versioning.MFADeleteStatus == MFADeleteEnabled
}
func Quote(val string) string {

View file

@ -143,6 +143,7 @@ const (
ErrInvalidTagDirective
// Add new error codes here.
ErrNotSupported
ErrMFAAuthNeeded
// SSE-S3 related API errors.
ErrInvalidEncryptionMethod
@ -287,7 +288,7 @@ const (
ErrPostPolicyConditionInvalidFormat
//CORS configuration errors.
// CORS configuration errors.
ErrCORSUnsupportedMethod
ErrCORSWildcardExposeHeaders
@ -388,6 +389,12 @@ var errorCodes = errorCodeMap{
Description: "Access Denied.",
HTTPStatusCode: http.StatusForbidden,
},
ErrMFAAuthNeeded: {
ErrCode: ErrMFAAuthNeeded,
Code: "AccessDenied",
Description: "Mfa Authentication must be used for this request",
HTTPStatusCode: http.StatusForbidden,
},
ErrAccessControlListNotSupported: {
ErrCode: ErrAccessControlListNotSupported,
Code: "AccessControlListNotSupported",

View file

@ -11,6 +11,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/mfa"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
@ -24,6 +25,7 @@ type (
cfg Config
ape APE
frostfsid FrostFSID
mfa *mfa.MFA
}
// Config contains data which handler needs to keep.
@ -68,7 +70,7 @@ const (
var _ api.Handler = (*handler)(nil)
// New creates new api.Handler using given logger and client.
func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid FrostFSID) (api.Handler, error) {
func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid FrostFSID, mfaCli *mfa.MFA) (api.Handler, error) {
switch {
case obj == nil:
return nil, errors.New("empty FrostFS Object Layer")
@ -86,6 +88,7 @@ func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid Frost
cfg: cfg,
ape: storage,
frostfsid: ffsid,
mfa: mfaCli,
}, nil
}

View file

@ -14,6 +14,7 @@ import (
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"github.com/pquerna/otp/totp"
)
// limitation of AWS https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html
@ -81,6 +82,26 @@ func (h *handler) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
return
}
if len(versionID) > 0 && bktSettings.MFADeleteEnabled() {
serialNumber, token, err := h.getMFAHeader(r)
if err != nil {
h.logAndSendError(ctx, w, "could not get mfa header", reqInfo, err)
return
}
device, err := h.mfa.GetMFADevice(ctx, reqInfo.Namespace, nameFromArn(serialNumber))
if err != nil {
h.logAndSendError(ctx, w, "could not get mfa device", reqInfo, err)
return
}
validate := totp.Validate(token, device.Key.Secret())
if !validate {
h.logAndSendError(ctx, w, "could not validate token", reqInfo, fmt.Errorf("mfa Authentication must be used for this request"))
return
}
}
networkInfo, err := h.obj.GetNetworkInfo(ctx)
if err != nil {
h.logAndSendError(ctx, w, "could not get network info", reqInfo, err)
@ -188,6 +209,26 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
return
}
if haveVersionedObjects(requested.Objects) && bktSettings.MFADeleteEnabled() {
serialNumber, token, err := h.getMFAHeader(r)
if err != nil {
h.logAndSendError(ctx, w, "could not get mfa header", reqInfo, err)
return
}
device, err := h.mfa.GetMFADevice(ctx, reqInfo.Namespace, nameFromArn(serialNumber))
if err != nil {
h.logAndSendError(ctx, w, "could not get mfa device", reqInfo, err)
return
}
validate := totp.Validate(token, device.Key.Secret())
if !validate {
h.logAndSendError(ctx, w, "could not validate token", reqInfo, fmt.Errorf("mfa Authentication must be used for this request"))
return
}
}
networkInfo, err := h.obj.GetNetworkInfo(ctx)
if err != nil {
h.logAndSendError(ctx, w, "could not get network info", reqInfo, err)
@ -287,3 +328,12 @@ func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
func haveVersionedObjects(objects []ObjectIdentifier) bool {
for _, o := range objects {
if len(o.VersionID) > 0 {
return true
}
}
return false
}

View file

@ -437,7 +437,10 @@ func createTestBucketWithLock(hc *handlerContext, bktName string, conf *data.Obj
sp := &layer.PutSettingsParams{
BktInfo: bktInfo,
Settings: &data.BucketSettings{
Versioning: data.VersioningEnabled,
Versioning: data.Versioning{
VersioningStatus: data.VersioningEnabled,
MFADeleteStatus: data.MFADeleteDisabled,
},
LockConfiguration: conf,
OwnerKey: key.PublicKey(),
},

View file

@ -826,12 +826,15 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
Settings: &data.BucketSettings{
CannedACL: cannedACL,
OwnerKey: key,
Versioning: data.VersioningUnversioned,
Versioning: data.Versioning{
VersioningStatus: data.VersioningUnversioned,
MFADeleteStatus: data.MFADeleteDisabled,
},
},
}
if p.ObjectLockEnabled {
sp.Settings.Versioning = data.VersioningEnabled
sp.Settings.Versioning.VersioningStatus = data.VersioningEnabled
}
err = retryer.MakeWithRetry(ctx, func() error {

View file

@ -137,3 +137,17 @@ func parseRange(s string) (*layer.RangeParams, error) {
End: values[1],
}, nil
}
func nameFromArn(arn string) string {
pts := strings.Split(arn, "/")
return pts[len(pts)-1]
}
func (h *handler) getMFAHeader(r *http.Request) (string, string, error) {
parts := strings.Split(r.Header.Get(api.AmzMFA), " ")
if len(parts) != 2 {
return "", "", fmt.Errorf("%w: invalid mfa header", apierr.GetAPIError(apierr.ErrMFAAuthNeeded))
}
return parts[0], parts[1], nil
}

View file

@ -7,6 +7,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"github.com/pquerna/otp/totp"
)
func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
@ -31,14 +32,47 @@ func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
return
}
// settings pointer is stored in the cache, so modify a copy of the settings
newSettings := *settings
if len(configuration.MfaDelete) > 0 {
serialNumber, token, err := h.getMFAHeader(r)
if err != nil {
h.logAndSendError(ctx, w, "invalid x-amz-mfa header", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
return
}
name := nameFromArn(serialNumber)
device, err := h.mfa.GetMFADevice(ctx, reqInfo.Namespace, name)
if err != nil {
h.logAndSendError(ctx, w, "get mfa device", reqInfo, err)
return
}
ok := totp.Validate(token, device.Key.Secret())
if !ok {
h.logAndSendError(ctx, w, "validation error", reqInfo, nil)
return
}
switch configuration.MfaDelete {
case data.MFADeleteEnabled:
newSettings.Versioning.MFADeleteStatus = data.MFADeleteEnabled
newSettings.Versioning.MFASerialNumber = serialNumber
case data.MFADeleteDisabled:
newSettings.Versioning.MFADeleteStatus = data.MFADeleteDisabled
default:
h.logAndSendError(ctx, w, "failed to get mfa configuration", reqInfo, nil)
return
}
}
if configuration.Status != data.VersioningEnabled && configuration.Status != data.VersioningSuspended {
h.logAndSendError(ctx, w, "invalid versioning configuration", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
return
}
// settings pointer is stored in the cache, so modify a copy of the settings
newSettings := *settings
newSettings.Versioning = configuration.Status
newSettings.Versioning.VersioningStatus = configuration.Status
p := &layer.PutSettingsParams{
BktInfo: bktInfo,
@ -80,7 +114,7 @@ func (h *handler) GetBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
func formVersioningConfiguration(settings *data.BucketSettings) *VersioningConfiguration {
res := &VersioningConfiguration{}
if !settings.Unversioned() {
res.Status = settings.Versioning
res.Status = settings.Versioning.VersioningStatus
}
return res

View file

@ -15,6 +15,7 @@ const (
AmzCopySource = "X-Amz-Copy-Source"
AmzCopySourceRange = "X-Amz-Copy-Source-Range"
AmzDate = "X-Amz-Date"
AmzMFA = "X-Amz-Mfa"
LastModified = "Last-Modified"
Date = "Date"

View file

@ -13,7 +13,10 @@ func TestObjectLockAttributes(t *testing.T) {
tc := prepareContext(t)
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo,
Settings: &data.BucketSettings{Versioning: data.VersioningEnabled},
Settings: &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningEnabled,
MFADeleteStatus: data.MFADeleteDisabled,
}},
})
require.NoError(t, err)

View file

@ -216,7 +216,10 @@ func (n *Layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo)
if !errors.Is(err, tree.ErrNodeNotFound) {
return nil, err
}
settings = &data.BucketSettings{Versioning: data.VersioningUnversioned}
settings = &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningUnversioned,
MFADeleteStatus: data.MFADeleteDisabled,
}}
n.reqLogger(ctx).Debug(logs.BucketSettingsNotFoundUseDefaults)
}

View file

@ -197,7 +197,10 @@ func TestSimpleVersioning(t *testing.T) {
tc := prepareContext(t)
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo,
Settings: &data.BucketSettings{Versioning: data.VersioningEnabled},
Settings: &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningEnabled,
MFADeleteStatus: data.MFADeleteDisabled,
}},
})
require.NoError(t, err)
@ -234,7 +237,10 @@ func TestSimpleNoVersioning(t *testing.T) {
func TestVersioningDeleteObject(t *testing.T) {
tc := prepareContext(t)
settings := &data.BucketSettings{Versioning: data.VersioningEnabled}
settings := &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningEnabled,
MFADeleteStatus: data.MFADeleteDisabled,
}}
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo,
Settings: settings,
@ -256,7 +262,10 @@ func TestGetUnversioned(t *testing.T) {
objContent := []byte("content obj1 v1")
objInfo := tc.putObject(objContent)
settings := &data.BucketSettings{Versioning: data.VersioningUnversioned}
settings := &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningUnversioned,
MFADeleteStatus: data.MFADeleteDisabled,
}}
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo,
Settings: settings,
@ -270,7 +279,10 @@ func TestGetUnversioned(t *testing.T) {
func TestVersioningDeleteSpecificObjectVersion(t *testing.T) {
tc := prepareContext(t)
settings := &data.BucketSettings{Versioning: data.VersioningEnabled}
settings := &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningEnabled,
MFADeleteStatus: data.MFADeleteDisabled,
}}
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo,
Settings: settings,

View file

@ -36,6 +36,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy/contract"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/services"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/mfa"
internalnet "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/net"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
@ -595,7 +596,7 @@ func (s *appSettings) TombstoneLifetime() uint64 {
func (a *App) initAPI(ctx context.Context) {
a.initLayer(ctx)
a.initHandler()
a.initHandler(ctx)
}
func (a *App) initMetrics() {
@ -1134,15 +1135,51 @@ func getFrostfsIDCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config {
return cacheCfg
}
func (a *App) initHandler() {
func (a *App) initHandler(ctx context.Context) {
var err error
a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid)
var mfaCnrInfo *data.BucketInfo
if a.cfg.IsSet(cfgContainersMFA) {
mfaCnrInfo, err = a.fetchContainerInfo(ctx, cfgContainersMFA)
if err != nil {
a.log.Fatal(logs.CouldNotFetchMFAContainerInfo, zap.Error(err))
}
}
mfaConfig, err := a.fetchMFAConfig(mfaCnrInfo.CID)
if err != nil {
a.log.Fatal(logs.CouldNotInitMFAClient, zap.Error(err))
}
mfaCli, err := mfa.NewMFA(mfaConfig)
if err != nil {
a.log.Fatal(logs.CouldNotInitMFAClient, zap.Error(err))
}
a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid, mfaCli)
if err != nil {
a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err))
}
}
func (a *App) fetchMFAConfig(id cid.ID) (mfa.Config, error) {
mfaFrostFS := frostfs.NewMFAFrostFS(frostfs.MFAFrostFSConfig{
Pool: a.pool,
TreePool: a.treePool,
Key: a.key,
Logger: a.log,
})
config := mfa.Config{
Storage: mfaFrostFS,
Key: a.key,
Container: id,
Logger: a.log,
}
return config, nil
}
func (a *App) getServer(address string) Server {
for i := range a.servers {
if a.servers[i].Address() == address {

View file

@ -222,6 +222,7 @@ const ( // Settings.
cfgContainersCORS = "containers.cors"
cfgContainersLifecycle = "containers.lifecycle"
cfgContainersAccessBox = "containers.accessbox"
cfgContainersMFA = "containers.mfa"
// Multinet.
cfgMultinetEnabled = "multinet.enabled"

View file

@ -262,6 +262,7 @@ S3_GW_RETRY_STRATEGY=exponential
# Containers properties
S3_GW_CONTAINERS_CORS=AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
S3_GW_CONTAINERS_LIFECYCLE=AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
S3_GW_CONTAINERS_MFA=HV9h4zbp7Dti2VXef2oFSsBSRyJUR6NfMeswuv12fjZu
# Multinet properties
# Enable multinet support

View file

@ -310,6 +310,7 @@ retry:
containers:
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
mfa: HV9h4zbp7Dti2VXef2oFSsBSRyJUR6NfMeswuv12fjZu
# Multinet properties
multinet:

View file

@ -330,7 +330,7 @@ func file_creds_accessbox_accessbox_proto_rawDescGZIP() []byte {
}
var file_creds_accessbox_accessbox_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_creds_accessbox_accessbox_proto_goTypes = []any{
var file_creds_accessbox_accessbox_proto_goTypes = []interface{}{
(*AccessBox)(nil), // 0: accessbox.AccessBox
(*Tokens)(nil), // 1: accessbox.Tokens
(*AccessBox_Gate)(nil), // 2: accessbox.AccessBox.Gate
@ -352,7 +352,7 @@ func file_creds_accessbox_accessbox_proto_init() {
return
}
if !protoimpl.UnsafeEnabled {
file_creds_accessbox_accessbox_proto_msgTypes[0].Exporter = func(v any, i int) any {
file_creds_accessbox_accessbox_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AccessBox); i {
case 0:
return &v.state
@ -364,7 +364,7 @@ func file_creds_accessbox_accessbox_proto_init() {
return nil
}
}
file_creds_accessbox_accessbox_proto_msgTypes[1].Exporter = func(v any, i int) any {
file_creds_accessbox_accessbox_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Tokens); i {
case 0:
return &v.state
@ -376,7 +376,7 @@ func file_creds_accessbox_accessbox_proto_init() {
return nil
}
}
file_creds_accessbox_accessbox_proto_msgTypes[2].Exporter = func(v any, i int) any {
file_creds_accessbox_accessbox_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AccessBox_Gate); i {
case 0:
return &v.state
@ -388,7 +388,7 @@ func file_creds_accessbox_accessbox_proto_init() {
return nil
}
}
file_creds_accessbox_accessbox_proto_msgTypes[3].Exporter = func(v any, i int) any {
file_creds_accessbox_accessbox_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AccessBox_ContainerPolicy); i {
case 0:
return &v.state

View file

@ -822,6 +822,7 @@ containers:
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
accessbox: ExnA1gSY3kzgomi2wJxNyWo1ytWv9VAKXRE55fNXEPL2
mfa: HV9h4zbp7Dti2VXef2oFSsBSRyJUR6NfMeswuv12fjZu
```
| Parameter | Type | SIGHUP reload | Default value | Description |
@ -829,6 +830,7 @@ containers:
| `cors` | `string` | no | | Container name for CORS configurations. If not set, container of the bucket is used. |
| `lifecycle` | `string` | no | | Container name for lifecycle configurations. If not set, container of the bucket is used. |
| `accessbox` | `string` | no | | Container name to lookup accessbox if custom aws credentials is used. If not set, custom credentials are not supported. |
| `mfa` | `string` | no | | Container name for virtual MFA devices. If not set, MFADeleteBucket are not supported. |
# `vhs` section

2
go.mod
View file

@ -21,6 +21,7 @@ require (
github.com/mr-tron/base58 v1.2.0
github.com/nspcc-dev/neo-go v0.106.3
github.com/panjf2000/ants/v2 v2.5.0
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.19.0
github.com/prometheus/client_model v0.5.0
github.com/spf13/cobra v1.8.1
@ -64,6 +65,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect

5
go.sum
View file

@ -104,6 +104,8 @@ github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5M
github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -309,6 +311,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -345,6 +349,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

183
internal/frostfs/mfa.go Normal file
View file

@ -0,0 +1,183 @@
package frostfs
import (
"context"
"errors"
"fmt"
"io"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/mfa"
apitree "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/tree"
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"
treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"go.uber.org/zap"
)
// MFAFrostFS is a mediator which implements mfa.Storage through pool.Pool and treepool.Pool.
type MFAFrostFS struct {
frostFS *FrostFS
treePool *treepool.Pool
log *zap.Logger
}
type MFAFrostFSConfig struct {
Pool *pool.Pool
TreePool *treepool.Pool
Key *keys.PrivateKey
Logger *zap.Logger
}
type treeNode struct {
ID uint64
ParentID uint64
TimeStamp uint64
Meta map[string]string
}
type multiSystemNode struct {
// the first element is latest
nodes []*treeNode
}
const (
fileNameKey = "FileName"
mfaTreeName = "mfa"
)
// NewMFAFrostFS creates new MFAFrostFS using provided pool.Pool.
func NewMFAFrostFS(cfg MFAFrostFSConfig) *MFAFrostFS {
return &MFAFrostFS{
frostFS: NewFrostFS(cfg.Pool, cfg.Key),
treePool: cfg.TreePool,
log: cfg.Logger,
}
}
func (m *MFAFrostFS) GetObject(ctx context.Context, addr oid.Address) ([]byte, error) {
res, err := m.frostFS.GetObject(ctx, frostfs.PrmObjectGet{
Container: addr.Container(),
Object: addr.Object(),
})
if err != nil {
return nil, err
}
defer func() {
if closeErr := res.Payload.Close(); closeErr != nil {
// TODO add log
// middleware.GetReqLog(ctx).Warn(logs.CloseMFAObjectPayload, zap.Error(closeErr))
middleware.GetReqLog(ctx).Warn("logs.CloseMFAObjectPayload", zap.Error(closeErr))
}
}()
return io.ReadAll(res.Payload)
}
func (m *MFAFrostFS) GetTreeNode(ctx context.Context, cnrID cid.ID, name string) (*mfa.TreeMultiNode, error) {
multiNode, err := m.getTreeNode(ctx, cnrID, pathFromName(name))
if err != nil {
return nil, fmt.Errorf("couldn't get node: %w", err)
}
return multiNode.ToMFAMultiNode(), nil
}
func (m *MFAFrostFS) getTreeNode(ctx context.Context, cnrID cid.ID, path []string) (*multiSystemNode, error) {
prmGetNodes := treepool.GetNodesParams{
CID: cnrID,
TreeID: mfaTreeName,
Path: path,
PathAttribute: fileNameKey,
LatestOnly: true,
AllAttrs: true,
}
nodes, err := m.treePool.GetNodes(ctx, prmGetNodes)
if err != nil {
if errors.Is(err, treepool.ErrNodeNotFound) {
return nil, fmt.Errorf("%s: %s", "mfa.ErrTreeNodeNotFound", err.Error())
// return nil, fmt.Errorf("%w: %s", mfa.ErrTreeNodeNotFound, err.Error())
}
return nil, fmt.Errorf("get nodes: %w", err)
}
if len(nodes) == 0 {
// return nil, mfa.ErrTreeNodeNotFound
return nil, err
}
// if len(nodes) != 1 {
// // m.log.Warn("logs.FoundMultiNode", zap.Strings("path", path))
// }
return newMultiNode(nodes)
}
func newMultiNode(nodes []*apitree.GetNodeByPathResponseInfo) (*multiSystemNode, error) {
var (
err error
index int
maxTimestamp uint64
)
if len(nodes) == 0 {
return nil, errors.New("multi node must have at least one node")
}
treeNodes := make([]*treeNode, len(nodes))
for i, node := range nodes {
if treeNodes[i] = newTreeNode(node); err != nil {
return nil, fmt.Errorf("parse tree node response: %w", err)
}
if maxTimestamp < node.GetTimestamp() {
index = i
maxTimestamp = node.GetTimestamp()
}
}
treeNodes[0], treeNodes[index] = treeNodes[index], treeNodes[0]
return &multiSystemNode{
nodes: treeNodes,
}, nil
}
func (m *multiSystemNode) ToMFAMultiNode() *mfa.TreeMultiNode {
res := &mfa.TreeMultiNode{
Current: mfa.TreeNode{Meta: m.nodes[0].Meta},
Old: make([]*mfa.TreeNode, len(m.nodes[1:])),
}
for i, node := range m.nodes[1:] {
res.Old[i] = &mfa.TreeNode{Meta: node.Meta}
}
return res
}
func newTreeNode(nodeInfo *apitree.GetNodeByPathResponseInfo) *treeNode {
tNode := &treeNode{
ID: nodeInfo.GetNodeID(),
ParentID: nodeInfo.GetParentID(),
TimeStamp: nodeInfo.GetTimestamp(),
Meta: make(map[string]string, len(nodeInfo.GetMeta())),
}
for _, kv := range nodeInfo.GetMeta() {
tNode.Meta[kv.GetKey()] = string(kv.GetValue())
}
return tNode
}
// pathFromName splits name by '/'.
func pathFromName(name string) []string {
return strings.Split(name, "/")
}

View file

@ -164,6 +164,8 @@ const (
CouldntDeleteLifecycleObject = "couldn't delete lifecycle configuration object"
CouldntCacheLifecycleConfiguration = "couldn't cache lifecycle configuration"
CouldNotFetchLifecycleContainerInfo = "couldn't fetch lifecycle container info"
CouldNotFetchMFAContainerInfo = "couldn't fetch mfa container info"
CouldNotInitMFAClient = "couldn't init MFA client"
BucketLifecycleNodeHasMultipleIDs = "bucket lifecycle node has multiple ids"
GetBucketLifecycle = "get bucket lifecycle"
WarnDuplicateNamespaceVHS = "duplicate namespace with enabled VHS, config value skipped"

161
internal/mfa/mfa.go Normal file
View file

@ -0,0 +1,161 @@
package mfa
import (
"context"
"errors"
"fmt"
"strings"
"github.com/pquerna/otp"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"go.uber.org/zap"
)
const (
FilePathKey = "FilePath"
OIDKey = "OID"
EnableDateKey = "EnableDate"
EnabledKey = "EnabledKey"
UserIDKey = "UserIDKey"
TagPrefix = "tag-"
)
type (
Storage interface {
GetObject(context.Context, oid.Address) ([]byte, error)
GetTreeNode(ctx context.Context, cnrID cid.ID, name string) (*TreeMultiNode, error)
}
TreeNode struct {
Meta map[string]string
}
TreeMultiNode struct {
Current TreeNode
Old []*TreeNode
}
)
type Device struct {
Namespace string
Name string
OID oid.ID
Meta map[string]string
}
type SecretDevice struct {
Device
Key *otp.Key
}
type MFA struct {
storage Storage
iamKey *keys.PrivateKey
container cid.ID
logger *zap.Logger
// settings Settings
}
type Config struct {
Storage Storage
Key *keys.PrivateKey
Container cid.ID
Logger *zap.Logger
// Settings Settings
}
func NewMFA(cfg Config) (*MFA, 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.Key == nil {
return nil, errors.New("mfa iam key is nil")
}
return &MFA{
storage: cfg.Storage,
container: cfg.Container,
iamKey: cfg.Key,
logger: cfg.Logger,
}, nil
}
type Settings interface {
ServicePubKeys() []*keys.PublicKey
}
func (m *MFA) GetMFADevice(ctx context.Context, ns, mfaName string) (*SecretDevice, error) {
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(m.iamKey, mfaBox)
if err != nil {
return nil, fmt.Errorf("unpack mfa box: %w", err)
}
key, err := otp.NewKeyFromURL(secrets.GetMFAURL())
if err != nil {
return nil, err
}
dev, err := newDevice(&node.Current)
if err != nil {
return nil, err
}
return &SecretDevice{
Device: *dev,
Key: key,
}, nil
}
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
}
func getTreePath(ns, mfaName string) string {
return ns + "/" + mfaName
}

247
internal/mfa/mfabox.go Normal file
View file

@ -0,0 +1,247 @@
package mfa
import (
"bytes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/pquerna/otp"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
"google.golang.org/protobuf/proto"
)
const (
secretLength = 32
saltLength = 16
)
// Marshal returns the wire-format of MFABox.
func (x *MFABox) Marshal() ([]byte, error) {
return proto.Marshal(x)
}
// Unmarshal parses the wire-format message and put data to x.
func (x *MFABox) Unmarshal(data []byte) error {
return proto.Unmarshal(data, x)
}
func PackMFABox(key *otp.Key, iamSvcKeys []*keys.PublicKey, useSalt bool) (*MFABox, error) {
if len(iamSvcKeys) == 0 {
return nil, errors.New("list of public keys to encrypt box must not be empty")
}
symmetricKey, err := generateRandomBytes(secretLength)
if err != nil {
return nil, fmt.Errorf("generate symmetric key: %w", err)
}
data, err := proto.Marshal(&Secrets{MFAURL: key.URL()})
if err != nil {
return nil, fmt.Errorf("marshal secrets: %w", err)
}
var salt []byte
if useSalt {
salt, err = generateRandomBytes(saltLength)
if err != nil {
return nil, fmt.Errorf("generate salt for mfa secrets: %w", err)
}
}
encryptedSecrets, err := encryptSymmetric(symmetricKey, data, salt)
if err != nil {
return nil, fmt.Errorf("failed to encrypt mfa secrets: %w", err)
}
boxKey, err := keys.NewPrivateKey()
if err != nil {
return nil, fmt.Errorf("create ephemeral key: %w", err)
}
iamSvcEncrypted, err := encryptedSymmetricKeyForEachIAMSvc(boxKey, iamSvcKeys, symmetricKey, useSalt)
if err != nil {
return nil, fmt.Errorf("failed to add tokens to mfabox: %w", err)
}
return &MFABox{
IAMServices: iamSvcEncrypted,
SeedPublicKey: boxKey.PublicKey().Bytes(),
EncryptedSecrets: encryptedSecrets,
SecretsSalt: salt,
}, err
}
func UnpackMFABox(iamPrivateKey *keys.PrivateKey, box *MFABox) (*Secrets, error) {
iamPublicKeyBytes := iamPrivateKey.PublicKey().Bytes()
boxPublicKey, err := keys.NewPublicKeyFromBytes(box.SeedPublicKey, elliptic.P256())
if err != nil {
return nil, err
}
for _, iamSvc := range box.IAMServices {
if !bytes.Equal(iamPublicKeyBytes, iamSvc.GetSvcPublicKey()) {
continue
}
symmetricKey, err := decryptECDH(iamPrivateKey, boxPublicKey, iamSvc.GetEncryptedSymmetricKey(), iamSvc.SymmetricKeySalt)
if err != nil {
return nil, fmt.Errorf("decrypt symmetric key: %w", err)
}
secretsData, err := decryptSymmetric(symmetricKey, box.GetEncryptedSecrets(), box.SecretsSalt)
if err != nil {
return nil, fmt.Errorf("decrypt secrets: %w", err)
}
secrets := new(Secrets)
if err = proto.Unmarshal(secretsData, secrets); err != nil {
return nil, fmt.Errorf("unmarshal secrets: %w", err)
}
return secrets, nil
}
return nil, fmt.Errorf("no box data for key '%x' was found", iamPublicKeyBytes)
}
func encryptedSymmetricKeyForEachIAMSvc(boxKey *keys.PrivateKey, iamSvcKeys []*keys.PublicKey, symmetricKey []byte, useSalt bool) ([]*MFABox_IAMService, error) {
var err error
res := make([]*MFABox_IAMService, len(iamSvcKeys))
for i, iamKey := range iamSvcKeys {
var salt []byte
if useSalt {
salt, err = generateRandomBytes(saltLength)
if err != nil {
return nil, fmt.Errorf("generate salt for symmetric key: %w", err)
}
}
res[i], err = encryptSymmetricKeyForIAMSvc(boxKey, iamKey, symmetricKey, salt)
if err != nil {
return nil, fmt.Errorf("encode symmetric key for iam svc: %w", err)
}
}
return res, nil
}
func encryptSymmetricKeyForIAMSvc(boxKey *keys.PrivateKey, iamPublicKey *keys.PublicKey, symmetricKey, salt []byte) (*MFABox_IAMService, error) {
encrypted, err := encryptECDH(boxKey, iamPublicKey, symmetricKey, salt)
if err != nil {
return nil, fmt.Errorf("encrypt symmetricKey: %w", err)
}
s3svc := new(MFABox_IAMService)
s3svc.SvcPublicKey = iamPublicKey.Bytes()
s3svc.EncryptedSymmetricKey = encrypted
s3svc.SymmetricKeySalt = salt
return s3svc, nil
}
func encryptECDH(boxPrivateKey *keys.PrivateKey, iamPublicKey *keys.PublicKey, data, salt []byte) ([]byte, error) {
enc, err := getCipherECDH(boxPrivateKey, iamPublicKey, salt)
if err != nil {
return nil, fmt.Errorf("get chiper ecdh: %w", err)
}
return encrypt(enc, data)
}
func decryptECDH(iamPrivateKey *keys.PrivateKey, boxPublicKey *keys.PublicKey, data, salt []byte) ([]byte, error) {
dec, err := getCipherECDH(iamPrivateKey, boxPublicKey, salt)
if err != nil {
return nil, fmt.Errorf("get chiper ecdh: %w", err)
}
return decrypt(dec, data)
}
func encryptSymmetric(secret, data, salt []byte) ([]byte, error) {
enc, err := getCipher(secret, salt)
if err != nil {
return nil, fmt.Errorf("get chiper: %w", err)
}
return encrypt(enc, data)
}
func decryptSymmetric(secret, data, salt []byte) ([]byte, error) {
dec, err := getCipher(secret, salt)
if err != nil {
return nil, fmt.Errorf("get chiper: %w", err)
}
return decrypt(dec, data)
}
func encrypt(enc cipher.AEAD, data []byte) ([]byte, error) {
nonce := make([]byte, enc.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, fmt.Errorf("generate random nonce: %w", err)
}
return enc.Seal(nonce, nonce, data, nil), nil
}
func decrypt(dec cipher.AEAD, data []byte) ([]byte, error) {
if ld, ns := len(data), dec.NonceSize(); ld < ns {
return nil, fmt.Errorf("wrong data size (%d), should be greater than %d", ld, ns)
}
nonce, cypher := data[:dec.NonceSize()], data[dec.NonceSize():]
return dec.Open(nil, nonce, cypher, nil)
}
func getCipherECDH(owner *keys.PrivateKey, sender *keys.PublicKey, salt []byte) (cipher.AEAD, error) {
secret, err := generateECDH(owner, sender)
if err != nil {
return nil, fmt.Errorf("generate shared key: %w", err)
}
return getCipher(secret, salt)
}
func getCipher(secret, salt []byte) (cipher.AEAD, error) {
key, err := deriveKey(secret, salt)
if err != nil {
return nil, fmt.Errorf("derive key: %w", err)
}
return chacha20poly1305.NewX(key)
}
func generateECDH(prv *keys.PrivateKey, pub *keys.PublicKey) (sk []byte, err error) {
prvECDH, err := prv.ECDH()
if err != nil {
return nil, fmt.Errorf("invalid ECDH private key: %w", err)
}
pubECDH, err := (*ecdsa.PublicKey)(pub).ECDH()
if err != nil {
return nil, fmt.Errorf("invalid ECDH public key: %w", err)
}
return prvECDH.ECDH(pubECDH)
}
func deriveKey(secret, salt []byte) ([]byte, error) {
hash := sha256.New
kdf := hkdf.New(hash, secret, salt, nil)
key := make([]byte, 32)
_, err := io.ReadFull(kdf, key)
return key, err
}
func generateRandomBytes(length int) ([]byte, error) {
b := make([]byte, length)
_, err := rand.Read(b)
return b, err
}

333
internal/mfa/mfabox.pb.go Normal file
View file

@ -0,0 +1,333 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc v3.21.9
// source: internal/mfa/mfabox.proto
package mfa
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type MFABox struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// iam-service, contains iam keys and encrypted data for that keys
IAMServices []*MFABox_IAMService `protobuf:"bytes,1,rep,name=IAMServices,json=iamServices,proto3" json:"IAMServices,omitempty"`
// Seed public key for asymmetric encryption of IAMServicesKey
SeedPublicKey []byte `protobuf:"bytes,2,opt,name=seedPublicKey,proto3" json:"seedPublicKey,omitempty"`
// MFA secrets, which are encrypted by symmetric cipher
EncryptedSecrets []byte `protobuf:"bytes,3,opt,name=encryptedSecrets,proto3" json:"encryptedSecrets,omitempty"`
// salt used to derivation encrypted key to encrypt/decrypt MFA secrets
SecretsSalt []byte `protobuf:"bytes,4,opt,name=secretsSalt,proto3" json:"secretsSalt,omitempty"`
}
func (x *MFABox) Reset() {
*x = MFABox{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_mfa_mfabox_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MFABox) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MFABox) ProtoMessage() {}
func (x *MFABox) ProtoReflect() protoreflect.Message {
mi := &file_internal_mfa_mfabox_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MFABox.ProtoReflect.Descriptor instead.
func (*MFABox) Descriptor() ([]byte, []int) {
return file_internal_mfa_mfabox_proto_rawDescGZIP(), []int{0}
}
func (x *MFABox) GetIAMServices() []*MFABox_IAMService {
if x != nil {
return x.IAMServices
}
return nil
}
func (x *MFABox) GetSeedPublicKey() []byte {
if x != nil {
return x.SeedPublicKey
}
return nil
}
func (x *MFABox) GetEncryptedSecrets() []byte {
if x != nil {
return x.EncryptedSecrets
}
return nil
}
func (x *MFABox) GetSecretsSalt() []byte {
if x != nil {
return x.SecretsSalt
}
return nil
}
type Secrets struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// MFA URL
MFAURL string `protobuf:"bytes,2,opt,name=MFAURL,json=mfaURL,proto3" json:"MFAURL,omitempty"`
}
func (x *Secrets) Reset() {
*x = Secrets{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_mfa_mfabox_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Secrets) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Secrets) ProtoMessage() {}
func (x *Secrets) ProtoReflect() protoreflect.Message {
mi := &file_internal_mfa_mfabox_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Secrets.ProtoReflect.Descriptor instead.
func (*Secrets) Descriptor() ([]byte, []int) {
return file_internal_mfa_mfabox_proto_rawDescGZIP(), []int{1}
}
func (x *Secrets) GetMFAURL() string {
if x != nil {
return x.MFAURL
}
return ""
}
type MFABox_IAMService struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// iam-service public key
SvcPublicKey []byte `protobuf:"bytes,1,opt,name=svcPublicKey,proto3" json:"svcPublicKey,omitempty"`
// encrypted symmetric key used to encrypt/decrypt mfa secrets
EncryptedSymmetricKey []byte `protobuf:"bytes,2,opt,name=encryptedSymmetricKey,proto3" json:"encryptedSymmetricKey,omitempty"`
// salt used to derivation encrypted key to encrypt/decrypt symmetricKey
SymmetricKeySalt []byte `protobuf:"bytes,3,opt,name=symmetricKeySalt,proto3" json:"symmetricKeySalt,omitempty"`
}
func (x *MFABox_IAMService) Reset() {
*x = MFABox_IAMService{}
if protoimpl.UnsafeEnabled {
mi := &file_internal_mfa_mfabox_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MFABox_IAMService) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MFABox_IAMService) ProtoMessage() {}
func (x *MFABox_IAMService) ProtoReflect() protoreflect.Message {
mi := &file_internal_mfa_mfabox_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MFABox_IAMService.ProtoReflect.Descriptor instead.
func (*MFABox_IAMService) Descriptor() ([]byte, []int) {
return file_internal_mfa_mfabox_proto_rawDescGZIP(), []int{0, 0}
}
func (x *MFABox_IAMService) GetSvcPublicKey() []byte {
if x != nil {
return x.SvcPublicKey
}
return nil
}
func (x *MFABox_IAMService) GetEncryptedSymmetricKey() []byte {
if x != nil {
return x.EncryptedSymmetricKey
}
return nil
}
func (x *MFABox_IAMService) GetSymmetricKeySalt() []byte {
if x != nil {
return x.SymmetricKeySalt
}
return nil
}
var File_internal_mfa_mfabox_proto protoreflect.FileDescriptor
var file_internal_mfa_mfabox_proto_rawDesc = []byte{
0x0a, 0x19, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x66, 0x61, 0x2f, 0x6d,
0x66, 0x61, 0x62, 0x6f, 0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x6d, 0x66, 0x61,
0x22, 0xcb, 0x02, 0x0a, 0x06, 0x4d, 0x46, 0x41, 0x42, 0x6f, 0x78, 0x12, 0x38, 0x0a, 0x0b, 0x49,
0x41, 0x4d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x16, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x4d, 0x46, 0x41, 0x42, 0x6f, 0x78, 0x2e, 0x49, 0x41,
0x4d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x0b, 0x69, 0x61, 0x6d, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x65, 0x65, 0x64, 0x50, 0x75, 0x62,
0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x73, 0x65,
0x65, 0x64, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x10, 0x65,
0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, 0x63, 0x72, 0x65,
0x74, 0x73, 0x53, 0x61, 0x6c, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x73, 0x65,
0x63, 0x72, 0x65, 0x74, 0x73, 0x53, 0x61, 0x6c, 0x74, 0x1a, 0x92, 0x01, 0x0a, 0x0a, 0x49, 0x41,
0x4d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x76, 0x63, 0x50,
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c,
0x73, 0x76, 0x63, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x15,
0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x53, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72,
0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x65, 0x6e, 0x63,
0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x53, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b,
0x65, 0x79, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x79, 0x6d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b,
0x65, 0x79, 0x53, 0x61, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x73, 0x79,
0x6d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x53, 0x61, 0x6c, 0x74, 0x22, 0x21,
0x0a, 0x07, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x46, 0x41,
0x55, 0x52, 0x4c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x66, 0x61, 0x55, 0x52,
0x4c, 0x42, 0x19, 0x5a, 0x17, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x66,
0x61, 0x2f, 0x6d, 0x66, 0x61, 0x62, 0x6f, 0x78, 0x3b, 0x6d, 0x66, 0x61, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
var (
file_internal_mfa_mfabox_proto_rawDescOnce sync.Once
file_internal_mfa_mfabox_proto_rawDescData = file_internal_mfa_mfabox_proto_rawDesc
)
func file_internal_mfa_mfabox_proto_rawDescGZIP() []byte {
file_internal_mfa_mfabox_proto_rawDescOnce.Do(func() {
file_internal_mfa_mfabox_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_mfa_mfabox_proto_rawDescData)
})
return file_internal_mfa_mfabox_proto_rawDescData
}
var file_internal_mfa_mfabox_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_internal_mfa_mfabox_proto_goTypes = []interface{}{
(*MFABox)(nil), // 0: mfa.MFABox
(*Secrets)(nil), // 1: mfa.Secrets
(*MFABox_IAMService)(nil), // 2: mfa.MFABox.IAMService
}
var file_internal_mfa_mfabox_proto_depIdxs = []int32{
2, // 0: mfa.MFABox.IAMServices:type_name -> mfa.MFABox.IAMService
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_internal_mfa_mfabox_proto_init() }
func file_internal_mfa_mfabox_proto_init() {
if File_internal_mfa_mfabox_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_internal_mfa_mfabox_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*MFABox); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_mfa_mfabox_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Secrets); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_internal_mfa_mfabox_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*MFABox_IAMService); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_internal_mfa_mfabox_proto_rawDesc,
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_internal_mfa_mfabox_proto_goTypes,
DependencyIndexes: file_internal_mfa_mfabox_proto_depIdxs,
MessageInfos: file_internal_mfa_mfabox_proto_msgTypes,
}.Build()
File_internal_mfa_mfabox_proto = out.File
file_internal_mfa_mfabox_proto_rawDesc = nil
file_internal_mfa_mfabox_proto_goTypes = nil
file_internal_mfa_mfabox_proto_depIdxs = nil
}

34
internal/mfa/mfabox.proto Normal file
View file

@ -0,0 +1,34 @@
syntax = "proto3";
package mfa;
option go_package = "internal/mfa/mfabox;mfa";
message MFABox {
message IAMService {
// iam-service public key
bytes svcPublicKey = 1 [json_name = "svcPublicKey"];
// encrypted symmetric key used to encrypt/decrypt mfa secrets
bytes encryptedSymmetricKey = 2 [json_name = "encryptedSymmetricKey"];
// salt used to derivation encrypted key to encrypt/decrypt symmetricKey
bytes symmetricKeySalt = 3 [json_name = "symmetricKeySalt"];
}
// iam-service, contains iam keys and encrypted data for that keys
repeated IAMService IAMServices = 1 [json_name = "iamServices"];
// Seed public key for asymmetric encryption of IAMServicesKey
bytes seedPublicKey = 2 [json_name = "seedPublicKey"];
// MFA secrets, which are encrypted by symmetric cipher
bytes encryptedSecrets = 3 [json_name = "encryptedSecrets"];
// salt used to derivation encrypted key to encrypt/decrypt MFA secrets
bytes secretsSalt = 4 [json_name = "secretsSalt"];
}
message Secrets {
// MFA URL
string MFAURL = 2 [json_name = "mfaURL"];
}

View file

@ -89,6 +89,8 @@ var (
const (
versioningKV = "Versioning"
mfaDeleteEnabledKV = "MFADelete"
mfaSerialNumberKV = "SerialNumber"
cannedACLKV = "cannedACL"
ownerKeyKV = "ownerKey"
lockConfigurationKV = "LockConfiguration"
@ -500,9 +502,20 @@ func (c *Tree) GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (*
node := multiNode.Latest()
settings := &data.BucketSettings{Versioning: data.VersioningUnversioned}
settings := &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningUnversioned,
MFADeleteStatus: data.MFADeleteDisabled,
}}
if versioningValue, ok := node.Get(versioningKV); ok {
settings.Versioning = versioningValue
settings.Versioning.VersioningStatus = versioningValue
}
if mfaDeleteValue, ok := node.Get(mfaDeleteEnabledKV); ok {
settings.Versioning.MFADeleteStatus = mfaDeleteValue
if settings.MFADeleteEnabled() {
if mfaSerialNumberValue, ok := node.Get(mfaSerialNumberKV); ok {
settings.Versioning.MFASerialNumber = mfaSerialNumberValue
}
}
}
if lockConfigurationValue, ok := node.Get(lockConfigurationKV); ok {
@ -1789,7 +1802,8 @@ func metaFromSettings(settings *data.BucketSettings) map[string]string {
results := make(map[string]string, 3)
results[FileNameKey] = settingsFileName
results[versioningKV] = settings.Versioning
results[versioningKV] = settings.Versioning.VersioningStatus
results[mfaDeleteEnabledKV] = settings.Versioning.MFADeleteStatus
results[lockConfigurationKV] = encodeLockConfiguration(settings.LockConfiguration)
results[cannedACLKV] = settings.CannedACL
if settings.OwnerKey != nil {

View file

@ -119,7 +119,10 @@ func TestTreeServiceSettings(t *testing.T) {
require.NoError(t, err)
settings := &data.BucketSettings{
Versioning: "Versioning",
Versioning: data.Versioning{
VersioningStatus: data.VersioningEnabled,
MFADeleteStatus: data.MFADeleteDisabled,
},
LockConfiguration: &data.ObjectLockConfiguration{
ObjectLockEnabled: "Enabled",
Rule: &data.ObjectLockRule{