Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9170451
Implemented RBAC middleware support with example
goginenibhavani2000 Jul 17, 2025
a123a03
Add route specific override and fix userRole read from context
goginenibhavani2000 Aug 10, 2025
e59fd18
remove unneccessary changes
goginenibhavani2000 Aug 10, 2025
993dd2e
Merge branch 'development' into RBAC-middleware-support
goginenibhavani2000 Aug 10, 2025
23073f7
Added unit tests to rbac package
goginenibhavani2000 Aug 11, 2025
0a0334f
fix linters
goginenibhavani2000 Aug 11, 2025
2cb75bf
Merge branch 'development' into RBAC-middleware-support
goginenibhavani2000 Aug 13, 2025
21b6309
add go.od to separate out RBAC module
goginenibhavani2000 Aug 13, 2025
c57a2e1
Merge branch 'development' into RBAC-middleware-support
Umang01-hash Aug 20, 2025
0f9226d
Merge branch 'development' into RBAC-middleware-support
Sep 1, 2025
03b7ab9
defining roles at route level and using assert.equal in test files
Sep 2, 2025
4c42ab9
Merge branch 'development' into RBAC-middleware-support
Umang01-hash Oct 6, 2025
ab56bcb
extending the capabilities to db and jwt
coolwednesday Nov 20, 2025
6412e61
Merge branch 'development' into RBAC-middleware-support
coolwednesday Nov 20, 2025
4412798
refactored docs and corrected tests
coolwednesday Nov 21, 2025
648022e
removed unrelated changes
coolwednesday Nov 21, 2025
fa4950c
Merge branch 'development' into RBAC-middleware-support
coolwednesday Nov 21, 2025
7949f53
fixed linters
coolwednesday Nov 23, 2025
b5c5f90
Merge branch 'development' into RBAC-middleware-support
coolwednesday Nov 23, 2025
b1d82ae
fixed linters
coolwednesday Nov 23, 2025
b222b3d
Merge branch 'development' into RBAC-middleware-support
coolwednesday Nov 24, 2025
c34436e
refactored tests to suit CI env
coolwednesday Nov 24, 2025
68b1a9b
Merge branch 'development' into RBAC-middleware-support
coolwednesday Nov 26, 2025
f111d32
refactored according to review comments
coolwednesday Nov 27, 2025
a7d40d2
fixed go mod
coolwednesday Nov 27, 2025
ca5cbd6
resolved merge conflicts
coolwednesday Nov 27, 2025
6827154
fixed go.mod changes
coolwednesday Nov 27, 2025
1445919
fixing commit versions
coolwednesday Nov 27, 2025
0348ca4
Merge branch 'development' into RBAC-middleware-support
coolwednesday Nov 27, 2025
913ecce
adding modules in go work
coolwednesday Nov 27, 2025
9241c6d
Merge remote-tracking branch 'bhavani/RBAC-middleware-support' into R…
coolwednesday Nov 27, 2025
197b230
fixed workspace inconsistencies
coolwednesday Nov 27, 2025
a1c84f6
final fix hopefully
coolwednesday Nov 27, 2025
b33cfff
fix go.mod
coolwednesday Nov 27, 2025
8dcc67b
resolved review comments
coolwednesday Nov 27, 2025
1ecb0a4
resolving review comments and fixing linters
coolwednesday Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions examples/rbac/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"roles": {
"admin": ["*"],
"editor": ["/posts/*", "/dashboard"],
"user": ["/profile", "/home", "/sayhello*","/greet"]
}
}
39 changes: 39 additions & 0 deletions examples/rbac/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"net/http"

"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/rbac"
)

func main() {
app := gofr.New()

// json config path file is required
rbacConfigs, err := rbac.LoadPermissions("config.json")
if err != nil {
return
}

overrides := map[string]bool{"user1": true}

rbacConfigs.OverRides = overrides

rbacConfigs.RoleExtractorFunc = extractor

app.UseMiddleware(rbac.Middleware(rbacConfigs))

app.GET("/sayhello/123", handler)
app.GET("/greet", rbac.RequireRole("user1", handler))

app.Run() // listens and serves on localhost:8000
}

func extractor(req *http.Request, _ ...any) (string, error) {
return req.Header.Get("X-USER-ROLE"), nil
}

func handler(ctx *gofr.Context) (any, error) {
return "Hello World!", nil
}
28 changes: 28 additions & 0 deletions pkg/gofr/rbac/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package rbac

import (
"encoding/json"
"net/http"
"os"
)

type Config struct {
RoleWithPermissions map[string][]string `json:"roles"` // Role: [Allowed routes]
RoleExtractorFunc func(req *http.Request, args ...any) (string, error)
OverRides map[string]bool // role: [override bool]
}

func LoadPermissions(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}

var config Config

if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}

return &config, nil
}
79 changes: 79 additions & 0 deletions pkg/gofr/rbac/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package rbac

import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"testing"
)

// Helper function to create a temporary JSON file for testing.
func createTempJSONFile(t *testing.T, data any) string {
t.Helper()
dir := t.TempDir()
file := filepath.Join(dir, "test.json")

jsonBytes, err := json.Marshal(data)
if err != nil {
t.Fatalf("error marshaling: %v", err)
}

if err := os.WriteFile(file, jsonBytes, 0600); err != nil {
t.Fatalf("error writing file: %v", err)
}

return file
}

func TestLoadPermissions_Success(t *testing.T) {
expected := Config{
RoleWithPermissions: map[string][]string{
"admin": {"/admin", "/dashboard"},
"viewer": {"/dashboard"},
},
}
file := createTempJSONFile(t, struct {
RoleWithPermissions map[string][]string `json:"roles"`
}{
RoleWithPermissions: expected.RoleWithPermissions,
})

got, err := LoadPermissions(file)

if err != nil {
t.Fatalf("LoadPermissions returned error: %v", err)
}

if !reflect.DeepEqual(got.RoleWithPermissions, expected.RoleWithPermissions) {
t.Errorf("RoleWithPermissions mismatch: got %v, want %v", got.RoleWithPermissions, expected.RoleWithPermissions)
}

if !reflect.DeepEqual(got.OverRides, expected.OverRides) {
t.Errorf("OverRides mismatch: got %v, want %v", got.OverRides, expected.OverRides)
}
}

func TestLoadPermissions_FileNotFound(t *testing.T) {
// Act
_, err := LoadPermissions("nonexistentpath.json")
// Assert
if err == nil {
t.Fatalf("expected error for missing file, got nil")
}
}

func TestLoadPermissions_InvalidJSON(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "bad.json")

// Write invalid JSON
if err := os.WriteFile(file, []byte("{invalid json"), 0600); err != nil {
t.Fatalf("could not write test file: %v", err)
}

_, err := LoadPermissions(file)
if err == nil {
t.Fatalf("expected JSON unmarshal error, got nil")
}
}
17 changes: 17 additions & 0 deletions pkg/gofr/rbac/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package rbac

import "gofr.dev/pkg/gofr"

func HasRole(ctx *gofr.Context, role string) bool {
expRole, _ := ctx.Context.Value(userRole).(string)
return expRole == role
}

func IsAdmin(ctx *gofr.Context) bool {
return HasRole(ctx, "admin")
}

func GetUserRole(ctx *gofr.Context) string {
role, _ := ctx.Context.Value(userRole).(string)
return role
}
67 changes: 67 additions & 0 deletions pkg/gofr/rbac/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package rbac

import (
"context"
"testing"

"gofr.dev/pkg/gofr"
)

func TestHasRole(t *testing.T) {
tests := []struct {
name string
ctxRoleVal string
checkRole string
expectedRes bool
}{
{"matching role", "admin", "admin", true},
{"non-matching role", "viewer", "admin", false},
{"empty role in context", "", "admin", false},
{"nil role in context", "", "", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create base context with the userRole value
baseCtx := context.WithValue(t.Context(), userRole, tt.ctxRoleVal)

// Wrap baseCtx in gofr.Context
gofrCtx := &gofr.Context{Context: baseCtx}

got := HasRole(gofrCtx, tt.checkRole)
if got != tt.expectedRes {
t.Errorf("HasRole() = %v, want %v", got, tt.expectedRes)
}
})
}
}

func TestIsAdmin(t *testing.T) {
baseCtx := context.WithValue(t.Context(), userRole, "admin")
gofrCtx := &gofr.Context{Context: baseCtx}

if !IsAdmin(gofrCtx) {
t.Errorf("IsAdmin() = false, want true")
}

nonAdminCtx := &gofr.Context{Context: context.WithValue(t.Context(), userRole, "viewer")}
if IsAdmin(nonAdminCtx) {
t.Errorf("IsAdmin() = true, want false")
}
}

func TestGetUserRole(t *testing.T) {
expectedRole := "editor"
baseCtx := context.WithValue(t.Context(), userRole, expectedRole)
gofrCtx := &gofr.Context{Context: baseCtx}

if role := GetUserRole(gofrCtx); role != expectedRole {
t.Errorf("GetUserRole() = %v, want %v", role, expectedRole)
}

// Test no role set should return ""
emptyCtx := &gofr.Context{Context: t.Context()}
if role := GetUserRole(emptyCtx); role != "" {
t.Errorf("GetUserRole() with no role = %v, want empty string", role)
}
}
45 changes: 45 additions & 0 deletions pkg/gofr/rbac/match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package rbac

import (
"path"
"strings"
)

func isPathAllowed(role, route string, config *Config) bool {
allowedPaths := config.RoleWithPermissions[role]

for _, pattern := range allowedPaths {
// Allow simple wildcard "*"
if pattern == "*" {
return true
}

// Ensure pattern ends with * if it's a prefix match
if pattern == route {
return true
}
// Normalize pattern and path to avoid trailing slash issues
normalizedPattern := strings.TrimSuffix(pattern, "/")
normalizedPath := strings.TrimSuffix(route, "/")

// Allow matching wildcard like /admin/* or /users/*
if ok, _ := path.Match(normalizedPattern, normalizedPath); ok {
return true
}

// Support prefix match with * (e.g. /users/* should match /users/123)
if strings.HasSuffix(pattern, "*") {
prefix := strings.TrimSuffix(pattern, "*")
if strings.HasPrefix(route, prefix) {
return true
}
}
}

// override with role match
if config.OverRides[role] {
return true
}

return false
}
Loading