feat(authz): support multi-role claim evaluation and role-aware permission checks

Parse and normalize user and project role claims (role_id + projects[].role_id)
Intersect requested roles with JWT-available roles before authorization
Evaluate permissions across candidate roles in both cached and non-cached flows
Fix claim field fallbacks (user_id/email) and role ID log formatting
Update tests and SQL mock expectations for new role-resolution behavior
This commit is contained in:
2026-02-27 08:39:33 +08:00
parent ae1831e61f
commit 6262c875b7
11 changed files with 293 additions and 127 deletions
+17 -32
View File
@@ -5,7 +5,6 @@ import (
"database/sql"
"errors"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
)
@@ -105,19 +104,10 @@ func TestGetUserByIDSuccess(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
testTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
rows := sqlmock.NewRows([]string{"users_id", "email_address"}).
AddRow("user123", "john@example.com")
rows := sqlmock.NewRows([]string{
"users_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
"home_address", "contact_number",
"role_id", "is_deleted", "created_at", "updated_at",
}).AddRow(
"user123", "John", "M", "Doe", "Jr", "john@example.com",
"EMP001", "Y", "Y", "123 Main St", "1234567890", "device001",
1, "N", "secret", "Y", testTime, testTime,
)
mock.ExpectQuery("SELECT users_id, first_name").
mock.ExpectQuery("SELECT users_id, email_address").
WithArgs("user123").
WillReturnRows(rows)
@@ -132,8 +122,8 @@ func TestGetUserByIDSuccess(t *testing.T) {
if user.UsersID != "user123" {
t.Errorf("Expected UsersID 'user123', got '%s'", user.UsersID)
}
if user.FirstName != "John" {
t.Errorf("Expected FirstName 'John', got '%s'", user.FirstName)
if user.EmailAddress != "john@example.com" {
t.Errorf("Expected EmailAddress 'john@example.com', got '%s'", user.EmailAddress)
}
}
@@ -141,7 +131,7 @@ func TestGetUserByIDNotFound(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
mock.ExpectQuery("SELECT users_id, first_name").
mock.ExpectQuery("SELECT users_id, email_address").
WithArgs("nonexistent").
WillReturnError(sql.ErrNoRows)
@@ -180,12 +170,12 @@ func TestGetAllPolicyAttributesSuccess(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
rows := sqlmock.NewRows([]string{"id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"}).
rows := sqlmock.NewRows([]string{"policy_attributes_id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"}).
AddRow(1, "department", "user", "=", "engineering", 1).
AddRow(2, "level", "user", ">=", "5", 1).
AddRow(3, "role", "user", "=", "admin", 2)
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, id").
mock.ExpectQuery("SELECT policy_attributes_id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, policy_attributes_id").
WillReturnRows(rows)
attrs, err := GetAllPolicyAttributes()
@@ -208,9 +198,9 @@ func TestGetAllPolicyAttributesEmpty(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
rows := sqlmock.NewRows([]string{"id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"})
rows := sqlmock.NewRows([]string{"policy_attributes_id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"})
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, id").
mock.ExpectQuery("SELECT policy_attributes_id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, policy_attributes_id").
WillReturnRows(rows)
attrs, err := GetAllPolicyAttributes()
@@ -313,14 +303,9 @@ func TestGetUserByIDEmptyID(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
rows := sqlmock.NewRows([]string{
"users_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
"home_address", "contact_number",
"role_id", "is_deleted", "created_at", "updated_at",
})
rows := sqlmock.NewRows([]string{"users_id", "email_address"})
// Match the actual query format with all the fields
mock.ExpectQuery(`SELECT users_id, first_name, middle_initial, last_name, suffix, email_address`).
mock.ExpectQuery(`SELECT users_id, email_address`).
WithArgs("").
WillReturnRows(rows)
@@ -339,7 +324,7 @@ func TestGetUserByIDDatabaseError(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
mock.ExpectQuery("SELECT id, username, role, email, created_at, updated_at FROM users WHERE id = \\?").
mock.ExpectQuery("SELECT users_id, email_address").
WithArgs("user123").
WillReturnError(errors.New("database connection failed"))
@@ -415,7 +400,7 @@ func TestGetAllPolicyAttributesDatabaseError(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, id").
mock.ExpectQuery("SELECT policy_attributes_id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, policy_attributes_id").
WillReturnError(errors.New("connection lost"))
attrs, err := GetAllPolicyAttributes()
@@ -432,7 +417,7 @@ func TestGetAllPolicyAttributesManyPermissions(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
rows := sqlmock.NewRows([]string{"id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"})
rows := sqlmock.NewRows([]string{"policy_attributes_id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"})
// Add attributes for multiple permissions
for permID := 1; permID <= 50; permID++ {
@@ -441,7 +426,7 @@ func TestGetAllPolicyAttributesManyPermissions(t *testing.T) {
}
}
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, id").
mock.ExpectQuery("SELECT policy_attributes_id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, policy_attributes_id").
WillReturnRows(rows)
attrs, err := GetAllPolicyAttributes()
@@ -465,7 +450,7 @@ func TestGetUserAttributesDatabaseError(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
mock.ExpectQuery("SELECT attribute_name, attribute_value, attribute_type FROM user_attributes WHERE users_id = \\?").
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE users_id = \\?").
WithArgs("user123").
WillReturnError(errors.New("timeout"))