DEV Community

Cover image for Role-based Access Control in Golang with jwt-go
Benson Macharia
Benson Macharia

Posted on • Edited on

42 1 1

Role-based Access Control in Golang with jwt-go

As a security engineer, what's the first thing you check when testing a web or mobile application? Access control misconfigurations must be at the top of your list if you like going for the low hanging fruits first.

Surprisingly, access control is one of the components that application developers think will be simple, but ends up taking much of their time with no end user value, and even worse ends being poorly done, tremendous exposing the application to security risks. It is no coincidence that Authentication and Authorization related issues take the first two positions in the OWASP Top 10 - 2023 list of common API security risks.

For this reason, in this article I am going to illustrate the application of Role-based Access Control; a simple and yet secure access control approach for enterprise APIs.

Role-based Access Control (RBAC)
Role-based Access Control is a mechanism that restricts access to an information system based on user job functions or roles. Access to data is restricted through permissions and privileges that are attached to different user roles.

Let's Build
By the end of this article we are going build a simple room booking API with Go that will allow visitors to view available rooms as well as register, login and book rooms. There will also be an administrator who will have the permissions map user to permissions as well as add and update rooms information.

Image description

Prerequisites

  • Basic knowledge of Go
  • Patience 😄

Source Code

$ git clone https://github.com/bensonmacharia/jwt-go-rbac.git
Enter fullscreen mode Exit fullscreen mode

Step 1: Setting up the environment

  • Get your favourite IDE, mine is vscode
  • Download and install Go
  • Confirm installed go version
$ go version    
go version go1.19.4 darwin/arm64
Enter fullscreen mode Exit fullscreen mode
// confirm installation by checking version
$ mysql --version
mysql  Ver 8.0.32 for macos12.6 on arm64 (Homebrew)

// test connection
$ mysql -u bmacharia -p
Enter password: 
mysql>
Enter fullscreen mode Exit fullscreen mode
  • Create a database
// create database
$ mysql> CREATE DATABASE jwt_go_rbac;
Query OK, 1 row affected (0.07 sec)

// confirm database creation
$ mysql> show databases;
+-------------------------+
| Database                |
+-------------------------+             |
| information_schema      |
| jwt_go_rbac             |
| mysql                   |
| performance_schema      |
| sys                     |
+-------------------------+
5 rows in set (0.01 sec)
Enter fullscreen mode Exit fullscreen mode

Step 2: Setting up the project

  • Create project folder and initialise go project
$ mkdir jwt-go-rbac                                                                
$ cd jwt-go-rbac            
$ go mod init bmacharia/jwt-go-rbac            
go: creating new go.mod: module bmacharia/jwt-go-rbac
Enter fullscreen mode Exit fullscreen mode
  • Create the main.go and .env files
// main go application file
$ touch main.go

// environment variables configuration file
$ touch .env
Enter fullscreen mode Exit fullscreen mode
  • Open the project in your IDE and edit the .env file as below
// replace values inside <<>> with your custom values
# Database credentials
DB_HOST="localhost"
DB_DRIVER=mysql
DB_USER="<<DB_USER>>"
DB_PASSWORD="<<DB_PASSWORD>>"
DB_NAME="jwt_go_rbac"
DB_PORT="3306"
# Default Admin User
ADMIN_USERNAME="<<ADMIN_USERNAME>>"
ADMIN_EMAIL="<<ADMIN_EMAIL>>"
ADMIN_PASSWORD="<<ADMIN_PASSWORD>>"
# Authentication credentials
TOKEN_TTL="1800"
JWT_PRIVATE_KEY="<<JWT_KEY>>"
view raw .env.example hosted with ❤ by GitHub

Step 3: Configure the database connection

  • Install required packages
// package to load .env file
$ go get github.com/joho/godotenv
// package to allow connection to MySQL database
$ go get -u gorm.io/driver/mysql
// a simple Go HTTP web framework
$ go get github.com/gin-gonic/gin
// ORM (Object Relational Mapping) library for Go
$ go get -u gorm.io/gorm
// jwt-go package
$ go get -u github.com/golang-jwt/jwt/v5
Enter fullscreen mode Exit fullscreen mode
  • Create a database connection file
// create a database folder and a file inside it
$ mkdir database
$ touch database/database.go
Enter fullscreen mode Exit fullscreen mode
  • Edit the database.go file as below. We are using GORM to initiate a connection to MySQL database.
package database
import (
"fmt"
"log"
"os"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var Db *gorm.DB
func InitDb() *gorm.DB {
Db = connectDB()
return Db
}
func connectDB() *gorm.DB {
var err error
host := os.Getenv("DB_HOST")
username := os.Getenv("DB_USER")
password := os.Getenv("DB_PASSWORD")
dbname := os.Getenv("DB_NAME")
port := os.Getenv("DB_PORT")
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", username, password, host, port, dbname)
//log.Println("dsn : ", dsn)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Error connecting to database :", err)
return nil
}
log.Println("`Successfully connected to the database")
return db
}
view raw database.go hosted with ❤ by GitHub
  • Edit the main.go file to test database connection. In this file we are loading the .env file, database connection and starting the gin web server on port 8000

    package main
    // load required packages
    import (
    "bmacharia/jwt-go-rbac/database"
    "fmt"
    "log"
    "github.com/gin-gonic/gin"
    "github.com/joho/godotenv"
    )
    func main() {
    // load environment file
    loadEnv()
    // load database configuration and connection
    loadDatabase()
    // start the server
    serveApplication()
    }
    func loadEnv() {
    err := godotenv.Load(".env")
    if err != nil {
    log.Fatal("Error loading .env file")
    }
    log.Println(".env file loaded successfully")
    }
    func loadDatabase() {
    database.InitDb()
    }
    func serveApplication() {
    router := gin.Default()
    router.Run(":8000")
    fmt.Println("Server running on port 8000")
    }
    view raw main.go hosted with ❤ by GitHub
  • Test database connection by running the go run command

$ go run main.go
2023/05/01 23:25:13 .env file loaded successfully
2023/05/01 23:25:13 `Successfully connected to the database
[GIN-debug] Listening and serving HTTP on :8000
Enter fullscreen mode Exit fullscreen mode

Step 4: Create database models

  • Create the user and role models. The user model defines the user object details as well as user properties in the database. On the other hand, the role model details properties about a role to be assigned to each user.
// create a user.go file inside the model directory
$ mkdir model
$ touch model/user.go
$ touch model/role.go
Enter fullscreen mode Exit fullscreen mode
  • model/user.go
    package model
    import (
    "bmacharia/jwt-go-rbac/database"
    "html"
    "strings"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
    )
    // User model
    type User struct {
    gorm.Model
    ID uint `gorm:"primary_key"`
    RoleID uint `gorm:"not null;DEFAULT:3" json:"role_id"`
    Username string `gorm:"size:255;not null;unique" json:"username"`
    Email string `gorm:"size:255;not null;unique" json:"email"`
    Password string `gorm:"size:255;not null" json:"-"`
    Role Role `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
    }
    // Save user details
    func (user *User) Save() (*User, error) {
    err := database.Db.Create(&user).Error
    if err != nil {
    return &User{}, err
    }
    return user, nil
    }
    // Generate encrypted password
    func (user *User) BeforeSave(*gorm.DB) error {
    passwordHash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
    if err != nil {
    return err
    }
    user.Password = string(passwordHash)
    user.Username = html.EscapeString(strings.TrimSpace(user.Username))
    return nil
    }
    // Get all users
    func GetUsers(User *[]User) (err error) {
    err = database.Db.Find(User).Error
    if err != nil {
    return err
    }
    return nil
    }
    // Get user by username
    func GetUserByUsername(username string) (User, error) {
    var user User
    err := database.Db.Where("username=?", username).Find(&user).Error
    if err != nil {
    return User{}, err
    }
    return user, nil
    }
    // Validate user password
    func (user *User) ValidateUserPassword(password string) error {
    return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
    }
    // Get user by id
    func GetUserById(id uint) (User, error) {
    var user User
    err := database.Db.Where("id=?", id).Find(&user).Error
    if err != nil {
    return User{}, err
    }
    return user, nil
    }
    // Get user by id
    func GetUser(User *User, id int) (err error) {
    err = database.Db.Where("id = ?", id).First(User).Error
    if err != nil {
    return err
    }
    return nil
    }
    // Update user
    func UpdateUser(User *User) (err error) {
    err = database.Db.Omit("password").Updates(User).Error
    if err != nil {
    return err
    }
    return nil
    }
    view raw user.go hosted with ❤ by GitHub
  • model/role.go
    package model
    import (
    "bmacharia/jwt-go-rbac/database"
    "gorm.io/gorm"
    )
    // Role model
    type Role struct {
    gorm.Model
    ID uint `gorm:"primary_key"`
    Name string `gorm:"size:50;not null;unique" json:"name"`
    Description string `gorm:"size:255;not null" json:"description"`
    }
    // Create a role
    func CreateRole(Role *Role) (err error) {
    err = database.Db.Create(Role).Error
    if err != nil {
    return err
    }
    return nil
    }
    // Get all roles
    func GetRoles(Role *[]Role) (err error) {
    err = database.Db.Find(Role).Error
    if err != nil {
    return err
    }
    return nil
    }
    // Get role by id
    func GetRole(Role *Role, id int) (err error) {
    err = database.Db.Where("id = ?", id).First(Role).Error
    if err != nil {
    return err
    }
    return nil
    }
    // Update role
    func UpdateRole(Role *Role) (err error) {
    database.Db.Save(Role)
    return nil
    }
    view raw role.go hosted with ❤ by GitHub
  • Run database migrations
//edit main.go file to add automigration script
...
// run database migrations and add seed data
func loadDatabase() {
    database.InitDb()
    database.Db.AutoMigrate(&model.Role{})
    database.Db.AutoMigrate(&model.User{})
    seedData()
}

// load seed data into the database
func seedData() {
    var roles = []model.Role{{Name: "admin", Description: "Administrator role"}, {Name: "customer", Description: "Authenticated customer role"}, {Name: "anonymous", Description: "Unauthenticated customer role"}}
    var user = []model.User{{Username: os.Getenv("ADMIN_USERNAME"), Email: os.Getenv("ADMIN_EMAIL"), Password: os.Getenv("ADMIN_PASSWORD"), RoleID: 1}}
    database.Db.Save(&roles)
    database.Db.Save(&user)
}
// run migration
$ go run main.go
..
Enter fullscreen mode Exit fullscreen mode
  • Confirm that users and roles tables have been created
mysql> show tables;
+-----------------------+
| Tables_in_jwt_go_rbac |
+-----------------------+
| roles                 |
| users                 |
+-----------------------+
2 rows in set (0.01 sec)
mysql> desc users;
mysql> desc roles;
mysql> select * from roles;
mysql> select * from users;
Enter fullscreen mode Exit fullscreen mode
  • Create models for user registration, login and update
$ touch model/register.go
$ touch model/login.go
$ touch model/update.go
Enter fullscreen mode Exit fullscreen mode
  • model/register.go
    package model
    type Register struct {
    Username string `json:"username" binding:"required"`
    Email string `json:"email" binding:"required"`
    Password string `json:"password" binding:"required"`
    }
    view raw register.go hosted with ❤ by GitHub
  • model/login.go
    package model
    type Login struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
    }
    view raw login.go hosted with ❤ by GitHub
  • model/update.go
    package model
    type Update struct {
    Username string `json:"username" binding:"required"`
    Email string `json:"email" binding:"required"`
    RoleID uint `gorm:"not null" json:"role_id"`
    }
    view raw update.go hosted with ❤ by GitHub

Step 5: Create controllers to interact with database content

  • Create the role and user controller files
$ touch controller/role.go
$ touch controller/user.go
Enter fullscreen mode Exit fullscreen mode
  • controller/role.go
    package controller
    import (
    "bmacharia/jwt-go-rbac/model"
    "errors"
    "net/http"
    "strconv"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
    )
    // create Role
    func CreateRole(c *gin.Context) {
    var Role model.Role
    c.BindJSON(&Role)
    err := model.CreateRole(&Role)
    if err != nil {
    c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
    return
    }
    c.JSON(http.StatusOK, Role)
    }
    // get Roles
    func GetRoles(c *gin.Context) {
    var Role []model.Role
    err := model.GetRoles(&Role)
    if err != nil {
    c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
    return
    }
    c.JSON(http.StatusOK, Role)
    }
    // get Role by id
    func GetRole(c *gin.Context) {
    id, _ := strconv.Atoi(c.Param("id"))
    var Role model.Role
    err := model.GetRole(&Role, id)
    if err != nil {
    if errors.Is(err, gorm.ErrRecordNotFound) {
    c.AbortWithStatus(http.StatusNotFound)
    return
    }
    c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
    return
    }
    c.JSON(http.StatusOK, Role)
    }
    // update Role
    func UpdateRole(c *gin.Context) {
    var Role model.Role
    id, _ := strconv.Atoi(c.Param("id"))
    err := model.GetRole(&Role, id)
    if err != nil {
    if errors.Is(err, gorm.ErrRecordNotFound) {
    c.AbortWithStatus(http.StatusNotFound)
    return
    }
    c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
    return
    }
    c.BindJSON(&Role)
    err = model.UpdateRole(&Role)
    if err != nil {
    c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
    return
    }
    c.JSON(http.StatusOK, Role)
    }
  • controller/user.go
    package controller
    import (
    "bmacharia/jwt-go-rbac/model"
    "errors"
    "fmt"
    "net/http"
    "strconv"
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
    "gorm.io/gorm"
    )
    // Register user
    func Register(context *gin.Context) {
    var input model.Register
    if err := context.ShouldBindJSON(&input); err != nil {
    context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
    }
    user := model.User{
    Username: input.Username,
    Email: input.Email,
    Password: input.Password,
    RoleID: 3,
    }
    savedUser, err := user.Save()
    if err != nil {
    context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
    }
    context.JSON(http.StatusCreated, gin.H{"user": savedUser})
    }
    // User Login
    func Login(context *gin.Context) {
    var input model.Login
    if err := context.ShouldBindJSON(&input); err != nil {
    var errorMessage string
    var validationErrors validator.ValidationErrors
    if errors.As(err, &validationErrors) {
    validationError := validationErrors[0]
    if validationError.Tag() == "required" {
    errorMessage = fmt.Sprintf("%s not provided", validationError.Field())
    }
    }
    context.JSON(http.StatusBadRequest, gin.H{"error": errorMessage})
    return
    }
    user, err := model.GetUserByUsername(input.Username)
    if err != nil {
    context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
    }
    err = user.ValidateUserPassword(input.Password)
    if err != nil {
    context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
    }
    context.JSON(http.StatusOK, gin.H{"username": input.Username, "message": "Successfully logged in"})
    }
    // get all users
    func GetUsers(context *gin.Context) {
    var user []model.User
    err := model.GetUsers(&user)
    if err != nil {
    context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
    return
    }
    context.JSON(http.StatusOK, user)
    }
    // get user by id
    func GetUser(context *gin.Context) {
    id, _ := strconv.Atoi(context.Param("id"))
    var user model.User
    err := model.GetUser(&user, id)
    if err != nil {
    if errors.Is(err, gorm.ErrRecordNotFound) {
    context.AbortWithStatus(http.StatusNotFound)
    return
    }
    context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
    return
    }
    context.JSON(http.StatusOK, user)
    }
    // update user
    func UpdateUser(c *gin.Context) {
    //var input model.Update
    var User model.User
    id, _ := strconv.Atoi(c.Param("id"))
    err := model.GetUser(&User, id)
    if err != nil {
    if errors.Is(err, gorm.ErrRecordNotFound) {
    c.AbortWithStatus(http.StatusNotFound)
    return
    }
    c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
    return
    }
    c.BindJSON(&User)
    err = model.UpdateUser(&User)
    if err != nil {
    c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
    return
    }
    c.JSON(http.StatusOK, User)
    }
  • Add registration and login routes
// edit main.go
func serveApplication() {
    router := gin.Default()
    authRoutes := router.Group("/auth/user")
        // registration route
    authRoutes.POST("/register", controller.Register)
        // login route
    authRoutes.POST("/login", controller.Login)

    router.Run(":8000")
    fmt.Println("Server running on port 8000")
}
Enter fullscreen mode Exit fullscreen mode
  • Test user registration and login
// run the application
$ go run main.go
// register user
$ curl -X POST http://localhost:8000/auth/user/register \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"username": "test","email":"test@bmacharia.com","password":"super^Secret!007"}'
// test user login
$ curl -X POST http://localhost:8000/auth/user/login \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"username": "test","password":"super^Secret!007"}' 
Enter fullscreen mode Exit fullscreen mode

Step 6. Add JWT utility for token creation and validation

  • To create a secure Role-based authentication scheme, we need to generate a unique token when the user authenticates. This is then used to track their assigned role as they consume the availed resources. In this project we are going to use the jwt-go package to generate a JWT token that will encapsulate the user details, assigned role and permissions.
  • Create the JWT utility files
// create a util directory
$ mkdir util
$ touch jwt.go
$ touch jwtAuth.go
Enter fullscreen mode Exit fullscreen mode
  • jwt.go
    package util
    import (
    "bmacharia/jwt-go-rbac/model"
    "errors"
    "fmt"
    "os"
    "strconv"
    "strings"
    "time"
    "github.com/golang-jwt/jwt/v5"
    "github.com/gin-gonic/gin"
    )
    // retrieve JWT key from .env file
    var privateKey = []byte(os.Getenv("JWT_PRIVATE_KEY"))
    // generate JWT token
    func GenerateJWT(user model.User) (string, error) {
    tokenTTL, _ := strconv.Atoi(os.Getenv("TOKEN_TTL"))
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    "id": user.ID,
    "role": user.RoleID,
    "iat": time.Now().Unix(),
    "eat": time.Now().Add(time.Second * time.Duration(tokenTTL)).Unix(),
    })
    return token.SignedString(privateKey)
    }
    // validate JWT token
    func ValidateJWT(context *gin.Context) error {
    token, err := getToken(context)
    if err != nil {
    return err
    }
    _, ok := token.Claims.(jwt.MapClaims)
    if ok && token.Valid {
    return nil
    }
    return errors.New("invalid token provided")
    }
    // validate Admin role
    func ValidateAdminRoleJWT(context *gin.Context) error {
    token, err := getToken(context)
    if err != nil {
    return err
    }
    claims, ok := token.Claims.(jwt.MapClaims)
    userRole := uint(claims["role"].(float64))
    if ok && token.Valid && userRole == 1 {
    return nil
    }
    return errors.New("invalid admin token provided")
    }
    // validate Customer role
    func ValidateCustomerRoleJWT(context *gin.Context) error {
    token, err := getToken(context)
    if err != nil {
    return err
    }
    claims, ok := token.Claims.(jwt.MapClaims)
    userRole := uint(claims["role"].(float64))
    if ok && token.Valid && userRole == 2 || userRole == 1 {
    return nil
    }
    return errors.New("invalid author token provided")
    }
    // fetch user details from the token
    func CurrentUser(context *gin.Context) model.User {
    err := ValidateJWT(context)
    if err != nil {
    return model.User{}
    }
    token, _ := getToken(context)
    claims, _ := token.Claims.(jwt.MapClaims)
    userId := uint(claims["id"].(float64))
    user, err := model.GetUserById(userId)
    if err != nil {
    return model.User{}
    }
    return user
    }
    // check token validity
    func getToken(context *gin.Context) (*jwt.Token, error) {
    tokenString := getTokenFromRequest(context)
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
    return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
    }
    return privateKey, nil
    })
    return token, err
    }
    // extract token from request Authorization header
    func getTokenFromRequest(context *gin.Context) string {
    bearerToken := context.Request.Header.Get("Authorization")
    splitToken := strings.Split(bearerToken, " ")
    if len(splitToken) == 2 {
    return splitToken[1]
    }
    return ""
    }
    view raw jwt.go hosted with ❤ by GitHub
  • jwtAuth.go

    package util
    import (
    "net/http"
    "github.com/gin-gonic/gin"
    )
    // check for valid admin token
    func JWTAuth() gin.HandlerFunc {
    return func(context *gin.Context) {
    err := ValidateJWT(context)
    if err != nil {
    context.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
    context.Abort()
    return
    }
    error := ValidateAdminRoleJWT(context)
    if error != nil {
    context.JSON(http.StatusUnauthorized, gin.H{"error": "Only Administrator is allowed to perform this action"})
    context.Abort()
    return
    }
    context.Next()
    }
    }
    // check for valid customer token
    func JWTAuthCustomer() gin.HandlerFunc {
    return func(context *gin.Context) {
    err := ValidateJWT(context)
    if err != nil {
    context.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
    context.Abort()
    return
    }
    error := ValidateCustomerRoleJWT(context)
    if error != nil {
    context.JSON(http.StatusUnauthorized, gin.H{"error": "Only registered Customers are allowed to perform this action"})
    context.Abort()
    return
    }
    context.Next()
    }
    }
    view raw jwtAuth.go hosted with ❤ by GitHub
  • Add token to user login response

// edit user controller and append 
func Login(context *gin.Context) {
     jwt, err := util.GenerateJWT(user)
     if err != nil {
        context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    context.JSON(http.StatusOK, gin.H{"token": jwt, "username": input.Username, "message": "Successfully logged in"})
}
Enter fullscreen mode Exit fullscreen mode
  • Add admin functions routes
// edit main.go
func serveApplication() {
        adminRoutes := router.Group("/admin")
    adminRoutes.Use(util.JWTAuth())
    adminRoutes.GET("/users", controller.GetUsers)
    adminRoutes.GET("/user/:id", controller.GetUser)
    adminRoutes.PUT("/user/:id", controller.UpdateUser)
    adminRoutes.POST("/user/role", controller.CreateRole)
    adminRoutes.GET("/user/roles", controller.GetRoles)
    adminRoutes.PUT("/user/role/:id", controller.UpdateRole)
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Run and Test Admin Funtions

  • Run API
$  go run main.go
Enter fullscreen mode Exit fullscreen mode
  • Admin login
// admin user login
$ curl -X POST http://localhost:8000/auth/user/login \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"username": "test","password":"super^Secret!007"}' 
Enter fullscreen mode Exit fullscreen mode
  • Get All Users
// use admin token from login response
$ curl -X GET http://localhost:8000/admin/users \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8" \
     -d '{"username": "test","password":"super^Secret!007"}'
Enter fullscreen mode Exit fullscreen mode
  • Get User by ID
$ curl -X GET http://localhost:8000/admin/user/1 \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8"
Enter fullscreen mode Exit fullscreen mode
  • Update User
$ curl -X PUT http://localhost:8000/admin/user/2 \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8" \
     -d '{"username": "test","email":"test@gmail.com","role_id":"2"}'
Enter fullscreen mode Exit fullscreen mode
  • Create Role
$ curl -X POST http://localhost:8000/admin/user/role \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8" \
     -d '{"name": "testing","description":"Test user role"}'
Enter fullscreen mode Exit fullscreen mode
  • Get All Roles
$ curl -X GET http://localhost:8000/admin/user/roles \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8"
Enter fullscreen mode Exit fullscreen mode
  • Update Role
$ curl -X PUT http://localhost:8000/admin/user/role/4 \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8" \
     -d '{"name":"accountant","description":"Accountant user role"}'
Enter fullscreen mode Exit fullscreen mode
  • Add Room
$ curl -X POST http://localhost:8000/admin/room/add \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8" \
     -d '{"name": "Room 9","location":"Second Floor"}'
Enter fullscreen mode Exit fullscreen mode
  • List all Rooms
$ curl -X GET http://localhost:8000/api/view/rooms \
     -H "Content-Type: application/json" \
     -H "Accept: application/json"
Enter fullscreen mode Exit fullscreen mode
  • Get Room by ID
$ curl -X GET http://localhost:8000/api/view/room/3 \
     -H "Content-Type: application/json" \
     -H "Accept: application/json"
Enter fullscreen mode Exit fullscreen mode

Step 8: Test the room booking service

  • Book a Room
$ curl -X POST http://localhost:8000/api/room/book \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODUyMjk0MDEsImlhdCI6MTY4NTIyNzYwMSwiaWQiOjI0LCJyb2xlIjoyfQ.h8R51DA5N_xeCa8xR1HLeOo4JTmIGjUp3oMPJLuBv3g" \
     -d '{"room_id": 3}'
Enter fullscreen mode Exit fullscreen mode
  • List all Bookings
$ curl -X GET http://localhost:8000/admin/room/bookings \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODcyOTkyMTAsImlhdCI6MTY4NzI5NzQxMCwiaWQiOjEsInJvbGUiOjF9.3oztz8EgE-l3byKWzCI760FE-BmRY7B-BohnYydDElc" \
     -H "Accept: application/json"
Enter fullscreen mode Exit fullscreen mode
  • List all User Bookings
$ curl -X GET http://localhost:8000/api/rooms/booked \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODUyMjk0MDEsImlhdCI6MTY4NTIyNzYwMSwiaWQiOjI0LCJyb2xlIjoyfQ.h8R51DA5N_xeCa8xR1HLeOo4JTmIGjUp3oMPJLuBv3g" \
     -H "Accept: application/json"
Enter fullscreen mode Exit fullscreen mode

Winding up
RBAC presents the simplest form of access control that can help prevent unauthorised access to data. This facilitates compliance to various regulatory and compliance requirements especially those related to data protection, privacy and system access. With RBAC, it's also easy to maintain an audit trail of all user activities which can significantly help speed up incident response process.

Let's connect
LinkedIn
Twitter
Blog

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (10)

Collapse
 
alfiedang3r profile image
alfie.danger • Edited

how do you generate the JWT token for the .env file?

Collapse
 
bensonmacharia profile image
Benson Macharia

Hello alfie,
The JWT_PRIVATE_KEY="<>" in the .env file should be a random string that is being used as a secret to generate the JWT token. It should be long enough from 12 characters and contain a mixture of special characters and alphanumeric to ensure that the generated JWT token is secure. You can for instance make use of the LastPass random string generator here - lastpass.com/features/password-gen...

Collapse
 
arthasmx profile image
Arthas MX

AFAI-understand, you have to do it manually; think it's like a salted logic or some...hope Benson has time to tell us about it

Collapse
 
abdulrahmandaudmiraj profile image
Abdulrahman Daud Miraj

This is really good article

Collapse
 
bensonmacharia profile image
Benson Macharia

Much appreciation

Collapse
 
pansergiusz profile image
SERHII MALTSEV

what is the correct approach plz if i want to separate the permissions basing on REST API methods. say i want to allow all registered users to do GET but only Admin and resource owner can DELETE and only owner can PUT

Collapse
 
pansergiusz profile image
SERHII MALTSEV

what is the correct approach if i want to separate the permissions basing on REST API methods. say i want to allow all registered users to do GET but only Admin and resource owner can DELETE and only owner can PUT

Collapse
 
diofanto33 profile image
diofanto33

Hello,

Very good contribution !

...Is it possible that when registering a user, you are saving the password without encryption?

Thank you very much!

Collapse
 
klrfl profile image
Efraim Munthe

You can, just insert the password in plain text to the database just like any other data, but as you might have probably knew this is so so very unsafe. I would advise you
against doing that.

Collapse
 
munz profile image
Munz

What should I do if I want to change my password

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more