Overview
OmniEHR implements Role-Based Access Control (RBAC) to enforce the principle of least privilege. Users are assigned one of three roles, each with specific permissions for accessing and modifying healthcare data.
User Roles
From ~/workspace/source/server/src/models/User.js:26, the system supports three roles:
role : {
type : String ,
enum : [ "admin" , "practitioner" , "auditor" ],
default : "practitioner"
}
Role Hierarchy
Admin
Full system access - manage users, view all data, configure system
Practitioner
Clinical access - create/update patients, appointments, and tasks within their scope
Auditor
Read-only access - view data and audit logs for compliance monitoring
Role Definitions
Admin Role
Capabilities:
✅ Create, read, update user accounts
✅ Read, create, update, delete all FHIR resources
✅ View audit logs
✅ Manage practitioners
✅ Full access to patient data
✅ System configuration
Restrictions:
None - admins have unrestricted access
Use Cases:
System administrators
Practice managers
IT support staff
Practitioner Role
Capabilities:
✅ Read, create, update FHIR resources (Patient, Appointment, Task, etc.)
✅ View their own practitioner profile
✅ Create appointments for their own schedule
✅ Manage tasks assigned to them
✅ Search and view patient data
Restrictions:
❌ Cannot create or modify other users
❌ Cannot view audit logs
❌ Cannot create patients (admin-only)
❌ Cannot book appointments for other practitioners
❌ Cannot access tasks assigned to other practitioners
Use Cases:
Physicians
Nurses
Healthcare providers
Auditor Role
Capabilities:
✅ Read all FHIR resources
✅ View audit logs
✅ Search and view patient data
✅ Generate compliance reports
Restrictions:
❌ Cannot create or modify any data
❌ Cannot create or modify users
❌ Read-only access to all resources
Use Cases:
Compliance officers
Security auditors
Quality assurance staff
Authorization Middleware
From ~/workspace/source/server/src/middleware/auth.js:22, the authorize middleware enforces role-based permissions:
export const authorize = ( ... roles ) => ( req , _res , next ) => {
if ( ! req . user ) {
return next ( new ApiError ( 401 , "Authentication required" ));
}
if ( ! roles . includes ( req . user . role )) {
return next ( new ApiError ( 403 , "Insufficient permissions" ));
}
return next ();
};
Usage Example
import { authenticate , authorize } from './middleware/auth.js' ;
// Only admins can access
router . get ( '/admin/users' , authenticate , authorize ( 'admin' ), handler );
// Admins and auditors can access
router . get ( '/admin/audit-logs' , authenticate , authorize ( 'admin' , 'auditor' ), handler );
// Admins and practitioners can access
router . post ( '/api/fhir/Appointment' , authenticate , authorize ( 'admin' , 'practitioner' ), handler );
Permission Matrix
From ~/workspace/source/server/src/routes/fhirRoutes.js:53, the FHIR API defines role-based access:
const readRoles = [ "admin" , "practitioner" , "auditor" ];
const writeRoles = [ "admin" , "practitioner" ];
const patientWriteRoles = [ "admin" ];
FHIR Resources
Resource Create Read Update Delete Patient Admin All roles Admin Admin Appointment Admin, Practitioner* All roles Admin, Practitioner* Admin, Practitioner* Task Admin, Practitioner* All roles Admin, Practitioner* Admin, Practitioner* Observation Admin, Practitioner All roles Admin, Practitioner Admin, Practitioner DiagnosticReport Admin, Practitioner All roles Admin, Practitioner Admin, Practitioner
*With scope restrictions (see below)
Administrative Endpoints
Endpoint Admin Practitioner Auditor GET /admin/users ✅ ❌ ❌ POST /admin/users ✅ ❌ ❌ GET /admin/practitioners ✅ ✅* ❌ GET /admin/audit-logs ✅ ❌ ✅
*Practitioners can only see their own profile
Scope Restrictions
Practitioner Scope Limitations
Practitioners have additional scope restrictions beyond role-based permissions:
Appointment Booking
From ~/workspace/source/server/src/routes/fhirRoutes.js:88:
const ensureBookingPermission = ( requestingUser , practitionerUserId ) => {
if (
requestingUser . role === "practitioner" &&
String ( practitionerUserId ) !== String ( requestingUser . sub )
) {
throw new ApiError ( 403 , "Practitioners can only book appointments under their own schedule" );
}
};
Rule : Practitioners can only create/update appointments where they are the assigned practitioner.
Appointment Queries
From ~/workspace/source/server/src/routes/fhirRoutes.js:830:
if ( req . user . role === "practitioner" ) {
filter . practitionerUserId = req . user . sub ;
}
Rule : When searching appointments, practitioners automatically see only their own appointments.
Task Management
From ~/workspace/source/server/src/routes/fhirRoutes.js:97:
const ensureTaskOwnerPermission = ( requestingUser , ownerUserId ) => {
if ( requestingUser . role !== "practitioner" ) {
return ;
}
if ( ! ownerUserId || String ( ownerUserId ) !== String ( requestingUser . sub )) {
throw new ApiError ( 403 , "Practitioners can only assign or update tasks under their own worklist" );
}
};
Rule : Practitioners can only create/update tasks assigned to themselves.
Practitioner List Access
From ~/workspace/source/server/src/routes/adminRoutes.js:67:
const filter =
req . user . role === "practitioner"
? { _id: req . user . sub , role: "practitioner" , active: true }
: { role: "practitioner" , active: true };
const practitioners = await User . find ( filter ). sort ({ fullName: 1 });
Rule : Practitioners requesting the practitioner list only see themselves; admins see all practitioners.
Implementation Examples
Protecting Admin Routes
From ~/workspace/source/server/src/routes/adminRoutes.js:26:
import { authenticate , authorize } from "../middleware/auth.js" ;
const router = express . Router ();
// All admin routes require authentication
router . use ( authenticate );
// Only admins can view all users
router . get (
"/users" ,
authorize ( "admin" ),
asyncHandler ( async ( _req , res ) => {
const users = await User . find (). sort ({ createdAt: - 1 });
res . json ({
data: users . map ( formatUser ),
total: users . length
});
})
);
// Only admins can create users
router . post (
"/users" ,
authorize ( "admin" ),
asyncHandler ( async ( req , res ) => {
// User creation logic
})
);
Multi-Role Access
From ~/workspace/source/server/src/routes/adminRoutes.js:63:
// Admins and practitioners can access practitioner list
router . get (
"/practitioners" ,
authorize ( "admin" , "practitioner" ),
asyncHandler ( async ( req , res ) => {
// Scope filtering based on role
const filter =
req . user . role === "practitioner"
? { _id: req . user . sub , role: "practitioner" , active: true }
: { role: "practitioner" , active: true };
const practitioners = await User . find ( filter ). sort ({ fullName: 1 });
res . json ({
data: practitioners . map ( formatUser ),
total: practitioners . length
});
})
);
Auditor Access
From ~/workspace/source/server/src/routes/adminRoutes.js:81:
// Admins and auditors can view audit logs
router . get (
"/audit-logs" ,
authorize ( "admin" , "auditor" ),
asyncHandler ( async ( req , res ) => {
const pagination = paginationSchema . parse ( req . query );
const filter = {};
// Apply filters from query parameters
if ( req . query . outcome ) {
filter . outcome = req . query . outcome ;
}
if ( req . query . resourceType ) {
filter . resourceType = req . query . resourceType ;
}
const [ total , logs ] = await Promise . all ([
AuditLog . countDocuments ( filter ),
AuditLog . find ( filter )
. sort ({ createdAt: - 1 })
. skip (( pagination . page - 1 ) * pagination . limit )
. limit ( pagination . limit )
. lean ()
]);
res . json ({
page: pagination . page ,
limit: pagination . limit ,
total ,
data: logs
});
})
);
User Management
Creating Users
Only admins can create new users through the /admin/users endpoint:
curl -X POST https://api.omniehr.com/admin/users \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected] ",
"password": "secure-password",
"fullName": "Dr. Jane Smith",
"organization": "General Hospital",
"role": "practitioner"
}'
Validation
From ~/workspace/source/server/src/services/validation.js:17, role validation is enforced:
role : z . enum ([ "admin" , "practitioner" , "auditor" ]). optional (). default ( "practitioner" )
Valid roles : admin, practitioner, auditor
Default role : practitioner (safest default)
User Schema
From ~/workspace/source/server/src/models/User.js:3:
const userSchema = new mongoose . Schema (
{
email: {
type: String ,
required: true ,
unique: true ,
trim: true ,
lowercase: true
},
fullName: {
type: String ,
required: true ,
trim: true
},
organization: {
type: String ,
trim: true ,
default: ""
},
passwordHash: {
type: String ,
required: true
},
role: {
type: String ,
enum: [ "admin" , "practitioner" , "auditor" ],
default: "practitioner"
},
active: {
type: Boolean ,
default: true
},
lastLoginAt: Date
},
{
timestamps: true
}
);
Testing Permissions
Testing Role Access
import { authenticate , authorize } from './middleware/auth.js' ;
import request from 'supertest' ;
import app from './app.js' ;
describe ( 'RBAC Tests' , () => {
let adminToken , practitionerToken , auditorToken ;
beforeAll ( async () => {
// Login as different roles
const adminRes = await request ( app )
. post ( '/api/auth/login' )
. send ({ email: '[email protected] ' , password: 'password' });
adminToken = adminRes . body . token ;
const practitionerRes = await request ( app )
. post ( '/api/auth/login' )
. send ({ email: '[email protected] ' , password: 'password' });
practitionerToken = practitionerRes . body . token ;
const auditorRes = await request ( app )
. post ( '/api/auth/login' )
. send ({ email: '[email protected] ' , password: 'password' });
auditorToken = auditorRes . body . token ;
});
test ( 'Admin can access user list' , async () => {
const res = await request ( app )
. get ( '/admin/users' )
. set ( 'Authorization' , `Bearer ${ adminToken } ` );
expect ( res . status ). toBe ( 200 );
});
test ( 'Practitioner cannot access user list' , async () => {
const res = await request ( app )
. get ( '/admin/users' )
. set ( 'Authorization' , `Bearer ${ practitionerToken } ` );
expect ( res . status ). toBe ( 403 );
expect ( res . body . message ). toBe ( 'Insufficient permissions' );
});
test ( 'Auditor can access audit logs' , async () => {
const res = await request ( app )
. get ( '/admin/audit-logs' )
. set ( 'Authorization' , `Bearer ${ auditorToken } ` );
expect ( res . status ). toBe ( 200 );
});
test ( 'Practitioner cannot access audit logs' , async () => {
const res = await request ( app )
. get ( '/admin/audit-logs' )
. set ( 'Authorization' , `Bearer ${ practitionerToken } ` );
expect ( res . status ). toBe ( 403 );
});
});
Error Responses
Insufficient Permissions
When a user attempts to access a resource they don’t have permission for:
{
"status" : "error" ,
"message" : "Insufficient permissions" ,
"statusCode" : 403
}
Authentication Required
When accessing a protected route without authentication:
{
"status" : "error" ,
"message" : "Authentication required" ,
"statusCode" : 401
}
Scope Violation Examples
Practitioner trying to book for another practitioner:
{
"status" : "error" ,
"message" : "Practitioners can only book appointments under their own schedule" ,
"statusCode" : 403
}
Practitioner trying to assign task to another user:
{
"status" : "error" ,
"message" : "Practitioners can only assign or update tasks under their own worklist" ,
"statusCode" : 403
}
Best Practices
Principle of Least Privilege Assign users the minimum role necessary for their job function
Regular Audits Periodically review user roles and deactivate unused accounts
Separation of Duties Use auditor role for compliance monitoring, separate from admins
Scope Enforcement Apply additional scope restrictions beyond role-based permissions
Recommendations
Default to Practitioner : New users should default to practitioner role
Limit Admin Access : Only grant admin role to trusted personnel
Use Auditor Role : Assign auditor role for compliance staff who need read-only access
Deactivate, Don’t Delete : Use the active flag to disable users instead of deleting
Log Role Changes : Audit when user roles are modified
Changing a user’s role takes effect immediately. The user must log in again to receive a new JWT with the updated role.
Frontend Integration
Conditional UI Rendering
import { useAuth } from './hooks/useAuth' ;
function Dashboard () {
const { user } = useAuth ();
return (
< div >
< h1 > Dashboard </ h1 >
{ /* Show admin panel only to admins */ }
{ user . role === 'admin' && (
< AdminPanel />
) }
{ /* Show clinical tools to admins and practitioners */ }
{ [ 'admin' , 'practitioner' ]. includes ( user . role ) && (
< ClinicalTools />
) }
{ /* Show audit tools to admins and auditors */ }
{ [ 'admin' , 'auditor' ]. includes ( user . role ) && (
< AuditTools />
) }
</ div >
);
}
Role-Based Routing
import { Navigate } from 'react-router-dom' ;
import { useAuth } from './hooks/useAuth' ;
function ProtectedRoute ({ children , allowedRoles }) {
const { user } = useAuth ();
if ( ! user ) {
return < Navigate to = "/login" /> ;
}
if ( allowedRoles && ! allowedRoles . includes ( user . role )) {
return < Navigate to = "/unauthorized" /> ;
}
return children ;
}
// Usage
< Route path = "/admin/users" element = {
< ProtectedRoute allowedRoles = { [ 'admin' ] } >
< UsersPage />
</ ProtectedRoute >
} />
Frontend role checks are for UX only. Always enforce permissions on the backend as frontend code can be bypassed.
Next Steps
Authentication Learn about JWT authentication and login flow
HIPAA Overview Understand HIPAA compliance measures
API Reference View admin API endpoints
User Guide Learn how to manage users