Loading learning content...
Consider a real-world scenario: A large enterprise has 50,000 employees across 12 regional divisions, each containing hundreds of teams, managing millions of documents. A regional director needs access to all documents in their region. A team lead needs access to their team's documents. An individual contributor needs access to their own documents.
Without inheritance, you'd need to explicitly grant the regional director access to every single document in their region—and update those grants as documents are created, moved, or deleted. With millions of documents and thousands of permission changes daily, this approach collapses under its own weight.
Permission inheritance solves this by allowing permissions to flow through hierarchies. Grant access to a container (region, team, folder), and that access automatically extends to everything within. This transforms an unmanageable explosion of individual grants into a structured, maintainable permission model.
But inheritance introduces complexity: What happens when permissions conflict? Can children override parents? How do you revoke inherited access? This page explores these questions with the depth required to implement inheritance correctly in production systems.
By the end of this page, you will understand hierarchical permission models and when to use them, know the major inheritance strategies (additive, restrictive, override), implement efficient inheritance resolution at scale, handle inheritance in multi-tenant and multi-hierarchy systems, and avoid common pitfalls that lead to security vulnerabilities.
Permission inheritance operates on hierarchical structures. Understanding the types of hierarchies and how they relate to authorization is foundational.
Types of Hierarchies in Authorization:
| Hierarchy Type | Structure | Example | Inheritance Direction |
|---|---|---|---|
| Organizational | Company → Division → Department → Team → User | Enterprise org charts | Parent → Children (access flows down) |
| Resource/Container | Folder → Subfolder → Document | File systems, document repositories | Container → Contents |
| Role | Admin → Manager → Employee | RBAC role hierarchies | Parent → Child (permissions aggregate up) |
| Group Membership | Group → Subgroup → User | Directory services, IAM | Group → Members |
| Geographic | Global → Region → Country → Site | Multi-region deployments | Territory → Territories within |
Hierarchy Shapes:
Tree (Single Parent):
Root
/ \
A B
/ \ \
C D E
DAG (Multiple Parents):
Root
/ \
A B
\ /
C
Forest (Multiple Trees):
Root1 Root2
/ \ |
A B C
Inheritance vs. Direct Grants:
Permissions can arise from two sources:
Effective permissions are the combination of direct and inherited grants:
Effective(subject, resource) =
DirectGrants(subject, resource) ∪
Inherited(subject, resource.parent) ∪
Inherited(subject, resource.parent.parent) ∪ ...
This recursive definition shows why inheritance can become computationally expensive—checking permissions may require traversing entire ancestor chains.
Deep hierarchies (10+ levels) significantly impact permission resolution performance. Each traversal level adds latency. Consider flattening deeply nested structures or implementing materialized permission views for frequently-accessed hierarchies.
How permissions combine as they flow through hierarchies defines the inheritance strategy. Different strategies suit different use cases.
Strategy 1: Additive (Union) Inheritance
Permissions accumulate as you traverse the hierarchy. Child effective permissions are the union of their own grants plus all ancestor grants.
Effective(child) = Grants(child) ∪ Effective(parent)
Example:
Strategy 2: Restrictive (Intersection) Inheritance
Permissions are restricted as you traverse down. Child effective permissions are the intersection of their own grants with ancestor grants.
Effective(child) = Grants(child) ∩ Effective(parent)
Example:
Use case: Regulatory environments where upper levels set maximum allowed access, and lower levels cannot exceed those limits.
Strategy 3: Override Inheritance
Child grants completely replace parent grants. No inheritance occurs; only the most specific grant applies.
Effective(resource) = Grants(resource) if exists, else Effective(parent)
Example:
Use case: When specific resources require different access than their default container context.
Strategy 4: Mixed (Additive with Denies)
The most common real-world approach: Additive inheritance with explicit denies that can block inherited permissions.
Effective(resource) = (Grants(resource) ∪ Inherited(parent)) - Denies(resource) - InheritedDenies
Example:
This strategy provides flexibility: broad access via inheritance, with surgical restrictions where needed.
Most systems use additive inheritance with denies (Strategy 4). Pure additive is too permissive; restrictive is often too limiting. Override breaks intuitive inheritance expectations. Additive-with-denies balances broad access with precise control.
When permissions from multiple sources apply to the same subject-resource pair, conflicts arise. Deterministic conflict resolution is essential for predictable authorization.
Conflict Scenarios:
| Scenario | Example | Resolution Question |
|---|---|---|
| Direct vs. Inherited | User has direct deny, inherits permit from parent | Does direct override inherited? |
| Multiple Inheritance | Resource has two parent folders with different permissions | How to combine multiple parents? |
| Permit vs. Deny | User has permit from role, deny from group | Does deny always win? |
| Different Grantors | Admin grants access, owner revokes | Whose grant takes precedence? |
| Specificity Conflict | Broad permit on type, specific deny on instance | Does specific override general? |
Resolution Strategies:
1. Deny-Takes-Precedence:
Any deny overrides any permit. Strictest approach.
if any_deny exists:
return DENY
else if any_permit exists:
return PERMIT
else:
return DENY (default)
Pros: Secure by default, explicit restrictions always work Cons: Can't override denies, may be too restrictive
2. Most-Specific-Wins:
Direct grants override inherited. Closer inheritance beats distant.
if direct_grant exists:
return direct_grant
else if parent_grant exists:
return parent_grant
else if grandparent_grant exists:
return grandparent_grant
...
Pros: Intuitive, allows exceptions Cons: Requires clear specificity ordering
3. Priority-Based:
Assign priorities to grant sources. Highest priority wins.
grant_priorities = [
("explicit_deny", 100),
("direct_grant", 80),
("inherited_deny", 60),
("inherited_grant", 40),
("default", 0)
]
Pros: Flexible, configurable Cons: Priority values must be carefully designed, can be confusing
Google Zanzibar Resolution Model:
Google's Zanzibar (used by Google Docs, Drive, etc.) uses a relationship-based model with clear resolution:
# Zanzibar-style check
def check(user, object, permission):
# Find all ways user could have permission
permit_paths = find_permit_paths(user, object, permission)
# If any permit path exists, check for explicit blocks
if permit_paths:
deny_relations = find_deny_relations(user, object)
if not any_applicable_deny(deny_relations):
return PERMIT
return DENY
AWS IAM Resolution:
AWS uses a different model:
This means policies cannot override explicit denies, providing strong security boundaries.
Whatever resolution strategy you choose, document it exhaustively. Users will form mental models about how permissions work. If the system behaves unexpectedly, trust erodes. Provide clear documentation and debugging tools that explain why a specific decision was reached.
Naively implementing inheritance by traversing the hierarchy at check time doesn't scale. Production systems require efficient computation strategies.
Approach 1: Recursive Query (Naive)
-- Check permission: does user have access to resource or any ancestor?
WITH RECURSIVE ancestors AS (
-- Base: the target resource
SELECT id, parent_id FROM resources WHERE id = :resource_id
UNION ALL
-- Recursive: all ancestors
SELECT r.id, r.parent_id
FROM resources r
JOIN ancestors a ON r.id = a.parent_id
)
SELECT EXISTS (
SELECT 1 FROM permissions p
JOIN ancestors a ON p.resource_id = a.id
WHERE p.subject_id = :user_id
AND p.permission = :permission
);
Pros: Always accurate, simple logic Cons: O(depth) database queries, slow for deep hierarchies, doesn't cache well
Approach 2: Materialized Path (Closure Table)
Precompute all ancestor-descendant relationships:
-- Closure table stores all ancestor-descendant pairs
CREATE TABLE resource_closure (
ancestor_id UUID NOT NULL,
descendant_id UUID NOT NULL,
depth INT NOT NULL,
PRIMARY KEY (ancestor_id, descendant_id)
);
-- Fast permission check: single query, no recursion
SELECT EXISTS (
SELECT 1 FROM permissions p
JOIN resource_closure rc ON p.resource_id = rc.ancestor_id
WHERE rc.descendant_id = :resource_id
AND p.subject_id = :user_id
AND p.permission = :permission
);
Pros: O(1) lookup, very fast reads Cons: O(n²) storage worst case, expensive hierarchy updates, must maintain closure on every change
Approach 3: Materialized Permissions
Precompute and store effective permissions for every subject-resource pair:
-- Materialized effective permissions
CREATE TABLE effective_permissions (
subject_id UUID NOT NULL,
resource_id UUID NOT NULL,
permissions TEXT[] NOT NULL, -- Array of permission strings
PRIMARY KEY (subject_id, resource_id)
);
-- Blazing fast lookup
SELECT :permission = ANY(permissions)
FROM effective_permissions
WHERE subject_id = :user_id AND resource_id = :resource_id;
Pros: Fastest possible reads Cons: Enormous storage, expensive recalculation on any change, stale data window
Approach 4: Hybrid with Caching
Combine approaches based on access patterns:
def check_permission(user_id, resource_id, permission):
# Level 1: Check hot cache (in-memory, TTL 30s)
cache_key = f"{user_id}:{resource_id}:{permission}"
if (cached := hot_cache.get(cache_key)) is not None:
return cached
# Level 2: Check materialized table (for frequently accessed resources)
if is_frequently_accessed(resource_id):
result = check_materialized(user_id, resource_id, permission)
else:
# Level 3: Compute on demand for cold resources
result = compute_with_inheritance(user_id, resource_id, permission)
hot_cache.set(cache_key, result, ttl=30)
return result
Start with recursive queries. Profile actual access patterns. If 90% of checks hit 10% of resources, materialize those hot paths. Don't pre-optimize—inheritance performance depends heavily on your specific hierarchy shape and access patterns.
Multi-tenant systems introduce an additional complexity layer: inheritance must respect tenant isolation while potentially supporting cross-tenant relationships.
Tenant as Root Hierarchy:
System
┌───────────┼───────────┐
Tenant A Tenant B Tenant C
│ │ │
[hierarchy] [hierarchy] [hierarchy]
Isolation Requirements:
Strict Isolation: Permissions never cross tenant boundaries. Each tenant's hierarchy is completely independent.
Hierarchical Tenancy: Parent tenants can access child tenants (enterprise with sub-organizations).
Selective Sharing: Explicit cross-tenant grants for partnerships, vendors, or shared resources.
Pattern: Tenant-Scoped Inheritance
def check_permission(subject, resource, permission):
# First: Verify tenant isolation
if subject.tenant_id != resource.tenant_id:
if not has_cross_tenant_grant(subject, resource.tenant_id):
return DENY # Tenant boundary blocks inheritance
# Then: Check inheritance within tenant
return check_inheritance_chain(subject, resource, permission)
def check_inheritance_chain(subject, resource, permission):
# All ancestors must be in same tenant
for ancestor in get_ancestors(resource):
if ancestor.tenant_id != resource.tenant_id:
raise SecurityException("Ancestor crosses tenant boundary")
if has_grant(subject, ancestor, permission):
return PERMIT
return DENY
Pattern: Tenant Hierarchy with Inheritance
-- Tenant hierarchy table
CREATE TABLE tenant_hierarchy (
tenant_id UUID PRIMARY KEY,
parent_tenant_id UUID REFERENCES tenant_hierarchy(tenant_id)
);
-- Permission check respects tenant hierarchy
WITH RECURSIVE tenant_chain AS (
SELECT tenant_id, parent_tenant_id FROM tenant_hierarchy
WHERE tenant_id = :subject_tenant_id
UNION ALL
SELECT t.tenant_id, t.parent_tenant_id
FROM tenant_hierarchy t
JOIN tenant_chain c ON t.tenant_id = c.parent_tenant_id
)
SELECT EXISTS (
SELECT 1
WHERE :resource_tenant_id IN (SELECT tenant_id FROM tenant_chain)
AND has_permission(:subject_id, :resource_id, :permission)
);
Cross-Tenant Sharing Model:
-- Explicit cross-tenant grants
CREATE TABLE cross_tenant_grants (
id UUID PRIMARY KEY,
granting_tenant_id UUID NOT NULL,
receiving_tenant_id UUID NOT NULL,
resource_id UUID NOT NULL, -- Specific resource or root for all
permissions TEXT[] NOT NULL,
expires_at TIMESTAMPTZ,
CONSTRAINT unique_grant UNIQUE (granting_tenant_id, receiving_tenant_id, resource_id)
);
-- Check includes cross-tenant grants
def check_with_cross_tenant(subject, resource, permission):
# Same tenant: normal check
if subject.tenant_id == resource.tenant_id:
return check_inheritance_chain(subject, resource, permission)
# Different tenant: check for cross-tenant grant
grant = find_cross_tenant_grant(
granting_tenant=resource.tenant_id,
receiving_tenant=subject.tenant_id,
resource_id=resource.id
)
if grant and permission in grant.permissions:
if grant.expires_at is None or grant.expires_at > now():
return PERMIT
return DENY
Cross-tenant access is high-risk. Implement defense in depth: explicit grants only (never implicit), audit logging for all cross-tenant access, expiration on all cross-tenant grants, and regular review of active cross-tenant relationships. A bug here affects multiple customers.
When permissions change, cached or materialized inheritance data must be invalidated. This is one of the hardest problems in permission systems.
Change Events Requiring Invalidation:
Invalidation Strategies:
1. Eager Propagation (Consistency-First):
Immediately update all affected materialized permissions.
def revoke_permission(resource_id, subject_id, permission):
# Remove direct grant
delete_grant(resource_id, subject_id, permission)
# Find all descendants
descendants = get_all_descendants(resource_id)
# Recompute effective permissions for each
for descendant in descendants:
new_effective = compute_effective_permissions(descendant, subject_id)
update_materialized(descendant, subject_id, new_effective)
# Also invalidate for all group members if subject is a group
if is_group(subject_id):
for member in get_group_members(subject_id):
# Recursively handle member invalidation
invalidate_for_subject(member, resource_id, descendants)
Pros: Strong consistency, no stale data Cons: Slow writes, locks during propagation, can cascade to millions of updates
2. Lazy Invalidation (Availability-First):
Mark data as potentially stale; recompute on next access.
def revoke_permission(resource_id, subject_id, permission):
# Remove direct grant
delete_grant(resource_id, subject_id, permission)
# Mark affected cache entries as invalid
invalidation_broadcast({
"type": "permission_change",
"resource_id": resource_id,
"subject_id": subject_id,
"scope": "descendants"
})
def check_permission(subject_id, resource_id, permission):
cached = get_cached(subject_id, resource_id, permission)
if cached and not is_invalidated(cached):
return cached.decision
# Recompute on cache miss or invalidation
decision = compute_effective_permission(subject_id, resource_id, permission)
cache_result(subject_id, resource_id, permission, decision)
return decision
Pros: Fast writes, degraded consistency acceptable Cons: Stale reads possible, thundering herd on popular resources after invalidation
3. Version-Based Invalidation:
Track versions; cache entries from old versions are invalid.
# Global or per-subtree version counter
versions = {
"resource:{id}": version_number,
...
}
def change_permission(resource_id, ...):
# Increment version for this subtree
increment_version(resource_id)
propagate_version_to_ancestors(resource_id)
def check_permission(subject_id, resource_id, permission):
cached = get_cached(subject_id, resource_id, permission)
current_version = get_current_version(resource_id)
if cached and cached.version >= current_version:
return cached.decision
# Version mismatch: recompute
decision = compute_effective_permission(subject_id, resource_id, permission)
cache_result(subject_id, resource_id, permission, decision, version=current_version)
return decision
Broad invalidation ("invalidate everything for this user") is safe but expensive. Try to scope invalidation precisely: the specific resource subtree and specific subjects affected. This requires careful tracking of what's cached and why.
Permission inheritance is deceptively complex. Many security vulnerabilities arise from subtle implementation errors.
Case Study: Dropbox Permission Bypass
In 2014, Dropbox had a vulnerability where users could access files by guessing share links, even for files they shouldn't access. The issue: share links didn't properly check the full inheritance chain, assuming the link itself was sufficient authorization.
Lesson: Every access path must verify authorization, including shared links, embed URLs, and API references. Don't assume any access mechanism is pre-authorized.
Case Study: Confused Deputy via Inheritance
A system allowed service accounts to create resources. Service A creates a resource under User B's folder. Does Service A now have access to the resource it created? Does User B have access because it's in their folder?
Without clear ownership and inheritance rules, the answer is ambiguous—a confused deputy scenario where the service may act on data it shouldn't access, or users gain access to service-created data.
Lesson: Clearly define what "in a container" means for ownership versus inheritance. Creation inside a folder may grant parent permissions without changing ownership.
| Check | Purpose | Frequency |
|---|---|---|
| Orphan detection | Find resources without valid parent chains | Daily |
| Circular reference detection | Identify loops in hierarchy or groups | On change + daily |
| Cross-boundary grants audit | Review all grants that cross trust boundaries | Weekly |
| Stale grant review | Find unused or expired grants | Monthly |
| Privilege escalation paths | Analyze how low-privilege users could gain access | Quarterly |
Inheritance bugs often manifest as permissions being too broad, not too narrow. Explicitly test that: moving a resource revokes old-parent inheritance, deleting a parent properly cascades, and direct access to children still checks parent permissions.
We've completed our deep dive into permission inheritance—the mechanism that makes large-scale authorization manageable. Let's consolidate the key insights:
Module Conclusion: Authorization Patterns
Over these five pages, we've systematically explored the landscape of authorization:
What is Authorization — The fundamental distinction from authentication, core concepts, and the challenges of distributed authorization.
Role-Based Access Control (RBAC) — The most widely deployed pattern, its formal model, implementation strategies, and limitations that drive the need for more sophisticated approaches.
Attribute-Based Access Control (ABAC) — Policy-driven authorization using attributes, enabling virtually any authorization requirement expressible in natural language.
Policy-Based Authorization — Externalizing authorization into testable, governable, deployable policies with proper lifecycle management.
Permission Inheritance — How permissions flow through hierarchies, enabling scalable management of complex organizational and resource structures.
Together, these patterns provide the toolkit for implementing authorization in any system—from simple role checks to complex, context-aware, relationship-based access control in massive distributed architectures.
You now have a comprehensive understanding of authorization patterns—RBAC, ABAC, policy-based systems, and permission inheritance. You can design and implement authorization for systems of any complexity, understanding the trade-offs between simplicity and flexibility, performance and consistency, security and usability.