-
Notifications
You must be signed in to change notification settings - Fork 1.7k
RBAC middleware support #2144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
goginenibhavani2000
wants to merge
36
commits into
gofr-dev:development
Choose a base branch
from
goginenibhavani2000:RBAC-middleware-support
base: development
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
RBAC middleware support #2144
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 a123a03
Add route specific override and fix userRole read from context
goginenibhavani2000 e59fd18
remove unneccessary changes
goginenibhavani2000 993dd2e
Merge branch 'development' into RBAC-middleware-support
goginenibhavani2000 23073f7
Added unit tests to rbac package
goginenibhavani2000 0a0334f
fix linters
goginenibhavani2000 2cb75bf
Merge branch 'development' into RBAC-middleware-support
goginenibhavani2000 21b6309
add go.od to separate out RBAC module
goginenibhavani2000 c57a2e1
Merge branch 'development' into RBAC-middleware-support
Umang01-hash 0f9226d
Merge branch 'development' into RBAC-middleware-support
03b7ab9
defining roles at route level and using assert.equal in test files
4c42ab9
Merge branch 'development' into RBAC-middleware-support
Umang01-hash ab56bcb
extending the capabilities to db and jwt
coolwednesday 6412e61
Merge branch 'development' into RBAC-middleware-support
coolwednesday 4412798
refactored docs and corrected tests
coolwednesday 648022e
removed unrelated changes
coolwednesday fa4950c
Merge branch 'development' into RBAC-middleware-support
coolwednesday 7949f53
fixed linters
coolwednesday b5c5f90
Merge branch 'development' into RBAC-middleware-support
coolwednesday b1d82ae
fixed linters
coolwednesday b222b3d
Merge branch 'development' into RBAC-middleware-support
coolwednesday c34436e
refactored tests to suit CI env
coolwednesday 68b1a9b
Merge branch 'development' into RBAC-middleware-support
coolwednesday f111d32
refactored according to review comments
coolwednesday a7d40d2
fixed go mod
coolwednesday ca5cbd6
resolved merge conflicts
coolwednesday 6827154
fixed go.mod changes
coolwednesday 1445919
fixing commit versions
coolwednesday 0348ca4
Merge branch 'development' into RBAC-middleware-support
coolwednesday 913ecce
adding modules in go work
coolwednesday 9241c6d
Merge remote-tracking branch 'bhavani/RBAC-middleware-support' into R…
coolwednesday 197b230
fixed workspace inconsistencies
coolwednesday a1c84f6
final fix hopefully
coolwednesday b33cfff
fix go.mod
coolwednesday 8dcc67b
resolved review comments
coolwednesday 1ecb0a4
resolving review comments and fixing linters
coolwednesday File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "roles": { | ||
| "admin": ["*"], | ||
| "editor": ["/posts/*", "/dashboard"], | ||
| "user": ["/profile", "/home", "/sayhello*","/greet"] | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) | ||
coolwednesday marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| app.Run() // listens and serves on localhost:8000 | ||
| } | ||
|
|
||
| func extractor(req *http.Request, _ ...any) (string, error) { | ||
| return req.Header.Get("X-USER-ROLE"), nil | ||
coolwednesday marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| func handler(ctx *gofr.Context) (any, error) { | ||
| return "Hello World!", nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
coolwednesday marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return HasRole(ctx, "admin") | ||
| } | ||
|
|
||
| func GetUserRole(ctx *gofr.Context) string { | ||
coolwednesday marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| role, _ := ctx.Context.Value(userRole).(string) | ||
| return role | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
coolwednesday marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
coolwednesday marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.