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 |
|
No comment on field. |
| Policies |
|
No comment on field. |
| Apis |
|
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 |
|
No comment on field. |
| logger |
|
No comment on field. |
| orgID |
|
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 |
|
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 |
|
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 |
|
No comment on field. |
| didRateLimit |
|
No comment on field. |
| didAcl |
|
No comment on field. |
| didComplexity |
|
No comment on field. |
| didPerAPI |
|
No comment on field. |
| didPartition |
|
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
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
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
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
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
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
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
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
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 |
|
No comment on field. |
| policies |
|
No comment on field. |
| errMatch |
|
No comment on field. |
| sessMatch |
|
No comment on field. |
| session |
|
No comment on field. |
| reverseOrder |
|
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
}