Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
11 changes: 7 additions & 4 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,18 @@ func validateAuthorization(c *fiber.Ctx, input interface{}, authService Authoriz
return &AuthError{StatusCode: 401, Message: lastErr.Error()}
}

// checkRequiredRoles checks that the authenticated user has all required roles.
// checkRequiredRoles checks that the authenticated user has at least one of the required roles (OR semantics).
// Called inside validateAuthorization after auth context is established, before resource access checks.
func checkRequiredRoles(authCtx *AuthContext, authService AuthorizationService, requiredRoles []string) error {
if len(requiredRoles) == 0 {
return nil
}
for _, role := range requiredRoles {
if !authService.HasRole(authCtx, role) {
return &AuthError{StatusCode: 403, Message: fmt.Sprintf("required role missing: %s", role)}
if authService.HasRole(authCtx, role) {
return nil
}
}
return nil
return &AuthError{StatusCode: 403, Message: fmt.Sprintf("none of the required roles found: %v", requiredRoles)}
}

// validateResourceAccess validates resource access based on tags
Expand Down
14 changes: 7 additions & 7 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,10 +593,10 @@ func TestRequiredRoles(t *testing.T) {
return fiber.Map{"ok": true}, nil
}, WithRoles(OpenAPIOptions{Summary: "User profile"}, "user"))

// Route requiring multiple roles (AND semantics): "admin" AND "user"
// Route requiring multiple roles (OR semantics): "admin" OR "editor"
Get(oapi, "/admin/settings", func(c *fiber.Ctx, input struct{}) (fiber.Map, *ErrorResponse) {
return fiber.Map{"ok": true}, nil
}, WithRoles(OpenAPIOptions{Summary: "Admin settings"}, "admin", "user"))
}, WithRoles(OpenAPIOptions{Summary: "Admin settings"}, "admin", "editor"))

// Route with no required roles
Get(oapi, "/public/info", func(c *fiber.Ctx, input struct{}) (fiber.Map, *ErrorResponse) {
Expand Down Expand Up @@ -670,9 +670,9 @@ func TestRequiredRoles(t *testing.T) {
}
})

// Multi-role AND semantics tests
// admin-token has roles ["admin", "user"] -> should pass
t.Run("multi-role: token with all roles accepted", func(t *testing.T) {
// Multi-role OR semantics tests
// admin-token has roles ["admin", "user"] -> has "admin", should pass
t.Run("multi-role: token with one matching role accepted", func(t *testing.T) {
req := httptest.NewRequest("GET", "/admin/settings", nil)
req.Header.Set("Authorization", "Bearer admin-token")
resp, _ := app.Test(req)
Expand All @@ -681,8 +681,8 @@ func TestRequiredRoles(t *testing.T) {
}
})

// valid-token has roles ["user"] -> missing "admin", should be rejected
t.Run("multi-role: token missing one role rejected", func(t *testing.T) {
// valid-token has roles ["user"] -> has neither "admin" nor "editor", should be rejected
t.Run("multi-role: token with no matching role rejected", func(t *testing.T) {
req := httptest.NewRequest("GET", "/admin/settings", nil)
req.Header.Set("Authorization", "Bearer valid-token")
resp, _ := app.Test(req)
Expand Down
Loading