forked from TrueCloudLab/frostfs-s3-gw
[#680] Move policy engine converter to s3-gw
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
parent
e788bb6ec9
commit
0ba6989197
21 changed files with 4325 additions and 50 deletions
|
@ -19,6 +19,7 @@ import (
|
|||
"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/internal/logs"
|
||||
s3common "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine/common"
|
||||
engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"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)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -248,7 +249,7 @@ func (h *handler) GetBucketPolicyStatusHandler(w http.ResponseWriter, r *http.Re
|
|||
return
|
||||
}
|
||||
|
||||
jsonPolicy, err := h.ape.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
|
||||
jsonPolicy, err := h.policyEngine.APE.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
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
|
||||
}
|
||||
|
||||
jsonPolicy, err := h.ape.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
|
||||
jsonPolicy, err := h.policyEngine.APE.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
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)}
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -360,7 +361,7 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
var bktPolicy engineiam.Policy
|
||||
var bktPolicy s3common.Policy
|
||||
if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil {
|
||||
h.logAndSendError(ctx, w, "could not parse bucket policy", reqInfo, err)
|
||||
return
|
||||
|
@ -372,7 +373,7 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
|||
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))
|
||||
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 {
|
||||
h.logAndSendError(ctx, w, "could not convert s3 policy to chain policy", reqInfo, err)
|
||||
return
|
||||
}
|
||||
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 {
|
||||
nativeChain.ID = getBucketChainID(chain.Ingress, bktInfo)
|
||||
} 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)
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -419,15 +420,15 @@ type nativeResolver struct {
|
|||
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 {
|
||||
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{
|
||||
FrostFSID: h.frostfsid,
|
||||
namespace: ns,
|
||||
|
|
|
@ -189,14 +189,14 @@ func TestDeleteBucketWithPolicy(t *testing.T) {
|
|||
|
||||
putBucketPolicy(hc, bktName, newPolicy)
|
||||
|
||||
require.Len(t, hc.h.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).policyMap, 1)
|
||||
require.Len(t, hc.h.policyEngine.APE.(*apeMock).chainMap[engine.ContainerTarget(bi.CID.EncodeToString())], 4)
|
||||
|
||||
hc.owner = bi.Owner
|
||||
deleteBucket(t, hc, bktName, http.StatusNoContent)
|
||||
|
||||
require.Empty(t, hc.h.ape.(*apeMock).policyMap)
|
||||
chains, err := hc.h.ape.(*apeMock).ListChains(engine.ContainerTarget(bi.CID.EncodeToString()))
|
||||
require.Empty(t, hc.h.policyEngine.APE.(*apeMock).policyMap)
|
||||
chains, err := hc.h.policyEngine.APE.(*apeMock).ListChains(engine.ContainerTarget(bi.CID.EncodeToString()))
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, chains)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"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"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
|
@ -20,12 +21,12 @@ import (
|
|||
|
||||
type (
|
||||
handler struct {
|
||||
log *zap.Logger
|
||||
obj *layer.Layer
|
||||
cfg Config
|
||||
ape APE
|
||||
frostfsid FrostFSID
|
||||
mfa *mfa.Manager
|
||||
log *zap.Logger
|
||||
obj *layer.Layer
|
||||
cfg Config
|
||||
policyEngine PolicyEngine
|
||||
frostfsid FrostFSID
|
||||
mfa *mfa.Manager
|
||||
}
|
||||
|
||||
// Config contains data which handler needs to keep.
|
||||
|
@ -54,6 +55,11 @@ type (
|
|||
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 interface {
|
||||
PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chains []*chain.Chain) error
|
||||
|
@ -73,14 +79,14 @@ 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, 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 {
|
||||
case obj == nil:
|
||||
return nil, errors.New("empty FrostFS Object Layer")
|
||||
case log == nil:
|
||||
return nil, errors.New("empty logger")
|
||||
case storage == nil:
|
||||
return nil, errors.New("empty policy storage")
|
||||
case policyEngine.APE == nil || policyEngine.Converter == nil:
|
||||
return nil, errors.New("empty policy engine")
|
||||
case ffsid == nil:
|
||||
return nil, errors.New("empty frostfsid")
|
||||
case mfaMgr == nil:
|
||||
|
@ -88,12 +94,12 @@ func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid Frost
|
|||
}
|
||||
|
||||
return &handler{
|
||||
log: log,
|
||||
obj: obj,
|
||||
cfg: cfg,
|
||||
ape: storage,
|
||||
frostfsid: ffsid,
|
||||
mfa: mfaMgr,
|
||||
log: log,
|
||||
obj: obj,
|
||||
cfg: cfg,
|
||||
policyEngine: policyEngine,
|
||||
frostfsid: ffsid,
|
||||
mfa: mfaMgr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -345,7 +345,7 @@ func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|||
getBucketCannedChainID(chain.S3, 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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||
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"
|
||||
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
|
||||
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),
|
||||
}
|
||||
h := &handler{
|
||||
log: log,
|
||||
obj: layer.NewLayer(ctx, log, tp, layerCfg),
|
||||
cfg: cfg,
|
||||
ape: newAPEMock(),
|
||||
log: log,
|
||||
obj: layer.NewLayer(ctx, log, tp, layerCfg),
|
||||
cfg: cfg,
|
||||
policyEngine: PolicyEngine{
|
||||
APE: newAPEMock(),
|
||||
Converter: policyengine.NewConverter(policyengine.Config{
|
||||
VersionFetcher: apeConverterMock{version: policyengine.V1},
|
||||
}),
|
||||
},
|
||||
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 {
|
||||
chainMap map[engine.Target][]*chain.Chain
|
||||
policyMap map[string][]byte
|
||||
|
|
|
@ -860,7 +860,7 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
|
|||
zap.Stringer("container_id", bktInfo.CID), logs.TagField(logs.TagExternalStorage))
|
||||
|
||||
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)
|
||||
h.logAndSendError(ctx, w, "failed to add morph rule chain", reqInfo, err, zap.NamedError("cleanup_error", cleanErr))
|
||||
return
|
||||
|
@ -913,7 +913,7 @@ func (h *handler) cleanupBucketCreation(ctx context.Context, reqInfo *middleware
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1006,7 +1006,7 @@ func TestCreateBucketWithoutPermissions(t *testing.T) {
|
|||
hc := prepareHandlerContext(t)
|
||||
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)
|
||||
createBucketAssertS3Error(hc, bktName, box, apierr.ErrInternalError)
|
||||
|
|
|
@ -44,6 +44,7 @@ import (
|
|||
"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/metrics"
|
||||
policyengine "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
|
||||
v2container "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||
|
@ -149,6 +150,7 @@ type (
|
|||
removeOnReplaceTimeout time.Duration
|
||||
corsCopiesNumbers []uint32
|
||||
lifecycleCopiesNumbers []uint32
|
||||
apeConvertersVersion policyengine.ConverterVersion
|
||||
}
|
||||
|
||||
maxClientsConfig struct {
|
||||
|
@ -407,6 +409,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
|
|||
removeOnReplaceTimeout := fetchRemoveOnReplaceTimeout(v)
|
||||
corsCopiesNumbers := fetchCopiesNumbers(log, v, cfgCORSCopiesNumbers)
|
||||
lifecycleCopiesNumbers := fetchCopiesNumbers(log, v, cfgLifecycleCopiesNumbers)
|
||||
apeConvertersVersion := fetchAPEConvertersVersion(log, v)
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
@ -445,6 +448,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
|
|||
s.removeOnReplaceTimeout = removeOnReplaceTimeout
|
||||
s.corsCopiesNumbers = corsCopiesNumbers
|
||||
s.lifecycleCopiesNumbers = lifecycleCopiesNumbers
|
||||
s.apeConvertersVersion = apeConvertersVersion
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
a.initLayer(ctx, rpcCli)
|
||||
|
||||
|
@ -1294,7 +1304,13 @@ func (a *App) initMfaManager(ctx context.Context) *mfa.Manager {
|
|||
func (a *App) initHandler(ctx context.Context) {
|
||||
var err error
|
||||
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 {
|
||||
a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err), logs.TagField(logs.TagApp))
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
internalnet "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/net"
|
||||
"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/pool"
|
||||
"github.com/spf13/pflag"
|
||||
|
@ -81,6 +82,8 @@ const (
|
|||
|
||||
defaultRemoveOnReplaceTimeout = 30 * time.Second
|
||||
defaultRemoveOnReplaceQueue = 10000
|
||||
|
||||
defaultFeaturesAPEConvertersVersion = policyengine.V1
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -291,6 +294,9 @@ const (
|
|||
cfgRemoveOnReplaceTimeout = "features.remove_on_replace.timeout"
|
||||
cfgRemoveOnReplaceQueue = "features.remove_on_replace.queue"
|
||||
|
||||
// APE.
|
||||
cfgFeaturesAPEConvertersVersion = "features.ape.converters_version"
|
||||
|
||||
// FrostfsID.
|
||||
cfgFrostfsIDContract = "frostfsid.contract"
|
||||
cfgFrostfsIDValidationEnabled = "frostfsid.validation.enabled"
|
||||
|
@ -660,6 +666,19 @@ func fetchCopiesNumbers(l *zap.Logger, v *viper.Viper, param string) []uint32 {
|
|||
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 {
|
||||
UseDefaultXMLNS bool
|
||||
BypassContentEncodingCheckInChunks bool
|
||||
|
|
|
@ -236,6 +236,8 @@ S3_GW_FEATURES_REMOVE_ON_REPLACE_ENABLED=false
|
|||
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.
|
||||
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
|
||||
# request, including the body. A zero or negative value means
|
||||
|
|
|
@ -277,6 +277,9 @@ features:
|
|||
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.
|
||||
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:
|
||||
# ReadTimeout is the maximum duration for reading the entire
|
||||
|
|
|
@ -753,16 +753,31 @@ features:
|
|||
enabled: false
|
||||
timeout: 30s
|
||||
queue: 10000
|
||||
ape:
|
||||
converters_version: v1
|
||||
```
|
||||
|
||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||
|-----------------------------|-------------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `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. |
|
||||
| `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.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. |
|
||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||
|-----------------------------|--------------------------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `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. |
|
||||
| `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.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. |
|
||||
| `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
|
||||
|
||||
|
@ -979,8 +994,8 @@ encryption:
|
|||
|
||||
```yaml
|
||||
contracts:
|
||||
container:
|
||||
name: container.frostfs
|
||||
container:
|
||||
name: container.frostfs
|
||||
```
|
||||
|
||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||
|
|
|
@ -98,6 +98,7 @@ const (
|
|||
InitRPCClientFailed = "init rpc client failed"
|
||||
CouldNotFetchMFAContainerInfo = "couldn't fetch mfa container info"
|
||||
CouldNotInitMFAClient = "couldn't init MFA client"
|
||||
InvalidAPEConvertersVersion = "invalid ape converters version, default will be used"
|
||||
)
|
||||
|
||||
// Datapath.
|
||||
|
|
427
pkg/policy-engine/common/converter.go
Normal file
427
pkg/policy-engine/common/converter.go
Normal 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
|
||||
}
|
338
pkg/policy-engine/common/policy.go
Normal file
338
pkg/policy-engine/common/policy.go
Normal 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
|
||||
}
|
479
pkg/policy-engine/common/policy_test.go
Normal file
479
pkg/policy-engine/common/policy_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
15
pkg/policy-engine/common/resolver.go
Normal file
15
pkg/policy-engine/common/resolver.go
Normal 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)
|
||||
}
|
118
pkg/policy-engine/converter.go
Normal file
118
pkg/policy-engine/converter.go
Normal 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
|
||||
}
|
420
pkg/policy-engine/v2/iam/converter_native.go
Normal file
420
pkg/policy-engine/v2/iam/converter_native.go
Normal 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
|
||||
}
|
269
pkg/policy-engine/v2/iam/converter_s3.go
Normal file
269
pkg/policy-engine/v2/iam/converter_s3.go
Normal 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
|
||||
}
|
2132
pkg/policy-engine/v2/iam/converter_test.go
Normal file
2132
pkg/policy-engine/v2/iam/converter_test.go
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue