Skip to main content

API Quotas and Rate Limiting

This example demonstrates how to use MPE for API subscription tier enforcement, feature flags, and quota-based access control. While MPE doesn't perform real-time rate limiting (that's typically done at the gateway level), it can enforce policy-level access based on subscription tiers, quotas, and feature entitlements.

Overview

Overview

Modern APIs often have tiered access levels:

TierFeaturesUse Case
FreeBasic endpoints, low quotasTesting, hobby projects
ProMore endpoints, higher quotasSmall businesses
EnterpriseAll endpoints, custom quotasLarge organizations

This PolicyDomain enforces:

  • Tier-based endpoint access: Some endpoints only available to paid tiers
  • Feature flags: Enable/disable features per subscription
  • Quota visibility: Pass quota information to policies
  • API key scopes: Limit what API keys can do

Design

Subscription Model

Each API consumer has a subscription with:

  • Tier: free, pro, or enterprise
  • Features: List of enabled feature flags
  • Quotas: Limits for various operations
  • API Key Scopes: Restrictions on individual API keys

Complete PolicyDomain

apiVersion: iamlite.manetu.io/v1alpha4
kind: PolicyDomain
metadata:
name: api-quotas
spec:
# ============================================================
# Policy Libraries
# ============================================================
policy-libraries:
- mrn: &lib-utils "mrn:iam:library:utils"
name: utils
description: "Common utility functions"
rego: |
package utils

import rego.v1

# Check if request has a valid principal (authenticated)
has_principal if {
input.principal != {}
input.principal.sub != ""
}

- mrn: &lib-tier-helpers "mrn:iam:library:tier-helpers"
name: tier-helpers
description: "Subscription tier helper functions"
rego: |
package tier_helpers

import rego.v1

# Tier hierarchy (higher = more access)
tier_level("free") := 1
tier_level("pro") := 2
tier_level("enterprise") := 3

# Get principal's tier level
principal_tier_level(principal) := level if {
tier := principal.mannotations.subscription_tier
level := tier_level(tier)
}

principal_tier_level(principal) := 0 if {
not principal.mannotations.subscription_tier
}

# Check if principal's tier meets minimum requirement
has_tier(principal, required_tier) if {
principal_tier_level(principal) >= tier_level(required_tier)
}

# Check if a feature is enabled for this principal
has_feature(principal, feature_name) if {
feature_name in principal.mannotations.enabled_features
}

# Check quota limits
within_quota(principal, resource_type, current_usage) if {
quotas := principal.mannotations.quotas
limit := quotas[resource_type]
current_usage < limit
}

# For resources without quota tracking
within_quota(principal, resource_type, current_usage) if {
not principal.mannotations.quotas[resource_type]
}

# Get required tier for an operation based on endpoint patterns
required_tier_for_operation(operation) := "free" if {
glob.match("api:basic:*", [], operation)
}

required_tier_for_operation(operation) := "pro" if {
some pattern in {"api:advanced:*", "api:export:*", "api:analytics:*"}
glob.match(pattern, [], operation)
}

required_tier_for_operation(operation) := "enterprise" if {
some pattern in {"api:enterprise:*", "api:admin:*", "api:bulk:*"}
glob.match(pattern, [], operation)
}

# Default to free tier if no pattern matches
default required_tier_for_operation(_) := "free"

# Feature requirements for specific operations
required_feature("api:export:csv") := "export_csv"
required_feature("api:export:pdf") := "export_pdf"
required_feature("api:analytics:realtime") := "realtime_analytics"
required_feature("api:bulk:import") := "bulk_import"
required_feature("api:bulk:export") := "bulk_export"

# Check if operation requires a feature
requires_feature(operation) if {
required_feature(operation)
}

# ============================================================
# Policies
# ============================================================
policies:
# Operation phase - validate API key and basic access
- mrn: &policy-api-auth "mrn:iam:policy:api-auth"
name: api-auth
description: "Validate API key presence"
dependencies:
- *lib-utils
rego: |
package authz

import rego.v1
import data.utils

# Tri-level: negative=DENY, 0=GRANT, positive=GRANT Override
# Default deny - only grant if authenticated or public
default allow = -1

# Helper: check if this is a public endpoint
is_public if {
glob.match("api:public:*", [], input.operation)
}

# Public endpoints bypass auth (grant-override)
allow = 1 if is_public

# Grant authenticated requests
allow = 0 if utils.has_principal

# Identity phase - check tier access
- mrn: &policy-tier-access "mrn:iam:policy:tier-access"
name: tier-access
description: "Check subscription tier allows endpoint"
dependencies:
- *lib-tier-helpers
rego: |
package authz

import rego.v1
import data.tier_helpers

default allow = false

# Allow if principal's tier meets endpoint requirement
allow if {
required := tier_helpers.required_tier_for_operation(input.operation)
tier_helpers.has_tier(input.principal, required)
}

# Resource phase - check features and quotas
- mrn: &policy-feature-check "mrn:iam:policy:feature-check"
name: feature-check
description: "Check feature flags and quotas"
dependencies:
- *lib-tier-helpers
rego: |
package authz

import rego.v1
import data.tier_helpers

default allow = false

# Allow if operation doesn't require a feature
allow if {
not tier_helpers.requires_feature(input.operation)
check_quota
}

# Allow if required feature is enabled
allow if {
tier_helpers.requires_feature(input.operation)
feature := tier_helpers.required_feature(input.operation)
tier_helpers.has_feature(input.principal, feature)
check_quota
}

# Quota check helper
check_quota if {
resource_type := input.resource.annotations.resource_type
current_usage := input.context.current_usage
tier_helpers.within_quota(input.principal, resource_type, current_usage)
}

check_quota if {
not input.resource.annotations.resource_type
}

check_quota if {
not input.context.current_usage
}

# Scope phase - API key restrictions
- mrn: &policy-api-key-scope "mrn:iam:policy:api-key-scope"
name: api-key-scope
description: "Enforce API key scope restrictions"
rego: |
package authz

import rego.v1

default allow = false

# Helper: check if operation is read-only
is_read_only if {
some pattern in {"*:read", "*:list", "*:get"}
glob.match(pattern, [], input.operation)
}

# Read-only scope allows read operations
allow if is_read_only

# Full access scope policy
- mrn: &policy-full-access-scope "mrn:iam:policy:full-access-scope"
name: full-access-scope
description: "Full API access scope"
rego: |
package authz
default allow = true

# ============================================================
# Roles - Subscription tiers
# ============================================================
roles:
- mrn: &role-free-tier "mrn:iam:role:free-tier"
name: free-tier
description: "Free tier subscriber"
policy: *policy-tier-access
annotations:
- name: "subscription_tier"
value: "\"free\""
- name: "enabled_features"
value: "[]"
merge: "union"
- name: "quotas"
value: '{"api_calls": 1000, "storage_mb": 100}'

- mrn: &role-pro-tier "mrn:iam:role:pro-tier"
name: pro-tier
description: "Pro tier subscriber"
policy: *policy-tier-access
annotations:
- name: "subscription_tier"
value: "\"pro\""
- name: "enabled_features"
value: '["export_csv", "realtime_analytics"]'
merge: "union"
- name: "quotas"
value: '{"api_calls": 100000, "storage_mb": 10000}'

- mrn: &role-enterprise-tier "mrn:iam:role:enterprise-tier"
name: enterprise-tier
description: "Enterprise tier subscriber"
policy: *policy-tier-access
annotations:
- name: "subscription_tier"
value: "\"enterprise\""
- name: "enabled_features"
value: '["export_csv", "export_pdf", "realtime_analytics", "bulk_import", "bulk_export"]'
merge: "union"
- name: "quotas"
value: '{"api_calls": 10000000, "storage_mb": 1000000}'

# ============================================================
# Groups - Example organizations
# ============================================================
groups:
- mrn: "mrn:iam:group:hobby-developers"
name: hobby-developers
description: "Hobby/free tier users"
roles:
- *role-free-tier

- mrn: "mrn:iam:group:small-business"
name: small-business
description: "Small business pro tier"
roles:
- *role-pro-tier
annotations:
# Override default quotas for this specific customer
- name: "quotas"
value: '{"api_calls": 200000, "storage_mb": 20000}'

- mrn: "mrn:iam:group:enterprise-customer"
name: enterprise-customer
description: "Enterprise tier customer"
roles:
- *role-enterprise-tier
annotations:
# Enterprise gets custom quota
- name: "quotas"
value: '{"api_calls": -1, "storage_mb": -1}' # -1 = unlimited

# ============================================================
# Scopes - API key restrictions
# ============================================================
scopes:
- mrn: "mrn:iam:scope:read-only"
name: read-only
description: "Read-only API access"
policy: *policy-api-key-scope

- mrn: "mrn:iam:scope:full-access"
name: full-access
description: "Full API access"
policy: *policy-full-access-scope

# ============================================================
# Resource Groups
# ============================================================
resource-groups:
- mrn: "mrn:iam:resource-group:api-resources"
name: api-resources
description: "Standard API resources"
default: true
policy: *policy-feature-check

# ============================================================
# Operations
# ============================================================
operations:
- name: all-api-operations
selector:
- "api:.*"
policy: *policy-api-auth

Test Cases

Test 1: Free Tier Basic Access

A free tier user can access basic endpoints:

{
"principal": {
"sub": "user-free-123",
"mroles": ["mrn:iam:role:free-tier"],
"mgroups": ["mrn:iam:group:hobby-developers"],
"mannotations": {
"subscription_tier": "free",
"enabled_features": [],
"quotas": {"api_calls": 1000, "storage_mb": 100}
}
},
"operation": "api:basic:users:list",
"resource": {
"id": "mrn:api:users",
"group": "mrn:iam:resource-group:api-resources"
},
"context": {}
}

Expected:

GRANT

Test 2: Free Tier Advanced Endpoint Denied

A free tier user cannot access pro endpoints:

{
"principal": {
"sub": "user-free-123",
"mroles": ["mrn:iam:role:free-tier"],
"mgroups": ["mrn:iam:group:hobby-developers"],
"mannotations": {
"subscription_tier": "free",
"enabled_features": [],
"quotas": {"api_calls": 1000, "storage_mb": 100}
}
},
"operation": "api:advanced:reports:generate",
"resource": {
"id": "mrn:api:reports",
"group": "mrn:iam:resource-group:api-resources"
},
"context": {}
}

Expected:

DENY
(advanced requires pro tier)

Test 3: Pro Tier Advanced Access

A pro tier user can access advanced endpoints:

{
"principal": {
"sub": "user-pro-456",
"mroles": ["mrn:iam:role:pro-tier"],
"mgroups": ["mrn:iam:group:small-business"],
"mannotations": {
"subscription_tier": "pro",
"enabled_features": ["export_csv", "realtime_analytics"],
"quotas": {"api_calls": 200000, "storage_mb": 20000}
}
},
"operation": "api:advanced:reports:generate",
"resource": {
"id": "mrn:api:reports",
"group": "mrn:iam:resource-group:api-resources"
},
"context": {}
}

Expected:

GRANT

Test 4: Pro Tier Enterprise Endpoint Denied

A pro tier user cannot access enterprise endpoints:

{
"principal": {
"sub": "user-pro-456",
"mroles": ["mrn:iam:role:pro-tier"],
"mgroups": ["mrn:iam:group:small-business"],
"mannotations": {
"subscription_tier": "pro",
"enabled_features": ["export_csv", "realtime_analytics"],
"quotas": {"api_calls": 200000, "storage_mb": 20000}
}
},
"operation": "api:admin:users:create",
"resource": {
"id": "mrn:api:admin:users",
"group": "mrn:iam:resource-group:api-resources"
},
"context": {}
}

Expected:

DENY
(admin requires enterprise tier)

Test 5: Feature Flag Enabled

A pro user with CSV export feature can export:

{
"principal": {
"sub": "user-pro-456",
"mroles": ["mrn:iam:role:pro-tier"],
"mgroups": ["mrn:iam:group:small-business"],
"mannotations": {
"subscription_tier": "pro",
"enabled_features": ["export_csv", "realtime_analytics"],
"quotas": {"api_calls": 200000}
}
},
"operation": "api:export:csv",
"resource": {
"id": "mrn:api:export",
"group": "mrn:iam:resource-group:api-resources"
},
"context": {}
}

Expected:

GRANT
(export_csv feature is enabled)

Test 6: Feature Flag Disabled

A pro user without PDF export feature cannot export PDF:

{
"principal": {
"sub": "user-pro-456",
"mroles": ["mrn:iam:role:pro-tier"],
"mgroups": ["mrn:iam:group:small-business"],
"mannotations": {
"subscription_tier": "pro",
"enabled_features": ["export_csv", "realtime_analytics"],
"quotas": {"api_calls": 200000}
}
},
"operation": "api:export:pdf",
"resource": {
"id": "mrn:api:export",
"group": "mrn:iam:resource-group:api-resources"
},
"context": {}
}

Expected:

DENY
(export_pdf feature not enabled for pro)

Test 7: Within Quota

A user within their quota can proceed:

{
"principal": {
"sub": "user-pro-456",
"mroles": ["mrn:iam:role:pro-tier"],
"mgroups": ["mrn:iam:group:small-business"],
"mannotations": {
"subscription_tier": "pro",
"enabled_features": ["export_csv"],
"quotas": {"api_calls": 200000, "storage_mb": 20000}
}
},
"operation": "api:basic:data:read",
"resource": {
"id": "mrn:api:data",
"group": "mrn:iam:resource-group:api-resources",
"annotations": {
"resource_type": "api_calls"
}
},
"context": {
"current_usage": 50000
}
}

Expected:

GRANT
(50000 < 200000 quota)

Test 8: Over Quota

A user over their quota is denied:

{
"principal": {
"sub": "user-free-123",
"mroles": ["mrn:iam:role:free-tier"],
"mgroups": ["mrn:iam:group:hobby-developers"],
"mannotations": {
"subscription_tier": "free",
"enabled_features": [],
"quotas": {"api_calls": 1000, "storage_mb": 100}
}
},
"operation": "api:basic:data:read",
"resource": {
"id": "mrn:api:data",
"group": "mrn:iam:resource-group:api-resources",
"annotations": {
"resource_type": "api_calls"
}
},
"context": {
"current_usage": 1500
}
}

Expected:

DENY
(1500 >= 1000 quota)

Test 9: Enterprise Full Access

An enterprise user has access to everything:

{
"principal": {
"sub": "user-enterprise-789",
"mroles": ["mrn:iam:role:enterprise-tier"],
"mgroups": ["mrn:iam:group:enterprise-customer"],
"mannotations": {
"subscription_tier": "enterprise",
"enabled_features": ["export_csv", "export_pdf", "realtime_analytics", "bulk_import", "bulk_export"],
"quotas": {"api_calls": -1, "storage_mb": -1}
}
},
"operation": "api:admin:users:create",
"resource": {
"id": "mrn:api:admin:users",
"group": "mrn:iam:resource-group:api-resources"
},
"context": {}
}

Expected:

GRANT
(enterprise has access to all endpoints)

Test 10: Public Endpoint No Auth

Public endpoints work without authentication:

{
"principal": {},
"operation": "api:public:health:check",
"resource": {
"id": "mrn:api:health",
"group": "mrn:iam:resource-group:api-resources"
},
"context": {}
}

Expected:

GRANT
(public endpoints use tri-level override)

Test 11: Read-Only API Key Scope

An API key with read-only scope cannot write:

{
"principal": {
"sub": "user-pro-456",
"mroles": ["mrn:iam:role:pro-tier"],
"mgroups": ["mrn:iam:group:small-business"],
"scopes": ["mrn:iam:scope:read-only"],
"mannotations": {
"subscription_tier": "pro",
"enabled_features": ["export_csv"],
"quotas": {"api_calls": 200000}
}
},
"operation": "api:basic:data:create",
"resource": {
"id": "mrn:api:data",
"group": "mrn:iam:resource-group:api-resources"
},
"context": {}
}

Expected:

DENY
(read-only scope blocks write operations)

Key Concepts Demonstrated

1. Tier-Based Endpoint Access

Operations are mapped to minimum tier requirements using individual rules:

required_tier_for_operation(operation) := "free" if {
glob.match("api:basic:*", [], operation)
}

required_tier_for_operation(operation) := "pro" if {
some pattern in {"api:advanced:*", "api:export:*", "api:analytics:*"}
glob.match(pattern, [], operation)
}

required_tier_for_operation(operation) := "enterprise" if {
some pattern in {"api:enterprise:*", "api:admin:*", "api:bulk:*"}
glob.match(pattern, [], operation)
}

# Default to free tier if no pattern matches
default required_tier_for_operation(_) := "free"

2. Feature Flags with Union Merge

Specific operations require specific features to be enabled:

required_feature("api:export:pdf") := "export_pdf"
required_feature("api:export:csv") := "export_csv"

The policy checks if the feature is in the principal's enabled_features list. Using merge: "union" on feature annotations ensures that users with multiple roles or group memberships accumulate features rather than having one set replace another:

annotations:
- name: "enabled_features"
value: '["export_csv", "realtime_analytics"]'
merge: "union"

See Annotation Merge Strategies for more details on available merge options.

3. Quota Checking

Usage is passed in the context and compared against limits:

within_quota(principal, resource_type, current_usage) if {
quotas := principal.mannotations.quotas
limit := quotas[resource_type]
current_usage < limit
}

4. API Key Scopes

Scopes provide an additional restriction layer with DRY helper functions:

# Helper: check if operation is read-only
is_read_only if {
some pattern in {"*:read", "*:list", "*:get"}
glob.match(pattern, [], input.operation)
}

# Read-only scope allows read operations
allow if is_read_only

5. Annotation Inheritance

Groups can override role-level quotas:

groups:
- mrn: "mrn:iam:group:small-business"
annotations:
- name: "quotas"
value: '{"api_calls": 200000}' # Overrides role default

Extending This Example

Adding Burst Limits

Incorporate burst limit checking into your quota policy:

within_burst_limit(principal, context) if {
burst_limit := principal.mannotations.burst_limit
context.requests_last_minute < burst_limit
}

# Then use in your allow rule:
allow if {
# ... other checks ...
within_burst_limit(input.principal, input.context)
}

Soft Quota Warnings

To warn users approaching their quota limit, handle this at the application layer. The policy can enforce a hard limit, while your API gateway or application tracks usage and returns warning headers (e.g., X-RateLimit-Remaining) before the policy denies access.