Skip to main content
Permission Mongo provides a sophisticated Role-Based Access Control (RBAC) system that controls access to collections and documents through role definitions and policy rules.

Overview

The RBAC engine evaluates policies based on user roles, document attributes, and context information. It supports:
  • Role inheritance - Roles can inherit permissions from other roles
  • Fine-grained policies - Control access at the collection and document level
  • Expression language - Powerful conditional expressions using document and user context
  • Field-level security - Control which fields users can read or modify
  • Field masking - Automatically mask sensitive data

Role definitions

Roles are defined in policy.yml with inheritance support:
policy.yml
roles:
  admin:
    description: "Full system access"
  
  manager:
    description: "Can manage users and documents within their tenant"
    inherits: ["user"]
  
  user:
    description: "Regular user with limited access"
    inherits: ["viewer"]
  
  viewer:
    description: "Read-only access"
When a user has the manager role, they automatically inherit permissions from both user and viewer roles.

Collection policies

Policies define what actions each role can perform on a collection:
policy.yml
policies:
  documents:
    admin:
      actions: [create, read, update, delete, restore]
    
    manager:
      actions: [create, read, update, delete, restore]
      when: "resource.tenant_id == user.tenant_id"
    
    user:
      actions: [create, read, update, delete]
      when: "resource.owner_id == user._id"
    
    viewer:
      actions: [read]
      when: "resource.tenant_id == user.tenant_id && resource.status == 'published'"

Policy structure

  • actions - Array of permitted actions: create, read, update, delete, restore, aggregate
  • when - Optional condition expression that must evaluate to true for access to be granted
  • fields - Optional field-level restrictions (see below)

Expression language

The when clause uses a custom expression language to define conditional access rules.

Variables

resource
object
The document being accessed. Use dot notation to access nested fields: resource.owner_id, resource.metadata.status
user
object
The authenticated user context with these fields:
  • user.id - User ID
  • user.tenant_id - Tenant ID
  • user.roles - Array of user roles
  • user.$subordinates - Array of subordinate user IDs (from hierarchy)
  • user.$directReports - Array of direct report user IDs
  • user.$ancestors - Array of ancestor user IDs
  • user.claims.* - Custom JWT claims

Operators

Comparison operators:
# Equality
when: "resource.status == 'active'"
when: "resource.priority != 'low'"

# Numeric comparisons
when: "resource.amount > 1000"
when: "resource.score >= 80"
when: "resource.age < 18"
when: "resource.rating <= 5"
Logical operators:
# AND - both conditions must be true
when: "resource.tenant_id == user.tenant_id && resource.status == 'active'"

# OR - at least one condition must be true
when: "resource.owner_id == user.id || resource.created_by == user.id"

# NOT - negates a condition
when: "!(resource.archived == true)"
Membership operators:
# Check if value is in array
when: "user.id in resource.collaborators"

# Check if value is not in array
when: "resource.status not in ['archived', 'deleted']"

Expression examples

# Tenant isolation
when: "resource.tenant_id == user.tenant_id"

# Owner-based access
when: "resource.owner_id == user.id"

# Hierarchical access (manager can access subordinate documents)
when: "resource.owner_id in user.$subordinates"

# Status-based visibility
when: "resource.status == 'published' || resource.owner_id == user.id"

# Role-based with custom claims
when: "user.claims.department == resource.department"

# Complex multi-condition access
when: "resource.tenant_id == user.tenant_id && (resource.visibility == 'public' || resource.owner_id == user.id)"

Policy evaluation

The RBAC engine evaluates access in this order:
  1. Check if user has any applicable roles for the collection
  2. For each role (including inherited roles):
    • Check if the role policy grants the requested action
    • If a when clause exists, compile it to a MongoDB filter
    • Evaluate the condition against the document
  3. Grant access if any role allows the action
  4. Deny access if no role grants permission
// CheckAccess checks if a user can perform an action on a collection
func (e *Engine) CheckAccess(ctx *auth.AuthContext, collection string, action schema.Action) (*AccessResult, error) {
    if ctx == nil {
        return &AccessResult{
            Allowed: false,
            Reason:  "no authentication context provided",
        }, nil
    }

    // Get all roles to check (user's roles + inherited roles)
    rolesToCheck := e.getAllRolesToCheck(ctx.Roles)

    // Check each role for access
    for _, role := range rolesToCheck {
        rolePolicy := e.policy.GetRolePolicy(collection, role)
        if rolePolicy == nil {
            continue
        }

        if !e.hasAction(rolePolicy, configAction) {
            continue
        }

        // If there's a "when" clause, compile it to a query filter
        var queryFilter bson.M
        if rolePolicy.When != "" {
            compileCtx := e.authContextToCompileContext(ctx)
            filter, err := e.compiler.Compile(rolePolicy.When, compileCtx)
            if err != nil {
                continue
            }
            queryFilter = filter
        }

        // Access granted by this role
        return &AccessResult{
            Allowed:     true,
            Reason:      fmt.Sprintf("access granted by role %q", role),
            Policy:      rolePolicy,
            QueryFilter: queryFilter,
        }, nil
    }

    return &AccessResult{
        Allowed: false,
        Reason:  fmt.Sprintf("no policy grants access"),
    }, nil
}

Field-level security

Restrict access to specific fields within documents:
policy.yml
policies:
  users:
    manager:
      actions: [read, update]
      when: "resource.tenant_id == user.tenant_id"
      fields:
        deny_write: ["role", "tenant_id"]
    
    user:
      actions: [read, update]
      when: "resource._id == user._id"
      fields:
        deny_write: ["role", "tenant_id", "email"]
    
    viewer:
      actions: [read]
      when: "resource.tenant_id == user.tenant_id"
      fields:
        deny: ["email"]
        mask:
          name: partial

Field policies

allow
array
Whitelist of allowed fields. If specified, only these fields are readable/writable
deny
array
Blacklist of fields that cannot be read
deny_write
array
Fields that can be read but not modified
mask
object
Fields to mask with transformation types:
  • email - Masks email: j***@example.com
  • phone - Masks phone: +1-***-***-4567
  • partial - Shows first/last 4 chars: 1234****5678

Query filtering

For read operations, the engine automatically applies filters based on the when clause:
// GetQueryFilter returns MongoDB filter for queries
func (e *Engine) GetQueryFilter(ctx *auth.AuthContext, collection string, action schema.Action) (bson.M, error) {
    rolesToCheck := e.getAllRolesToCheck(ctx.Roles)
    compileCtx := e.authContextToCompileContext(ctx)

    var filters []bson.M
    hasUnconditionalAccess := false

    for _, role := range rolesToCheck {
        rolePolicy := e.policy.GetRolePolicy(collection, role)
        if rolePolicy == nil {
            continue
        }

        if !e.hasAction(rolePolicy, configAction) {
            continue
        }

        // If there's no "when" clause, this role has unconditional access
        if rolePolicy.When == "" {
            hasUnconditionalAccess = true
            break
        }

        // Compile the "when" clause to a MongoDB filter
        filter, err := e.compiler.Compile(rolePolicy.When, compileCtx)
        if err != nil {
            continue
        }

        filters = append(filters, filter)
    }

    // If any role has unconditional access, return empty filter
    if hasUnconditionalAccess {
        return bson.M{}, nil
    }

    // Multiple filters - combine with $or
    if len(filters) > 1 {
        return bson.M{"$or": filters}, nil
    }

    return filters[0], nil
}
This ensures users only see documents they have permission to access.

Default policy

Configure default behavior when no policy matches:
policy.yml
defaults:
  deny_all: true
  audit_log: true
  • deny_all: true - Deny access by default if no policy grants permission (recommended)
  • deny_all: false - Allow access if no explicit policy exists (not recommended)
  • audit_log: true - Log all access attempts including denials

Best practices

Define base roles like viewer and extend them with user, manager, admin to avoid policy duplication.
Always include tenant checks in your when clauses: resource.tenant_id == user.tenant_id
Set deny_all: true in defaults to ensure secure-by-default behavior.
Use the engine’s CheckDocumentAccess method to validate policies against test documents before deployment.
Mask or deny access to sensitive fields like emails, phone numbers, or financial data for lower-privilege roles.

Build docs developers (and LLMs) love