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/api/middleware"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||||
|
s3common "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine/common"
|
||||||
engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam"
|
engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
@ -177,7 +178,7 @@ func (h *handler) putBucketACLAPEHandler(w http.ResponseWriter, r *http.Request,
|
||||||
}
|
}
|
||||||
|
|
||||||
chainRules := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID)
|
chainRules := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID)
|
||||||
if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), chainRules); err != nil {
|
if err = h.policyEngine.APE.SaveACLChains(bktInfo.CID.EncodeToString(), chainRules); err != nil {
|
||||||
h.logAndSendError(ctx, w, "failed to add morph rule chains", reqInfo, err)
|
h.logAndSendError(ctx, w, "failed to add morph rule chains", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -248,7 +249,7 @@ func (h *handler) GetBucketPolicyStatusHandler(w http.ResponseWriter, r *http.Re
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonPolicy, err := h.ape.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
|
jsonPolicy, err := h.policyEngine.APE.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "not found") {
|
if strings.Contains(err.Error(), "not found") {
|
||||||
err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error())
|
err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error())
|
||||||
|
@ -293,7 +294,7 @@ func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonPolicy, err := h.ape.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
|
jsonPolicy, err := h.policyEngine.APE.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "not found") {
|
if strings.Contains(err.Error(), "not found") {
|
||||||
err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error())
|
err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error())
|
||||||
|
@ -323,7 +324,7 @@ func (h *handler) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
|
|
||||||
chainIDs := []chain.ID{getBucketChainID(chain.S3, bktInfo), getBucketChainID(chain.Ingress, bktInfo)}
|
chainIDs := []chain.ID{getBucketChainID(chain.S3, bktInfo), getBucketChainID(chain.Ingress, bktInfo)}
|
||||||
if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
|
if err = h.policyEngine.APE.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
|
||||||
h.logAndSendError(ctx, w, "failed to delete policy from storage", reqInfo, err)
|
h.logAndSendError(ctx, w, "failed to delete policy from storage", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -360,7 +361,7 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var bktPolicy engineiam.Policy
|
var bktPolicy s3common.Policy
|
||||||
if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil {
|
if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil {
|
||||||
h.logAndSendError(ctx, w, "could not parse bucket policy", reqInfo, err)
|
h.logAndSendError(ctx, w, "could not parse bucket policy", reqInfo, err)
|
||||||
return
|
return
|
||||||
|
@ -372,7 +373,7 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(stat.NotPrincipal) != 0 && stat.Effect == engineiam.AllowEffect {
|
if len(stat.NotPrincipal) != 0 && stat.Effect == s3common.AllowEffect {
|
||||||
h.logAndSendError(ctx, w, "invalid NotPrincipal", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicyNotPrincipal))
|
h.logAndSendError(ctx, w, "invalid NotPrincipal", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicyNotPrincipal))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -385,14 +386,14 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s3Chain, err := engineiam.ConvertToS3Chain(bktPolicy, h.frostfsid)
|
s3Chain, err := h.policyEngine.Converter.ToS3Chain(bktPolicy, h.frostfsid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logAndSendError(ctx, w, "could not convert s3 policy to chain policy", reqInfo, err)
|
h.logAndSendError(ctx, w, "could not convert s3 policy to chain policy", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s3Chain.ID = getBucketChainID(chain.S3, bktInfo)
|
s3Chain.ID = getBucketChainID(chain.S3, bktInfo)
|
||||||
|
|
||||||
nativeChain, err := engineiam.ConvertToNativeChain(bktPolicy, h.nativeResolver(reqInfo.Namespace, bktInfo))
|
nativeChain, err := h.policyEngine.Converter.ToNativeChain(bktPolicy, h.nativeResolver(reqInfo.Namespace, bktInfo))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
nativeChain.ID = getBucketChainID(chain.Ingress, bktInfo)
|
nativeChain.ID = getBucketChainID(chain.Ingress, bktInfo)
|
||||||
} else if !stderrors.Is(err, engineiam.ErrActionsNotApplicable) {
|
} else if !stderrors.Is(err, engineiam.ErrActionsNotApplicable) {
|
||||||
|
@ -407,7 +408,7 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
||||||
chainsToSave = append(chainsToSave, nativeChain)
|
chainsToSave = append(chainsToSave, nativeChain)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = h.ape.PutBucketPolicy(reqInfo.Namespace, bktInfo.CID, jsonPolicy, chainsToSave); err != nil {
|
if err = h.policyEngine.APE.PutBucketPolicy(reqInfo.Namespace, bktInfo.CID, jsonPolicy, chainsToSave); err != nil {
|
||||||
h.logAndSendError(ctx, w, "failed to update policy in contract", reqInfo, err)
|
h.logAndSendError(ctx, w, "failed to update policy in contract", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -419,15 +420,15 @@ type nativeResolver struct {
|
||||||
bktInfo *data.BucketInfo
|
bktInfo *data.BucketInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *nativeResolver) GetBucketInfo(bucket string) (*engineiam.BucketInfo, error) {
|
func (n *nativeResolver) GetBucketInfo(bucket string) (*s3common.BucketInfo, error) {
|
||||||
if n.bktInfo.Name != bucket {
|
if n.bktInfo.Name != bucket {
|
||||||
return nil, fmt.Errorf("invalid bucket %s: %w", bucket, errors.GetAPIError(errors.ErrMalformedPolicy))
|
return nil, fmt.Errorf("invalid bucket %s: %w", bucket, errors.GetAPIError(errors.ErrMalformedPolicy))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &engineiam.BucketInfo{Namespace: n.namespace, Container: n.bktInfo.CID.EncodeToString()}, nil
|
return &s3common.BucketInfo{Namespace: n.namespace, Container: n.bktInfo.CID.EncodeToString()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) nativeResolver(ns string, bktInfo *data.BucketInfo) engineiam.NativeResolver {
|
func (h *handler) nativeResolver(ns string, bktInfo *data.BucketInfo) s3common.NativeResolver {
|
||||||
return &nativeResolver{
|
return &nativeResolver{
|
||||||
FrostFSID: h.frostfsid,
|
FrostFSID: h.frostfsid,
|
||||||
namespace: ns,
|
namespace: ns,
|
||||||
|
|
|
@ -189,14 +189,14 @@ func TestDeleteBucketWithPolicy(t *testing.T) {
|
||||||
|
|
||||||
putBucketPolicy(hc, bktName, newPolicy)
|
putBucketPolicy(hc, bktName, newPolicy)
|
||||||
|
|
||||||
require.Len(t, hc.h.ape.(*apeMock).policyMap, 1)
|
require.Len(t, hc.h.policyEngine.APE.(*apeMock).policyMap, 1)
|
||||||
require.Len(t, hc.h.ape.(*apeMock).chainMap[engine.ContainerTarget(bi.CID.EncodeToString())], 4)
|
require.Len(t, hc.h.policyEngine.APE.(*apeMock).chainMap[engine.ContainerTarget(bi.CID.EncodeToString())], 4)
|
||||||
|
|
||||||
hc.owner = bi.Owner
|
hc.owner = bi.Owner
|
||||||
deleteBucket(t, hc, bktName, http.StatusNoContent)
|
deleteBucket(t, hc, bktName, http.StatusNoContent)
|
||||||
|
|
||||||
require.Empty(t, hc.h.ape.(*apeMock).policyMap)
|
require.Empty(t, hc.h.policyEngine.APE.(*apeMock).policyMap)
|
||||||
chains, err := hc.h.ape.(*apeMock).ListChains(engine.ContainerTarget(bi.CID.EncodeToString()))
|
chains, err := hc.h.policyEngine.APE.(*apeMock).ListChains(engine.ContainerTarget(bi.CID.EncodeToString()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, chains)
|
require.Empty(t, chains)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa"
|
"git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||||
|
policyengine "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine"
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||||
|
@ -20,12 +21,12 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
handler struct {
|
handler struct {
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
obj *layer.Layer
|
obj *layer.Layer
|
||||||
cfg Config
|
cfg Config
|
||||||
ape APE
|
policyEngine PolicyEngine
|
||||||
frostfsid FrostFSID
|
frostfsid FrostFSID
|
||||||
mfa *mfa.Manager
|
mfa *mfa.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config contains data which handler needs to keep.
|
// Config contains data which handler needs to keep.
|
||||||
|
@ -54,6 +55,11 @@ type (
|
||||||
GetUserKey(account, name string) (string, error)
|
GetUserKey(account, name string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PolicyEngine struct {
|
||||||
|
APE APE
|
||||||
|
Converter *policyengine.Converter
|
||||||
|
}
|
||||||
|
|
||||||
// APE is Access Policy Engine that needs to save policy and acl info to different places.
|
// APE is Access Policy Engine that needs to save policy and acl info to different places.
|
||||||
APE interface {
|
APE interface {
|
||||||
PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chains []*chain.Chain) error
|
PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chains []*chain.Chain) error
|
||||||
|
@ -73,14 +79,14 @@ const (
|
||||||
var _ api.Handler = (*handler)(nil)
|
var _ api.Handler = (*handler)(nil)
|
||||||
|
|
||||||
// New creates new api.Handler using given logger and client.
|
// New creates new api.Handler using given logger and client.
|
||||||
func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid FrostFSID, mfaMgr *mfa.Manager) (api.Handler, error) {
|
func New(log *zap.Logger, obj *layer.Layer, cfg Config, policyEngine PolicyEngine, ffsid FrostFSID, mfaMgr *mfa.Manager) (api.Handler, error) {
|
||||||
switch {
|
switch {
|
||||||
case obj == nil:
|
case obj == nil:
|
||||||
return nil, errors.New("empty FrostFS Object Layer")
|
return nil, errors.New("empty FrostFS Object Layer")
|
||||||
case log == nil:
|
case log == nil:
|
||||||
return nil, errors.New("empty logger")
|
return nil, errors.New("empty logger")
|
||||||
case storage == nil:
|
case policyEngine.APE == nil || policyEngine.Converter == nil:
|
||||||
return nil, errors.New("empty policy storage")
|
return nil, errors.New("empty policy engine")
|
||||||
case ffsid == nil:
|
case ffsid == nil:
|
||||||
return nil, errors.New("empty frostfsid")
|
return nil, errors.New("empty frostfsid")
|
||||||
case mfaMgr == nil:
|
case mfaMgr == nil:
|
||||||
|
@ -88,12 +94,12 @@ func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid Frost
|
||||||
}
|
}
|
||||||
|
|
||||||
return &handler{
|
return &handler{
|
||||||
log: log,
|
log: log,
|
||||||
obj: obj,
|
obj: obj,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
ape: storage,
|
policyEngine: policyEngine,
|
||||||
frostfsid: ffsid,
|
frostfsid: ffsid,
|
||||||
mfa: mfaMgr,
|
mfa: mfaMgr,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -345,7 +345,7 @@ func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
getBucketCannedChainID(chain.S3, bktInfo.CID),
|
getBucketCannedChainID(chain.S3, bktInfo.CID),
|
||||||
getBucketCannedChainID(chain.Ingress, bktInfo.CID),
|
getBucketCannedChainID(chain.Ingress, bktInfo.CID),
|
||||||
}
|
}
|
||||||
if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
|
if err = h.policyEngine.APE.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
|
||||||
h.logAndSendError(ctx, w, "failed to delete policy from storage", reqInfo, err)
|
h.logAndSendError(ctx, w, "failed to delete policy from storage", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||||
intmfa "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/mfa"
|
intmfa "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/mfa"
|
||||||
|
policyengine "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
|
||||||
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
|
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
@ -287,10 +288,15 @@ func prepareHandlerContextBase(config *handlerConfig, log *zap.Logger) (*handler
|
||||||
placementPolicies: make(map[string]netmap.PlacementPolicy),
|
placementPolicies: make(map[string]netmap.PlacementPolicy),
|
||||||
}
|
}
|
||||||
h := &handler{
|
h := &handler{
|
||||||
log: log,
|
log: log,
|
||||||
obj: layer.NewLayer(ctx, log, tp, layerCfg),
|
obj: layer.NewLayer(ctx, log, tp, layerCfg),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
ape: newAPEMock(),
|
policyEngine: PolicyEngine{
|
||||||
|
APE: newAPEMock(),
|
||||||
|
Converter: policyengine.NewConverter(policyengine.Config{
|
||||||
|
VersionFetcher: apeConverterMock{version: policyengine.V1},
|
||||||
|
}),
|
||||||
|
},
|
||||||
frostfsid: newFrostfsIDMock(),
|
frostfsid: newFrostfsIDMock(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,6 +383,14 @@ func getMinCacheConfig(logger *zap.Logger) *layer.CachesConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type apeConverterMock struct {
|
||||||
|
version policyengine.ConverterVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a apeConverterMock) ConverterVersion() policyengine.ConverterVersion {
|
||||||
|
return a.version
|
||||||
|
}
|
||||||
|
|
||||||
type apeMock struct {
|
type apeMock struct {
|
||||||
chainMap map[engine.Target][]*chain.Chain
|
chainMap map[engine.Target][]*chain.Chain
|
||||||
policyMap map[string][]byte
|
policyMap map[string][]byte
|
||||||
|
|
|
@ -860,7 +860,7 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
|
||||||
zap.Stringer("container_id", bktInfo.CID), logs.TagField(logs.TagExternalStorage))
|
zap.Stringer("container_id", bktInfo.CID), logs.TagField(logs.TagExternalStorage))
|
||||||
|
|
||||||
chains := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID)
|
chains := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID)
|
||||||
if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), chains); err != nil {
|
if err = h.policyEngine.APE.SaveACLChains(bktInfo.CID.EncodeToString(), chains); err != nil {
|
||||||
cleanErr := h.cleanupBucketCreation(ctx, reqInfo, bktInfo, boxData, chains)
|
cleanErr := h.cleanupBucketCreation(ctx, reqInfo, bktInfo, boxData, chains)
|
||||||
h.logAndSendError(ctx, w, "failed to add morph rule chain", reqInfo, err, zap.NamedError("cleanup_error", cleanErr))
|
h.logAndSendError(ctx, w, "failed to add morph rule chain", reqInfo, err, zap.NamedError("cleanup_error", cleanErr))
|
||||||
return
|
return
|
||||||
|
@ -913,7 +913,7 @@ func (h *handler) cleanupBucketCreation(ctx context.Context, reqInfo *middleware
|
||||||
chainIDs[i] = c.ID
|
chainIDs[i] = c.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
|
if err := h.policyEngine.APE.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
|
||||||
return fmt.Errorf("delete bucket acl policy: %w", err)
|
return fmt.Errorf("delete bucket acl policy: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1006,7 +1006,7 @@ func TestCreateBucketWithoutPermissions(t *testing.T) {
|
||||||
hc := prepareHandlerContext(t)
|
hc := prepareHandlerContext(t)
|
||||||
bktName := "bkt-name"
|
bktName := "bkt-name"
|
||||||
|
|
||||||
hc.h.ape.(*apeMock).err = errors.New("no permissions")
|
hc.h.policyEngine.APE.(*apeMock).err = errors.New("no permissions")
|
||||||
|
|
||||||
box, _ := createAccessBox(t)
|
box, _ := createAccessBox(t)
|
||||||
createBucketAssertS3Error(hc, bktName, box, apierr.ErrInternalError)
|
createBucketAssertS3Error(hc, bktName, box, apierr.ErrInternalError)
|
||||||
|
|
|
@ -44,6 +44,7 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics"
|
||||||
|
policyengine "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
|
||||||
v2container "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container"
|
v2container "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||||
|
@ -149,6 +150,7 @@ type (
|
||||||
removeOnReplaceTimeout time.Duration
|
removeOnReplaceTimeout time.Duration
|
||||||
corsCopiesNumbers []uint32
|
corsCopiesNumbers []uint32
|
||||||
lifecycleCopiesNumbers []uint32
|
lifecycleCopiesNumbers []uint32
|
||||||
|
apeConvertersVersion policyengine.ConverterVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
maxClientsConfig struct {
|
maxClientsConfig struct {
|
||||||
|
@ -407,6 +409,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
|
||||||
removeOnReplaceTimeout := fetchRemoveOnReplaceTimeout(v)
|
removeOnReplaceTimeout := fetchRemoveOnReplaceTimeout(v)
|
||||||
corsCopiesNumbers := fetchCopiesNumbers(log, v, cfgCORSCopiesNumbers)
|
corsCopiesNumbers := fetchCopiesNumbers(log, v, cfgCORSCopiesNumbers)
|
||||||
lifecycleCopiesNumbers := fetchCopiesNumbers(log, v, cfgLifecycleCopiesNumbers)
|
lifecycleCopiesNumbers := fetchCopiesNumbers(log, v, cfgLifecycleCopiesNumbers)
|
||||||
|
apeConvertersVersion := fetchAPEConvertersVersion(log, v)
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
@ -445,6 +448,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
|
||||||
s.removeOnReplaceTimeout = removeOnReplaceTimeout
|
s.removeOnReplaceTimeout = removeOnReplaceTimeout
|
||||||
s.corsCopiesNumbers = corsCopiesNumbers
|
s.corsCopiesNumbers = corsCopiesNumbers
|
||||||
s.lifecycleCopiesNumbers = lifecycleCopiesNumbers
|
s.lifecycleCopiesNumbers = lifecycleCopiesNumbers
|
||||||
|
s.apeConvertersVersion = apeConvertersVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appSettings) prepareVHSNamespaces(v *viper.Viper, log *zap.Logger, defaultNamespaces []string) map[string]bool {
|
func (s *appSettings) prepareVHSNamespaces(v *viper.Viper, log *zap.Logger, defaultNamespaces []string) map[string]bool {
|
||||||
|
@ -718,6 +722,12 @@ func (s *appSettings) LifecycleCopiesNumbers() []uint32 {
|
||||||
return s.lifecycleCopiesNumbers
|
return s.lifecycleCopiesNumbers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *appSettings) ConverterVersion() policyengine.ConverterVersion {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.apeConvertersVersion
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) initAPI(ctx context.Context, rpcCli *rpcclient.Client) {
|
func (a *App) initAPI(ctx context.Context, rpcCli *rpcclient.Client) {
|
||||||
a.initLayer(ctx, rpcCli)
|
a.initLayer(ctx, rpcCli)
|
||||||
|
|
||||||
|
@ -1294,7 +1304,13 @@ func (a *App) initMfaManager(ctx context.Context) *mfa.Manager {
|
||||||
func (a *App) initHandler(ctx context.Context) {
|
func (a *App) initHandler(ctx context.Context) {
|
||||||
var err error
|
var err error
|
||||||
manager := a.initMfaManager(ctx)
|
manager := a.initMfaManager(ctx)
|
||||||
a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid, manager)
|
policyEngine := handler.PolicyEngine{
|
||||||
|
APE: a.policyStorage,
|
||||||
|
Converter: policyengine.NewConverter(policyengine.Config{
|
||||||
|
VersionFetcher: a.settings,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
a.api, err = handler.New(a.log, a.obj, a.settings, policyEngine, a.frostfsid, manager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err), logs.TagField(logs.TagApp))
|
a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err), logs.TagField(logs.TagApp))
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||||
internalnet "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/net"
|
internalnet "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/net"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
|
||||||
|
policyengine "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/policy-engine"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
@ -81,6 +82,8 @@ const (
|
||||||
|
|
||||||
defaultRemoveOnReplaceTimeout = 30 * time.Second
|
defaultRemoveOnReplaceTimeout = 30 * time.Second
|
||||||
defaultRemoveOnReplaceQueue = 10000
|
defaultRemoveOnReplaceQueue = 10000
|
||||||
|
|
||||||
|
defaultFeaturesAPEConvertersVersion = policyengine.V1
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -291,6 +294,9 @@ const (
|
||||||
cfgRemoveOnReplaceTimeout = "features.remove_on_replace.timeout"
|
cfgRemoveOnReplaceTimeout = "features.remove_on_replace.timeout"
|
||||||
cfgRemoveOnReplaceQueue = "features.remove_on_replace.queue"
|
cfgRemoveOnReplaceQueue = "features.remove_on_replace.queue"
|
||||||
|
|
||||||
|
// APE.
|
||||||
|
cfgFeaturesAPEConvertersVersion = "features.ape.converters_version"
|
||||||
|
|
||||||
// FrostfsID.
|
// FrostfsID.
|
||||||
cfgFrostfsIDContract = "frostfsid.contract"
|
cfgFrostfsIDContract = "frostfsid.contract"
|
||||||
cfgFrostfsIDValidationEnabled = "frostfsid.validation.enabled"
|
cfgFrostfsIDValidationEnabled = "frostfsid.validation.enabled"
|
||||||
|
@ -660,6 +666,19 @@ func fetchCopiesNumbers(l *zap.Logger, v *viper.Viper, param string) []uint32 {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchAPEConvertersVersion(l *zap.Logger, v *viper.Viper) policyengine.ConverterVersion {
|
||||||
|
val := v.GetString(cfgFeaturesAPEConvertersVersion)
|
||||||
|
switch ver := policyengine.ConverterVersion(val); ver {
|
||||||
|
case policyengine.V1, policyengine.V2:
|
||||||
|
return ver
|
||||||
|
default:
|
||||||
|
l.Warn(logs.InvalidAPEConvertersVersion, zap.String("version", val),
|
||||||
|
zap.String("default", string(defaultFeaturesAPEConvertersVersion)),
|
||||||
|
logs.TagField(logs.TagApp))
|
||||||
|
return defaultFeaturesAPEConvertersVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type KludgeParams struct {
|
type KludgeParams struct {
|
||||||
UseDefaultXMLNS bool
|
UseDefaultXMLNS bool
|
||||||
BypassContentEncodingCheckInChunks bool
|
BypassContentEncodingCheckInChunks bool
|
||||||
|
|
|
@ -236,6 +236,8 @@ S3_GW_FEATURES_REMOVE_ON_REPLACE_ENABLED=false
|
||||||
S3_GW_FEATURES_REMOVE_ON_REPLACE_TIMEOUT=30s
|
S3_GW_FEATURES_REMOVE_ON_REPLACE_TIMEOUT=30s
|
||||||
# Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that.
|
# Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that.
|
||||||
S3_GW_FEATURES_REMOVE_ON_REPLACE_QUEUE=10000
|
S3_GW_FEATURES_REMOVE_ON_REPLACE_QUEUE=10000
|
||||||
|
#Version of Access Policy converters (Supported versions: `v1`, `v2`). Need to transform AWS IAM policy to supported FrostFS chain format.
|
||||||
|
S3_GW_FEATURES_APE_CONVERTERS_VERSION=v1
|
||||||
|
|
||||||
# ReadTimeout is the maximum duration for reading the entire
|
# ReadTimeout is the maximum duration for reading the entire
|
||||||
# request, including the body. A zero or negative value means
|
# request, including the body. A zero or negative value means
|
||||||
|
|
|
@ -277,6 +277,9 @@ features:
|
||||||
timeout: 30s
|
timeout: 30s
|
||||||
# Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that.
|
# Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that.
|
||||||
queue: 10000
|
queue: 10000
|
||||||
|
ape:
|
||||||
|
# Version of Access Policy converters (Supported versions: `v1`, `v2`). Need to transform AWS IAM policy to supported FrostFS chain format.
|
||||||
|
converters_version: v1
|
||||||
|
|
||||||
web:
|
web:
|
||||||
# ReadTimeout is the maximum duration for reading the entire
|
# ReadTimeout is the maximum duration for reading the entire
|
||||||
|
|
|
@ -753,16 +753,31 @@ features:
|
||||||
enabled: false
|
enabled: false
|
||||||
timeout: 30s
|
timeout: 30s
|
||||||
queue: 10000
|
queue: 10000
|
||||||
|
ape:
|
||||||
|
converters_version: v1
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||||
|-----------------------------|-------------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------------|--------------------------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `md5.enabled` | `bool` | yes | `false` | Flag to enable return MD5 checksum in ETag headers and fields. |
|
| `md5.enabled` | `bool` | yes | `false` | Flag to enable return MD5 checksum in ETag headers and fields. |
|
||||||
| `policy.deny_by_default` | `bool` | yes | `false` | Enable denying access for request that doesn't match any policy chain rules. |
|
| `policy.deny_by_default` | `bool` | yes | `false` | Enable denying access for request that doesn't match any policy chain rules. |
|
||||||
| `tree_pool_netmap_support` | `bool` | no | `false` | Enable using new version of tree pool, which uses netmap to select nodes, for requests to tree service. |
|
| `tree_pool_netmap_support` | `bool` | no | `false` | Enable using new version of tree pool, which uses netmap to select nodes, for requests to tree service. |
|
||||||
| `remove_on_replace.enabled` | `bool` | yes | `false` | Enable removing old object during PUT operation in unversioned/suspened bucket. |
|
| `remove_on_replace.enabled` | `bool` | yes | `false` | Enable removing old object during PUT operation in unversioned/suspened bucket. |
|
||||||
| `remove_on_replace.timeout` | `durations` | yes | `30s` | Timeout to one delete operation in background. |
|
| `remove_on_replace.timeout` | `durations` | yes | `30s` | Timeout to one delete operation in background. |
|
||||||
| `remove_on_replace.queue` | `int` | false | `10000` | Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that. |
|
| `remove_on_replace.queue` | `int` | false | `10000` | Buffer size for objects to delete. If buffer is full creation new unversioned object won't remove old one. Lifecycler will do that. |
|
||||||
|
| `ape` | [[]APE](#ape-subsection) | | | Access Policy Engine configuration. |
|
||||||
|
|
||||||
|
#### `ape` subsection
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
features:
|
||||||
|
ape:
|
||||||
|
converters_version: v1
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||||
|
|----------------------|----------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `converters_version` | `string` | yes | `v1` | Version of Access Policy converters (Supported versions: `v1`, `v2`). Need to transform AWS IAM policy to supported FrostFS chain format. |
|
||||||
|
|
||||||
# `web` section
|
# `web` section
|
||||||
|
|
||||||
|
@ -979,8 +994,8 @@ encryption:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
contracts:
|
contracts:
|
||||||
container:
|
container:
|
||||||
name: container.frostfs
|
name: container.frostfs
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||||
|
|
|
@ -98,6 +98,7 @@ const (
|
||||||
InitRPCClientFailed = "init rpc client failed"
|
InitRPCClientFailed = "init rpc client failed"
|
||||||
CouldNotFetchMFAContainerInfo = "couldn't fetch mfa container info"
|
CouldNotFetchMFAContainerInfo = "couldn't fetch mfa container info"
|
||||||
CouldNotInitMFAClient = "couldn't init MFA client"
|
CouldNotInitMFAClient = "couldn't init MFA client"
|
||||||
|
InvalidAPEConvertersVersion = "invalid ape converters version, default will be used"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Datapath.
|
// Datapath.
|
||||||
|
|
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