One key component to maintaining system security is controlling who has access to what and what permissions are needed to access certain resources. This is what is called access control, and the two main methods in Node.js for this are:
Role-based access control is the means by which a system restricts access to different CRUD actions to different users, based on their assigned roles.
CRUD actions are:
- C for Creating a resource
- R for Reading a resource
- U for Updating a resource
- D for Deleting a resource
The users are given roles, and these roles dictate what actions the users can perform. For example, a user in an app can be the owner, or can be an admin, or can be the viewer. These owners, admins, and viewers will then be the roles a user can assume.
Here, the owner of the app has access to all activities: they can remove admins and viewers, add new admins and viewers, and edit existing admins and viewers.
In the case of admins, they can add, edit, and delete viewers. They can also add other admins, but they cannot edit or delete fellow admins. Admins also cannot add, edit, or delete owners.
Viewers cannot add, edit, or delete fellow viewers, admins, or owners.
Let’s see how we can use the roles above to control access in a Nodejs server.
Using Mongoose, we can set the team schema like so:
let UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true
},
email: {
type: String,
unique: true
},
password: String,
teams: [{
role: String,
id: { type: mongoose.Schema.Types.ObjectId, ref: "Team" }
}]
})
module.exports = mongoose.model('User', UserSchema)
This schema holds user information. It has a teams
array that holds all the teams that the user belongs to. Each object in teams
holds the role of the user in the role
property and the reference to the Team
model in the id
property.
Let’s see how to use it to check for access rights in our endpoints.
route.route("/add-user")
.post((req, res, next) => {
})
The endpoint above adds a new user to the system. Only admin or owner roles can do this. So, we have to check the role of the user performing the action to see if it matches the roles assigned to this operation.
// allows only users with "admin" or "owner" privileges.
route.route("/add-user")
.post((req, res, next) => {
const {
userToAdd
} = req.body
const user = req.user
if(user.team.role == "admin" || user.team.role == "owner") {
// add user
} else {
res.send({ error: "You have no privileges to perform this action." })
}
...
})
We retrieved the user and used the team
property to check if the user is an admin or an owner and to see whether that user can add a new user. If not, an error message is sent telling the user that they do not have the privileges to perform this action.
Let’s look at deleting a user.
// allows only users with "admin" or "owner" privileges, but "admin"s can't delete "owner".
route.route("/delete-user")
.delete((req, res, next) => {
const {
userToDelete
} = req.body
const user = req.user
if(user.team.role == "admin" || user.team.role == "owner") {
if(userToDelete.team.role !== "owner" && user.team.role == "admin") {
if(userToDelete.team.role !== "admin" && user.team.role !== "admin") {
// add user
}
}
} else {
res.send({ error: "You have no privileges to perform this action." })
}
...
})
Here, the admins and owners can perform this operation, but admins do not have the privilege to delete an owner.
if
statement checks if a user is either an admin or an owner.if
statement checks that the user to be deleted is not an admin and the user deleting the user is also not an admin (because admins cannot remove an admin).ABAC looks at contextual information before giving privileges and can be used in conjunction with RBAC. As such, it defines the policies for accessing and manipulating resources using attributes.
These attributes can be of the user, resource, or environment.
Let’s say in a blog platform, we have roles like the owner, admin, and viewers. Blog posts from users can be visible, private, or public.
The visibility
attribute defines the access that will be granted to users based on their roles.
Let’s see the implementation:
router.route("get-post")
.get((req, res, next) => {
const postId = req.params.postId
const user = req.user
const post = // ... get post with postId
// user is the owner of the post
if(post.user == user) {
} else {
if(post.visibility == "public" && user.team.role == "admin") {
// ...
}
}
})
In this case, the visibility
attribute of the post determines who can view it or not, along with the role.
In this shot, we learned about access control methods in Node.js, i.e., role-based access control (RBAC) and attribute-based access control (ABAC). RBAC restricts access to resources using the roles assigned to users, and ABAC checks restrictions on the attribute in order to access resources.