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
Step 1: Allow Lists (Whitelisting)
Restrict readable fields to a specific set:
policies :
employees :
employee :
actions : [ read ]
fields :
allow : [ "name" , "email" , "department" , "hire_date" ]
GET /employees/507f1f77bcf86cd799439011
{
"_id" : "507f1f77bcf86cd799439011" ,
"name" : "John Smith" ,
"email" : "[email protected] " ,
"department" : "Engineering" ,
"hire_date" : "2020-05-15"
// salary, performance_rating, etc. excluded
}
Source: /home/daytona/workspace/source/pkg/rbac/engine.go:341-382
Step 2: Deny Lists (Blacklisting)
Exclude specific sensitive fields:
policies :
employees :
manager :
actions : [ read ]
fields :
deny : [ "ssn" , "bank_account" , "tax_info" ]
Response (sensitive fields removed):
{
"_id" : "507f1f77bcf86cd799439011" ,
"name" : "John Smith" ,
"email" : "[email protected] " ,
"salary" : 85000 ,
"department" : "Engineering"
// ssn, bank_account, tax_info excluded
}
Step 3: Deny-Write Protection
Allow reading but prevent modification:
policies :
employees :
employee :
actions : [ read , update ]
fields :
deny_write : [ "salary" , "title" , "manager_id" ]
PUT /employees/507f1f77bcf86cd799439011
Content-Type: application/json
{
"phone" : "555-1234",
"salary" : 100000 // Denied!
}
{
"error" : {
"code" : "forbidden" ,
"message" : "You don't have permission to modify this field" ,
"details" : {
"field" : "salary"
}
}
}
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-6789 → 123-**-***9
Credit Card: 4532-1234-5678-9010 → 4532-****-****-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:
User can only access own record or direct reports (condition)
Even when authorized, only sees allowed fields
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