Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-12897] Merge path based permissions when combining policies #6597

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions internal/policy/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,24 @@ tasks:
- defer: { task: services:down }
- goimports -w .
- go fmt ./...
- task: test
vars:
run: '{{.run}}'

test:
desc: "Run tests"
requires:
vars: [run]
cmds:
- go test -count=1 -run='({{.run}})' -cover -coverprofile=pkg.cov -v .

stress:
desc: "Run stress tests"
requires:
vars: [run]
cmds:
- go test -count=2000 -run='({{.run}})' -cover -coverprofile=pkg.cov .

cover:
desc: "Show source coverage"
aliases: [coverage, cov]
Expand Down
26 changes: 9 additions & 17 deletions internal/policy/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Repository interface {
PolicyByID(string) (user.Policy, bool)
}

// Service represents the policy service for gateway.
type Service struct {
storage Repository
logger *logrus.Logger
Expand All @@ -30,6 +31,7 @@ type Service struct {
orgID *string
}

// New creates a new policy.Service object.
func New(orgID *string, storage Repository, logger *logrus.Logger) *Service {
return &Service{
orgID: orgID,
Expand Down Expand Up @@ -107,7 +109,8 @@ func (t *Service) Apply(session *user.SessionState) error {
)

storage := t.storage
customPolicies, err := session.CustomPolicies()

customPolicies, err := session.GetCustomPolicies()
if err != nil {
policyIDs = session.PolicyIDs()
} else {
Expand Down Expand Up @@ -242,8 +245,9 @@ func (t *Service) Apply(session *user.SessionState) error {
return nil
}

func (t *Service) Logger() *logrus.Logger {
return t.logger
// Logger implements a typical logger signature with service context.
func (t *Service) Logger() *logrus.Entry {
return logrus.NewEntry(t.logger)
}

// ApplyRateLimits will write policy limits to session and apiLimits.
Expand Down Expand Up @@ -355,7 +359,7 @@ func (t *Service) applyPartitions(policy user.Policy, session *user.SessionState
if !usePartitions || policy.Partitions.Acl {
applyState.didAcl[k] = true

ar.AllowedURLs = copyAllowedURLs(v.AllowedURLs)
ar.AllowedURLs = MergeAllowedURLs(ar.AllowedURLs, v.AllowedURLs)

// Merge ACLs for the same API
if r, ok := rights[k]; ok {
Expand All @@ -365,19 +369,7 @@ func (t *Service) applyPartitions(policy user.Policy, session *user.SessionState
}
r.Versions = appendIfMissing(rights[k].Versions, v.Versions...)

for _, u := range v.AllowedURLs {
found := false
for ai, au := range r.AllowedURLs {
if u.URL == au.URL {
found = true
r.AllowedURLs[ai].Methods = appendIfMissing(au.Methods, u.Methods...)
}
}

if !found {
r.AllowedURLs = append(r.AllowedURLs, v.AllowedURLs...)
}
}
r.AllowedURLs = MergeAllowedURLs(r.AllowedURLs, v.AllowedURLs)

for _, t := range v.RestrictedTypes {
for ri, rt := range r.RestrictedTypes {
Expand Down
57 changes: 29 additions & 28 deletions internal/policy/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ import (
"testing"

"github.com/sirupsen/logrus"

"github.com/TykTechnologies/graphql-go-tools/pkg/graphql"

"github.com/stretchr/testify/assert"

"github.com/TykTechnologies/graphql-go-tools/pkg/graphql"
"github.com/TykTechnologies/tyk/internal/policy"
"github.com/TykTechnologies/tyk/user"
)
Expand All @@ -22,9 +20,9 @@ import (
var testDataFS embed.FS

func TestApplyRateLimits_PolicyLimits(t *testing.T) {
svc := &policy.Service{}

t.Run("policy limits unset", func(t *testing.T) {
svc := &policy.Service{}

session := &user.SessionState{
Rate: 5,
Per: 10,
Expand All @@ -44,6 +42,8 @@ func TestApplyRateLimits_PolicyLimits(t *testing.T) {
})

t.Run("policy limits apply all", func(t *testing.T) {
svc := &policy.Service{}

session := &user.SessionState{
Rate: 5,
Per: 10,
Expand All @@ -69,6 +69,8 @@ func TestApplyRateLimits_PolicyLimits(t *testing.T) {
// changes are applied to api limits, but skipped on
// the session as the session has a higher allowance.
t.Run("policy limits apply per-api", func(t *testing.T) {
svc := &policy.Service{}

session := &user.SessionState{
Rate: 15,
Per: 10,
Expand All @@ -93,6 +95,8 @@ func TestApplyRateLimits_PolicyLimits(t *testing.T) {
// As the policy defined a lower rate than apiLimits,
// no changes to api limits are applied.
t.Run("policy limits skip", func(t *testing.T) {
svc := &policy.Service{}

session := &user.SessionState{
Rate: 5,
Per: 10,
Expand All @@ -117,29 +121,26 @@ func TestApplyRateLimits_PolicyLimits(t *testing.T) {
func TestApplyRateLimits_FromCustomPolicies(t *testing.T) {
svc := &policy.Service{}

t.Run("Custom policies", func(t *testing.T) {
session := &user.SessionState{}
session.SetCustomPolicies([]user.Policy{
{
ID: "pol1",
Partitions: user.PolicyPartitions{RateLimit: true},
Rate: 8,
Per: 1,
AccessRights: map[string]user.AccessDefinition{"a": {}},
},
{
ID: "pol2",
Partitions: user.PolicyPartitions{RateLimit: true},
Rate: 10,
Per: 1,
AccessRights: map[string]user.AccessDefinition{"a": {}},
},
})

svc.Apply(session)

assert.Equal(t, 10, int(session.Rate))
session := &user.SessionState{}
session.SetCustomPolicies([]user.Policy{
{
ID: "pol1",
Partitions: user.PolicyPartitions{RateLimit: true},
Rate: 8,
Per: 1,
AccessRights: map[string]user.AccessDefinition{"a": {}},
},
{
ID: "pol2",
Partitions: user.PolicyPartitions{RateLimit: true},
Rate: 10,
Per: 1,
AccessRights: map[string]user.AccessDefinition{"a": {}},
},
})

assert.NoError(t, svc.Apply(session))
assert.Equal(t, 10, int(session.Rate))
}

func TestApplyEndpointLevelLimits(t *testing.T) {
Expand Down Expand Up @@ -190,7 +191,7 @@ func testPrepareApplyPolicies(tb testing.TB) (*policy.Service, []testApplyPolici
err = json.Unmarshal(f, &repoPols)
assert.NoError(tb, err)

store := policy.NewStore(repoPols)
store := policy.NewStoreMap(repoPols)
orgID := ""
service := policy.New(&orgID, store, logrus.StandardLogger())

Expand Down
27 changes: 20 additions & 7 deletions internal/policy/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,45 @@ import (
"github.com/TykTechnologies/tyk/user"
)

// Store is an in-memory policy storage object that
// implements the repository for policy access. We
// do not implement concurrency protections here.
// Store is an in-memory policy storage object that implements the
// repository for policy access. We do not implement concurrency
// protections here. Where order is important, use this.
type Store struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this changed?

Copy link
Contributor Author

@titpetric titpetric Oct 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom policies doesn't preserve order (map policyID => policy) and would fail tests due to the order changes. The PolicyIDs() doesn't return ids always in the same order, meaning Methods can be returned out of order failing the assertion(s), requiring a deeper assertion to account for the typical go range over map behaviour.

Store{} preserves order.
StoreMap{} doesn't.

-customPolicies, err := session.CustomPolicies()      <--- used to be a map
+customPolicies, err := session.GetCustomPolicies()   <--- provides a list of policies now
if err != nil {
        policyIDs = session.PolicyIDs()
} else {
        storage = NewStore(customPolicies)
        policyIDs = storage.PolicyIDs()               <--- maintains policy list order
}

This change ensures a deterministic order for applying policies, and in turn the merged access rights Methods are sorted with the order the policies are applied in, simplifying the assertion in tests.

No other uses in gw. If the implementing ticket wasn't so bad with detail, I could easily consider custom policies functions internal scope. The underlying storage is a []any inside a map[string]any MetaData.

Considered but rejected a deeper assertion in tests for methods (ElementsEqual...). Not preserving custom policy order was a code smell, the added GetCustomPolicies works around the restriction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sidenote:

  • This could have been a CustomPolicies []Policy on SessionState, not sure why it needs the API around a map[string]any;
  • I'm not sure why custom policies were stuck into metadata other than to piggyback on an accessible key/value store field. No public API changes ensure whatever is coupled to it continues to work.

policies map[string]user.Policy
policies []user.Policy
}

func NewStore(policies map[string]user.Policy) *Store {
// NewStore returns a new policy.Store.
func NewStore(policies []user.Policy) *Store {
return &Store{
policies: policies,
}
}

// PolicyIDs returns a list policy IDs in the store.
// It will return nil if no policies exist.
func (s *Store) PolicyIDs() []string {
if len(s.policies) == 0 {
return nil
}

policyIDs := make([]string, 0, len(s.policies))
for _, val := range s.policies {
policyIDs = append(policyIDs, val.ID)
}
return policyIDs
}

// PolicyByID returns a policy by ID.
func (s *Store) PolicyByID(id string) (user.Policy, bool) {
v, ok := s.policies[id]
return v, ok
for _, pol := range s.policies {
if pol.ID == id {
return pol, true
}
}
return user.Policy{}, false
}

// PolicyCount returns the number of policies in the store.
func (s *Store) PolicyCount() int {
return len(s.policies)
}
46 changes: 46 additions & 0 deletions internal/policy/store_map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package policy

import (
"github.com/TykTechnologies/tyk/user"
)

// StoreMap is same as Store, but doesn't preserve order.
type StoreMap struct {
policies map[string]user.Policy
}

// NewStoreMap returns a new policy.StoreMap.
func NewStoreMap(policies map[string]user.Policy) *StoreMap {
if len(policies) == 0 {
policies = make(map[string]user.Policy)
}

return &StoreMap{
policies: policies,
}
}

// PolicyIDs returns a list policy IDs in the store.
// It will return nil if no policies exist.
func (s *StoreMap) PolicyIDs() []string {
if len(s.policies) == 0 {
return nil
}

policyIDs := make([]string, 0, len(s.policies))
for _, val := range s.policies {
policyIDs = append(policyIDs, val.ID)
}
return policyIDs
}

// PolicyByID returns a policy by ID.
func (s *StoreMap) PolicyByID(id string) (user.Policy, bool) {
v, ok := s.policies[id]
return v, ok
}

// PolicyCount returns the number of policies in the store.
func (s *StoreMap) PolicyCount() int {
return len(s.policies)
}
Loading
Loading