[#680] Move policy engine converter to s3-gw

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2025-04-04 18:04:58 +03:00 committed by Alexey Vanin
parent e788bb6ec9
commit 0ba6989197
21 changed files with 4325 additions and 50 deletions

View file

@ -19,6 +19,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
s3common "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine/common"
engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam" engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
@ -177,7 +178,7 @@ func (h *handler) putBucketACLAPEHandler(w http.ResponseWriter, r *http.Request,
} }
chainRules := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID) chainRules := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID)
if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), chainRules); err != nil { if err = h.policyEngine.APE.SaveACLChains(bktInfo.CID.EncodeToString(), chainRules); err != nil {
h.logAndSendError(ctx, w, "failed to add morph rule chains", reqInfo, err) h.logAndSendError(ctx, w, "failed to add morph rule chains", reqInfo, err)
return return
} }
@ -248,7 +249,7 @@ func (h *handler) GetBucketPolicyStatusHandler(w http.ResponseWriter, r *http.Re
return return
} }
jsonPolicy, err := h.ape.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID) jsonPolicy, err := h.policyEngine.APE.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "not found") { if strings.Contains(err.Error(), "not found") {
err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error()) err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error())
@ -293,7 +294,7 @@ func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
return return
} }
jsonPolicy, err := h.ape.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID) jsonPolicy, err := h.policyEngine.APE.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "not found") { if strings.Contains(err.Error(), "not found") {
err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error()) err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error())
@ -323,7 +324,7 @@ func (h *handler) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Reque
} }
chainIDs := []chain.ID{getBucketChainID(chain.S3, bktInfo), getBucketChainID(chain.Ingress, bktInfo)} chainIDs := []chain.ID{getBucketChainID(chain.S3, bktInfo), getBucketChainID(chain.Ingress, bktInfo)}
if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil { if err = h.policyEngine.APE.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
h.logAndSendError(ctx, w, "failed to delete policy from storage", reqInfo, err) h.logAndSendError(ctx, w, "failed to delete policy from storage", reqInfo, err)
return return
} }
@ -360,7 +361,7 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
return return
} }
var bktPolicy engineiam.Policy var bktPolicy s3common.Policy
if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil { if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil {
h.logAndSendError(ctx, w, "could not parse bucket policy", reqInfo, err) h.logAndSendError(ctx, w, "could not parse bucket policy", reqInfo, err)
return return
@ -372,7 +373,7 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
return return
} }
if len(stat.NotPrincipal) != 0 && stat.Effect == engineiam.AllowEffect { if len(stat.NotPrincipal) != 0 && stat.Effect == s3common.AllowEffect {
h.logAndSendError(ctx, w, "invalid NotPrincipal", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicyNotPrincipal)) h.logAndSendError(ctx, w, "invalid NotPrincipal", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicyNotPrincipal))
return return
} }
@ -385,14 +386,14 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
} }
} }
s3Chain, err := engineiam.ConvertToS3Chain(bktPolicy, h.frostfsid) s3Chain, err := h.policyEngine.Converter.ToS3Chain(bktPolicy, h.frostfsid)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "could not convert s3 policy to chain policy", reqInfo, err) h.logAndSendError(ctx, w, "could not convert s3 policy to chain policy", reqInfo, err)
return return
} }
s3Chain.ID = getBucketChainID(chain.S3, bktInfo) s3Chain.ID = getBucketChainID(chain.S3, bktInfo)
nativeChain, err := engineiam.ConvertToNativeChain(bktPolicy, h.nativeResolver(reqInfo.Namespace, bktInfo)) nativeChain, err := h.policyEngine.Converter.ToNativeChain(bktPolicy, h.nativeResolver(reqInfo.Namespace, bktInfo))
if err == nil { if err == nil {
nativeChain.ID = getBucketChainID(chain.Ingress, bktInfo) nativeChain.ID = getBucketChainID(chain.Ingress, bktInfo)
} else if !stderrors.Is(err, engineiam.ErrActionsNotApplicable) { } else if !stderrors.Is(err, engineiam.ErrActionsNotApplicable) {
@ -407,7 +408,7 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
chainsToSave = append(chainsToSave, nativeChain) chainsToSave = append(chainsToSave, nativeChain)
} }
if err = h.ape.PutBucketPolicy(reqInfo.Namespace, bktInfo.CID, jsonPolicy, chainsToSave); err != nil { if err = h.policyEngine.APE.PutBucketPolicy(reqInfo.Namespace, bktInfo.CID, jsonPolicy, chainsToSave); err != nil {
h.logAndSendError(ctx, w, "failed to update policy in contract", reqInfo, err) h.logAndSendError(ctx, w, "failed to update policy in contract", reqInfo, err)
return return
} }
@ -419,15 +420,15 @@ type nativeResolver struct {
bktInfo *data.BucketInfo bktInfo *data.BucketInfo
} }
func (n *nativeResolver) GetBucketInfo(bucket string) (*engineiam.BucketInfo, error) { func (n *nativeResolver) GetBucketInfo(bucket string) (*s3common.BucketInfo, error) {
if n.bktInfo.Name != bucket { if n.bktInfo.Name != bucket {
return nil, fmt.Errorf("invalid bucket %s: %w", bucket, errors.GetAPIError(errors.ErrMalformedPolicy)) return nil, fmt.Errorf("invalid bucket %s: %w", bucket, errors.GetAPIError(errors.ErrMalformedPolicy))
} }
return &engineiam.BucketInfo{Namespace: n.namespace, Container: n.bktInfo.CID.EncodeToString()}, nil return &s3common.BucketInfo{Namespace: n.namespace, Container: n.bktInfo.CID.EncodeToString()}, nil
} }
func (h *handler) nativeResolver(ns string, bktInfo *data.BucketInfo) engineiam.NativeResolver { func (h *handler) nativeResolver(ns string, bktInfo *data.BucketInfo) s3common.NativeResolver {
return &nativeResolver{ return &nativeResolver{
FrostFSID: h.frostfsid, FrostFSID: h.frostfsid,
namespace: ns, namespace: ns,

View file

@ -189,14 +189,14 @@ func TestDeleteBucketWithPolicy(t *testing.T) {
putBucketPolicy(hc, bktName, newPolicy) putBucketPolicy(hc, bktName, newPolicy)
require.Len(t, hc.h.ape.(*apeMock).policyMap, 1) require.Len(t, hc.h.policyEngine.APE.(*apeMock).policyMap, 1)
require.Len(t, hc.h.ape.(*apeMock).chainMap[engine.ContainerTarget(bi.CID.EncodeToString())], 4) require.Len(t, hc.h.policyEngine.APE.(*apeMock).chainMap[engine.ContainerTarget(bi.CID.EncodeToString())], 4)
hc.owner = bi.Owner hc.owner = bi.Owner
deleteBucket(t, hc, bktName, http.StatusNoContent) deleteBucket(t, hc, bktName, http.StatusNoContent)
require.Empty(t, hc.h.ape.(*apeMock).policyMap) require.Empty(t, hc.h.policyEngine.APE.(*apeMock).policyMap)
chains, err := hc.h.ape.(*apeMock).ListChains(engine.ContainerTarget(bi.CID.EncodeToString())) chains, err := hc.h.policyEngine.APE.(*apeMock).ListChains(engine.ContainerTarget(bi.CID.EncodeToString()))
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, chains) require.Empty(t, chains)
} }

View file

@ -12,6 +12,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa" "git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa"
"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"
policyengine "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine"
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"
@ -20,12 +21,12 @@ import (
type ( type (
handler struct { handler struct {
log *zap.Logger log *zap.Logger
obj *layer.Layer obj *layer.Layer
cfg Config cfg Config
ape APE policyEngine PolicyEngine
frostfsid FrostFSID frostfsid FrostFSID
mfa *mfa.Manager mfa *mfa.Manager
} }
// Config contains data which handler needs to keep. // Config contains data which handler needs to keep.
@ -54,6 +55,11 @@ type (
GetUserKey(account, name string) (string, error) GetUserKey(account, name string) (string, error)
} }
PolicyEngine struct {
APE APE
Converter *policyengine.Converter
}
// APE is Access Policy Engine that needs to save policy and acl info to different places. // APE is Access Policy Engine that needs to save policy and acl info to different places.
APE interface { APE interface {
PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chains []*chain.Chain) error PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chains []*chain.Chain) error
@ -73,14 +79,14 @@ 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, mfaMgr *mfa.Manager) (api.Handler, error) { func New(log *zap.Logger, obj *layer.Layer, cfg Config, policyEngine PolicyEngine, ffsid FrostFSID, mfaMgr *mfa.Manager) (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")
case log == nil: case log == nil:
return nil, errors.New("empty logger") return nil, errors.New("empty logger")
case storage == nil: case policyEngine.APE == nil || policyEngine.Converter == nil:
return nil, errors.New("empty policy storage") return nil, errors.New("empty policy engine")
case ffsid == nil: case ffsid == nil:
return nil, errors.New("empty frostfsid") return nil, errors.New("empty frostfsid")
case mfaMgr == nil: case mfaMgr == nil:
@ -88,12 +94,12 @@ func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid Frost
} }
return &handler{ return &handler{
log: log, log: log,
obj: obj, obj: obj,
cfg: cfg, cfg: cfg,
ape: storage, policyEngine: policyEngine,
frostfsid: ffsid, frostfsid: ffsid,
mfa: mfaMgr, mfa: mfaMgr,
}, nil }, nil
} }

View file

@ -345,7 +345,7 @@ func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
getBucketCannedChainID(chain.S3, bktInfo.CID), getBucketCannedChainID(chain.S3, bktInfo.CID),
getBucketCannedChainID(chain.Ingress, bktInfo.CID), getBucketCannedChainID(chain.Ingress, bktInfo.CID),
} }
if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil { if err = h.policyEngine.APE.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
h.logAndSendError(ctx, w, "failed to delete policy from storage", reqInfo, err) h.logAndSendError(ctx, w, "failed to delete policy from storage", reqInfo, err)
return return
} }

View file

@ -26,6 +26,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
intmfa "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/mfa" intmfa "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/mfa"
policyengine "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test" bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
@ -287,10 +288,15 @@ func prepareHandlerContextBase(config *handlerConfig, log *zap.Logger) (*handler
placementPolicies: make(map[string]netmap.PlacementPolicy), placementPolicies: make(map[string]netmap.PlacementPolicy),
} }
h := &handler{ h := &handler{
log: log, log: log,
obj: layer.NewLayer(ctx, log, tp, layerCfg), obj: layer.NewLayer(ctx, log, tp, layerCfg),
cfg: cfg, cfg: cfg,
ape: newAPEMock(), policyEngine: PolicyEngine{
APE: newAPEMock(),
Converter: policyengine.NewConverter(policyengine.Config{
VersionFetcher: apeConverterMock{version: policyengine.V1},
}),
},
frostfsid: newFrostfsIDMock(), frostfsid: newFrostfsIDMock(),
} }
@ -377,6 +383,14 @@ func getMinCacheConfig(logger *zap.Logger) *layer.CachesConfig {
} }
} }
type apeConverterMock struct {
version policyengine.ConverterVersion
}
func (a apeConverterMock) ConverterVersion() policyengine.ConverterVersion {
return a.version
}
type apeMock struct { type apeMock struct {
chainMap map[engine.Target][]*chain.Chain chainMap map[engine.Target][]*chain.Chain
policyMap map[string][]byte policyMap map[string][]byte

View file

@ -860,7 +860,7 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
zap.Stringer("container_id", bktInfo.CID), logs.TagField(logs.TagExternalStorage)) zap.Stringer("container_id", bktInfo.CID), logs.TagField(logs.TagExternalStorage))
chains := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID) chains := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID)
if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), chains); err != nil { if err = h.policyEngine.APE.SaveACLChains(bktInfo.CID.EncodeToString(), chains); err != nil {
cleanErr := h.cleanupBucketCreation(ctx, reqInfo, bktInfo, boxData, chains) cleanErr := h.cleanupBucketCreation(ctx, reqInfo, bktInfo, boxData, chains)
h.logAndSendError(ctx, w, "failed to add morph rule chain", reqInfo, err, zap.NamedError("cleanup_error", cleanErr)) h.logAndSendError(ctx, w, "failed to add morph rule chain", reqInfo, err, zap.NamedError("cleanup_error", cleanErr))
return return
@ -913,7 +913,7 @@ func (h *handler) cleanupBucketCreation(ctx context.Context, reqInfo *middleware
chainIDs[i] = c.ID chainIDs[i] = c.ID
} }
if err := h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil { if err := h.policyEngine.APE.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
return fmt.Errorf("delete bucket acl policy: %w", err) return fmt.Errorf("delete bucket acl policy: %w", err)
} }

View file

@ -1006,7 +1006,7 @@ func TestCreateBucketWithoutPermissions(t *testing.T) {
hc := prepareHandlerContext(t) hc := prepareHandlerContext(t)
bktName := "bkt-name" bktName := "bkt-name"
hc.h.ape.(*apeMock).err = errors.New("no permissions") hc.h.policyEngine.APE.(*apeMock).err = errors.New("no permissions")
box, _ := createAccessBox(t) box, _ := createAccessBox(t)
createBucketAssertS3Error(hc, bktName, box, apierr.ErrInternalError) createBucketAssertS3Error(hc, bktName, box, apierr.ErrInternalError)

View file

@ -44,6 +44,7 @@ import (
"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"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics"
policyengine "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
v2container "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container" v2container "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
@ -149,6 +150,7 @@ type (
removeOnReplaceTimeout time.Duration removeOnReplaceTimeout time.Duration
corsCopiesNumbers []uint32 corsCopiesNumbers []uint32
lifecycleCopiesNumbers []uint32 lifecycleCopiesNumbers []uint32
apeConvertersVersion policyengine.ConverterVersion
} }
maxClientsConfig struct { maxClientsConfig struct {
@ -407,6 +409,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
removeOnReplaceTimeout := fetchRemoveOnReplaceTimeout(v) removeOnReplaceTimeout := fetchRemoveOnReplaceTimeout(v)
corsCopiesNumbers := fetchCopiesNumbers(log, v, cfgCORSCopiesNumbers) corsCopiesNumbers := fetchCopiesNumbers(log, v, cfgCORSCopiesNumbers)
lifecycleCopiesNumbers := fetchCopiesNumbers(log, v, cfgLifecycleCopiesNumbers) lifecycleCopiesNumbers := fetchCopiesNumbers(log, v, cfgLifecycleCopiesNumbers)
apeConvertersVersion := fetchAPEConvertersVersion(log, v)
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -445,6 +448,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
s.removeOnReplaceTimeout = removeOnReplaceTimeout s.removeOnReplaceTimeout = removeOnReplaceTimeout
s.corsCopiesNumbers = corsCopiesNumbers s.corsCopiesNumbers = corsCopiesNumbers
s.lifecycleCopiesNumbers = lifecycleCopiesNumbers s.lifecycleCopiesNumbers = lifecycleCopiesNumbers
s.apeConvertersVersion = apeConvertersVersion
} }
func (s *appSettings) prepareVHSNamespaces(v *viper.Viper, log *zap.Logger, defaultNamespaces []string) map[string]bool { func (s *appSettings) prepareVHSNamespaces(v *viper.Viper, log *zap.Logger, defaultNamespaces []string) map[string]bool {
@ -718,6 +722,12 @@ func (s *appSettings) LifecycleCopiesNumbers() []uint32 {
return s.lifecycleCopiesNumbers return s.lifecycleCopiesNumbers
} }
func (s *appSettings) ConverterVersion() policyengine.ConverterVersion {
s.mu.RLock()
defer s.mu.RUnlock()
return s.apeConvertersVersion
}
func (a *App) initAPI(ctx context.Context, rpcCli *rpcclient.Client) { func (a *App) initAPI(ctx context.Context, rpcCli *rpcclient.Client) {
a.initLayer(ctx, rpcCli) a.initLayer(ctx, rpcCli)
@ -1294,7 +1304,13 @@ func (a *App) initMfaManager(ctx context.Context) *mfa.Manager {
func (a *App) initHandler(ctx context.Context) { func (a *App) initHandler(ctx context.Context) {
var err error var err error
manager := a.initMfaManager(ctx) manager := a.initMfaManager(ctx)
a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid, manager) policyEngine := handler.PolicyEngine{
APE: a.policyStorage,
Converter: policyengine.NewConverter(policyengine.Config{
VersionFetcher: a.settings,
}),
}
a.api, err = handler.New(a.log, a.obj, a.settings, policyEngine, a.frostfsid, manager)
if err != nil { if err != nil {
a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err), logs.TagField(logs.TagApp)) a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err), logs.TagField(logs.TagApp))
} }

View file

@ -19,6 +19,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
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"
policyengine "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -81,6 +82,8 @@ const (
defaultRemoveOnReplaceTimeout = 30 * time.Second defaultRemoveOnReplaceTimeout = 30 * time.Second
defaultRemoveOnReplaceQueue = 10000 defaultRemoveOnReplaceQueue = 10000
defaultFeaturesAPEConvertersVersion = policyengine.V1
) )
var ( var (
@ -291,6 +294,9 @@ const (
cfgRemoveOnReplaceTimeout = "features.remove_on_replace.timeout" cfgRemoveOnReplaceTimeout = "features.remove_on_replace.timeout"
cfgRemoveOnReplaceQueue = "features.remove_on_replace.queue" cfgRemoveOnReplaceQueue = "features.remove_on_replace.queue"
// APE.
cfgFeaturesAPEConvertersVersion = "features.ape.converters_version"
// FrostfsID. // FrostfsID.
cfgFrostfsIDContract = "frostfsid.contract" cfgFrostfsIDContract = "frostfsid.contract"
cfgFrostfsIDValidationEnabled = "frostfsid.validation.enabled" cfgFrostfsIDValidationEnabled = "frostfsid.validation.enabled"
@ -660,6 +666,19 @@ func fetchCopiesNumbers(l *zap.Logger, v *viper.Viper, param string) []uint32 {
return result return result
} }
func fetchAPEConvertersVersion(l *zap.Logger, v *viper.Viper) policyengine.ConverterVersion {
val := v.GetString(cfgFeaturesAPEConvertersVersion)
switch ver := policyengine.ConverterVersion(val); ver {
case policyengine.V1, policyengine.V2:
return ver
default:
l.Warn(logs.InvalidAPEConvertersVersion, zap.String("version", val),
zap.String("default", string(defaultFeaturesAPEConvertersVersion)),
logs.TagField(logs.TagApp))
return defaultFeaturesAPEConvertersVersion
}
}
type KludgeParams struct { type KludgeParams struct {
UseDefaultXMLNS bool UseDefaultXMLNS bool
BypassContentEncodingCheckInChunks bool BypassContentEncodingCheckInChunks bool

View file

@ -236,6 +236,8 @@ S3_GW_FEATURES_REMOVE_ON_REPLACE_ENABLED=false
S3_GW_FEATURES_REMOVE_ON_REPLACE_TIMEOUT=30s S3_GW_FEATURES_REMOVE_ON_REPLACE_TIMEOUT=30s
# Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that. # Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that.
S3_GW_FEATURES_REMOVE_ON_REPLACE_QUEUE=10000 S3_GW_FEATURES_REMOVE_ON_REPLACE_QUEUE=10000
#Version of Access Policy converters (Supported versions: `v1`, `v2`). Need to transform AWS IAM policy to supported FrostFS chain format.
S3_GW_FEATURES_APE_CONVERTERS_VERSION=v1
# ReadTimeout is the maximum duration for reading the entire # ReadTimeout is the maximum duration for reading the entire
# request, including the body. A zero or negative value means # request, including the body. A zero or negative value means

View file

@ -277,6 +277,9 @@ features:
timeout: 30s timeout: 30s
# Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that. # Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that.
queue: 10000 queue: 10000
ape:
# Version of Access Policy converters (Supported versions: `v1`, `v2`). Need to transform AWS IAM policy to supported FrostFS chain format.
converters_version: v1
web: web:
# ReadTimeout is the maximum duration for reading the entire # ReadTimeout is the maximum duration for reading the entire

View file

@ -753,16 +753,31 @@ features:
enabled: false enabled: false
timeout: 30s timeout: 30s
queue: 10000 queue: 10000
ape:
converters_version: v1
``` ```
| Parameter | Type | SIGHUP reload | Default value | Description | | Parameter | Type | SIGHUP reload | Default value | Description |
|-----------------------------|-------------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------|--------------------------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
| `md5.enabled` | `bool` | yes | `false` | Flag to enable return MD5 checksum in ETag headers and fields. | | `md5.enabled` | `bool` | yes | `false` | Flag to enable return MD5 checksum in ETag headers and fields. |
| `policy.deny_by_default` | `bool` | yes | `false` | Enable denying access for request that doesn't match any policy chain rules. | | `policy.deny_by_default` | `bool` | yes | `false` | Enable denying access for request that doesn't match any policy chain rules. |
| `tree_pool_netmap_support` | `bool` | no | `false` | Enable using new version of tree pool, which uses netmap to select nodes, for requests to tree service. | | `tree_pool_netmap_support` | `bool` | no | `false` | Enable using new version of tree pool, which uses netmap to select nodes, for requests to tree service. |
| `remove_on_replace.enabled` | `bool` | yes | `false` | Enable removing old object during PUT operation in unversioned/suspened bucket. | | `remove_on_replace.enabled` | `bool` | yes | `false` | Enable removing old object during PUT operation in unversioned/suspened bucket. |
| `remove_on_replace.timeout` | `durations` | yes | `30s` | Timeout to one delete operation in background. | | `remove_on_replace.timeout` | `durations` | yes | `30s` | Timeout to one delete operation in background. |
| `remove_on_replace.queue` | `int` | false | `10000` | Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that. | | `remove_on_replace.queue` | `int` | false | `10000` | Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that. |
| `ape` | [[]APE](#ape-subsection) | | | Access Policy Engine configuration. |
#### `ape` subsection
```yaml
features:
ape:
converters_version: v1
```
| Parameter | Type | SIGHUP reload | Default value | Description |
|----------------------|----------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------|
| `converters_version` | `string` | yes | `v1` | Version of Access Policy converters (Supported versions: `v1`, `v2`). Need to transform AWS IAM policy to supported FrostFS chain format. |
# `web` section # `web` section
@ -979,8 +994,8 @@ encryption:
```yaml ```yaml
contracts: contracts:
container: container:
name: container.frostfs name: container.frostfs
``` ```
| Parameter | Type | SIGHUP reload | Default value | Description | | Parameter | Type | SIGHUP reload | Default value | Description |

View file

@ -98,6 +98,7 @@ const (
InitRPCClientFailed = "init rpc client failed" InitRPCClientFailed = "init rpc client failed"
CouldNotFetchMFAContainerInfo = "couldn't fetch mfa container info" CouldNotFetchMFAContainerInfo = "couldn't fetch mfa container info"
CouldNotInitMFAClient = "couldn't init MFA client" CouldNotInitMFAClient = "couldn't init MFA client"
InvalidAPEConvertersVersion = "invalid ape converters version, default will be used"
) )
// Datapath. // Datapath.

View file

@ -0,0 +1,427 @@
package common
import (
"errors"
"fmt"
"net/netip"
"strconv"
"strings"
"time"
"unicode/utf8"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
)
const (
S3ActionAbortMultipartUpload = "s3:AbortMultipartUpload"
S3ActionCreateBucket = "s3:CreateBucket"
S3ActionDeleteBucket = "s3:DeleteBucket"
S3ActionDeleteBucketPolicy = "s3:DeleteBucketPolicy"
S3ActionDeleteObject = "s3:DeleteObject"
S3ActionDeleteObjectTagging = "s3:DeleteObjectTagging"
S3ActionDeleteObjectVersion = "s3:DeleteObjectVersion"
S3ActionDeleteObjectVersionTagging = "s3:DeleteObjectVersionTagging"
S3ActionGetBucketACL = "s3:GetBucketAcl"
S3ActionGetBucketCORS = "s3:GetBucketCORS"
S3ActionGetBucketLocation = "s3:GetBucketLocation"
S3ActionGetBucketNotification = "s3:GetBucketNotification"
S3ActionGetBucketObjectLockConfiguration = "s3:GetBucketObjectLockConfiguration"
S3ActionGetBucketPolicy = "s3:GetBucketPolicy"
S3ActionGetBucketPolicyStatus = "s3:GetBucketPolicyStatus"
S3ActionGetBucketTagging = "s3:GetBucketTagging"
S3ActionGetBucketVersioning = "s3:GetBucketVersioning"
S3ActionGetLifecycleConfiguration = "s3:GetLifecycleConfiguration"
S3ActionGetObject = "s3:GetObject"
S3ActionGetObjectACL = "s3:GetObjectAcl"
S3ActionGetObjectAttributes = "s3:GetObjectAttributes"
S3ActionGetObjectLegalHold = "s3:GetObjectLegalHold"
S3ActionGetObjectRetention = "s3:GetObjectRetention"
S3ActionGetObjectTagging = "s3:GetObjectTagging"
S3ActionGetObjectVersion = "s3:GetObjectVersion"
S3ActionGetObjectVersionACL = "s3:GetObjectVersionAcl"
S3ActionGetObjectVersionAttributes = "s3:GetObjectVersionAttributes"
S3ActionGetObjectVersionTagging = "s3:GetObjectVersionTagging"
S3ActionListAllMyBuckets = "s3:ListAllMyBuckets"
S3ActionListBucket = "s3:ListBucket"
S3ActionListBucketMultipartUploads = "s3:ListBucketMultipartUploads"
S3ActionListBucketVersions = "s3:ListBucketVersions"
S3ActionListMultipartUploadParts = "s3:ListMultipartUploadParts"
S3ActionPutBucketACL = "s3:PutBucketAcl"
S3ActionPutBucketCORS = "s3:PutBucketCORS"
S3ActionPutBucketNotification = "s3:PutBucketNotification"
S3ActionPutBucketObjectLockConfiguration = "s3:PutBucketObjectLockConfiguration"
S3ActionPutBucketPolicy = "s3:PutBucketPolicy"
S3ActionPutBucketTagging = "s3:PutBucketTagging"
S3ActionPutBucketVersioning = "s3:PutBucketVersioning"
S3ActionPutLifecycleConfiguration = "s3:PutLifecycleConfiguration"
S3ActionPutObject = "s3:PutObject"
S3ActionPutObjectACL = "s3:PutObjectAcl"
S3ActionPutObjectLegalHold = "s3:PutObjectLegalHold"
S3ActionPutObjectRetention = "s3:PutObjectRetention"
S3ActionPutObjectTagging = "s3:PutObjectTagging"
S3ActionPutObjectVersionACL = "s3:PutObjectVersionAcl"
S3ActionPutObjectVersionTagging = "s3:PutObjectVersionTagging"
S3ActionPatchObject = "s3:PatchObject"
S3ActionPutBucketPublicAccessBlock = "s3:PutBucketPublicAccessBlock"
S3ActionGetBucketPublicAccessBlock = "s3:GetBucketPublicAccessBlock"
)
const (
CondKeyAWSPrincipalARN = "aws:PrincipalArn"
CondKeyAWSSourceIP = "aws:SourceIp"
CondKeyAWSPrincipalTagPrefix = "aws:PrincipalTag/"
CondKeyAWSRequestTagPrefix = "aws:RequestTag/"
CondKeyAWSResourceTagPrefix = "aws:ResourceTag/"
UserClaimTagPrefix = "tag-"
)
const (
// String condition operators.
CondStringEquals string = "StringEquals"
CondStringNotEquals string = "StringNotEquals"
CondStringEqualsIgnoreCase string = "StringEqualsIgnoreCase"
CondStringNotEqualsIgnoreCase string = "StringNotEqualsIgnoreCase"
CondStringLike string = "StringLike"
CondStringNotLike string = "StringNotLike"
// Numeric condition operators.
CondNumericEquals string = "NumericEquals"
CondNumericNotEquals string = "NumericNotEquals"
CondNumericLessThan string = "NumericLessThan"
CondNumericLessThanEquals string = "NumericLessThanEquals"
CondNumericGreaterThan string = "NumericGreaterThan"
CondNumericGreaterThanEquals string = "NumericGreaterThanEquals"
// Date condition operators.
CondDateEquals string = "DateEquals"
CondDateNotEquals string = "DateNotEquals"
CondDateLessThan string = "DateLessThan"
CondDateLessThanEquals string = "DateLessThanEquals"
CondDateGreaterThan string = "DateGreaterThan"
CondDateGreaterThanEquals string = "DateGreaterThanEquals"
// Bolean condition operators.
CondBool string = "Bool"
// IP address condition operators.
CondIPAddress string = "IpAddress"
CondNotIPAddress string = "NotIpAddress"
// ARN condition operators.
CondArnEquals string = "ArnEquals"
CondArnLike string = "ArnLike"
CondArnNotEquals string = "ArnNotEquals"
CondArnNotLike string = "ArnNotLike"
// Custom condition operators.
CondSliceContains string = "SliceContains"
)
const (
ArnIAMPrefix = "arn:aws:iam::"
S3ResourcePrefix = "arn:aws:s3:::"
S3ActionPrefix = "s3:"
IamActionPrefix = "iam:"
)
var (
// ErrInvalidPrincipalFormat occurs when principal has unknown/unsupported format.
ErrInvalidPrincipalFormat = errors.New("invalid principal format")
// ErrInvalidResourceFormat occurs when resource has unknown/unsupported format.
ErrInvalidResourceFormat = errors.New("invalid resource format")
// ErrInvalidActionFormat occurs when action has unknown/unsupported format.
ErrInvalidActionFormat = errors.New("invalid action format")
// ErrActionsNotApplicable occurs when failed to convert any actions.
ErrActionsNotApplicable = errors.New("actions not applicable")
)
type FormPrincipalConditionFunc func(string) chain.Condition
type TransformConditionFunc func(gr GroupedConditions) (GroupedConditions, error)
func ConvertToChainConditions(c Conditions, transformer TransformConditionFunc) ([]GroupedConditions, error) {
conditions, err := ConvertToChainCondition(c)
if err != nil {
return nil, err
}
for i := range conditions {
if conditions[i], err = transformer(conditions[i]); err != nil {
return nil, fmt.Errorf("transform condition: %w", err)
}
}
return conditions, nil
}
type GroupedConditions struct {
Conditions []chain.Condition
Any bool
}
func ConvertToChainCondition(c Conditions) ([]GroupedConditions, error) {
var grouped []GroupedConditions
for op, KVs := range c {
condType, convertValue, err := getConditionTypeAndConverter(op)
if err != nil {
return nil, err
}
for key, values := range KVs {
group := GroupedConditions{
Conditions: make([]chain.Condition, len(values)),
Any: len(values) > 1,
}
for i, val := range values {
converted, err := convertValue(val)
if err != nil {
return nil, err
}
group.Conditions[i] = chain.Condition{
Op: condType,
Kind: chain.KindRequest,
Key: transformKey(key),
Value: converted,
}
}
grouped = append(grouped, group)
}
}
return grouped, nil
}
func transformKey(key string) string {
tagName, isTag := strings.CutPrefix(key, CondKeyAWSPrincipalTagPrefix)
if isTag {
return fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, UserClaimTagPrefix+tagName)
}
switch key {
case CondKeyAWSSourceIP:
return common.PropertyKeyFrostFSSourceIP
}
return key
}
func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFunction, error) {
switch {
case strings.HasPrefix(op, "String"):
switch op {
case CondStringEquals:
return chain.CondStringEquals, noConvertFunction, nil
case CondStringNotEquals:
return chain.CondStringNotEquals, noConvertFunction, nil
case CondStringEqualsIgnoreCase:
return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil
case CondStringNotEqualsIgnoreCase:
return chain.CondStringNotEqualsIgnoreCase, noConvertFunction, nil
case CondStringLike:
return chain.CondStringLike, noConvertFunction, nil
case CondStringNotLike:
return chain.CondStringNotLike, noConvertFunction, nil
default:
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
case strings.HasPrefix(op, "Arn"):
switch op {
case CondArnEquals:
return chain.CondStringEquals, noConvertFunction, nil
case CondArnNotEquals:
return chain.CondStringNotEquals, noConvertFunction, nil
case CondArnLike:
return chain.CondStringLike, noConvertFunction, nil
case CondArnNotLike:
return chain.CondStringNotLike, noConvertFunction, nil
default:
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
case strings.HasPrefix(op, "Numeric"):
return numericConditionTypeAndConverter(op)
case strings.HasPrefix(op, "Date"):
switch op {
case CondDateEquals:
return chain.CondStringEquals, dateConvertFunction, nil
case CondDateNotEquals:
return chain.CondStringNotEquals, dateConvertFunction, nil
case CondDateLessThan:
return chain.CondStringLessThan, dateConvertFunction, nil
case CondDateLessThanEquals:
return chain.CondStringLessThanEquals, dateConvertFunction, nil
case CondDateGreaterThan:
return chain.CondStringGreaterThan, dateConvertFunction, nil
case CondDateGreaterThanEquals:
return chain.CondStringGreaterThanEquals, dateConvertFunction, nil
default:
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
case op == CondBool:
return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil
case op == CondIPAddress:
return chain.CondIPAddress, IPConvertFunction, nil
case op == CondNotIPAddress:
return chain.CondNotIPAddress, IPConvertFunction, nil
case op == CondSliceContains:
return chain.CondSliceContains, noConvertFunction, nil
default:
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
}
func numericConditionTypeAndConverter(op string) (chain.ConditionType, convertFunction, error) {
switch op {
case CondNumericEquals:
return chain.CondNumericEquals, numericConvertFunction, nil
case CondNumericNotEquals:
return chain.CondNumericNotEquals, numericConvertFunction, nil
case CondNumericLessThan:
return chain.CondNumericLessThan, numericConvertFunction, nil
case CondNumericLessThanEquals:
return chain.CondNumericLessThanEquals, numericConvertFunction, nil
case CondNumericGreaterThan:
return chain.CondNumericGreaterThan, numericConvertFunction, nil
case CondNumericGreaterThanEquals:
return chain.CondNumericGreaterThanEquals, numericConvertFunction, nil
default:
return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op)
}
}
type convertFunction func(string) (string, error)
func noConvertFunction(val string) (string, error) {
return val, nil
}
func numericConvertFunction(val string) (string, error) {
if _, err := fixedn.Fixed8FromString(val); err == nil {
return val, nil
}
return "", fmt.Errorf("invalid numeric value: '%s'", val)
}
func IPConvertFunction(val string) (string, error) {
if _, err := netip.ParsePrefix(val); err != nil {
if _, err = netip.ParseAddr(val); err != nil {
return "", err
}
val += "/32"
}
return val, nil
}
func dateConvertFunction(val string) (string, error) {
if _, err := strconv.ParseInt(val, 10, 64); err == nil {
return val, nil
}
parsed, err := time.Parse(time.RFC3339, val)
if err != nil {
return "", err
}
return strconv.FormatInt(parsed.UTC().Unix(), 10), nil
}
func ParsePrincipalAsIAMUser(principal string) (account string, user string, err error) {
if !strings.HasPrefix(principal, ArnIAMPrefix) {
return "", "", ErrInvalidPrincipalFormat
}
// iam arn format arn:aws:iam::<account>:user/<user-name-with-path>
iamResource := strings.TrimPrefix(principal, ArnIAMPrefix)
sepIndex := strings.Index(iamResource, ":user/")
if sepIndex < 0 {
return "", "", ErrInvalidPrincipalFormat
}
account = iamResource[:sepIndex]
user = iamResource[sepIndex+6:]
if len(user) == 0 {
return "", "", ErrInvalidPrincipalFormat
}
userNameIndex := strings.LastIndexByte(user, '/')
if userNameIndex > -1 {
user = user[userNameIndex+1:]
if len(user) == 0 {
return "", "", ErrInvalidPrincipalFormat
}
}
return account, user, nil
}
func ValidateResource(resource string) error {
if resource == Wildcard {
return nil
}
if !strings.HasPrefix(resource, S3ResourcePrefix) && !strings.HasPrefix(resource, ArnIAMPrefix) {
return ErrInvalidResourceFormat
}
index := strings.IndexByte(resource, Wildcard[0])
if index != -1 && index != utf8.RuneCountInString(resource)-1 {
return ErrInvalidResourceFormat
}
return nil
}
func ValidateAction(action string) (bool, error) {
isIAM := strings.HasPrefix(action, IamActionPrefix)
if !strings.HasPrefix(action, S3ActionPrefix) && !isIAM {
return false, ErrInvalidActionFormat
}
index := strings.IndexByte(action, Wildcard[0])
if index != -1 && index != utf8.RuneCountInString(action)-1 {
return false, ErrInvalidActionFormat
}
return isIAM, nil
}
func SplitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition {
var orConditions []chain.Condition
commonConditions := make([]chain.Condition, 0, len(groupedConditions))
for _, grouped := range groupedConditions {
if grouped.Any {
orConditions = append(orConditions, grouped.Conditions...)
} else {
commonConditions = append(commonConditions, grouped.Conditions...)
}
}
if len(orConditions) == 0 {
return [][]chain.Condition{commonConditions}
}
res := make([][]chain.Condition, len(orConditions))
for i, condition := range orConditions {
res[i] = append([]chain.Condition{condition}, commonConditions...)
}
return res
}
func FormStatus(statement Statement) chain.Status {
status := chain.AccessDenied
if statement.Effect == AllowEffect {
status = chain.Allow
}
return status
}

View file

@ -0,0 +1,338 @@
package common
import (
"encoding/json"
"errors"
"fmt"
)
type (
// Policy grammar https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html
Policy struct {
Version string `json:"Version,omitempty"`
ID string `json:"Id,omitempty"`
Statement Statements `json:"Statement"`
}
Statements []Statement
Statement struct {
ID string `json:"Id,omitempty"`
SID string `json:"Sid,omitempty"`
Principal Principal `json:"Principal,omitempty"`
NotPrincipal Principal `json:"NotPrincipal,omitempty"`
Effect Effect `json:"Effect"`
Action Action `json:"Action,omitempty"`
NotAction Action `json:"NotAction,omitempty"`
Resource Resource `json:"Resource,omitempty"`
NotResource Resource `json:"NotResource,omitempty"`
Conditions Conditions `json:"Condition,omitempty"`
}
Principal map[PrincipalType][]string
Effect string
Action []string
Resource []string
Conditions map[string]Condition
Condition map[string][]string
PolicyType int
PrincipalType string
)
const policyVersion = "2012-10-17"
const (
GeneralPolicyType PolicyType = iota
IdentityBasedPolicyType
ResourceBasedPolicyType
)
const Wildcard = "*"
const (
AllowEffect Effect = "Allow"
DenyEffect Effect = "Deny"
)
func (e Effect) IsValid() bool {
return e == AllowEffect || e == DenyEffect
}
const (
AWSPrincipalType PrincipalType = "AWS"
FederatedPrincipalType PrincipalType = "Federated"
ServicePrincipalType PrincipalType = "Service"
CanonicalUserPrincipalType PrincipalType = "CanonicalUser"
)
func (p PrincipalType) IsValid() bool {
return p == AWSPrincipalType || p == FederatedPrincipalType ||
p == ServicePrincipalType || p == CanonicalUserPrincipalType
}
func (s *Statements) UnmarshalJSON(data []byte) error {
var list []Statement
if err := json.Unmarshal(data, &list); err == nil {
*s = list
return nil
}
var elem Statement
if err := json.Unmarshal(data, &elem); err != nil {
return err
}
*s = []Statement{elem}
return nil
}
func (p *Principal) UnmarshalJSON(data []byte) error {
*p = make(Principal)
var str string
if err := json.Unmarshal(data, &str); err == nil {
if str != Wildcard {
return errors.New("invalid IAM string principal")
}
(*p)[Wildcard] = nil
return nil
}
m := make(map[PrincipalType]any)
if err := json.Unmarshal(data, &m); err != nil {
return err
}
val, ok := m[AWSPrincipalType]
if ok {
str, ok = val.(string)
if ok && str == Wildcard && len(m) == 1 {
(*p)[Wildcard] = nil
return nil
}
}
for key, val := range m {
element, ok := val.(string)
if ok {
(*p)[key] = []string{element}
continue
}
list, ok := val.([]any)
if !ok {
return errors.New("invalid principal format")
}
resList := make([]string, len(list))
for i := range list {
val, ok := list[i].(string)
if !ok {
return errors.New("invalid principal format")
}
resList[i] = val
}
(*p)[key] = resList
}
return nil
}
func (a *Action) UnmarshalJSON(data []byte) error {
var list []string
if err := json.Unmarshal(data, &list); err == nil {
*a = list
return nil
}
var elem string
if err := json.Unmarshal(data, &elem); err != nil {
return err
}
*a = []string{elem}
return nil
}
func (r *Resource) UnmarshalJSON(data []byte) error {
var list []string
if err := json.Unmarshal(data, &list); err == nil {
*r = list
return nil
}
var elem string
if err := json.Unmarshal(data, &elem); err != nil {
return err
}
*r = []string{elem}
return nil
}
func (c *Condition) UnmarshalJSON(data []byte) error {
*c = make(Condition)
m := make(map[string]any)
if err := json.Unmarshal(data, &m); err != nil {
return err
}
for key, val := range m {
element, ok := val.(string)
if ok {
(*c)[key] = []string{element}
continue
}
list, ok := val.([]any)
if !ok {
return errors.New("invalid principal format")
}
resList := make([]string, len(list))
for i := range list {
val, ok := list[i].(string)
if !ok {
return errors.New("invalid principal format")
}
resList[i] = val
}
(*c)[key] = resList
}
return nil
}
func (p Policy) Validate(typ PolicyType) error {
if err := p.validate(); err != nil {
return err
}
switch typ {
case IdentityBasedPolicyType:
return p.validateIdentityBased()
case ResourceBasedPolicyType:
return p.validateResourceBased()
default:
return nil
}
}
func (p Policy) validate() error {
if p.Version != policyVersion {
return fmt.Errorf("invalid policy version, expected '%s', actual: '%s'", policyVersion, p.Version)
}
if len(p.Statement) == 0 {
return errors.New("'Statement' is missing")
}
sids := make(map[string]struct{}, len(p.Statement))
for _, statement := range p.Statement {
if _, ok := sids[statement.SID]; ok && statement.SID != "" {
return fmt.Errorf("duplicate 'SID': %s", statement.SID)
}
sids[statement.SID] = struct{}{}
if !statement.Effect.IsValid() {
return fmt.Errorf("unknown effect: '%s'", statement.Effect)
}
if len(statement.Action) != 0 && len(statement.NotAction) != 0 {
return errors.New("'Actions' and 'NotAction' are mutually exclusive")
}
if statement.Resource != nil && statement.NotResource != nil {
return errors.New("'Resources' and 'NotResource' are mutually exclusive")
}
if len(statement.Resource) == 0 && len(statement.NotResource) == 0 {
return errors.New("one of 'Resources'/'NotResource' must be provided")
}
if len(statement.Principal) != 0 && len(statement.NotPrincipal) != 0 {
return errors.New("'Principal' and 'NotPrincipal' are mutually exclusive")
}
if len(statement.NotPrincipal) != 0 && statement.Effect != DenyEffect {
return errors.New("using 'NotPrincipal' with effect 'Allow' is not supported")
}
principal, _ := statement.GetPrincipal()
if err := principal.validate(); err != nil {
return err
}
}
return nil
}
func (p Policy) validateIdentityBased() error {
if len(p.ID) != 0 {
return errors.New("'Id' is not allowed for identity-based policy")
}
for _, statement := range p.Statement {
if len(statement.Principal) != 0 || len(statement.NotPrincipal) != 0 {
return errors.New("'Principal' and 'NotPrincipal' are not allowed for identity-based policy")
}
}
return nil
}
func (p Policy) validateResourceBased() error {
for _, statement := range p.Statement {
if len(statement.Principal) == 0 && len(statement.NotPrincipal) == 0 {
return errors.New("'Principal' or 'NotPrincipal' must be provided for resource-based policy")
}
}
return nil
}
func (s Statement) GetPrincipal() (Principal, bool) {
if len(s.NotPrincipal) != 0 {
return s.NotPrincipal, true
}
return s.Principal, false
}
func (s Statement) GetAction() (Action, bool) {
if len(s.NotAction) != 0 {
return s.NotAction, true
}
return s.Action, false
}
func (s Statement) GetResource() (Resource, bool) {
if len(s.NotResource) != 0 {
return s.NotResource, true
}
return s.Resource, false
}
func (p Principal) validate() error {
if _, ok := p[Wildcard]; ok && len(p) == 1 {
return nil
}
for key := range p {
if !key.IsValid() {
return fmt.Errorf("unknown principal type: '%s'", key)
}
}
return nil
}

View file

@ -0,0 +1,479 @@
package common
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestUnmarshalIAMPolicy(t *testing.T) {
t.Run("simple fields", func(t *testing.T) {
policy := `{
"Version": "2012-10-17",
"Id": "PutObjPolicy",
"Statement": {
"Sid": "DenyObjectsThatAreNotSSEKMS",
"Principal": "*",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": "true"
}
}
}
}`
expected := Policy{
Version: "2012-10-17",
ID: "PutObjPolicy",
Statement: []Statement{{
SID: "DenyObjectsThatAreNotSSEKMS",
Principal: map[PrincipalType][]string{
"*": nil,
},
Effect: DenyEffect,
Action: []string{"s3:PutObject"},
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
Conditions: map[string]Condition{
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": {"true"},
},
},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
})
t.Run("complex fields", func(t *testing.T) {
policy := `{
"Version": "2012-10-17",
"Statement": [{
"Principal":{
"AWS":[
"arn:aws:iam::111122223333:user/JohnDoe"
]
},
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
],
"Condition": {
"StringEquals": {
"s3:RequestObjectTag/Department": ["Finance"]
}
}
}]
}`
expected := Policy{
Version: "2012-10-17",
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
},
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
Conditions: map[string]Condition{
"StringEquals": {
"s3:RequestObjectTag/Department": {"Finance"},
},
},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
raw, err := json.Marshal(expected)
require.NoError(t, err)
require.JSONEq(t, policy, string(raw))
})
t.Run("check principal AWS", func(t *testing.T) {
policy := `{
"Statement": [{
"Principal":{
"AWS":"arn:aws:iam::111122223333:user/JohnDoe"
}
}]
}`
expected := Policy{
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
})
t.Run("native example", func(t *testing.T) {
policy := `
{
"Version": "xyz",
"Statement": [
{
"Effect": "Allow",
"Action": [
"native:*",
"s3:PutObject",
"s3:GetObject"
],
"Resource": ["*"],
"Principal": {"FrostFS": ["did:frostfs:039e3ee771a223361fe7862f532e9511b57baaae3c3e2622682e99d0e660f7671"]},
"Condition": {"StringEquals": {"native::object::attribute": "iamuser-admin"}}
}
]
}`
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
})
t.Run("condition array", func(t *testing.T) {
policy := `
{
"Statement": [{
"Condition": {"StringLike": {"ec2:InstanceType": ["t1.*", "t2.*", "m3.*"]}}
}]
}`
expected := Policy{
Statement: []Statement{{
Conditions: map[string]Condition{
"StringLike": {"ec2:InstanceType": {"t1.*", "t2.*", "m3.*"}},
},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
})
t.Run("'Not*' fields", func(t *testing.T) {
policy := `
{
"Id": "PutObjPolicy",
"Statement": [{
"NotPrincipal": {"AWS":["arn:aws:iam::111122223333:user/Alice"]},
"Effect": "Deny",
"NotAction": "s3:PutObject",
"NotResource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
}]
}`
expected := Policy{
ID: "PutObjPolicy",
Statement: []Statement{{
NotPrincipal: map[PrincipalType][]string{
AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"},
},
Effect: DenyEffect,
NotAction: []string{"s3:PutObject"},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
})
}
func TestValidatePolicies(t *testing.T) {
for _, tc := range []struct {
name string
policy Policy
typ PolicyType
isValid bool
}{
{
name: "valid permission boundaries",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: true,
},
{
name: "general invalid effect",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: "dummy",
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid principal block",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
Principal: map[PrincipalType][]string{Wildcard: nil},
NotPrincipal: map[PrincipalType][]string{Wildcard: nil},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid not principal",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid principal type",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{"dummy": {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid action block",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
NotAction: []string{"iam:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid resource block",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Resource: []string{Wildcard},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "invalid resource block",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Resource: []string{},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "missing resource block",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "missing statement block",
policy: Policy{},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "duplicate sid",
policy: Policy{
Version: policyVersion,
Statement: []Statement{
{
SID: "sid",
Effect: AllowEffect,
Action: []string{"s3:*"},
Resource: []string{Wildcard},
},
{
SID: "sid",
Effect: AllowEffect,
Action: []string{"cloudwatch:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "missing version",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "identity based valid",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
}},
},
typ: IdentityBasedPolicyType,
isValid: true,
},
{
name: "identity based invalid because of id presence",
policy: Policy{
ID: "some-id",
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
}},
},
typ: IdentityBasedPolicyType,
isValid: false,
},
{
name: "identity based invalid because of principal presence",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: IdentityBasedPolicyType,
isValid: false,
},
{
name: "identity based invalid because of not principal presence",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: IdentityBasedPolicyType,
isValid: false,
},
{
name: "resource based valid principal",
policy: Policy{
Version: policyVersion,
Statement: []Statement{{
Effect: DenyEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: ResourceBasedPolicyType,
isValid: true,
},
{
name: "resource based valid not principal",
policy: Policy{
ID: "some-id",
Version: policyVersion,
Statement: []Statement{{
Effect: DenyEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: ResourceBasedPolicyType,
isValid: true,
},
{
name: "resource based invalid missing principal",
policy: Policy{
ID: "some-id",
Version: policyVersion,
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
}},
},
typ: ResourceBasedPolicyType,
isValid: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
err := tc.policy.Validate(tc.typ)
if tc.isValid {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}

View file

@ -0,0 +1,15 @@
package common
type NativeResolver interface {
GetUserKey(account, name string) (string, error)
GetBucketInfo(bucket string) (*BucketInfo, error)
}
type BucketInfo struct {
Namespace string
Container string
}
type S3Resolver interface {
GetUserAddress(account, user string) (string, error)
}

View file

@ -0,0 +1,118 @@
package policyengine
import (
"fmt"
s3common "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine/common"
v2 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine/v2/iam"
v1 "git.frostfs.info/TrueCloudLab/policy-engine/iam"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
)
type Converter struct {
versionFetcher VersionFetcher
}
type Config struct {
VersionFetcher VersionFetcher
}
type VersionFetcher interface {
ConverterVersion() ConverterVersion
}
type ConverterVersion string
const (
V1 ConverterVersion = "v1"
V2 ConverterVersion = "v2"
)
func NewConverter(cfg Config) *Converter {
return &Converter{
versionFetcher: cfg.VersionFetcher,
}
}
func (c *Converter) ToS3Chain(p s3common.Policy, resolver s3common.S3Resolver) (*chain.Chain, error) {
switch v := c.versionFetcher.ConverterVersion(); v {
case V1:
return v1.ConvertToS3Chain(commonPolicyToV1(p), resolver)
case V2:
return v2.ConvertToS3Chain(p, resolver)
default:
return nil, fmt.Errorf("unknown converter version: %s", v)
}
}
func (c *Converter) ToNativeChain(p s3common.Policy, resolver s3common.NativeResolver) (*chain.Chain, error) {
switch v := c.versionFetcher.ConverterVersion(); v {
case V1:
return v1.ConvertToNativeChain(commonPolicyToV1(p), nativeResolverV1Wrapper{resolver: resolver})
case V2:
return v2.ConvertToNativeChain(p, resolver)
default:
return nil, fmt.Errorf("unknown converter version: %s", v)
}
}
func commonPolicyToV1(p s3common.Policy) v1.Policy {
res := v1.Policy{
Version: p.Version,
ID: p.ID,
Statement: make(v1.Statements, len(p.Statement)),
}
for i, statement := range p.Statement {
res.Statement[i] = v1.Statement{
ID: statement.ID,
SID: statement.SID,
Principal: make(map[v1.PrincipalType][]string, len(statement.Principal)),
NotPrincipal: make(map[v1.PrincipalType][]string, len(statement.NotPrincipal)),
Effect: v1.Effect(statement.Effect),
Action: v1.Action(statement.Action),
NotAction: v1.Action(statement.NotAction),
Resource: v1.Resource(statement.Resource),
NotResource: v1.Resource(statement.NotResource),
Conditions: make(map[string]v1.Condition, len(statement.Conditions)),
}
for k, principal := range statement.Principal {
res.Statement[i].Principal[v1.PrincipalType(k)] = principal
}
for k, principal := range statement.NotPrincipal {
res.Statement[i].NotPrincipal[v1.PrincipalType(k)] = principal
}
for k, condition := range statement.Conditions {
res.Statement[i].Conditions[k] = v1.Condition(condition)
}
}
return res
}
type nativeResolverV1Wrapper struct {
resolver s3common.NativeResolver
}
func (w nativeResolverV1Wrapper) GetUserKey(account, name string) (string, error) {
res, err := w.resolver.GetUserKey(account, name)
if err != nil {
return "", err
}
return res, nil
}
func (w nativeResolverV1Wrapper) GetBucketInfo(bucket string) (*v1.BucketInfo, error) {
res, err := w.resolver.GetBucketInfo(bucket)
if err != nil {
return nil, err
}
return &v1.BucketInfo{
Namespace: res.Namespace,
Container: res.Container,
}, nil
}

View file

@ -0,0 +1,420 @@
package iam
import (
"errors"
"fmt"
"strings"
s3common "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine/common"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
)
const PropertyKeyFilePath = "FilePath"
var actionToNativeOpMap = map[string][]string{
s3common.S3ActionAbortMultipartUpload: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionCreateBucket: {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionDeleteBucket: {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodGetObject},
s3common.S3ActionDeleteBucketPolicy: {native.MethodGetContainer},
s3common.S3ActionDeleteObject: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
s3common.S3ActionDeleteObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionDeleteObjectVersion: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
s3common.S3ActionDeleteObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionGetBucketACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject},
s3common.S3ActionGetBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
s3common.S3ActionGetBucketLocation: {native.MethodGetContainer},
s3common.S3ActionGetBucketNotification: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
s3common.S3ActionGetBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject},
s3common.S3ActionGetBucketPolicy: {native.MethodGetContainer},
s3common.S3ActionGetBucketPolicyStatus: {native.MethodGetContainer},
s3common.S3ActionGetBucketTagging: {native.MethodGetContainer, native.MethodGetObject},
s3common.S3ActionGetBucketVersioning: {native.MethodGetContainer, native.MethodGetObject},
s3common.S3ActionGetLifecycleConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
s3common.S3ActionGetObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
s3common.S3ActionGetObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
s3common.S3ActionGetObjectAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
s3common.S3ActionGetObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
s3common.S3ActionGetObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
s3common.S3ActionGetObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
s3common.S3ActionGetObjectVersion: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
s3common.S3ActionGetObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
s3common.S3ActionGetObjectVersionAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
s3common.S3ActionGetObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject},
s3common.S3ActionListAllMyBuckets: {native.MethodListContainers, native.MethodGetContainer},
s3common.S3ActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
s3common.S3ActionListBucketMultipartUploads: {native.MethodGetContainer, native.MethodGetObject},
s3common.S3ActionListBucketVersions: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
s3common.S3ActionListMultipartUploadParts: {native.MethodGetContainer, native.MethodGetObject},
s3common.S3ActionPutBucketACL: {native.MethodGetContainer, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionPutBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionPutBucketNotification: {native.MethodGetContainer, native.MethodHeadObject, native.MethodDeleteObject, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionPutBucketObjectLockConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionPutBucketPolicy: {native.MethodGetContainer},
s3common.S3ActionPutBucketTagging: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionPutBucketVersioning: {native.MethodGetContainer, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionPutLifecycleConfiguration: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPutObject, native.MethodDeleteObject},
s3common.S3ActionPutObject: {native.MethodGetContainer, native.MethodPutObject, native.MethodGetObject, native.MethodHeadObject, native.MethodRangeObject},
s3common.S3ActionPutObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
s3common.S3ActionPutObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionPutObjectRetention: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionPutObjectTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionPutObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
s3common.S3ActionPutObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject, native.MethodGetObject, native.MethodPutObject},
s3common.S3ActionPatchObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodPatchObject, native.MethodPutObject, native.MethodRangeObject},
s3common.S3ActionPutBucketPublicAccessBlock: {native.MethodGetContainer, native.MethodPutObject, native.MethodDeleteObject, native.MethodGetObject},
s3common.S3ActionGetBucketPublicAccessBlock: {native.MethodGetContainer, native.MethodGetObject},
}
var containerNativeOperations = map[string]struct{}{
native.MethodPutContainer: {},
native.MethodDeleteContainer: {},
native.MethodGetContainer: {},
native.MethodListContainers: {},
native.MethodSetContainerEACL: {},
native.MethodGetContainerEACL: {},
}
var objectNativeOperations = map[string]struct{}{
native.MethodGetObject: {},
native.MethodPutObject: {},
native.MethodHeadObject: {},
native.MethodDeleteObject: {},
native.MethodSearchObject: {},
native.MethodRangeObject: {},
native.MethodHashObject: {},
}
var errConditionKeyNotApplicable = errors.New("condition key is not applicable")
func ConvertToNativeChain(p s3common.Policy, resolver s3common.NativeResolver) (*chain.Chain, error) {
if err := p.Validate(s3common.ResourceBasedPolicyType); err != nil {
return nil, err
}
var engineChain chain.Chain
for _, statement := range p.Statement {
status := s3common.FormStatus(statement)
if status != chain.Allow {
// Most s3 methods share the same native operations. Deny rules must not affect shared native operations,
// therefore this code skips all deny rules for native protocol. Deny is applied for s3 protocol only, in this case.
continue
}
action, actionInverted := statement.GetAction()
nativeActions, err := formNativeActionNames(action)
if err != nil {
return nil, err
}
ruleAction := chain.Actions{Inverted: actionInverted, Names: nativeActions}
if len(ruleAction.Names) == 0 {
continue
}
resource, resourceInverted := statement.GetResource()
groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver, getActionTypes(nativeActions))
if err != nil {
return nil, err
}
groupedConditions, err := convertToNativeChainCondition(statement.Conditions, resolver)
if err != nil {
if errors.Is(err, errConditionKeyNotApplicable) {
continue
}
return nil, err
}
splitConditions := s3common.SplitGroupedConditions(groupedConditions)
principals, principalCondFn, err := getNativePrincipalsAndConditionFunc(statement, resolver)
if err != nil {
return nil, err
}
for _, groupedResource := range groupedResources {
for _, principal := range principals {
for _, conditions := range splitConditions {
var principalCondition []chain.Condition
if principal != s3common.Wildcard {
principalCondition = []chain.Condition{principalCondFn(principal)}
}
ruleConditions := append(principalCondition, groupedResource.Conditions...)
r := chain.Rule{
Status: status,
Actions: ruleAction,
Resources: chain.Resources{
Inverted: resourceInverted,
Names: groupedResource.Names,
},
Condition: append(ruleConditions, conditions...),
}
engineChain.Rules = append(engineChain.Rules, r)
}
}
}
}
if len(engineChain.Rules) == 0 {
return nil, s3common.ErrActionsNotApplicable
}
return &engineChain, nil
}
func getActionTypes(nativeActions []string) ActionTypes {
var res ActionTypes
for _, action := range nativeActions {
if res.Object && res.Container {
break
}
_, isObj := objectNativeOperations[action]
_, isCnr := containerNativeOperations[action]
res.Object = res.Object || isObj || action == s3common.Wildcard
res.Container = res.Container || isCnr || action == s3common.Wildcard
}
return res
}
func getNativePrincipalsAndConditionFunc(statement s3common.Statement, resolver s3common.NativeResolver) ([]string, s3common.FormPrincipalConditionFunc, error) {
var principals []string
var op chain.ConditionType
statementPrincipal, inverted := statement.GetPrincipal()
if _, ok := statementPrincipal[s3common.Wildcard]; ok { // this can be true only if 'inverted' false
principals = []string{s3common.Wildcard}
op = chain.CondStringLike
} else {
for principalType, principal := range statementPrincipal {
if principalType != s3common.AWSPrincipalType {
return nil, nil, fmt.Errorf("unsupported principal type '%s'", principalType)
}
parsedPrincipal, err := formNativePrincipal(principal, resolver)
if err != nil {
return nil, nil, fmt.Errorf("parse principal: %w", err)
}
principals = append(principals, parsedPrincipal...)
}
op = chain.CondStringEquals
if inverted {
op = chain.CondStringNotEquals
}
}
return principals, func(principal string) chain.Condition {
return chain.Condition{
Op: op,
Kind: chain.KindRequest,
Key: native.PropertyKeyActorPublicKey,
Value: principal,
}
}, nil
}
func convertToNativeChainCondition(c s3common.Conditions, resolver s3common.NativeResolver) ([]s3common.GroupedConditions, error) {
return s3common.ConvertToChainConditions(c, func(gr s3common.GroupedConditions) (s3common.GroupedConditions, error) {
res := s3common.GroupedConditions{
Conditions: make([]chain.Condition, 0, len(gr.Conditions)),
Any: gr.Any,
}
for i := range gr.Conditions {
switch {
case gr.Conditions[i].Key == condKeyAWSMFAPresent:
return s3common.GroupedConditions{}, errConditionKeyNotApplicable
case gr.Conditions[i].Key == s3common.CondKeyAWSPrincipalARN:
gr.Conditions[i].Key = native.PropertyKeyActorPublicKey
val, err := formPrincipalKey(gr.Conditions[i].Value, resolver)
if err != nil {
return s3common.GroupedConditions{}, err
}
gr.Conditions[i].Value = val
res.Conditions = append(res.Conditions, gr.Conditions[i])
case strings.HasPrefix(gr.Conditions[i].Key, s3common.CondKeyAWSRequestTagPrefix) ||
strings.HasPrefix(gr.Conditions[i].Key, s3common.CondKeyAWSResourceTagPrefix):
// Tags exist only in S3 requests, so native protocol should not process such conditions.
continue
default:
res.Conditions = append(res.Conditions, gr.Conditions[i])
}
}
return res, nil
})
}
type GroupedResources struct {
Names []string
Conditions []chain.Condition
}
type ActionTypes struct {
Object bool
Container bool
}
func formNativeResourceNamesAndConditions(names []string, resolver s3common.NativeResolver, actionTypes ActionTypes) ([]GroupedResources, error) {
if !actionTypes.Object && !actionTypes.Container {
return nil, s3common.ErrActionsNotApplicable
}
res := make([]GroupedResources, 0, len(names))
combined := make(map[string]struct{})
for _, resource := range names {
if err := s3common.ValidateResource(resource); err != nil {
return nil, err
}
if resource == s3common.Wildcard {
res = res[:0]
return append(res, formWildcardNativeResource(actionTypes)), nil
}
if !strings.HasPrefix(resource, s3common.S3ResourcePrefix) {
continue
}
var bkt, obj string
s3Resource := strings.TrimPrefix(resource, s3common.S3ResourcePrefix)
if s3Resource == s3common.Wildcard {
res = res[:0]
return append(res, formWildcardNativeResource(actionTypes)), nil
}
if sepIndex := strings.Index(s3Resource, "/"); sepIndex < 0 {
bkt = s3Resource
} else {
bkt = s3Resource[:sepIndex]
obj = s3Resource[sepIndex+1:]
if len(obj) == 0 {
obj = s3common.Wildcard
}
}
bktInfo, err := resolver.GetBucketInfo(bkt)
if err != nil {
return nil, err
}
if obj == s3common.Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/*
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
continue
}
if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
continue
}
res = append(res, GroupedResources{
Names: []string{
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container),
fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container),
},
Conditions: []chain.Condition{
{
Op: chain.CondStringLike,
Kind: chain.KindResource,
Key: PropertyKeyFilePath,
Value: obj,
},
},
})
}
if len(combined) != 0 {
gr := GroupedResources{Names: make([]string, 0, len(combined))}
for key := range combined {
gr.Names = append(gr.Names, key)
}
res = append(res, gr)
}
return res, nil
}
func formWildcardNativeResource(actionTypes ActionTypes) GroupedResources {
groupedNames := make([]string, 0, 2)
if actionTypes.Object {
groupedNames = append(groupedNames, native.ResourceFormatAllObjects)
}
if actionTypes.Container {
groupedNames = append(groupedNames, native.ResourceFormatAllContainers)
}
return GroupedResources{Names: groupedNames}
}
func formNativePrincipal(principal []string, resolver s3common.NativeResolver) ([]string, error) {
res := make([]string, len(principal))
var err error
for i := range principal {
if res[i], err = formPrincipalKey(principal[i], resolver); err != nil {
return nil, err
}
}
return res, nil
}
func formPrincipalKey(principal string, resolver s3common.NativeResolver) (string, error) {
account, user, err := s3common.ParsePrincipalAsIAMUser(principal)
if err != nil {
return "", err
}
key, err := resolver.GetUserKey(account, user)
if err != nil {
return "", fmt.Errorf("get user key: %w", err)
}
return key, nil
}
func formNativeActionNames(names []string) ([]string, error) {
uniqueActions := make(map[string]struct{}, len(names))
for _, action := range names {
if action == s3common.Wildcard {
return []string{s3common.Wildcard}, nil
}
isIAM, err := s3common.ValidateAction(action)
if err != nil {
return nil, err
}
if isIAM {
continue
}
if action[len(s3common.S3ActionPrefix):] == s3common.Wildcard {
return []string{s3common.Wildcard}, nil
}
nativeActions := actionToNativeOpMap[action]
if len(nativeActions) == 0 {
return nil, s3common.ErrActionsNotApplicable
}
for _, nativeAction := range nativeActions {
uniqueActions[nativeAction] = struct{}{}
}
}
res := make([]string, 0, len(uniqueActions))
for key := range uniqueActions {
res = append(res, key)
}
return res, nil
}

View file

@ -0,0 +1,269 @@
package iam
import (
"fmt"
"strings"
s3common "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine/common"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
)
const condKeyAWSMFAPresent = "aws:MultiFactorAuthPresent"
var actionToS3OpMap = map[string][]string{
s3common.S3ActionAbortMultipartUpload: {s3common.S3ActionAbortMultipartUpload},
s3common.S3ActionCreateBucket: {s3common.S3ActionCreateBucket},
s3common.S3ActionDeleteBucket: {s3common.S3ActionDeleteBucket},
s3common.S3ActionDeleteBucketPolicy: {s3common.S3ActionDeleteBucketPolicy},
s3common.S3ActionDeleteObjectTagging: {s3common.S3ActionDeleteObjectTagging},
s3common.S3ActionGetBucketLocation: {s3common.S3ActionGetBucketLocation},
s3common.S3ActionGetBucketNotification: {s3common.S3ActionGetBucketNotification},
s3common.S3ActionGetBucketPolicy: {s3common.S3ActionGetBucketPolicy},
s3common.S3ActionGetBucketPolicyStatus: {s3common.S3ActionGetBucketPolicyStatus},
s3common.S3ActionGetBucketTagging: {s3common.S3ActionGetBucketTagging},
s3common.S3ActionGetBucketVersioning: {s3common.S3ActionGetBucketVersioning},
s3common.S3ActionGetObjectAttributes: {s3common.S3ActionGetObjectAttributes},
s3common.S3ActionGetObjectLegalHold: {s3common.S3ActionGetObjectLegalHold},
s3common.S3ActionGetObjectRetention: {s3common.S3ActionGetObjectRetention},
s3common.S3ActionGetObjectTagging: {s3common.S3ActionGetObjectTagging},
s3common.S3ActionPutBucketNotification: {s3common.S3ActionPutBucketNotification},
s3common.S3ActionPutBucketPolicy: {s3common.S3ActionPutBucketPolicy},
s3common.S3ActionPutBucketVersioning: {s3common.S3ActionPutBucketVersioning},
s3common.S3ActionPutObjectLegalHold: {s3common.S3ActionPutObjectLegalHold},
s3common.S3ActionPutObjectRetention: {s3common.S3ActionPutObjectRetention},
s3common.S3ActionPutObjectTagging: {s3common.S3ActionPutObjectTagging},
s3common.S3ActionPatchObject: {s3common.S3ActionPatchObject},
s3common.S3ActionListAllMyBuckets: {"s3:ListBuckets"},
s3common.S3ActionListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"},
s3common.S3ActionListBucketVersions: {"s3:ListBucketObjectVersions"},
s3common.S3ActionListBucketMultipartUploads: {"s3:ListMultipartUploads"},
s3common.S3ActionGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"},
s3common.S3ActionGetLifecycleConfiguration: {"s3:GetBucketLifecycle"},
s3common.S3ActionGetBucketACL: {"s3:GetBucketACL"},
s3common.S3ActionGetBucketCORS: {"s3:GetBucketCors"},
s3common.S3ActionPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"},
s3common.S3ActionPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"},
s3common.S3ActionPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"},
s3common.S3ActionPutBucketACL: {"s3:PutBucketACL"},
s3common.S3ActionPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"},
s3common.S3ActionListMultipartUploadParts: {"s3:ListParts"},
s3common.S3ActionGetObjectACL: {"s3:GetObjectACL"},
s3common.S3ActionGetObject: {"s3:GetObject", "s3:HeadObject"},
s3common.S3ActionGetObjectVersion: {"s3:GetObject", "s3:HeadObject"},
s3common.S3ActionGetObjectVersionACL: {"s3:GetObjectACL"},
s3common.S3ActionGetObjectVersionAttributes: {"s3:GetObjectAttributes"},
s3common.S3ActionGetObjectVersionTagging: {"s3:GetObjectTagging"},
s3common.S3ActionPutObjectACL: {"s3:PutObjectACL"},
s3common.S3ActionPutObjectVersionACL: {"s3:PutObjectACL"},
s3common.S3ActionPutObjectVersionTagging: {"s3:PutObjectTagging"},
s3common.S3ActionPutObject: {
"s3:PutObject", "s3:PostObject", "s3:CopyObject",
"s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload",
},
s3common.S3ActionDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"},
s3common.S3ActionDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
s3common.S3ActionDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
s3common.S3ActionPutBucketPublicAccessBlock: {"s3:PutPublicAccessBlock", "s3:DeletePublicAccessBlock"},
s3common.S3ActionGetBucketPublicAccessBlock: {"s3:GetPublicAccessBlock"},
}
func ConvertToS3Chain(p s3common.Policy, resolver s3common.S3Resolver) (*chain.Chain, error) {
if err := p.Validate(s3common.ResourceBasedPolicyType); err != nil {
return nil, err
}
var engineChain chain.Chain
for _, statement := range p.Statement {
status := s3common.FormStatus(statement)
actions, actionInverted := statement.GetAction()
s3Actions, err := formS3ActionNames(actions)
if err != nil {
return nil, err
}
ruleAction := chain.Actions{Inverted: actionInverted, Names: s3Actions}
if len(ruleAction.Names) == 0 {
continue
}
resources, resourceInverted := statement.GetResource()
if err := validateS3ResourceNames(resources); err != nil {
return nil, err
}
ruleResource := chain.Resources{Inverted: resourceInverted, Names: resources}
groupedConditions, err := convertToS3ChainCondition(statement.Conditions, resolver)
if err != nil {
return nil, err
}
splitConditions := s3common.SplitGroupedConditions(groupedConditions)
principals, principalCondFn, err := getS3PrincipalsAndConditionFunc(statement, resolver)
if err != nil {
return nil, err
}
for _, principal := range principals {
for _, conditions := range splitConditions {
var principalCondition []chain.Condition
if principal != s3common.Wildcard {
principalCondition = []chain.Condition{principalCondFn(principal)}
}
r := chain.Rule{
Status: status,
Actions: ruleAction,
Resources: ruleResource,
Condition: append(principalCondition, conditions...),
}
engineChain.Rules = append(engineChain.Rules, r)
}
}
}
if len(engineChain.Rules) == 0 {
return nil, s3common.ErrActionsNotApplicable
}
return &engineChain, nil
}
func getS3PrincipalsAndConditionFunc(statement s3common.Statement, resolver s3common.S3Resolver) ([]string, s3common.FormPrincipalConditionFunc, error) {
var principals []string
var op chain.ConditionType
statementPrincipal, inverted := statement.GetPrincipal()
if _, ok := statementPrincipal[s3common.Wildcard]; ok { // this can be true only if 'inverted' false
principals = []string{s3common.Wildcard}
op = chain.CondStringLike
} else {
for principalType, principal := range statementPrincipal {
if principalType != s3common.AWSPrincipalType {
return nil, nil, fmt.Errorf("unsupported principal type '%s'", principalType)
}
parsedPrincipal, err := formS3Principal(principal, resolver)
if err != nil {
return nil, nil, fmt.Errorf("parse principal: %w", err)
}
principals = append(principals, parsedPrincipal...)
}
op = chain.CondStringEquals
if inverted {
op = chain.CondStringNotEquals
}
}
return principals, func(principal string) chain.Condition {
return chain.Condition{
Op: op,
Kind: chain.KindRequest,
Key: s3.PropertyKeyOwner,
Value: principal,
}
}, nil
}
func convertToS3ChainCondition(c s3common.Conditions, resolver s3common.S3Resolver) ([]s3common.GroupedConditions, error) {
return s3common.ConvertToChainConditions(c, func(gr s3common.GroupedConditions) (s3common.GroupedConditions, error) {
for i := range gr.Conditions {
switch {
case gr.Conditions[i].Key == s3common.CondKeyAWSPrincipalARN:
gr.Conditions[i].Key = s3.PropertyKeyOwner
val, err := formPrincipalOwner(gr.Conditions[i].Value, resolver)
if err != nil {
return s3common.GroupedConditions{}, err
}
gr.Conditions[i].Value = val
case gr.Conditions[i].Key == condKeyAWSMFAPresent:
gr.Conditions[i].Key = s3.PropertyKeyAccessBoxAttrMFA
case strings.HasPrefix(gr.Conditions[i].Key, s3common.CondKeyAWSResourceTagPrefix):
gr.Conditions[i].Kind = chain.KindResource
}
}
return gr, nil
})
}
func formS3Principal(principal []string, resolver s3common.S3Resolver) ([]string, error) {
res := make([]string, len(principal))
var err error
for i := range principal {
if res[i], err = formPrincipalOwner(principal[i], resolver); err != nil {
return nil, err
}
}
return res, nil
}
func formPrincipalOwner(principal string, resolver s3common.S3Resolver) (string, error) {
account, user, err := s3common.ParsePrincipalAsIAMUser(principal)
if err != nil {
return "", err
}
address, err := resolver.GetUserAddress(account, user)
if err != nil {
return "", fmt.Errorf("get user address: %w", err)
}
return address, nil
}
func validateS3ResourceNames(names []string) error {
for i := range names {
if err := s3common.ValidateResource(names[i]); err != nil {
return err
}
}
return nil
}
func formS3ActionNames(names []string) ([]string, error) {
uniqueActions := make(map[string]struct{}, len(names))
for _, action := range names {
if action == s3common.Wildcard {
return []string{s3common.Wildcard}, nil
}
isIAM, err := s3common.ValidateAction(action)
if err != nil {
return nil, err
}
if isIAM {
uniqueActions[action] = struct{}{}
continue
}
if action[len(s3common.S3ActionPrefix):] == s3common.Wildcard {
uniqueActions[action] = struct{}{}
continue
}
s3Actions := actionToS3OpMap[action]
if len(s3Actions) == 0 {
return nil, s3common.ErrActionsNotApplicable
}
for _, s3Action := range s3Actions {
uniqueActions[s3Action] = struct{}{}
}
}
res := make([]string, 0, len(uniqueActions))
for key := range uniqueActions {
res = append(res, key)
}
return res, nil
}

File diff suppressed because it is too large Load diff