Go API Documentation

github.com/TykTechnologies/tyk/internal/policy

No package summary is available.

Package

Files: 5. Third party imports: 1. Imports from organisation: 0. Tests: 0. Benchmarks: 0.

Vars

var (
	// ErrMixedPartitionAndPerAPIPolicies is the error to return when a mix of per api and partitioned policies are to be applied in a session.
	ErrMixedPartitionAndPerAPIPolicies = errors.New("cannot apply multiple policies when some have per_api set and some are partitioned")
)

Types

RPCDataLoaderMock

RPCDataLoaderMock is a policy-related test utility.

Field name Field type Comment
ShouldConnect

bool

No comment on field.
Policies

[]user.Policy

No comment on field.
Apis

[]model.MergedAPI

No comment on field.
type RPCDataLoaderMock struct {
	ShouldConnect	bool
	Policies	[]user.Policy
	Apis		[]model.MergedAPI
}

Service

Service represents the implementation for apply policies logic.

Field name Field type Comment
storage

model.PolicyProvider

No comment on field.
logger

*logrus.Logger

No comment on field.
orgID

*string

used for validation if not empty

type Service struct {
	storage	model.PolicyProvider
	logger	*logrus.Logger

	// used for validation if not empty
	orgID	*string
}

Store

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.

Field name Field type Comment
policies

[]user.Policy

No comment on field.
type Store struct {
	policies []user.Policy
}

StoreMap

StoreMap is same as Store, but doesn't preserve order.

Field name Field type Comment
policies

map[string]user.Policy

No comment on field.
type StoreMap struct {
	policies map[string]user.Policy
}

applyStatus

This type doesn't have documentation.

Field name Field type Comment
didQuota

map[string]bool

No comment on field.
didRateLimit

map[string]bool

No comment on field.
didAcl

map[string]bool

No comment on field.
didComplexity

map[string]bool

No comment on field.
didPerAPI

bool

No comment on field.
didPartition

bool

No comment on field.
type applyStatus struct {
	didQuota	map[string]bool
	didRateLimit	map[string]bool
	didAcl		map[string]bool
	didComplexity	map[string]bool
	didPerAPI	bool
	didPartition	bool
}

Functions

func MergeAllowedURLs

MergeAllowedURLs will merge s1 and s2 to produce a merged result. It maintains order of keys in s1 and s2 as they are seen. If the result is an empty set, nil is returned.

func MergeAllowedURLs(s1, s2 []user.AccessSpec) []user.AccessSpec {
	order := []string{}
	merged := map[string][]string{}

	// Loop input sets and merge through a map.
	for _, src := range [][]user.AccessSpec{s1, s2} {
		for _, r := range src {
			url := r.URL
			v, ok := merged[url]
			if !ok {
				// First time we see the spec
				merged[url] = r.Methods

				// Maintain order
				order = append(order, url)

				continue
			}
			merged[url] = appendIfMissing(v, r.Methods...)
		}
	}

	// Early exit without allocating.
	if len(order) == 0 {
		return nil
	}

	// Provide results in desired order.
	result := make([]user.AccessSpec, 0, len(order))
	for _, key := range order {
		spec := user.AccessSpec{
			Methods:	merged[key],
			URL:		key,
		}
		result = append(result, spec)
	}
	return result
}

Cognitive complexity: 17, Cyclomatic complexity: 6

Uses: user.AccessSpec.

func New

func New(orgID *string, storage model.PolicyProvider, logger *logrus.Logger) *Service {
	return &Service{
		orgID:		orgID,
		storage:	storage,
		logger:		logger,
	}
}

Cognitive complexity: 1, Cyclomatic complexity: 1

func NewStore

NewStore returns a new policy.Store.

func NewStore(policies []user.Policy) *Store {
	return &Store{
		policies: policies,
	}
}

Cognitive complexity: 1, Cyclomatic complexity: 1

func NewStoreMap

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,
	}
}

Cognitive complexity: 3, Cyclomatic complexity: 2

Uses: user.Policy.

func (*RPCDataLoaderMock) Connect

Connect will return the connection status.

func (s *RPCDataLoaderMock) Connect() bool {
	return s.ShouldConnect
}

Cognitive complexity: 0, Cyclomatic complexity: 1

func (*RPCDataLoaderMock) GetApiDefinitions

GetApiDefinitions returns the internal Apis as a json string.

func (s *RPCDataLoaderMock) GetApiDefinitions(_ string, tags []string) string {
	if len(tags) > 1 {
		panic("not implemented")
	}

	apiList, err := json.Marshal(s.Apis)
	if err != nil {
		return ""
	}
	return string(apiList)
}

Cognitive complexity: 4, Cyclomatic complexity: 3

Uses: json.Marshal.

func (*RPCDataLoaderMock) GetPolicies

GetPolicies returns the internal Policies as a json string.

func (s *RPCDataLoaderMock) GetPolicies(_ string) string {
	policyList, err := json.Marshal(s.Policies)
	if err != nil {
		return ""
	}
	return string(policyList)
}

Cognitive complexity: 2, Cyclomatic complexity: 2

Uses: json.Marshal.

func (*Service) Apply

Apply will check if any policies are loaded. If any are, it will overwrite the session state to use the policy values.

func (t *Service) Apply(session *user.SessionState) error {
	rights := make(map[string]user.AccessDefinition)
	tags := make(map[string]bool)
	if session.MetaData == nil {
		session.MetaData = make(map[string]interface{})
	}

	if err := t.ClearSession(session); err != nil {
		t.logger.WithError(err).Warn("error clearing session")
	}

	applyState := applyStatus{
		didQuota:	make(map[string]bool),
		didRateLimit:	make(map[string]bool),
		didAcl:		make(map[string]bool),
		didComplexity:	make(map[string]bool),
	}

	var (
		err		error
		policyIDs	[]string
	)

	storage := t.storage

	customPolicies, err := session.GetCustomPolicies()
	if err != nil {
		policyIDs = session.PolicyIDs()
	} else {
		storage = NewStore(customPolicies)
		policyIDs = storage.PolicyIDs()
	}

	for _, polID := range policyIDs {
		policy, ok := storage.PolicyByID(polID)
		if !ok {
			err := fmt.Errorf("policy not found: %q", polID)
			t.Logger().Error(err)
			if len(policyIDs) > 1 {
				continue
			}

			return err
		}
		// Check ownership, policy org owner must be the same as API,
		// otherwise you could overwrite a session key with a policy from a different org!
		if t.orgID != nil && policy.OrgID != *t.orgID {
			err := errors.New("attempting to apply policy from different organisation to key, skipping")
			t.Logger().Error(err)
			return err
		}

		if policy.Partitions.PerAPI && policy.Partitions.Enabled() {
			err := fmt.Errorf("cannot apply policy %s which has per_api and any of partitions set", policy.ID)
			t.logger.Error(err)
			return err
		}

		if policy.Partitions.PerAPI {
			if err := t.applyPerAPI(policy, session, rights, &applyState); err != nil {
				return err
			}
		} else {
			if err := t.applyPartitions(policy, session, rights, &applyState); err != nil {
				return err
			}
		}

		session.IsInactive = session.IsInactive || policy.IsInactive

		for _, tag := range policy.Tags {
			tags[tag] = true
		}

		for k, v := range policy.MetaData {
			session.MetaData[k] = v
		}

		if policy.LastUpdated > session.LastUpdated {
			session.LastUpdated = policy.LastUpdated
		}
	}

	for _, tag := range session.Tags {
		tags[tag] = true
	}

	// set tags
	session.Tags = []string{}
	for tag := range tags {
		session.Tags = appendIfMissing(session.Tags, tag)
	}

	if len(policyIDs) == 0 {
		for apiID, accessRight := range session.AccessRights {
			// check if the api in the session has per api limit
			if !accessRight.Limit.IsEmpty() {
				accessRight.AllowanceScope = apiID
				session.AccessRights[apiID] = accessRight
			}
		}
	}

	distinctACL := make(map[string]bool)

	for _, v := range rights {
		if v.Limit.SetBy != "" {
			distinctACL[v.Limit.SetBy] = true
		}
	}

	// If some APIs had only ACL partitions, inherit rest from session level
	for k, v := range rights {
		if !applyState.didAcl[k] {
			delete(rights, k)
			continue
		}

		if !applyState.didRateLimit[k] {
			v.Limit.Rate = session.Rate
			v.Limit.Per = session.Per
			v.Limit.Smoothing = session.Smoothing
			v.Limit.ThrottleInterval = session.ThrottleInterval
			v.Limit.ThrottleRetryLimit = session.ThrottleRetryLimit
			v.Endpoints = nil
		}

		if !applyState.didComplexity[k] {
			v.Limit.MaxQueryDepth = session.MaxQueryDepth
		}

		if !applyState.didQuota[k] {
			v.Limit.QuotaMax = session.QuotaMax
			v.Limit.QuotaRenewalRate = session.QuotaRenewalRate
			v.Limit.QuotaRenews = session.QuotaRenews
		}

		// If multime ACL
		if len(distinctACL) > 1 {
			if v.AllowanceScope == "" && v.Limit.SetBy != "" {
				v.AllowanceScope = v.Limit.SetBy
			}
		}

		v.Limit.SetBy = ""

		rights[k] = v
	}

	// If we have policies defining rules for one single API, update session root vars (legacy)
	t.updateSessionRootVars(session, rights, applyState)

	// Override session ACL if at least one policy define it
	if len(applyState.didAcl) > 0 {
		session.AccessRights = rights
	}

	if len(rights) == 0 && policyIDs != nil {
		return errors.New("key has no valid policies to be applied")
	}

	return nil
}

Cognitive complexity: 75, Cyclomatic complexity: 36

Uses: errors.New, fmt.Errorf, user.AccessDefinition.

func (*Service) ApplyEndpointLevelLimits

ApplyEndpointLevelLimits combines policyEndpoints and currEndpoints and returns the combined value. The returned endpoints would have the highest request rate from policyEndpoints and currEndpoints.

func (t *Service) ApplyEndpointLevelLimits(policyEndpoints user.Endpoints, currEndpoints user.Endpoints) user.Endpoints {
	currEPMap := currEndpoints.Map()
	if len(currEPMap) == 0 {
		return policyEndpoints
	}

	result := policyEndpoints.Map()
	if len(result) == 0 {
		return currEPMap.Endpoints()
	}

	for currEP, currRL := range currEPMap {
		policyRL, ok := result[currEP]
		if !ok {
			// merge missing endpoints
			result[currEP] = currRL
			continue
		}

		policyDur, currDur := policyRL.Duration(), currRL.Duration()
		if policyDur > currDur {
			result[currEP] = currRL
			continue
		}

		// when duration is equal, use higher rate and per
		// eg. when 10 per 60 and 5 per 30 comes in
		// Duration would be 6s each, in such a case higher rate of 10 per 60 would be picked up.
		if policyDur == currDur && currRL.Rate > policyRL.Rate {
			result[currEP] = currRL
		}
	}

	return result.Endpoints()
}

Cognitive complexity: 13, Cyclomatic complexity: 8

func (*Service) ApplyRateLimits

ApplyRateLimits will write policy limits to session and apiLimits. The limits get written if either are empty. The limits get written if filled and policyLimits allows a higher request rate.

func (t *Service) ApplyRateLimits(session *user.SessionState, policy user.Policy, apiLimits *user.APILimit) {
	policyLimits := policy.APILimit()
	if t.emptyRateLimit(policyLimits) {
		return
	}

	// duration is time between requests, e.g.:
	//
	// apiLimits: 500ms for 2 requests / second
	// policyLimits: 100ms for 10 requests / second
	//
	// if apiLimits > policyLimits (500ms > 100ms) then
	// we apply the higher rate from the policy.
	//
	// the policy-defined rate limits are enforced as
	// a minimum possible api rate limit setting,
	// raising apiLimits.

	if t.emptyRateLimit(*apiLimits) || apiLimits.Duration() > policyLimits.Duration() {
		apiLimits.Rate = policyLimits.Rate
		apiLimits.Per = policyLimits.Per
		apiLimits.Smoothing = policyLimits.Smoothing
	}

	// sessionLimits, similar to apiLimits, get policy
	// rate applied if the policy allows more requests.
	sessionLimits := session.APILimit()
	if t.emptyRateLimit(sessionLimits) || sessionLimits.Duration() > policyLimits.Duration() {
		session.Rate = policyLimits.Rate
		session.Per = policyLimits.Per
		session.Smoothing = policyLimits.Smoothing
	}
}

Cognitive complexity: 6, Cyclomatic complexity: 6

func (*Service) ClearSession

ClearSession clears the quota, rate limit and complexity values so that partitioned policies can apply their values. Otherwise, if the session has already a higher value, an applied policy will not win, and its values will be ignored.

func (t *Service) ClearSession(session *user.SessionState) error {
	policies := session.PolicyIDs()

	for _, polID := range policies {
		policy, ok := t.storage.PolicyByID(polID)
		if !ok {
			return fmt.Errorf("policy not found: %s", polID)
		}

		all := !(policy.Partitions.Quota || policy.Partitions.RateLimit || policy.Partitions.Acl || policy.Partitions.Complexity)

		if policy.Partitions.Quota || all {
			session.QuotaMax = 0
			session.QuotaRemaining = 0
		}

		if policy.Partitions.RateLimit || all {
			session.Rate = 0
			session.Per = 0
			session.Smoothing = nil
			session.ThrottleRetryLimit = 0
			session.ThrottleInterval = 0
		}

		if policy.Partitions.Complexity || all {
			session.MaxQueryDepth = 0
		}
	}

	return nil
}

Cognitive complexity: 11, Cyclomatic complexity: 12

Uses: fmt.Errorf.

func (*Service) Logger

Logger implements a typical logger signature with service context.

func (t *Service) Logger() *logrus.Entry {
	return logrus.NewEntry(t.logger)
}

Cognitive complexity: 0, Cyclomatic complexity: 1

Uses: logrus.NewEntry.

func (*Store) PolicyByID

PolicyByID returns a policy by ID.

func (s *Store) PolicyByID(id string) (user.Policy, bool) {
	for _, pol := range s.policies {
		if pol.ID == id {
			return pol, true
		}
	}
	return user.Policy{}, false
}

Cognitive complexity: 6, Cyclomatic complexity: 3

Uses: user.Policy.

func (*Store) PolicyCount

PolicyCount returns the number of policies in the store.

func (s *Store) PolicyCount() int {
	return len(s.policies)
}

Cognitive complexity: 0, Cyclomatic complexity: 1

func (*Store) PolicyIDs

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
}

Cognitive complexity: 5, Cyclomatic complexity: 3

func (*StoreMap) PolicyByID

PolicyByID returns a policy by ID.

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

Cognitive complexity: 0, Cyclomatic complexity: 1

func (*StoreMap) PolicyCount

PolicyCount returns the number of policies in the store.

func (s *StoreMap) PolicyCount() int {
	return len(s.policies)
}

Cognitive complexity: 0, Cyclomatic complexity: 1

func (*StoreMap) PolicyIDs

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
}

Cognitive complexity: 5, Cyclomatic complexity: 3

Private functions

func appendIfMissing

appendIfMissing ensures dest slice is unique with new items.

appendIfMissing (dest []string, in ...string) []string
References: slices.Contains.

func greaterThanInt

greaterThanInt checks whether first int value is bigger than second int value. -1 means infinite and the biggest value.

greaterThanInt (first,second int) bool

func greaterThanInt64

greaterThanInt64 checks whether first int64 value is bigger than second int64 value. -1 means infinite and the biggest value.

greaterThanInt64 (first,second int64) bool

func intersection

intersection gets intersection of the given two slices.

intersection (a []string, b []string) []string

func applyAPILevelLimits

applyAPILevelLimits (policyAD user.AccessDefinition, currAD user.AccessDefinition) user.AccessDefinition

func applyPartitions

applyPartitions (policy user.Policy, session *user.SessionState, rights map[string]user.AccessDefinition, applyState *applyStatus) error
References: user.AccessDefinition, user.FieldLimits.

func applyPerAPI

applyPerAPI (policy user.Policy, session *user.SessionState, rights map[string]user.AccessDefinition, applyState *applyStatus) error

func emptyRateLimit

emptyRateLimit (m user.APILimit) bool

func updateSessionRootVars

updateSessionRootVars (session *user.SessionState, rights map[string]user.AccessDefinition, applyState applyStatus)


Tests

Files: 2. Third party imports: 2. Imports from organisation: 1. Tests: 6. Benchmarks: 1.

Vars

var testDataFS embed.FS

Types

testApplyPoliciesData

This type doesn't have documentation.

Field name Field type Comment
name

string

No comment on field.
policies

[]string

No comment on field.
errMatch

string

No comment on field.
sessMatch

func(*testing.T, *user.SessionState)

No comment on field.
session

*user.SessionState

No comment on field.
reverseOrder

bool

reverseOrder executes the tests in reversed order of policies, in addition to the order specified in policies

type testApplyPoliciesData struct {
	name		string
	policies	[]string
	errMatch	string					// substring
	sessMatch	func(*testing.T, *user.SessionState)	// ignored if nil
	session		*user.SessionState
	// reverseOrder executes the tests in reversed order of policies,
	// in addition to the order specified in policies
	reverseOrder	bool
}

Test functions

TestApplyACL_FromCustomPolicies

References: assert.Equal, assert.NoError, policy.Service, testing.T, user.AccessDefinition, user.AccessSpec, user.Policy, user.PolicyPartitions, user.SessionState.

TestApplyEndpointLevelLimits

References: assert.ElementsMatch, assert.NoError, json.Unmarshal, policy.Service, testing.T, user.Endpoints.

TestApplyRateLimits_FromCustomPolicies

References: assert.Equal, assert.NoError, policy.Service, user.AccessDefinition, user.Policy, user.PolicyPartitions, user.SessionState.

TestApplyRateLimits_PolicyLimits

References: assert.Equal, policy.Service, testing.T, user.APILimit, user.Policy, user.RateLimit, user.SessionState.

TestMergeAllowedURLs

References: assert.Equal, assert.NoError, policy.Service, user.AccessDefinition, user.AccessSpec, user.Policy, user.SessionState.

TestService_Apply

References: assert.ErrorContains, fmt.Sprintf, slices.Reverse, testing.T, user.SessionState.

Benchmark functions

BenchmarkService_Apply

References: assert.NoError, user.SessionState.