Skip to main content
Permission Mongo provides granular field-level access control, allowing you to restrict which fields users can read or modify based on their roles.

Overview

Field-level features:
  • Allow lists - Whitelist specific readable fields
  • Deny lists - Blacklist specific fields from reading
  • Deny-write - Prevent modification of fields
  • Field masking - Automatically mask sensitive data (email, phone, partial)
  • Role-based field access - Different fields for different roles

Configuration

Basic Field Restrictions

Define field policies in policy.yaml:
policies:
  employees:
    employee:
      actions: [read, update]
      fields:
        allow: ["name", "email", "department", "phone"]  # Only these fields
        deny_write: ["salary", "performance_rating"]     # Cannot modify
    
    hr_manager:
      actions: [read, update]
      fields:
        allow: []  # Empty = all fields allowed
        deny_write: ["_id", "created_at", "created_by"]  # System fields
Source: /home/daytona/workspace/source/pkg/config/policy.go:79-84

Field Masking

Automatically mask sensitive data:
policies:
  customers:
    support_agent:
      actions: [read]
      fields:
        mask:
          email: email      # [email protected] → u***@example.com
          phone: phone      # +1-555-123-4567 → +1-***-***-4567
          ssn: partial      # 123-45-6789 → 123-**-***9
Source: /home/daytona/workspace/source/pkg/rbac/engine.go:40-45

Field Access Control

1
Step 1: Allow Lists (Whitelisting)
2
Restrict readable fields to a specific set:
3
policies:
  employees:
    employee:
      actions: [read]
      fields:
        allow: ["name", "email", "department", "hire_date"]
4
Read Request:
5
GET /employees/507f1f77bcf86cd799439011
6
Response (filtered):
7
{
  "_id": "507f1f77bcf86cd799439011",
  "name": "John Smith",
  "email": "[email protected]",
  "department": "Engineering",
  "hire_date": "2020-05-15"
  // salary, performance_rating, etc. excluded
}
8
Source: /home/daytona/workspace/source/pkg/rbac/engine.go:341-382
9
Step 2: Deny Lists (Blacklisting)
10
Exclude specific sensitive fields:
11
policies:
  employees:
    manager:
      actions: [read]
      fields:
        deny: ["ssn", "bank_account", "tax_info"]
12
Response (sensitive fields removed):
13
{
  "_id": "507f1f77bcf86cd799439011",
  "name": "John Smith",
  "email": "[email protected]",
  "salary": 85000,
  "department": "Engineering"
  // ssn, bank_account, tax_info excluded
}
14
Step 3: Deny-Write Protection
15
Allow reading but prevent modification:
16
policies:
  employees:
    employee:
      actions: [read, update]
      fields:
        deny_write: ["salary", "title", "manager_id"]
17
Update attempt:
18
PUT /employees/507f1f77bcf86cd799439011
Content-Type: application/json

{
  "phone": "555-1234",
  "salary": 100000  // Denied!
}
19
Error response:
20
{
  "error": {
    "code": "forbidden",
    "message": "You don't have permission to modify this field",
    "details": {
      "field": "salary"
    }
  }
}
21
Source: /home/daytona/workspace/source/pkg/api/handlers_batch.go:363-373

Field Masking

Automatic data masking protects sensitive information:

Email Masking

fields:
  mask:
    email: email
Original: [email protected]
Masked: j***@example.com
Original: [email protected]
Masked: a***@test.com
Source: /home/daytona/workspace/source/pkg/rbac/engine.go:721-736

Phone Masking

fields:
  mask:
    phone: phone
    mobile: phone
Original: +1-555-123-4567
Masked: +1-***-***-4567
Original: (555) 123-4567
Masked: (***) ***-4567
Source: /home/daytona/workspace/source/pkg/rbac/engine.go:738-789

Partial Masking

fields:
  mask:
    ssn: partial
    credit_card: partial
SSN: 123-45-6789123-**-***9
Credit Card: 4532-1234-5678-90104532-****-****-9010
Short strings: Keeps first and last characters
Source: /home/daytona/workspace/source/pkg/rbac/engine.go:791-808

Role-Based Field Access

Different roles see different fields:
policies:
  employees:
    employee:  # Regular employees
      actions: [read]
      fields:
        allow: ["name", "email", "department", "phone"]
        mask:
          phone: phone  # Mask their own phone
    
    manager:  # Managers
      actions: [read]
      fields:
        allow: ["name", "email", "department", "phone", "salary"]
        deny: ["ssn", "bank_account"]
        mask:
          salary: partial  # Partially mask salary
    
    hr_admin:  # HR has full access
      actions: [read, update]
      fields:
        allow: []  # All fields
        deny_write: ["_id", "created_at", "created_by"]

Example: Same Document, Different Views

Original Document:
{
  "_id": "507f1f77bcf86cd799439011",
  "name": "Jane Doe",
  "email": "[email protected]",
  "phone": "+1-555-123-4567",
  "department": "Engineering",
  "salary": 95000,
  "ssn": "123-45-6789",
  "bank_account": "9876543210"
}
Employee View:
{
  "_id": "507f1f77bcf86cd799439011",
  "name": "Jane Doe",
  "email": "[email protected]",
  "phone": "+1-***-***-4567",
  "department": "Engineering"
}
Manager View:
{
  "_id": "507f1f77bcf86cd799439011",
  "name": "Jane Doe",
  "email": "[email protected]",
  "phone": "+1-555-123-4567",
  "department": "Engineering",
  "salary": 9500**  // Partially masked
}
HR Admin View:
{
  "_id": "507f1f77bcf86cd799439011",
  "name": "Jane Doe",
  "email": "[email protected]",
  "phone": "+1-555-123-4567",
  "department": "Engineering",
  "salary": 95000,
  "ssn": "123-45-6789",
  "bank_account": "9876543210"
}

Nested Field Access

Control access to nested fields:
policies:
  users:
    user:
      actions: [read, update]
      fields:
        allow: [
          "name",
          "email",
          "profile.bio",
          "profile.avatar",
          "settings.notifications"
        ]
        deny_write: [
          "profile.verified",
          "settings.api_key"
        ]
Original:
{
  "name": "John",
  "email": "[email protected]",
  "profile": {
    "bio": "Engineer",
    "avatar": "https://...",
    "verified": true,
    "internal_notes": "Top performer"
  },
  "settings": {
    "notifications": true,
    "api_key": "sk_live_..."
  }
}
Filtered (user role):
{
  "name": "John",
  "email": "[email protected]",
  "profile": {
    "bio": "Engineer",
    "avatar": "https://..."
  },
  "settings": {
    "notifications": true
  }
}

Query Projections

Field restrictions apply to query results:
GET /employees?fields=name,email,salary&department=Engineering
Requested fields filtered by policy:
// User requests: name, email, salary
// Policy allows: name, email, department
// Actual projection: name, email
Source: /home/daytona/workspace/source/pkg/rbac/engine.go:288-337

Update Validations

Deny-write fields are enforced on updates:
PUT /employees/507f1f77bcf86cd799439011
Content-Type: application/json

{
  "phone": "555-9999",      // OK
  "salary": 100000,         // Denied!
  "department": "Sales"     // Denied!
}
Validation error:
{
  "error": {
    "code": "forbidden",
    "message": "You don't have permission to modify this field",
    "details": {
      "field": "salary"
    }
  }
}

Batch Operations

Field policies apply to batch operations:
POST /employees/batch
Content-Type: application/json

{
  "documents": [
    {"name": "Alice", "email": "[email protected]", "salary": 80000},
    {"name": "Bob", "email": "[email protected]", "salary": 85000}
  ]
}
If salary is in deny_write list, both documents fail validation. Source: /home/daytona/workspace/source/pkg/api/handlers_batch.go:363-373

Combining with Conditions

Field access can combine with RBAC conditions:
policies:
  employees:
    employee:
      actions: [read]
      when: |
        doc.id == user.id ||           // Own record
        doc.manager_id == user.id      // Direct report
      fields:
        allow: ["name", "email", "phone", "department"]
        mask:
          phone: phone  # Mask subordinates' phones
Logic:
  1. User can only access own record or direct reports (condition)
  2. Even when authorized, only sees allowed fields
  3. Phone is masked for privacy

Testing Field Permissions

func TestFieldLevelAccess(t *testing.T) {
	// Employee token
	employeeToken := createToken("user-123", "acme-corp", []string{"employee"})

	// Create employee record
	resp := createEmployee(employeeToken, map[string]interface{}{
		"name":       "John Doe",
		"email":      "[email protected]",
		"salary":     95000,
		"department": "Engineering",
	})

	employeeID := resp.JSON["_id"].(string)

	// Read as employee - should only see allowed fields
	resp = getEmployee(employeeToken, employeeID)
	assert.Equal(t, 200, resp.StatusCode)
	
	data := resp.JSON
	assert.Equal(t, "John Doe", data["name"])
	assert.Equal(t, "[email protected]", data["email"])
	assert.Nil(t, data["salary"]) // Excluded by allow list

	// Try to update salary - should fail
	resp = updateEmployee(employeeToken, employeeID, map[string]interface{}{
		"salary": 100000,
	})
	assert.Equal(t, 403, resp.StatusCode)
	assert.Equal(t, "forbidden", resp.JSON["error"].(map[string]interface{})["code"])
}

Best Practices

Use allow lists for high-security collections
Use deny lists for collections with many fields
Always protect system fields (_id, created_at) with deny_write
Mask PII (email, phone, SSN) for non-admin roles
Test field access with different role combinations
Document which fields each role can access
Use nested field paths for fine-grained control

Common Patterns

Pattern: Self-Service Profile

Users can update their own profile but not sensitive fields:
policies:
  users:
    user:
      actions: [read, update]
      when: doc.id == user.id
      fields:
        allow: [
          "name", "email", "phone",
          "profile.bio", "profile.avatar",
          "preferences.*"
        ]
        deny_write: [
          "email_verified", "role", "tenant_id",
          "created_at", "last_login"
        ]

Pattern: Progressive Disclosure

Show more fields as users gain seniority:
policies:
  financials:
    junior_analyst:
      actions: [read]
      fields:
        allow: ["revenue", "costs", "margin"]
        mask:
          margin: partial
    
    senior_analyst:
      actions: [read]
      fields:
        allow: ["revenue", "costs", "margin", "profit"]
    
    finance_director:
      actions: [read, update]
      fields:
        allow: []  # All fields

Pattern: Redacted for External Users

External users see masked versions:
policies:
  contacts:
    external_partner:
      actions: [read]
      fields:
        allow: ["name", "company", "email", "phone"]
        mask:
          email: email
          phone: phone
        deny: ["internal_notes", "pricing_tier", "contract_value"]

Troubleshooting

Fields Not Appearing

Cause: Field in deny list or not in allow list
Solution: Check role’s field policy

Mask Not Applied

Cause: Incorrect mask type
Solution: Use email, phone, or partial

Update Rejected

Cause: Field in deny_write list
Solution: Remove field from update or grant higher role

Next Steps

Hierarchical RBAC

Manage organizational hierarchies

Computed Fields

Dynamic field values from expressions

Build docs developers (and LLMs) love