forked from TrueCloudLab/frostfs-s3-gw
[#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:
parent
0064e7ab07
commit
552bd7b932
29 changed files with 1212 additions and 36 deletions
5
api/cache/cache_test.go
vendored
5
api/cache/cache_test.go
vendored
|
@ -173,7 +173,10 @@ func TestSettingsCacheType(t *testing.T) {
|
||||||
cache := NewSystemCache(DefaultSystemConfig(logger))
|
cache := NewSystemCache(DefaultSystemConfig(logger))
|
||||||
|
|
||||||
key := "key"
|
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)
|
err := cache.PutSettings(key, settings)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -20,6 +20,9 @@ const (
|
||||||
VersioningUnversioned = "Unversioned"
|
VersioningUnversioned = "Unversioned"
|
||||||
VersioningEnabled = "Enabled"
|
VersioningEnabled = "Enabled"
|
||||||
VersioningSuspended = "Suspended"
|
VersioningSuspended = "Suspended"
|
||||||
|
|
||||||
|
MFADeleteDisabled = "Disabled"
|
||||||
|
MFADeleteEnabled = "Enabled"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -55,12 +58,19 @@ type (
|
||||||
|
|
||||||
// BucketSettings stores settings such as versioning.
|
// BucketSettings stores settings such as versioning.
|
||||||
BucketSettings struct {
|
BucketSettings struct {
|
||||||
Versioning string
|
Versioning Versioning
|
||||||
LockConfiguration *ObjectLockConfiguration
|
LockConfiguration *ObjectLockConfiguration
|
||||||
CannedACL string
|
CannedACL string
|
||||||
OwnerKey *keys.PublicKey
|
OwnerKey *keys.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Versioning stores bucket versioning settings.
|
||||||
|
Versioning struct {
|
||||||
|
VersioningStatus string
|
||||||
|
MFADeleteStatus string
|
||||||
|
MFASerialNumber string
|
||||||
|
}
|
||||||
|
|
||||||
// CORSConfiguration stores CORS configuration of a request.
|
// CORSConfiguration stores CORS configuration of a request.
|
||||||
CORSConfiguration struct {
|
CORSConfiguration struct {
|
||||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CORSConfiguration" json:"-"`
|
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 {
|
func (b BucketSettings) Unversioned() bool {
|
||||||
return b.Versioning == VersioningUnversioned
|
return b.Versioning.VersioningStatus == VersioningUnversioned
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b BucketSettings) VersioningEnabled() bool {
|
func (b BucketSettings) VersioningEnabled() bool {
|
||||||
return b.Versioning == VersioningEnabled
|
return b.Versioning.VersioningStatus == VersioningEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b BucketSettings) VersioningSuspended() bool {
|
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 {
|
func Quote(val string) string {
|
||||||
|
|
|
@ -143,6 +143,7 @@ const (
|
||||||
ErrInvalidTagDirective
|
ErrInvalidTagDirective
|
||||||
// Add new error codes here.
|
// Add new error codes here.
|
||||||
ErrNotSupported
|
ErrNotSupported
|
||||||
|
ErrMFAAuthNeeded
|
||||||
|
|
||||||
// SSE-S3 related API errors.
|
// SSE-S3 related API errors.
|
||||||
ErrInvalidEncryptionMethod
|
ErrInvalidEncryptionMethod
|
||||||
|
@ -287,7 +288,7 @@ const (
|
||||||
|
|
||||||
ErrPostPolicyConditionInvalidFormat
|
ErrPostPolicyConditionInvalidFormat
|
||||||
|
|
||||||
//CORS configuration errors.
|
// CORS configuration errors.
|
||||||
ErrCORSUnsupportedMethod
|
ErrCORSUnsupportedMethod
|
||||||
ErrCORSWildcardExposeHeaders
|
ErrCORSWildcardExposeHeaders
|
||||||
|
|
||||||
|
@ -388,6 +389,12 @@ var errorCodes = errorCodeMap{
|
||||||
Description: "Access Denied.",
|
Description: "Access Denied.",
|
||||||
HTTPStatusCode: http.StatusForbidden,
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
|
ErrMFAAuthNeeded: {
|
||||||
|
ErrCode: ErrMFAAuthNeeded,
|
||||||
|
Code: "AccessDenied",
|
||||||
|
Description: "Mfa Authentication must be used for this request",
|
||||||
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
ErrAccessControlListNotSupported: {
|
ErrAccessControlListNotSupported: {
|
||||||
ErrCode: ErrAccessControlListNotSupported,
|
ErrCode: ErrAccessControlListNotSupported,
|
||||||
Code: "AccessControlListNotSupported",
|
Code: "AccessControlListNotSupported",
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
"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"
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||||
|
@ -24,6 +25,7 @@ type (
|
||||||
cfg Config
|
cfg Config
|
||||||
ape APE
|
ape APE
|
||||||
frostfsid FrostFSID
|
frostfsid FrostFSID
|
||||||
|
mfa *mfa.MFA
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config contains data which handler needs to keep.
|
// Config contains data which handler needs to keep.
|
||||||
|
@ -68,7 +70,7 @@ const (
|
||||||
var _ api.Handler = (*handler)(nil)
|
var _ api.Handler = (*handler)(nil)
|
||||||
|
|
||||||
// New creates new api.Handler using given logger and client.
|
// 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 {
|
switch {
|
||||||
case obj == nil:
|
case obj == nil:
|
||||||
return nil, errors.New("empty FrostFS Object Layer")
|
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,
|
cfg: cfg,
|
||||||
ape: storage,
|
ape: storage,
|
||||||
frostfsid: ffsid,
|
frostfsid: ffsid,
|
||||||
|
mfa: mfaCli,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
"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
|
// 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
|
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)
|
networkInfo, err := h.obj.GetNetworkInfo(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logAndSendError(ctx, w, "could not get network info", reqInfo, err)
|
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
|
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)
|
networkInfo, err := h.obj.GetNetworkInfo(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logAndSendError(ctx, w, "could not get network info", reqInfo, err)
|
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)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func haveVersionedObjects(objects []ObjectIdentifier) bool {
|
||||||
|
for _, o := range objects {
|
||||||
|
if len(o.VersionID) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -437,7 +437,10 @@ func createTestBucketWithLock(hc *handlerContext, bktName string, conf *data.Obj
|
||||||
sp := &layer.PutSettingsParams{
|
sp := &layer.PutSettingsParams{
|
||||||
BktInfo: bktInfo,
|
BktInfo: bktInfo,
|
||||||
Settings: &data.BucketSettings{
|
Settings: &data.BucketSettings{
|
||||||
Versioning: data.VersioningEnabled,
|
Versioning: data.Versioning{
|
||||||
|
VersioningStatus: data.VersioningEnabled,
|
||||||
|
MFADeleteStatus: data.MFADeleteDisabled,
|
||||||
|
},
|
||||||
LockConfiguration: conf,
|
LockConfiguration: conf,
|
||||||
OwnerKey: key.PublicKey(),
|
OwnerKey: key.PublicKey(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -824,14 +824,17 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
|
||||||
sp := &layer.PutSettingsParams{
|
sp := &layer.PutSettingsParams{
|
||||||
BktInfo: bktInfo,
|
BktInfo: bktInfo,
|
||||||
Settings: &data.BucketSettings{
|
Settings: &data.BucketSettings{
|
||||||
CannedACL: cannedACL,
|
CannedACL: cannedACL,
|
||||||
OwnerKey: key,
|
OwnerKey: key,
|
||||||
Versioning: data.VersioningUnversioned,
|
Versioning: data.Versioning{
|
||||||
|
VersioningStatus: data.VersioningUnversioned,
|
||||||
|
MFADeleteStatus: data.MFADeleteDisabled,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.ObjectLockEnabled {
|
if p.ObjectLockEnabled {
|
||||||
sp.Settings.Versioning = data.VersioningEnabled
|
sp.Settings.Versioning.VersioningStatus = data.VersioningEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
err = retryer.MakeWithRetry(ctx, func() error {
|
err = retryer.MakeWithRetry(ctx, func() error {
|
||||||
|
|
|
@ -137,3 +137,17 @@ func parseRange(s string) (*layer.RangeParams, error) {
|
||||||
End: values[1],
|
End: values[1],
|
||||||
}, nil
|
}, 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
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
"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/layer"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
|
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
|
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 {
|
if configuration.Status != data.VersioningEnabled && configuration.Status != data.VersioningSuspended {
|
||||||
h.logAndSendError(ctx, w, "invalid versioning configuration", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
|
h.logAndSendError(ctx, w, "invalid versioning configuration", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// settings pointer is stored in the cache, so modify a copy of the settings
|
newSettings.Versioning.VersioningStatus = configuration.Status
|
||||||
newSettings := *settings
|
|
||||||
newSettings.Versioning = configuration.Status
|
|
||||||
|
|
||||||
p := &layer.PutSettingsParams{
|
p := &layer.PutSettingsParams{
|
||||||
BktInfo: bktInfo,
|
BktInfo: bktInfo,
|
||||||
|
@ -80,7 +114,7 @@ func (h *handler) GetBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
|
||||||
func formVersioningConfiguration(settings *data.BucketSettings) *VersioningConfiguration {
|
func formVersioningConfiguration(settings *data.BucketSettings) *VersioningConfiguration {
|
||||||
res := &VersioningConfiguration{}
|
res := &VersioningConfiguration{}
|
||||||
if !settings.Unversioned() {
|
if !settings.Unversioned() {
|
||||||
res.Status = settings.Versioning
|
res.Status = settings.Versioning.VersioningStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
|
@ -15,6 +15,7 @@ const (
|
||||||
AmzCopySource = "X-Amz-Copy-Source"
|
AmzCopySource = "X-Amz-Copy-Source"
|
||||||
AmzCopySourceRange = "X-Amz-Copy-Source-Range"
|
AmzCopySourceRange = "X-Amz-Copy-Source-Range"
|
||||||
AmzDate = "X-Amz-Date"
|
AmzDate = "X-Amz-Date"
|
||||||
|
AmzMFA = "X-Amz-Mfa"
|
||||||
|
|
||||||
LastModified = "Last-Modified"
|
LastModified = "Last-Modified"
|
||||||
Date = "Date"
|
Date = "Date"
|
||||||
|
|
|
@ -12,8 +12,11 @@ import (
|
||||||
func TestObjectLockAttributes(t *testing.T) {
|
func TestObjectLockAttributes(t *testing.T) {
|
||||||
tc := prepareContext(t)
|
tc := prepareContext(t)
|
||||||
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
|
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
|
||||||
BktInfo: tc.bktInfo,
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|
|
@ -216,7 +216,10 @@ func (n *Layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo)
|
||||||
if !errors.Is(err, tree.ErrNodeNotFound) {
|
if !errors.Is(err, tree.ErrNodeNotFound) {
|
||||||
return nil, err
|
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)
|
n.reqLogger(ctx).Debug(logs.BucketSettingsNotFoundUseDefaults)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -196,8 +196,11 @@ func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext {
|
||||||
func TestSimpleVersioning(t *testing.T) {
|
func TestSimpleVersioning(t *testing.T) {
|
||||||
tc := prepareContext(t)
|
tc := prepareContext(t)
|
||||||
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
|
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
|
||||||
BktInfo: tc.bktInfo,
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -234,7 +237,10 @@ func TestSimpleNoVersioning(t *testing.T) {
|
||||||
|
|
||||||
func TestVersioningDeleteObject(t *testing.T) {
|
func TestVersioningDeleteObject(t *testing.T) {
|
||||||
tc := prepareContext(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{
|
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
|
||||||
BktInfo: tc.bktInfo,
|
BktInfo: tc.bktInfo,
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
|
@ -256,7 +262,10 @@ func TestGetUnversioned(t *testing.T) {
|
||||||
objContent := []byte("content obj1 v1")
|
objContent := []byte("content obj1 v1")
|
||||||
objInfo := tc.putObject(objContent)
|
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{
|
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
|
||||||
BktInfo: tc.bktInfo,
|
BktInfo: tc.bktInfo,
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
|
@ -270,7 +279,10 @@ func TestGetUnversioned(t *testing.T) {
|
||||||
|
|
||||||
func TestVersioningDeleteSpecificObjectVersion(t *testing.T) {
|
func TestVersioningDeleteSpecificObjectVersion(t *testing.T) {
|
||||||
tc := prepareContext(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{
|
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
|
||||||
BktInfo: tc.bktInfo,
|
BktInfo: tc.bktInfo,
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
|
|
|
@ -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/policy/contract"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/services"
|
"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/logs"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/mfa"
|
||||||
internalnet "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/net"
|
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/version"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
|
"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) {
|
func (a *App) initAPI(ctx context.Context) {
|
||||||
a.initLayer(ctx)
|
a.initLayer(ctx)
|
||||||
a.initHandler()
|
a.initHandler(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initMetrics() {
|
func (a *App) initMetrics() {
|
||||||
|
@ -1134,15 +1135,51 @@ func getFrostfsIDCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config {
|
||||||
return cacheCfg
|
return cacheCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initHandler() {
|
func (a *App) initHandler(ctx context.Context) {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err))
|
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 {
|
func (a *App) getServer(address string) Server {
|
||||||
for i := range a.servers {
|
for i := range a.servers {
|
||||||
if a.servers[i].Address() == address {
|
if a.servers[i].Address() == address {
|
||||||
|
|
|
@ -222,6 +222,7 @@ const ( // Settings.
|
||||||
cfgContainersCORS = "containers.cors"
|
cfgContainersCORS = "containers.cors"
|
||||||
cfgContainersLifecycle = "containers.lifecycle"
|
cfgContainersLifecycle = "containers.lifecycle"
|
||||||
cfgContainersAccessBox = "containers.accessbox"
|
cfgContainersAccessBox = "containers.accessbox"
|
||||||
|
cfgContainersMFA = "containers.mfa"
|
||||||
|
|
||||||
// Multinet.
|
// Multinet.
|
||||||
cfgMultinetEnabled = "multinet.enabled"
|
cfgMultinetEnabled = "multinet.enabled"
|
||||||
|
|
|
@ -262,6 +262,7 @@ S3_GW_RETRY_STRATEGY=exponential
|
||||||
# Containers properties
|
# Containers properties
|
||||||
S3_GW_CONTAINERS_CORS=AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
S3_GW_CONTAINERS_CORS=AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||||
S3_GW_CONTAINERS_LIFECYCLE=AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
S3_GW_CONTAINERS_LIFECYCLE=AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||||
|
S3_GW_CONTAINERS_MFA=HV9h4zbp7Dti2VXef2oFSsBSRyJUR6NfMeswuv12fjZu
|
||||||
|
|
||||||
# Multinet properties
|
# Multinet properties
|
||||||
# Enable multinet support
|
# Enable multinet support
|
||||||
|
|
|
@ -310,6 +310,7 @@ retry:
|
||||||
containers:
|
containers:
|
||||||
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||||
lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||||
|
mfa: HV9h4zbp7Dti2VXef2oFSsBSRyJUR6NfMeswuv12fjZu
|
||||||
|
|
||||||
# Multinet properties
|
# Multinet properties
|
||||||
multinet:
|
multinet:
|
||||||
|
|
|
@ -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_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
|
(*AccessBox)(nil), // 0: accessbox.AccessBox
|
||||||
(*Tokens)(nil), // 1: accessbox.Tokens
|
(*Tokens)(nil), // 1: accessbox.Tokens
|
||||||
(*AccessBox_Gate)(nil), // 2: accessbox.AccessBox.Gate
|
(*AccessBox_Gate)(nil), // 2: accessbox.AccessBox.Gate
|
||||||
|
@ -352,7 +352,7 @@ func file_creds_accessbox_accessbox_proto_init() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !protoimpl.UnsafeEnabled {
|
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 {
|
switch v := v.(*AccessBox); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
|
@ -364,7 +364,7 @@ func file_creds_accessbox_accessbox_proto_init() {
|
||||||
return nil
|
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 {
|
switch v := v.(*Tokens); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
|
@ -376,7 +376,7 @@ func file_creds_accessbox_accessbox_proto_init() {
|
||||||
return nil
|
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 {
|
switch v := v.(*AccessBox_Gate); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
|
@ -388,7 +388,7 @@ func file_creds_accessbox_accessbox_proto_init() {
|
||||||
return nil
|
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 {
|
switch v := v.(*AccessBox_ContainerPolicy); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
|
|
|
@ -822,6 +822,7 @@ containers:
|
||||||
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||||
lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||||
accessbox: ExnA1gSY3kzgomi2wJxNyWo1ytWv9VAKXRE55fNXEPL2
|
accessbox: ExnA1gSY3kzgomi2wJxNyWo1ytWv9VAKXRE55fNXEPL2
|
||||||
|
mfa: HV9h4zbp7Dti2VXef2oFSsBSRyJUR6NfMeswuv12fjZu
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
| 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. |
|
| `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. |
|
| `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. |
|
| `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
|
# `vhs` section
|
||||||
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -21,6 +21,7 @@ require (
|
||||||
github.com/mr-tron/base58 v1.2.0
|
github.com/mr-tron/base58 v1.2.0
|
||||||
github.com/nspcc-dev/neo-go v0.106.3
|
github.com/nspcc-dev/neo-go v0.106.3
|
||||||
github.com/panjf2000/ants/v2 v2.5.0
|
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_golang v1.19.0
|
||||||
github.com/prometheus/client_model v0.5.0
|
github.com/prometheus/client_model v0.5.0
|
||||||
github.com/spf13/cobra v1.8.1
|
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/ssooidc v1.26.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.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/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/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||||
|
|
5
go.sum
5
go.sum
|
@ -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/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 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
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 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
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=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
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=
|
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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
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.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.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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
|
183
internal/frostfs/mfa.go
Normal file
183
internal/frostfs/mfa.go
Normal 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, "/")
|
||||||
|
}
|
|
@ -164,6 +164,8 @@ const (
|
||||||
CouldntDeleteLifecycleObject = "couldn't delete lifecycle configuration object"
|
CouldntDeleteLifecycleObject = "couldn't delete lifecycle configuration object"
|
||||||
CouldntCacheLifecycleConfiguration = "couldn't cache lifecycle configuration"
|
CouldntCacheLifecycleConfiguration = "couldn't cache lifecycle configuration"
|
||||||
CouldNotFetchLifecycleContainerInfo = "couldn't fetch lifecycle container info"
|
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"
|
BucketLifecycleNodeHasMultipleIDs = "bucket lifecycle node has multiple ids"
|
||||||
GetBucketLifecycle = "get bucket lifecycle"
|
GetBucketLifecycle = "get bucket lifecycle"
|
||||||
WarnDuplicateNamespaceVHS = "duplicate namespace with enabled VHS, config value skipped"
|
WarnDuplicateNamespaceVHS = "duplicate namespace with enabled VHS, config value skipped"
|
||||||
|
|
161
internal/mfa/mfa.go
Normal file
161
internal/mfa/mfa.go
Normal 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
247
internal/mfa/mfabox.go
Normal 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
333
internal/mfa/mfabox.pb.go
Normal 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
34
internal/mfa/mfabox.proto
Normal 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"];
|
||||||
|
}
|
|
@ -89,6 +89,8 @@ var (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
versioningKV = "Versioning"
|
versioningKV = "Versioning"
|
||||||
|
mfaDeleteEnabledKV = "MFADelete"
|
||||||
|
mfaSerialNumberKV = "SerialNumber"
|
||||||
cannedACLKV = "cannedACL"
|
cannedACLKV = "cannedACL"
|
||||||
ownerKeyKV = "ownerKey"
|
ownerKeyKV = "ownerKey"
|
||||||
lockConfigurationKV = "LockConfiguration"
|
lockConfigurationKV = "LockConfiguration"
|
||||||
|
@ -500,9 +502,20 @@ func (c *Tree) GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (*
|
||||||
|
|
||||||
node := multiNode.Latest()
|
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 {
|
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 {
|
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 := make(map[string]string, 3)
|
||||||
|
|
||||||
results[FileNameKey] = settingsFileName
|
results[FileNameKey] = settingsFileName
|
||||||
results[versioningKV] = settings.Versioning
|
results[versioningKV] = settings.Versioning.VersioningStatus
|
||||||
|
results[mfaDeleteEnabledKV] = settings.Versioning.MFADeleteStatus
|
||||||
results[lockConfigurationKV] = encodeLockConfiguration(settings.LockConfiguration)
|
results[lockConfigurationKV] = encodeLockConfiguration(settings.LockConfiguration)
|
||||||
results[cannedACLKV] = settings.CannedACL
|
results[cannedACLKV] = settings.CannedACL
|
||||||
if settings.OwnerKey != nil {
|
if settings.OwnerKey != nil {
|
||||||
|
|
|
@ -119,7 +119,10 @@ func TestTreeServiceSettings(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
settings := &data.BucketSettings{
|
settings := &data.BucketSettings{
|
||||||
Versioning: "Versioning",
|
Versioning: data.Versioning{
|
||||||
|
VersioningStatus: data.VersioningEnabled,
|
||||||
|
MFADeleteStatus: data.MFADeleteDisabled,
|
||||||
|
},
|
||||||
LockConfiguration: &data.ObjectLockConfiguration{
|
LockConfiguration: &data.ObjectLockConfiguration{
|
||||||
ObjectLockEnabled: "Enabled",
|
ObjectLockEnabled: "Enabled",
|
||||||
Rule: &data.ObjectLockRule{
|
Rule: &data.ObjectLockRule{
|
||||||
|
|
Loading…
Add table
Reference in a new issue