Part 1:What is role and rights?
Role rights implementation is an important part of any software. Role is a position of responsibility and every responsibility enjoys some rights given to them. There may be some common rights between a few roles and some rights may strictly belong to a specific role.
Rights are Urls that a role is authorized to access. It is thus necessary to create a collection in DB storing information of rights to a role. We have role collection schema as
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const RoleSchema = new Schema({
roleId:{
type:String,
unique:true,
required:[true,"Role Id required"]
},
type:{
type:String,
unique:true,
required:[true,"Role type is required"]
},
rights:[{
name: String,
path: String,
url: String
}]
});
module.exports = Role = mongoose.model('role',RoleSchema);
Now, remember every role that is supposed to exist is in Role collection and of above schema type.
In schema rights array of the object we see the object has keys:
- name(for the name of URL like “set-username”)
- path(for base path hit “/users/”)
- url(requested URL or complete path “/users/set-username”)
Thus if a user with role user has right to change username then he can hit URL /users/set-username
.However a wanderer will not be able to access this url. A higher role like admin & superadmin should logically have access to all lower role rights(URLs).
Role in real application are:-
- Wanderer (Someone who is just visiting our site. He should be able to access all public routes. Simple URLs/Public URLs accessible to all thus need not make a separate role for this as it is not any authenticated right.)
- Guest (Someone who has registered but not verified say email not verified).
- User (Someone who has his verified email)
- Admin (Made an Admin by SuperAdmin after verifying.he enjoy most of the rights)
- Superadmin (Master of application.He enjoy some more sophisticated rights.More rights then admin)
Till now we have understood what exactly is right and how it is mapped to a role.
Part 1.5: Registered Urls/Config Urls
Here we have a file called registeredUrls.js
which is like:
module.exports = {
// globally accessible
simple: {
"/":[""],
'/users/':["login","register"],
},
auth:{
//admin level enpoint
'/admin/': ['load-users' , 'set-new-password', 'delete-user'],
'/teacher/': ["add-teacher", "delete-teacher", "edit-teacher"],
'/student/': [
"add-student",
"delete-student",
"edit-student",
"test-result"
],
// common user enpoint
'/test/': ["view-test", "submit-test"],
'/profile/': [
'change-username',
'update-profile-data',
'set-new-password',
'upload-pic',
'update-social-links'
],
'/teacher/':['load-teacher'],
'/student/':['load-student']
}
Similarly confgUrls.js
const configUrls= {
'/roles/': [
'get-rights',
'create',
'update-rights',
'load',
'delete',
'assign'
]
}
module.exports = configUrls;
Part 2:Creating SuperAdmin
This is the most essential part of the application. Whenever the server starts for the first time or restarts/reboots this step occurs. In config/init.js follow the procedure:
- Load All simple URLs(public) and Auth Urls(admin & users) & super-admin-specific URLs into superAdminRights[].
- Then run a function to create a user with role superadmin if doesn’t exist.
- Get a Role of type:”superadmin” if found:replace its rights with new rights(superAdminRights).else:create Role of type :”superadmin” and then fill its rights(superAdminRights).
At the end of this function call, we are always sure that we have a superadmin in application with all its sophisticated URLs/rights initialized.
Part 3:Super-Admin-Specific-URLs
These are rights that are enjoyed by super admin only and must be maintained in a separate file in parallel to the registered URL file. These include URL rights which map routes used only by superadmin. Here we have routes to create role, load roles, get-rights for a roleId, update-rights for roleId/role type, assign-role to a user, delete a role.
For each user in code, we need to change their role from guest to the user(say after email verification). Or guest/user to admin by superadmin using assign-role URL. Then updating admin rights using route update-rights.
The process ensures that every role has A collection Document and filled rights there.
Part 4:Authenticator Middleware
This heart of our RBACS logic. Here we use a middleware which follows the process:
// get all the URLs/endpoints in the system
const URLS = require("./registeredUrls");
// special endpoints
const CONFIG_URLS = require("./configUrls");
// create array of all endpoints and separate them by auth flows
// simple urls doesn't need any auth flow
const SIMPLE_URLS = [];
// AUTH_URL and SUPER_URL need auth flow
const AUTH_URLS = [];
const SUPER_URLS = [];
// the data-structure is { [rootURL]: [...subURLs..] }
// traverse all registered paths of simple URLs
// and make complete paths i.e `rootURL/subURL`
for (const rootURL in URLS.simple) {
const subURLs = URLS.simple[rootURL];
for (const subURL of subURLs) {
// register all these in SIMPLE_URLS
SIMPLE_URLS.push([rootURL, subURL].join("/"));
}
}
// same with AUTH...register as AUTH_URLS
for (const rootURL in URLS.auth) {
const subURLs = URLS.auth[rootURL];
for (const subURL of subURLs) {
AUTH_URLS.push([rootURL, subURL].join("/"));
}
}
// same with CONFIG_URLS...register as SUPER_URLS
for (const rootURL in CONFIG_URLS) {
const subURLs = CONFIG_URLS[rootURL];
for (const subURL of subURLs) {
SUPER_URLS.push([rootURL, subURL].join("/"));
// push super URLS in AUTH_URLS also as they need auth flow
AUTH_URLS.push([rootURL, subURL].join("/"));
}
}
// By now we have an array of URLs
// 1. Simple URLs don't need authentication flow SIMPLE_URLS
// 2. Authentication required URLs need auth-token
// 3. Config URLs are the highest roles URLs typically super admin
// and have the same flow as Auth URL
// in the node.js middleware callback
const middleware = (req, res, next) => {
// URL or endpoint or path requested
const reqURL = req.url;
// check where URL is
const isAuthURL = AUTH_URLS.includes(reqURL);
const isSimpleURL = SIMPLE_URLS.includes(reqURL);
// auth URLs need auth flows
if (isAuthURL) {
// get token from header
const token = getToken(req);
// validate
const isValidJWTToken = validateJWT(token);
if (!token || !isValidJWTToken) {
// send failed authentication response
// !token missing token required login
// !isValidJWTToken token is invalid or expired
return;
}
// token is valid but we have to check if the session exists in DB
const user_session = getUserSessionData(token);
// If no session in DB means this token may be mischievously generated
if (!user_session) {
// the user token might be compromised
return;
}
// now check if user_session.rights [] has requested URL
// if requested URL is assigned is in array means
// it is assigned that right/path
const hasRightToPath = user_session.rights.includes(reqURL);
if (!hasRightToPath) {
// user doesn't have access to this path
return;
}
// user has right to this path/endpoint/URL so let them do the thing
return next();
}
if (isSimpleURL) {
return next(); // simple URL doesn't need any flow simply pass it down
}
// if it matched none means it isn't a registered path in the system
return;
}