MongoDB Field Level Security
Introduction
Field Level Security (FLS) in MongoDB allows you to control which users can access specific fields within documents. Unlike document-level security, which restricts access to entire documents, field-level security provides more granular control, allowing you to hide sensitive fields from certain users while making other fields accessible.
This is particularly useful when:
- Different user roles need different views of the same data
- Some fields contain sensitive information (like personal data or financial details)
- You need to comply with data privacy regulations like GDPR or HIPAA
In this tutorial, we'll explore how to implement field-level security in MongoDB using various approaches.
Prerequisites
Before diving into field-level security, ensure you have:
- MongoDB Enterprise Server 3.2 or later (for built-in field-level redaction)
- Basic understanding of MongoDB's Role-Based Access Control (RBAC)
- MongoDB Compass or mongo shell for testing
Understanding Field-Level Security Approaches
MongoDB offers several ways to implement field-level security:
- Schema-Based Approach: Using different schemas or collections for different access levels
- View-Based Approach: Creating views that project only certain fields
- Role-Based Field-Level Redaction: Using the
$redact
operator with role-based restrictions - Application-Level Security: Implementing field restrictions in your application code
Let's explore each approach with practical examples.
1. View-Based Field-Level Security
MongoDB views allow you to create virtual collections with specific projections of documents. This is one of the simplest ways to implement field-level security.
Creating a Secured View
Imagine we have a patients
collection with medical records:
// Sample document in patients collection
{
"_id": ObjectId("5f8d7c0f9b8a3e2d6c1a4b5e"),
"patientId": "PT12345",
"name": "John Doe",
"dob": "1980-05-15",
"ssn": "123-45-6789",
"diagnosis": "Hypertension",
"medication": "Lisinopril",
"billingInfo": {
"insurance": "Blue Cross",
"policyNumber": "POL987654321",
"paymentMethod": "Credit Card"
}
}
Now, we can create different views for different roles:
// View for doctors (no billing info)
db.createView(
"patientsForDoctors",
"patients",
[
{
$project: {
patientId: 1,
name: 1,
dob: 1,
diagnosis: 1,
medication: 1,
ssn: 1
}
}
]
)
// View for front desk staff (no SSN or diagnosis details)
db.createView(
"patientsForStaff",
"patients",
[
{
$project: {
patientId: 1,
name: 1,
dob: 1,
billingInfo: 1
}
}
]
)
Configuring Access to Views
After creating the views, we need to assign appropriate permissions to different roles:
// Create roles
db.createRole({
role: "doctorRole",
privileges: [
{
resource: { db: "hospital", collection: "patientsForDoctors" },
actions: ["find"]
}
],
roles: []
})
db.createRole({
role: "staffRole",
privileges: [
{
resource: { db: "hospital", collection: "patientsForStaff" },
actions: ["find"]
}
],
roles: []
})
Now, doctors can only access fields through the patientsForDoctors
view, and staff can only access fields through the patientsForStaff
view.
2. Role-Based Field-Level Redaction
For more dynamic field-level security, MongoDB Enterprise offers the $redact
operator, which can be used with the $cond
operator to check user roles at query time.
Setting Up Role-Based Redaction
First, let's create a user with specific roles:
db.createUser({
user: "doctor",
pwd: "securePassword",
roles: [
{ role: "doctorRole", db: "hospital" }
]
})
Now we can apply redaction based on roles:
// When a doctor queries the collection
db.patients.aggregate([
{
$redact: {
$cond: {
if: { $in: ["doctorRole", "$$USER_ROLES"] },
then: "$$DESCEND",
else: "$$PRUNE"
}
}
}
])
This approach requires you to tag each document or field with the roles that are allowed to access it.
3. Application-Level Field Security
For more flexibility, you can implement field-level security at the application level.
Example in Node.js with Mongoose
// Define the patient schema with field-level access control
const patientSchema = new mongoose.Schema({
patientId: String,
name: String,
dob: String,
ssn: {
type: String,
access: 'restricted'
},
diagnosis: {
type: String,
access: 'medical-staff'
},
medication: String,
billingInfo: {
type: Object,
access: 'billing-staff'
}
});
// Add a method to filter fields based on user role
patientSchema.methods.toJSON = function() {
const obj = this.toObject();
const userRole = getCurrentUserRole(); // This function would get the role from your auth system
// Remove fields based on user role
if (userRole !== 'medical-staff' && userRole !== 'admin') {
delete obj.diagnosis;
}
if (userRole !== 'billing-staff' && userRole !== 'admin') {
delete obj.billingInfo;
}
if (userRole !== 'doctor' && userRole !== 'admin') {
delete obj.ssn;
}
return obj;
};
const Patient = mongoose.model('Patient', patientSchema);
With this approach, the fields returned will depend on the user's role, and sensitive information will be automatically filtered out.
4. Schema-Based Approach
Another simple approach is to store sensitive data in separate collections with different access controls.
Example of Split Collections
// Basic patient info - accessible to most staff
db.patientBasicInfo.insertOne({
patientId: "PT12345",
name: "John Doe",
dob: "1980-05-15"
});
// Medical records - accessible only to medical staff
db.patientMedicalInfo.insertOne({
patientId: "PT12345",
diagnosis: "Hypertension",
medication: "Lisinopril",
ssn: "123-45-6789"
});
// Billing info - accessible only to billing staff
db.patientBillingInfo.insertOne({
patientId: "PT12345",
insurance: "Blue Cross",
policyNumber: "POL987654321",
paymentMethod: "Credit Card"
});
You can then configure role-based access to each collection:
db.createRole({
role: "basicInfoAccess",
privileges: [
{ resource: { db: "hospital", collection: "patientBasicInfo" }, actions: ["find"] }
],
roles: []
});
db.createRole({
role: "medicalInfoAccess",
privileges: [
{ resource: { db: "hospital", collection: "patientMedicalInfo" }, actions: ["find"] }
],
roles: []
});
db.createRole({
role: "billingInfoAccess",
privileges: [
{ resource: { db: "hospital", collection: "patientBillingInfo" }, actions: ["find"] }
],
roles: []
});
Best Practices for Field-Level Security
- Layer your security approaches: Combine multiple security methods for defense in depth
- Don't rely on security by obscurity: Always implement proper authentication and authorization
- Use views for read-only access: Views work well for controlling read access but don't control write operations
- Audit access regularly: Monitor who accesses what data
- Encrypt sensitive fields: Use field-level encryption for highly sensitive data
Real-World Applications
Banking Application
In a banking application, different employees need different levels of access:
// Bank account document
{
"_id": ObjectId("..."),
"accountNumber": "1234567890",
"customerName": "Alice Johnson",
"balance": 5000.00,
"socialSecurityNumber": "123-45-6789",
"creditScore": 750,
"transactionHistory": [
{ "date": "2023-07-15", "amount": -120.50, "description": "Grocery Store" },
{ "date": "2023-07-14", "amount": 2500.00, "description": "Salary Deposit" }
]
}
// View for tellers (cannot see SSN or credit score)
db.createView(
"accountsForTellers",
"accounts",
[
{
$project: {
accountNumber: 1,
customerName: 1,
balance: 1,
transactionHistory: 1
}
}
]
)
// View for loan officers (can see everything except transaction details)
db.createView(
"accountsForLoanOfficers",
"accounts",
[
{
$project: {
accountNumber: 1,
customerName: 1,
balance: 1,
socialSecurityNumber: 1,
creditScore: 1
}
}
]
)
E-commerce Platform
For an e-commerce platform with both customers and administrators:
// User document
{
"_id": ObjectId("..."),
"username": "user123",
"email": "[email protected]",
"passwordHash": "$2a$10$...",
"phone": "555-123-4567",
"address": {
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
},
"paymentMethods": [
{
"type": "credit_card",
"lastFour": "1234",
"expiryDate": "05/25"
}
]
}
// View for customer service (can't see payment info or password hash)
db.createView(
"usersForCustomerService",
"users",
[
{
$project: {
username: 1,
email: 1,
phone: 1,
address: 1
}
}
]
)
Visualizing Field-Level Security
Here's a diagram showing how different roles access different fields in the patient example:
Performance Considerations
Field-level security can impact performance, especially with dynamic redaction:
- Views are efficient for read-only scenarios
- Application-level filtering adds CPU overhead
- Separate collections may increase join complexity
- Field-level encryption adds performance overhead
Always test your security implementation with realistic workloads to ensure performance remains acceptable.
Summary
Field-level security in MongoDB provides granular control over who can access specific fields within documents. We've explored several approaches:
- View-Based Security: Creating filtered views of collections for different roles
- Role-Based Redaction: Using the
$redact
operator for dynamic field filtering - Application-Level Security: Implementing field access control in the application code
- Schema-Based Approach: Splitting sensitive data into separate collections
Each approach has its own advantages and use cases. For most scenarios, a combination of these techniques provides the most robust security solution.
Additional Resources
Exercises
-
Create a view for a
products
collection that shows different fields based on whether the user is a customer or an inventory manager. -
Implement application-level field security for a user profile system where users can only see their own private information.
-
Design a field-level security system for a multi-tenant application where each tenant's data must be completely isolated.
-
Create a schema using separate collections for different sensitivity levels of medical data, and implement appropriate access controls.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)