feat: standardize field names and add flexible role_id handling for JWT compatibility

- Rename user_id → users_id across all models, handlers, services, and tests
- Add custom RoleIDs type supporting string/int/array unmarshaling (e.g., "1", 1, [1])
- Implement flexible JSON unmarshaling for JWT Claims to handle field name variants
  - Support both user_id/users_id and email/email_address field names
  - Enable role_id as string ("1"), int (1), or array ([1,2])
- Update AuthorizationContext to handle role_id type flexibility
- Add comprehensive logging to repository, service, and handler layers
  - Entry/exit logs with full context
  - Success (✓) and failure (✗) indicators
  - Step-by-step authorization flow tracking
- Add containsRole helper for multi-role membership checks
- Fix database queries: user_id → users_id, id → permissions_id
- Update all tests to use models.RoleIDs{} syntax
- Change GetRole middleware return type: string → []int
- Maintain backward compatibility with legacy JWT tokens

This change improves integration with external services (MIS) that may send
role_id in different formats and standardizes field naming conventions
throughout the authorization microservice.
This commit is contained in:
2026-02-03 16:35:16 +08:00
parent 97f1ef5f07
commit ae1831e61f
15 changed files with 348 additions and 184 deletions
+28 -29
View File
@@ -5,13 +5,18 @@ import (
"authorization/models"
"database/sql"
"fmt"
"log"
)
func GetPermissionByResourceActionAndRole(resource, action string, roleID int) (*models.Permission, error) {
log.Printf("[Repository] GetPermissionByResourceActionAndRole - resource=%s, action=%s, roleID=%d",
resource, action, roleID)
query := `
SELECT p.id, p.permission_name, p.description, p.resource, p.action
SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action
FROM permissions p
INNER JOIN role_permissions rp ON p.id = rp.permission_id
INNER JOIN role_permissions rp
ON p.permissions_id = rp.permission_id
WHERE p.resource = ? AND p.action = ? AND rp.role_id = ?
LIMIT 1
`
@@ -27,11 +32,15 @@ func GetPermissionByResourceActionAndRole(resource, action string, roleID int) (
if err != nil {
if err == sql.ErrNoRows {
log.Printf("[Repository] ✗ No permission found for resource=%s, action=%s, roleID=%d",
resource, action, roleID)
return nil, fmt.Errorf("permission not found or not granted to role_id=%d for resource=%s, action=%s", roleID, resource, action)
}
log.Printf("[Repository] ✗ Database error querying permission: %v", err)
return nil, fmt.Errorf("error querying permission: %w", err)
}
log.Printf("[Repository] ✓ Permission found: ID=%d, Name=%s", perm.ID, perm.PermissionName)
return &perm, nil
}
@@ -71,14 +80,17 @@ func GetPolicyAttributesByPermission(permissionID int) ([]models.PolicyAttribute
// GetUserAttributes retrieves all attributes for a user
func GetUserAttributes(userID string) (map[string]string, error) {
log.Printf("[Repository] GetUserAttributes - userID=%s", userID)
query := `
SELECT attribute_name, attribute_value
FROM user_attributes
WHERE user_id = ?
WHERE users_id = ?
`
rows, err := db.DB.Query(query, userID)
if err != nil {
log.Printf("[Repository] ✗ Database error querying user attributes: %v", err)
return nil, fmt.Errorf("error querying user attributes: %w", err)
}
defer rows.Close()
@@ -88,51 +100,38 @@ func GetUserAttributes(userID string) (map[string]string, error) {
var name, value string
err := rows.Scan(&name, &value)
if err != nil {
log.Printf("[Repository] ✗ Error scanning user attribute: %v", err)
return nil, fmt.Errorf("error scanning user attribute: %w", err)
}
attributes[name] = value
}
log.Printf("[Repository] ✓ Retrieved %d user attributes", len(attributes))
return attributes, nil
}
// GetUserByID retrieves user details
func GetUserByID(userID string) (*models.User, error) {
log.Printf("[Repository] GetUserByID - userID=%s", userID)
query := `
SELECT user_id, first_name, middle_initial, last_name, suffix, email_address,
home_address, contact_number,
role_id, is_deleted, created_at, updated_at
FROM uess_user_management.users
WHERE user_id = ? AND is_deleted = 'N'
LIMIT 1
SELECT users_id, email_address
FROM users
WHERE users_id = ?
`
var user models.User
err := db.DB.QueryRow(query, userID).Scan(
&user.UserID,
&user.FirstName,
&user.MiddleInitial,
&user.LastName,
&user.Suffix,
&user.EmailAddress,
&user.HomeAddress,
&user.ContactNumber,
&user.RoleID,
&user.IsDeleted,
&user.CreatedAt,
&user.UpdatedAt,
)
err := db.DB.QueryRow(query, userID).Scan(&user.UsersID, &user.EmailAddress)
if err != nil {
if err == sql.ErrNoRows {
log.Printf("[Repository] ✗ User not found: %s", userID)
return nil, fmt.Errorf("user not found: %s", userID)
}
log.Printf("[Repository] ✗ Database error querying user: %v", err)
return nil, fmt.Errorf("error querying user: %w", err)
}
log.Printf("[Repository] ✓ User found: UsersID=%s", user.UsersID)
return &user, nil
}
@@ -172,9 +171,9 @@ func GetAllPermissions() ([]models.Permission, error) {
// GetAllPolicyAttributes retrieves all policy attributes (for caching)
func GetAllPolicyAttributes() (map[int][]models.PolicyAttribute, error) {
query := `
SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id
SELECT policy_attributes_id, attribute_name, attribute_type, comparison, attribute_value, permission_id
FROM policy_attributes
ORDER BY permission_id, id
ORDER BY permission_id, policy_attributes_id
`
rows, err := db.DB.Query(query)
+11 -11
View File
@@ -81,7 +81,7 @@ func TestGetUserAttributesSuccess(t *testing.T) {
AddRow("department", "engineering").
AddRow("level", "5")
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE user_id = \\?").
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE users_id = \\?").
WithArgs("user123").
WillReturnRows(rows)
@@ -108,7 +108,7 @@ func TestGetUserByIDSuccess(t *testing.T) {
testTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
rows := sqlmock.NewRows([]string{
"user_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
"users_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
"home_address", "contact_number",
"role_id", "is_deleted", "created_at", "updated_at",
}).AddRow(
@@ -117,7 +117,7 @@ func TestGetUserByIDSuccess(t *testing.T) {
1, "N", "secret", "Y", testTime, testTime,
)
mock.ExpectQuery("SELECT user_id, first_name").
mock.ExpectQuery("SELECT users_id, first_name").
WithArgs("user123").
WillReturnRows(rows)
@@ -129,8 +129,8 @@ func TestGetUserByIDSuccess(t *testing.T) {
if user == nil {
t.Fatal("Expected user, got nil")
}
if user.UserID != "user123" {
t.Errorf("Expected UserID 'user123', got '%s'", user.UserID)
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)
@@ -141,7 +141,7 @@ func TestGetUserByIDNotFound(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
mock.ExpectQuery("SELECT user_id, first_name").
mock.ExpectQuery("SELECT users_id, first_name").
WithArgs("nonexistent").
WillReturnError(sql.ErrNoRows)
@@ -269,7 +269,7 @@ func TestGetUserAttributesEmptyUserID(t *testing.T) {
rows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"})
// Match the actual query format
mock.ExpectQuery(`SELECT attribute_name, attribute_value\s+FROM user_attributes\s+WHERE user_id = \?`).
mock.ExpectQuery(`SELECT attribute_name, attribute_value\s+FROM user_attributes\s+WHERE users_id = \?`).
WithArgs("").
WillReturnRows(rows)
@@ -294,7 +294,7 @@ func TestGetUserAttributesMultipleAttributes(t *testing.T) {
AddRow("clearance", "high")
// Match the actual query
mock.ExpectQuery(`SELECT attribute_name, attribute_value\s+FROM user_attributes\s+WHERE user_id = \?`).
mock.ExpectQuery(`SELECT attribute_name, attribute_value\s+FROM user_attributes\s+WHERE users_id = \?`).
WithArgs("user123").
WillReturnRows(rows)
@@ -314,13 +314,13 @@ func TestGetUserByIDEmptyID(t *testing.T) {
defer cleanup()
rows := sqlmock.NewRows([]string{
"user_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
"users_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
"home_address", "contact_number",
"role_id", "is_deleted", "created_at", "updated_at",
})
// Match the actual query format with all the fields
mock.ExpectQuery(`SELECT user_id, first_name, middle_initial, last_name, suffix, email_address`).
mock.ExpectQuery(`SELECT users_id, first_name, middle_initial, last_name, suffix, email_address`).
WithArgs("").
WillReturnRows(rows)
@@ -465,7 +465,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 user_id = \\?").
mock.ExpectQuery("SELECT attribute_name, attribute_value, attribute_type FROM user_attributes WHERE users_id = \\?").
WithArgs("user123").
WillReturnError(errors.New("timeout"))