From 47bcd346d3bdc9fd50e9fc98c8ee485da5b7f2c6 Mon Sep 17 00:00:00 2001 From: Airat Arifullin Date: Mon, 1 Jul 2024 16:37:56 +0300 Subject: [PATCH] [#1216] ape: Introduce `BearerChainFeedRouter` * Unlike default chain router, `BearerChainFedRouter` performs checks for overrides defined in the bearer token; * Add unit-test for the introduced router. Signed-off-by: Airat Arifullin --- pkg/ape/router/bearer_overrides.go | 94 ++++++++++++++ pkg/ape/router/bearer_overrides_test.go | 165 ++++++++++++++++++++++++ pkg/ape/router/single_pass.go | 25 ---- 3 files changed, 259 insertions(+), 25 deletions(-) create mode 100644 pkg/ape/router/bearer_overrides.go create mode 100644 pkg/ape/router/bearer_overrides_test.go diff --git a/pkg/ape/router/bearer_overrides.go b/pkg/ape/router/bearer_overrides.go new file mode 100644 index 000000000..2bc8ad614 --- /dev/null +++ b/pkg/ape/router/bearer_overrides.go @@ -0,0 +1,94 @@ +package router + +import ( + "errors" + "fmt" + + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" + cidSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" +) + +func newTarget(ct ape.ChainTarget) (policyengine.Target, error) { + var target policyengine.Target + switch ct.TargetType { + case ape.TargetTypeContainer: + var cid cidSDK.ID + err := cid.DecodeString(ct.Name) + if err != nil { + return target, fmt.Errorf("invalid cid format: %s", target.Name) + } + target.Type = policyengine.Container + case ape.TargetTypeGroup: + target.Type = policyengine.Group + case ape.TargetTypeNamespace: + target.Type = policyengine.Namespace + case ape.TargetTypeUser: + target.Type = policyengine.User + default: + return target, fmt.Errorf("unsupported target type: %v", ct.TargetType) + } + target.Name = ct.Name + return target, nil +} + +type morphReaderDecorator struct { + policyengine.MorphRuleChainStorageReader + + bearerTokenTarget policyengine.Target + + bearerTokenChains []*chain.Chain +} + +func newMorphReaderDecorator(r policyengine.MorphRuleChainStorageReader, override bearer.APEOverride) (*morphReaderDecorator, error) { + if r == nil { + return nil, errors.New("empty morph chain rule reader") + } + t, err := newTarget(override.Target) + if err != nil { + return nil, err + } + + bearerTokenChains := make([]*chain.Chain, len(override.Chains)) + for i := range override.Chains { + chain := new(chain.Chain) + if err := chain.DecodeBytes(override.Chains[i].Raw); err != nil { + return nil, fmt.Errorf("invalid ape chain: %w", err) + } + bearerTokenChains[i] = chain + } + + return &morphReaderDecorator{ + MorphRuleChainStorageReader: r, + bearerTokenTarget: t, + bearerTokenChains: bearerTokenChains, + }, nil +} + +func (m *morphReaderDecorator) ListMorphRuleChains(name chain.Name, target policyengine.Target) ([]*chain.Chain, error) { + if len(m.bearerTokenChains) > 0 && m.bearerTokenTarget.Type == target.Type { + if m.bearerTokenTarget.Name != target.Name { + return nil, fmt.Errorf("unexpected bearer token target: %s", m.bearerTokenTarget.Name) + } + return m.bearerTokenChains, nil + } + return m.MorphRuleChainStorageReader.ListMorphRuleChains(name, target) +} + +// BearerChainFeedRouter creates a chain router emplacing bearer token rule chains. +// Bearer token chains override only container target chains within Policy contract. This means the order of checking +// is as follows: +// +// 1. Local overrides; +// 2. Policy contract chains for a namespace target (as namespace chains have higher priority); +// 3. Bearer token chains for a container target - if they're not defined, then it checks Policy contract chains; +// 4. Checks for the remaining targets. +func BearerChainFeedRouter(localOverrideStorage policyengine.LocalOverrideStorage, morphChainStorage policyengine.MorphRuleChainStorageReader, override bearer.APEOverride) (policyengine.ChainRouter, error) { + mr, err := newMorphReaderDecorator(morphChainStorage, override) + if err != nil { + return nil, fmt.Errorf("create morph reader with bearer override error: %w", err) + } + return policyengine.NewDefaultChainRouterWithLocalOverrides(mr, localOverrideStorage), nil +} diff --git a/pkg/ape/router/bearer_overrides_test.go b/pkg/ape/router/bearer_overrides_test.go new file mode 100644 index 000000000..3c12ee6fa --- /dev/null +++ b/pkg/ape/router/bearer_overrides_test.go @@ -0,0 +1,165 @@ +package router_test + +import ( + "fmt" + "testing" + + "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/ape/router" + apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape" + bearerSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory" + resourcetest "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" + nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" + "github.com/stretchr/testify/require" +) + +const ( + container = "67ETTZzbzJC6WxdQhHHHsJNCttVMBqYrSoFaUFVDNfiX" + rootNs = "" +) + +var ( + allowBySourceIP = &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{nativeschema.MethodPutObject}}, + Resources: chain.Resources{Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainer, container)}}, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Kind: chain.KindRequest, + Key: "SourceIP", + Value: "10.122.1.20", + }, + }, + }, + }, + } + + denyBySourceIP = &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{Names: []string{nativeschema.MethodPutObject}}, + Resources: chain.Resources{Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainer, container)}}, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Kind: chain.KindRequest, + Key: "SourceIP", + Value: "10.122.1.20", + }, + }, + }, + }, + } +) + +func TestBearerChainFedRouter(t *testing.T) { + t.Run("no bearer token overrides", func(t *testing.T) { + inmem := inmemory.NewInMemoryLocalOverrides() + + inmem.LocalStorage().AddOverride(chain.Ingress, engine.ContainerTarget(container), denyBySourceIP) + inmem.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(container), allowBySourceIP) + + _, err := router.BearerChainFeedRouter(inmem.LocalStorage(), inmem.MorphRuleChainStorage(), bearerSDK.APEOverride{}) + require.Error(t, err) + }) + t.Run("allow by container with deny by bearer overrides", func(t *testing.T) { + inmem := inmemory.NewInMemoryLocalOverrides() + + inmem.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(container), allowBySourceIP) + + bt := bearerSDK.APEOverride{ + Target: apeSDK.ChainTarget{ + TargetType: apeSDK.TargetTypeContainer, + Name: container, + }, + Chains: []apeSDK.Chain{{ + Raw: denyBySourceIP.Bytes(), + }}, + } + + r, err := router.BearerChainFeedRouter(inmem.LocalStorage(), inmem.MorphRuleChainStorage(), bt) + require.NoError(t, err) + + req := resourcetest.NewRequest(nativeschema.MethodPutObject, + resourcetest.NewResource(fmt.Sprintf(nativeschema.ResourceFormatRootContainer, container), map[string]string{}), + map[string]string{ + "SourceIP": "10.122.1.20", + "Actor": "someOwner", + }, + ) + + st, found, err := r.IsAllowed(chain.Ingress, engine.NewRequestTarget(rootNs, container), req) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, st, chain.AccessDenied) + }) + t.Run("allow by namespace with deny by bearer overrides", func(t *testing.T) { + inmem := inmemory.NewInMemoryLocalOverrides() + + inmem.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(container), allowBySourceIP) + inmem.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(rootNs), allowBySourceIP) + + bt := bearerSDK.APEOverride{ + Target: apeSDK.ChainTarget{ + TargetType: apeSDK.TargetTypeContainer, + Name: container, + }, + Chains: []apeSDK.Chain{{ + Raw: denyBySourceIP.Bytes(), + }}, + } + + r, err := router.BearerChainFeedRouter(inmem.LocalStorage(), inmem.MorphRuleChainStorage(), bt) + require.NoError(t, err) + + req := resourcetest.NewRequest(nativeschema.MethodPutObject, + resourcetest.NewResource(fmt.Sprintf(nativeschema.ResourceFormatRootContainer, container), map[string]string{}), + map[string]string{ + "SourceIP": "10.122.1.20", + "Actor": "someOwner", + }, + ) + + st, found, err := r.IsAllowed(chain.Ingress, engine.NewRequestTarget(rootNs, container), req) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, st, chain.AccessDenied) + }) + t.Run("deny by namespace with allow by bearer overrides", func(t *testing.T) { + inmem := inmemory.NewInMemoryLocalOverrides() + + inmem.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(rootNs), denyBySourceIP) + + bt := bearerSDK.APEOverride{ + Target: apeSDK.ChainTarget{ + TargetType: apeSDK.TargetTypeContainer, + Name: container, + }, + Chains: []apeSDK.Chain{{ + Raw: allowBySourceIP.Bytes(), + }}, + } + + r, err := router.BearerChainFeedRouter(inmem.LocalStorage(), inmem.MorphRuleChainStorage(), bt) + require.NoError(t, err) + + req := resourcetest.NewRequest(nativeschema.MethodPutObject, + resourcetest.NewResource(fmt.Sprintf(nativeschema.ResourceFormatRootContainer, container), map[string]string{}), + map[string]string{ + "SourceIP": "10.122.1.20", + "Actor": "someOwner", + }, + ) + + st, found, err := r.IsAllowed(chain.Ingress, engine.NewRequestTarget(rootNs, container), req) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, st, chain.AccessDenied) + }) +} diff --git a/pkg/ape/router/single_pass.go b/pkg/ape/router/single_pass.go index c0c78db76..ec9244bae 100644 --- a/pkg/ape/router/single_pass.go +++ b/pkg/ape/router/single_pass.go @@ -3,37 +3,12 @@ package router import ( "fmt" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" - cidSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory" ) -func newTarget(ct ape.ChainTarget) (engine.Target, error) { - var target engine.Target - switch ct.TargetType { - case ape.TargetTypeContainer: - var cid cidSDK.ID - err := cid.DecodeString(ct.Name) - if err != nil { - return target, fmt.Errorf("invalid cid format: %s", target.Name) - } - target.Type = engine.Container - case ape.TargetTypeGroup: - target.Type = engine.Group - case ape.TargetTypeNamespace: - target.Type = engine.Namespace - case ape.TargetTypeUser: - target.Type = engine.User - default: - return target, fmt.Errorf("unsupported target type: %v", ct.TargetType) - } - target.Name = ct.Name - return target, nil -} - // SingleUseRouterWithBearerTokenChains creates chain router with inmemory storage implementation and // fed with APE chains defined in Bearer token. func SingleUseRouterWithBearerTokenChains(overrides []bearer.APEOverride) (engine.ChainRouter, error) {