This guide covers recommended patterns for implementing Policy Enforcement Points (PEPs) and integrating with the PolicyEngine.
The PolicyEngine's observable architecture enables a powerful approach to access control: start with strict policies and iteratively expand access based on observed needs.
Traditional approaches to access control often fail at the Principle of Least Privilege because:
The PolicyEngine solves this by making every decision observable through AccessRecords, enabling evidence-based policy refinement.
1. Deploy Strict Initial Policies
Start with policies that may be more restrictive than necessary. It's easier to safely expand access than to identify and close security gaps later:
# Start with minimal access - only grant what you're certain is needed
roles:
- mrn: "mrn:iam:role:new-service"
name: new-service
policy: "mrn:iam:policy:read-only" # Start conservative
2. Observe and Analyze Denials
Monitor the AccessRecord stream for denied requests. These denials are your evidence of what additional access may be needed:
# Find denied requests (Community example)
mpe serve ... 2>&1 | jq 'select(.decision == "DENY")'
# Analyze denial patterns
... | jq -r '.operation' | sort | uniq -c | sort -rn
3. Validate Before Expanding
Use policy replay to understand the impact of proposed changes before deployment:
4. Expand Precisely
Grant only the specific access that was demonstrated necessary, then continue monitoring.
For a detailed walkthrough with examples, see Iterative Policy Refinement.
The PEP's job is to formulate the request and enforce the decision—not to implement access control logic. Keep the logic in policies where it can be centrally managed and tested.
// Good: PEP just builds PORC and enforces the decision
func (m *AuthMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
porc := m.buildPORC(r)
allowed, err := m.pdp.Authorize(r.Context(), porc)
if err != nil || !allowed {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
// Bad: PEP contains access control logic
func (m *AuthMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := extractClaims(r)
// Don't do this - put this logic in policies!
if claims.Role == "admin" {
next.ServeHTTP(w, r)
return
}
if r.Method == "GET" && claims.Role == "viewer" {
next.ServeHTTP(w, r)
return
}
http.Error(w, "Forbidden", http.StatusForbidden)
})
}
Establish a naming convention for operations and document it:
<subsystem>:<resource-class>:<verb>
This makes it easier to write policies that match patterns:
# Allow all read operations across subsystems
allow {
glob.match("*:*:read", [], input.operation)
}
# Allow all operations on a specific resource type
allow {
glob.match("api:users:*", [], input.operation)
}
When building operations from HTTP requests:
| HTTP Method | Verb |
|---|---|
| GET | read |
| POST | create |
| PUT | update |
| PATCH | update |
| DELETE | delete |
func httpMethodToVerb(method string) string {
switch strings.ToUpper(method) {
case "GET":
return "read"
case "POST":
return "create"
case "PUT", "PATCH":
return "update"
case "DELETE":
return "delete"
default:
return strings.ToLower(method)
}
}
Using MRN strings with resource resolution:
// Recommended: Use MRN string
porc["resource"] = fmt.Sprintf("mrn:app:%s:document:%s", service, docID)
// Only when needed: Use fully-qualified descriptor
porc["resource"] = map[string]interface{}{
"id": fmt.Sprintf("mrn:app:%s:document:%s", service, docID),
"owner": doc.OwnerEmail,
// ...
}
Create MRNs that reflect your resource hierarchy:
// Good: Descriptive, hierarchical MRNs
"mrn:app:billing:invoice:INV-2024-001"
"mrn:api:users:profile:user-12345"
"mrn:storage:documents:report:annual-2024"
// Bad: Opaque, non-descriptive MRNs
"mrn:x:y:z:abc123"
"mrn:resource:1234"
Fail Closed: You should treat PDP failures as default-DENY.
allowed, err := pdp.Authorize(ctx, porc)
if err != nil {
log.Printf("PDP unavailable, denying access: %v", err)
return deny()
}
allowed, err := pdp.Authorize(ctx, porc)
if err != nil {
// Log the error for debugging
log.Printf("Authorization error: %v", err)
// Return 500 for PDP errors (not 403)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if !allowed {
// Return 403 for policy denials
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
For HTTP API integration, reuse connections:
// Create client once, reuse for all requests
var pdpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
Timeout: 500 * time.Millisecond,
}
When using the embedded library, pass maps directly to avoid JSON parsing:
// Faster: Pass the map directly
porc := map[string]interface{}{
"principal": principal,
"operation": operation,
"resource": resource,
"context": context,
}
allowed, _ := pe.Authorize(ctx, porc)
// Slower: JSON string requires parsing
porcJSON := `{"principal": {...}, ...}`
allowed, _ := pe.Authorize(ctx, porcJSON)
For UI capability checks (e.g., determining which buttons to show), you can use probe=true to disable audit logging and safely cache results. Probe mode is designed for scenarios where you need to check permissions without creating audit entries:
type PermissionCache struct {
cache *lru.Cache
ttl time.Duration
}
func (c *PermissionCache) CanPerform(ctx context.Context, principal, operation, resource string) bool {
key := fmt.Sprintf("%s:%s:%s", principal, operation, resource)
if cached, ok := c.cache.Get(key); ok {
return cached.(bool)
}
// Use probe=true for UI checks - disables audit logging
allowed := c.pdp.Authorize(ctx, buildPORC(principal, operation, resource), WithProbe(true))
c.cache.Add(key, allowed)
return allowed
}
:::warning
Only cache probe-mode results. Regular authorization checks (without probe=true) should never be cached, as this would bypass the audit log. See Audit for more information.
:::
type MockPDP struct {
decisions map[string]bool
}
func (m *MockPDP) Authorize(ctx context.Context, porc interface{}) (bool, error) {
// Return configured decision or default to deny
key := buildKey(porc)
if decision, ok := m.decisions[key]; ok {
return decision, nil
}
return false, nil
}
func TestAuthMiddleware(t *testing.T) {
mockPDP := &MockPDP{
decisions: map[string]bool{
"user@example.com:api:users:read:mrn:app:users": true,
},
}
middleware := NewAuthMiddleware(mockPDP)
// Test the middleware...
}
Use mpe test decision to test policies independently of PEP code. Create individual PORC input files for each test scenario:
test-admin-read.json:
{
"principal": {
"sub": "admin@example.com",
"mroles": ["mrn:iam:role:admin"]
},
"operation": "api:documents:read",
"resource": {
"id": "mrn:app:docs:document:123"
}
}
# Test that an admin can read resources
mpe test decision -b domain.yml -i test-admin-read.json | jq .decision
# Expected: "GRANT"
# Test that a viewer cannot delete (using stdin)
echo '{"principal":{"sub":"viewer@example.com","mroles":["mrn:iam:role:viewer"]},"operation":"api:documents:delete","resource":{"id":"mrn:app:docs:document:123"}}' | \
mpe test decision -b domain.yml -i - | jq .decision
# Expected: "DENY"
For comprehensive policy testing, consider creating a shell script that runs multiple test cases and validates the expected outcomes.
Always validate JWTs before trusting their claims:
func extractClaims(r *http.Request) (*Claims, error) {
token := extractBearerToken(r)
if token == "" {
return nil, errors.New("no token provided")
}
// Validate signature, expiration, issuer, audience
claims, err := validateAndParseJWT(token)
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
return claims, nil
}
When using fully-qualified resource descriptors, get metadata from authoritative sources:
// Good: Get metadata from your database
doc := db.GetDocument(docID)
resource := map[string]interface{}{
"id": doc.MRN,
"owner": doc.Owner,
"classification": doc.Classification,
}
// Bad: Trust client-provided metadata
resource := map[string]interface{}{
"id": r.URL.Path,
"classification": r.Header.Get("X-Classification"), // Don't do this!
}
Be careful about what you include in context:
context := map[string]interface{}{
"source_ip": r.RemoteAddr,
"user_agent": r.UserAgent(),
"request_id": r.Header.Get("X-Request-ID"),
// Don't include sensitive data like passwords or tokens
}
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |