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))
|
||||
|
||||
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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
@ -824,14 +824,17 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
|
|||
sp := &layer.PutSettingsParams{
|
||||
BktInfo: bktInfo,
|
||||
Settings: &data.BucketSettings{
|
||||
CannedACL: cannedACL,
|
||||
OwnerKey: key,
|
||||
Versioning: data.VersioningUnversioned,
|
||||
CannedACL: cannedACL,
|
||||
OwnerKey: key,
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -12,8 +12,11 @@ import (
|
|||
func TestObjectLockAttributes(t *testing.T) {
|
||||
tc := prepareContext(t)
|
||||
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
|
||||
BktInfo: tc.bktInfo,
|
||||
Settings: &data.BucketSettings{Versioning: data.VersioningEnabled},
|
||||
BktInfo: tc.bktInfo,
|
||||
Settings: &data.BucketSettings{Versioning: data.Versioning{
|
||||
VersioningStatus: data.VersioningEnabled,
|
||||
MFADeleteStatus: data.MFADeleteDisabled,
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -196,8 +196,11 @@ func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext {
|
|||
func TestSimpleVersioning(t *testing.T) {
|
||||
tc := prepareContext(t)
|
||||
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
|
||||
BktInfo: tc.bktInfo,
|
||||
Settings: &data.BucketSettings{Versioning: data.VersioningEnabled},
|
||||
BktInfo: tc.bktInfo,
|
||||
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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -222,6 +222,7 @@ const ( // Settings.
|
|||
cfgContainersCORS = "containers.cors"
|
||||
cfgContainersLifecycle = "containers.lifecycle"
|
||||
cfgContainersAccessBox = "containers.accessbox"
|
||||
cfgContainersMFA = "containers.mfa"
|
||||
|
||||
// Multinet.
|
||||
cfgMultinetEnabled = "multinet.enabled"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -310,6 +310,7 @@ retry:
|
|||
containers:
|
||||
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||
lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||
mfa: HV9h4zbp7Dti2VXef2oFSsBSRyJUR6NfMeswuv12fjZu
|
||||
|
||||
# Multinet properties
|
||||
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_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
|
||||
|
|
|
@ -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
2
go.mod
|
@ -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
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/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
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"
|
||||
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
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 (
|
||||
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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Add table
Reference in a new issue