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:
+37
-10
@@ -6,6 +6,7 @@ import (
|
||||
"authorization/models"
|
||||
"authorization/services"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
@@ -38,24 +39,39 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("JWT Claims: UsersID='%s', EmailAddress='%s', RoleID=%v", claims.UsersID, claims.EmailAddress, claims.RoleID)
|
||||
|
||||
var ctx models.AuthorizationContext
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&ctx)
|
||||
// Read and log raw request body
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
helper.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
log.Printf("Raw authorization request body: %s", string(bodyBytes))
|
||||
|
||||
// Decode JSON into AuthorizationContext
|
||||
if err := json.Unmarshal(bodyBytes, &ctx); err != nil {
|
||||
log.Printf("ERROR: Failed to unmarshal request body: %v", err)
|
||||
helper.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if ctx.UserID == "" || ctx.Resource == "" || ctx.Action == "" {
|
||||
helper.RespondWithError(w, http.StatusBadRequest, "Missing required fields: user_id, resource, action")
|
||||
log.Printf("Decoded authorization context: %+v", ctx)
|
||||
log.Printf("User ID ctx=%s, resource=%s, action=%s, roleID=%d", ctx.UsersID, ctx.Resource, ctx.Action, ctx.RoleID)
|
||||
if ctx.UsersID == "" || ctx.Resource == "" || ctx.Action == "" {
|
||||
log.Printf("ERROR: Missing required fields - UsersID=%s, Resource=%s, Action=%s", ctx.UsersID, ctx.Resource, ctx.Action)
|
||||
helper.RespondWithError(w, http.StatusBadRequest, "Missing required fields: users_id, resource, action")
|
||||
return
|
||||
}
|
||||
|
||||
log.Print("Authorization request for user=", ctx.UserID, ", resource=", ctx.Resource, ", action=", ctx.Action)
|
||||
log.Print("JWT claims user=", claims.UserID, ", role=", claims.RoleID)
|
||||
log.Print("Authorization request for user=", ctx.UsersID, ", resource=", ctx.Resource, ", action=", ctx.Action)
|
||||
log.Print("JWT claims user=", claims.UsersID, ", role=", claims.RoleID)
|
||||
// Verify JWT user matches request user (security check)
|
||||
if ctx.UserID != claims.UserID {
|
||||
if ctx.UsersID != claims.UsersID {
|
||||
log.Printf("ERROR: User ID mismatch - ctx.UsersID='%s' vs claims.UsersID='%s'", ctx.UsersID, claims.UsersID)
|
||||
helper.RespondWithError(w, http.StatusForbidden, "User ID mismatch")
|
||||
return
|
||||
}
|
||||
@@ -68,17 +84,19 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx.Environment = make(map[string]string)
|
||||
}
|
||||
|
||||
if ctx.RoleID != claims.RoleID {
|
||||
// containsRole checks if a role exists in a slice of roles
|
||||
|
||||
if !containsRole([]int(claims.RoleID), ctx.RoleID) {
|
||||
helper.RespondWithError(w, http.StatusForbidden, "Role ID mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
log.Print("User role verified: ", ctx.RoleID)
|
||||
// Perform authorization
|
||||
log.Printf("[Handler] Performing authorization check for user=%s, resource=%s, action=%s", ctx.UserID, ctx.Resource, ctx.Action)
|
||||
log.Printf("[Handler] Performing authorization check for user=%s, resource=%s, action=%s", ctx.UsersID, ctx.Resource, ctx.Action)
|
||||
result, err := services.AuthorizeWithCache(authService, &ctx)
|
||||
if err != nil {
|
||||
helper.LogError(err, "Authorization service error")
|
||||
log.Printf("✗ Authorization service error for user=%s: %v", ctx.UserID, err)
|
||||
log.Printf("✗ Authorization service error for user=%s: %v", ctx.UsersID, err)
|
||||
helper.RespondWithError(w, http.StatusInternalServerError, "Authorization check failed")
|
||||
return
|
||||
}
|
||||
@@ -102,3 +120,12 @@ func AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
helper.RespondWithJSON(w, http.StatusForbidden, response)
|
||||
}
|
||||
}
|
||||
|
||||
func containsRole(roles []int, role int) bool {
|
||||
for _, r := range roles {
|
||||
if r == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
+29
-29
@@ -44,8 +44,8 @@ func TestAuthorizeHandlerNoJWTClaims(t *testing.T) {
|
||||
func TestAuthorizeHandlerInvalidJSON(t *testing.T) {
|
||||
// Setup - no need to init service, we're testing JSON parsing before auth
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", AuthCheckEndpoint, bytes.NewBufferString("invalid json"))
|
||||
@@ -73,19 +73,19 @@ func TestAuthorizeHandlerMissingRequiredFields(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Missing Resource",
|
||||
payload: models.AuthorizationContext{UserID: "user123", Action: "read"},
|
||||
payload: models.AuthorizationContext{UsersID: "user123", Action: "read"},
|
||||
},
|
||||
{
|
||||
name: "Missing Action",
|
||||
payload: models.AuthorizationContext{UserID: "user123", Resource: "document"},
|
||||
payload: models.AuthorizationContext{UsersID: "user123", Resource: "document"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(tc.payload)
|
||||
@@ -106,12 +106,12 @@ func TestAuthorizeHandlerMissingRequiredFields(t *testing.T) {
|
||||
func TestAuthorizeHandlerUserIDMismatch(t *testing.T) {
|
||||
// Setup
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "differentUser",
|
||||
UsersID: "differentUser",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
}
|
||||
@@ -134,12 +134,12 @@ func TestAuthorizeHandlerUserIDMismatch(t *testing.T) {
|
||||
func TestAuthorizeHandlerNilMaps(t *testing.T) {
|
||||
// Test that nil maps don't cause additional panics beyond missing authService
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
ResourceData: nil, // nil map
|
||||
@@ -171,12 +171,12 @@ func TestAuthorizeHandlerNilMaps(t *testing.T) {
|
||||
|
||||
func TestAuthorizeHandlerEmptyUserID(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "",
|
||||
UsersID: "",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
}
|
||||
@@ -196,12 +196,12 @@ func TestAuthorizeHandlerEmptyUserID(t *testing.T) {
|
||||
|
||||
func TestAuthorizeHandlerEmptyResource(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "",
|
||||
Action: "read",
|
||||
}
|
||||
@@ -221,12 +221,12 @@ func TestAuthorizeHandlerEmptyResource(t *testing.T) {
|
||||
|
||||
func TestAuthorizeHandlerEmptyAction(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "document",
|
||||
Action: "",
|
||||
}
|
||||
@@ -261,8 +261,8 @@ func TestAuthorizeHandlerInvalidClaimsType(t *testing.T) {
|
||||
|
||||
func TestAuthorizeHandlerMalformedJSON(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
@@ -307,7 +307,7 @@ func TestAuthorizeHandlerSpecialCharactersInFields(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: tc.userID,
|
||||
UsersID: tc.userID,
|
||||
Resource: tc.resource,
|
||||
Action: tc.action,
|
||||
}
|
||||
@@ -317,8 +317,8 @@ func TestAuthorizeHandlerSpecialCharactersInFields(t *testing.T) {
|
||||
|
||||
// Update claims to match userID
|
||||
testClaims := &models.Claims{
|
||||
UserID: tc.userID,
|
||||
RoleID: "admin",
|
||||
UsersID: tc.userID,
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), models.ContextKey("claims"), testClaims)
|
||||
req = req.WithContext(ctx)
|
||||
@@ -343,12 +343,12 @@ func TestAuthorizeHandlerSpecialCharactersInFields(t *testing.T) {
|
||||
func TestAuthorizeHandlerWithResourceData(t *testing.T) {
|
||||
// Test that ResourceData is properly passed through to authorization
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{1},
|
||||
}
|
||||
|
||||
payload := models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "personnel",
|
||||
Action: "assign_role",
|
||||
ResourceData: map[string]string{
|
||||
|
||||
+8
-7
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
const (
|
||||
claimsKey models.ContextKey = "claims"
|
||||
userIDKey models.ContextKey = "user_id"
|
||||
userIDKey models.ContextKey = "users_id"
|
||||
roleIDKey models.ContextKey = "role_id"
|
||||
)
|
||||
|
||||
@@ -168,7 +168,7 @@ func parseAndValidateToken(tokenString string) (*models.Claims, error) {
|
||||
return nil, fmt.Errorf("invalid claims")
|
||||
}
|
||||
|
||||
log.Printf("Token verified successfully for user: (UserID: %s)", claims.UserID)
|
||||
log.Printf("Token verified successfully for user: (UserID: %s)", claims.UsersID)
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
@@ -239,8 +239,9 @@ func JWTAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
// buildContext efficiently builds context with claims (reduces allocations)
|
||||
func buildContext(parent context.Context, claims *models.Claims) context.Context {
|
||||
ctx := context.WithValue(parent, claimsKey, claims)
|
||||
ctx = context.WithValue(ctx, userIDKey, claims.UserID)
|
||||
ctx = context.WithValue(ctx, roleIDKey, claims.RoleID)
|
||||
ctx = context.WithValue(ctx, userIDKey, claims.UsersID)
|
||||
// Store plain []int in context for roles to keep middleware interfaces simple
|
||||
ctx = context.WithValue(ctx, roleIDKey, []int(claims.RoleID))
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -256,8 +257,8 @@ func GetUserID(r *http.Request) (string, bool) {
|
||||
return userID, ok
|
||||
}
|
||||
|
||||
// GetRole retrieves the role from the request context
|
||||
func GetRole(r *http.Request) (string, bool) {
|
||||
role, ok := r.Context().Value(roleIDKey).(string)
|
||||
// GetRole retrieves the roles from the request context
|
||||
func GetRole(r *http.Request) ([]int, bool) {
|
||||
role, ok := r.Context().Value(roleIDKey).([]int)
|
||||
return role, ok
|
||||
}
|
||||
|
||||
+27
-25
@@ -167,15 +167,15 @@ func TestParseAndValidateToken(t *testing.T) {
|
||||
|
||||
func TestBuildContext(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{3},
|
||||
}
|
||||
|
||||
parent := context.Background()
|
||||
ctx := buildContext(parent, claims)
|
||||
|
||||
// Check claims
|
||||
if val, ok := ctx.Value(claimsKey).(*models.Claims); !ok || val.UserID != "user123" {
|
||||
if val, ok := ctx.Value(claimsKey).(*models.Claims); !ok || val.UsersID != "user123" {
|
||||
t.Error("Claims not properly set in context")
|
||||
}
|
||||
|
||||
@@ -185,15 +185,15 @@ func TestBuildContext(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check role
|
||||
if val, ok := ctx.Value(roleIDKey).(string); !ok || val != "admin" {
|
||||
if val, ok := ctx.Value(roleIDKey).([]int); !ok || len(val) == 0 || val[0] != 3 {
|
||||
t.Error("Role not properly set in context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClaims(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{3},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
@@ -204,8 +204,8 @@ func TestGetClaims(t *testing.T) {
|
||||
if !ok {
|
||||
t.Error("Expected claims to be found")
|
||||
}
|
||||
if retrievedClaims.UserID != "user123" {
|
||||
t.Errorf("Expected UserID 'user123', got '%s'", retrievedClaims.UserID)
|
||||
if retrievedClaims.UsersID != "user123" {
|
||||
t.Errorf("Expected UserID 'user123', got '%s'", retrievedClaims.UsersID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,15 +225,17 @@ func TestGetUserID(t *testing.T) {
|
||||
|
||||
func TestGetRole(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), roleIDKey, "admin")
|
||||
ctx := context.WithValue(req.Context(), roleIDKey, []int{3})
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
role, ok := GetRole(req)
|
||||
if !ok {
|
||||
t.Error("Expected role to be found")
|
||||
}
|
||||
if role != "admin" {
|
||||
t.Errorf("Expected 'admin', got '%s'", role)
|
||||
if len(role) == 0 {
|
||||
t.Errorf("Expected at least one role, got '%v'", role)
|
||||
} else if role[0] != 3 {
|
||||
t.Errorf("Expected first role to be 3, got '%v'", role[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,13 +337,13 @@ func TestParseAndValidateTokenMalformedTokens(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildContextWithDifferentRoles(t *testing.T) {
|
||||
roles := []string{"admin", "user", "guest", "superadmin", "", "role-with-dash"}
|
||||
roles := []int{3, 4, 5, 6, 7, 8}
|
||||
|
||||
for _, role := range roles {
|
||||
t.Run("Role: "+role, func(t *testing.T) {
|
||||
t.Run("Role: "+string(rune(role)), func(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: role,
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{role},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
@@ -352,8 +354,8 @@ func TestBuildContextWithDifferentRoles(t *testing.T) {
|
||||
if !ok {
|
||||
t.Error("Claims not found in context")
|
||||
}
|
||||
if retrievedClaims.RoleID != role {
|
||||
t.Errorf("Role = %q, want %q", retrievedClaims.RoleID, role)
|
||||
if len(retrievedClaims.RoleID) == 0 || retrievedClaims.RoleID[0] != role {
|
||||
t.Errorf("Role = %v, want %v", retrievedClaims.RoleID, role)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -404,8 +406,8 @@ func TestGetRoleWithNoClaims(t *testing.T) {
|
||||
if ok {
|
||||
t.Error("Expected ok=false when no claims")
|
||||
}
|
||||
if role != "" {
|
||||
t.Errorf("Expected empty string, got %q", role)
|
||||
if role != nil && len(role) != 0 {
|
||||
t.Errorf("Expected no roles, got %v", role)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +446,7 @@ func TestJWTAuthTokenWithMissingClaims(t *testing.T) {
|
||||
{
|
||||
"Missing UserID",
|
||||
&models.Claims{
|
||||
RoleID: "admin",
|
||||
RoleID: models.RoleIDs{3},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
@@ -453,7 +455,7 @@ func TestJWTAuthTokenWithMissingClaims(t *testing.T) {
|
||||
{
|
||||
"Missing Role",
|
||||
&models.Claims{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
@@ -494,8 +496,8 @@ func TestJWTAuthConcurrentRequests(t *testing.T) {
|
||||
t.Skip("Requires RSA certificate setup - integration test")
|
||||
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{3},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
@@ -539,8 +541,8 @@ func TestJWTAuthTokenSignedWithWrongKey(t *testing.T) {
|
||||
|
||||
// Create token with wrong key
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{3},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ func RateLimiterMiddleware(config models.RateLimitConfig) func(http.HandlerFunc)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user identifier (prefer user_id from JWT, fallback to IP)
|
||||
// Extract user identifier (prefer users_id from JWT, fallback to IP)
|
||||
var identifier string
|
||||
if userID, ok := GetUserID(r); ok {
|
||||
identifier = "user:" + userID
|
||||
|
||||
+83
-4
@@ -1,13 +1,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type AuthorizationRequest struct {
|
||||
UserID string `json:"user_id"`
|
||||
UsersID string `json:"users_id"`
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
@@ -17,13 +21,88 @@ type AuthorizationResponse struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// RoleIDs represents one or more role IDs.
|
||||
// It is defined as a custom type so we can implement flexible JSON unmarshalling
|
||||
// that accepts a single string ("1"), a single number (1), or an array ([1,2,...]).
|
||||
type RoleIDs []int
|
||||
|
||||
// UnmarshalJSON allows RoleIDs to be populated from different JSON shapes:
|
||||
// string: "1" -> [1]
|
||||
// number: 1 -> [1]
|
||||
// array: [1] or [1,2,...] -> [1] or [1,2,...]
|
||||
func (r *RoleIDs) UnmarshalJSON(data []byte) error {
|
||||
// Handle null or empty
|
||||
trimmed := bytes.TrimSpace(data)
|
||||
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
|
||||
*r = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
switch trimmed[0] {
|
||||
case '"':
|
||||
// String value, e.g. "1"
|
||||
var s string
|
||||
if err := json.Unmarshal(trimmed, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == "" {
|
||||
*r = nil
|
||||
return nil
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*r = RoleIDs{v}
|
||||
return nil
|
||||
case '[':
|
||||
// Standard JSON array of ints
|
||||
var arr []int
|
||||
if err := json.Unmarshal(trimmed, &arr); err != nil {
|
||||
return err
|
||||
}
|
||||
*r = RoleIDs(arr)
|
||||
return nil
|
||||
default:
|
||||
// Try to decode as a single number
|
||||
var v int
|
||||
if err := json.Unmarshal(trimmed, &v); err == nil {
|
||||
*r = RoleIDs{v}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unsupported JSON for role_id: %s", string(trimmed))
|
||||
}
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
EmailAddress string `json:"email_address"`
|
||||
RoleID string `json:"role_id"`
|
||||
UsersID string `json:"users_id,omitempty"`
|
||||
EmailAddress string `json:"email_address,omitempty"`
|
||||
RoleID RoleIDs `json:"role_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles both "user_id" and "users_id" field names in JWT claims
|
||||
func (c *Claims) UnmarshalJSON(data []byte) error {
|
||||
type Alias Claims
|
||||
aux := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(c),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
// If UsersID is empty but UserID is set, copy UserID to UsersID
|
||||
if c.UsersID == "" && c.UsersID != "" {
|
||||
c.UsersID = c.UsersID
|
||||
}
|
||||
// If EmailAddress is empty but Email is set, copy Email to EmailAddress
|
||||
if c.EmailAddress == "" && c.EmailAddress != "" {
|
||||
c.EmailAddress = c.EmailAddress
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextKey is a custom type for context keys to avoid collisions
|
||||
type ContextKey string
|
||||
|
||||
|
||||
+57
-4
@@ -1,6 +1,11 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Permission represents a system permission
|
||||
type Permission struct {
|
||||
@@ -38,7 +43,7 @@ type UserAttribute struct {
|
||||
|
||||
// User represents a system user
|
||||
type User struct {
|
||||
UserID string `json:"user_id" db:"user_id"`
|
||||
UsersID string `json:"users_id" db:"user_id"`
|
||||
FirstName string `json:"first_name" db:"first_name"`
|
||||
MiddleInitial string `json:"middle_initial" db:"middle_initial"`
|
||||
LastName string `json:"last_name" db:"last_name"`
|
||||
@@ -57,15 +62,63 @@ type User struct {
|
||||
|
||||
// AuthorizationContext holds all context needed for authorization decisions
|
||||
type AuthorizationContext struct {
|
||||
UserID string `json:"user_id"`
|
||||
UsersID string `json:"users_id"`
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
RoleID string `json:"role_id"` // User's role ID
|
||||
RoleID int `json:"role_id"` // User's role ID
|
||||
UserAttributes map[string]string `json:"user_attributes"`
|
||||
ResourceData map[string]string `json:"resource_data"` // Additional resource context
|
||||
Environment map[string]string `json:"environment"` // Time, location, etc.
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles role_id as either string or int
|
||||
func (ac *AuthorizationContext) UnmarshalJSON(data []byte) error {
|
||||
type Alias AuthorizationContext
|
||||
aux := &struct {
|
||||
RoleIDRaw json.RawMessage `json:"role_id"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(ac),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle role_id as string, int, or array
|
||||
if len(aux.RoleIDRaw) > 0 {
|
||||
// Try unmarshaling as int first
|
||||
var roleInt int
|
||||
if err := json.Unmarshal(aux.RoleIDRaw, &roleInt); err == nil {
|
||||
ac.RoleID = roleInt
|
||||
} else {
|
||||
// Try as array of ints (take first element)
|
||||
var roleArray []int
|
||||
if err := json.Unmarshal(aux.RoleIDRaw, &roleArray); err == nil {
|
||||
if len(roleArray) > 0 {
|
||||
ac.RoleID = roleArray[0]
|
||||
}
|
||||
} else {
|
||||
// Try as string
|
||||
var roleStr string
|
||||
if err := json.Unmarshal(aux.RoleIDRaw, &roleStr); err == nil {
|
||||
if roleStr != "" {
|
||||
var convErr error
|
||||
ac.RoleID, convErr = strconv.Atoi(roleStr)
|
||||
if convErr != nil {
|
||||
return fmt.Errorf("invalid role_id: %s", roleStr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("role_id must be a number, numeric string, or array of numbers")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthorizationResult contains the result of an authorization check
|
||||
type AuthorizationResult struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
func Authorize(ctx *models.AuthorizationContext) (*models.AuthorizationResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
log.Printf("[AuthZ Step 0] Fetching user details for userID=%s", ctx.UserID)
|
||||
user, err := repository.GetUserByID(ctx.UserID)
|
||||
log.Printf("[AuthZ Step 0] Fetching user details for userID=%s", ctx.UsersID)
|
||||
user, err := repository.GetUserByID(ctx.UsersID)
|
||||
if err != nil {
|
||||
log.Printf("✗ User not found for userID=%s: %v", ctx.UserID, err)
|
||||
log.Printf("✗ User not found for userID=%s: %v", ctx.UsersID, err)
|
||||
return &models.AuthorizationResult{
|
||||
Allowed: false,
|
||||
Message: fmt.Sprintf("User not found: %v", err),
|
||||
@@ -35,10 +35,10 @@ func Authorize(ctx *models.AuthorizationContext) (*models.AuthorizationResult, e
|
||||
log.Printf("[AuthZ Step 1] Permission found: ID=%d, Name=%s", permission.ID, permission.PermissionName)
|
||||
|
||||
// Step 2: Get user attributes
|
||||
log.Printf("[AuthZ Step 2] Fetching user attributes for userID=%s", ctx.UserID)
|
||||
userAttrs, err := repository.GetUserAttributes(ctx.UserID)
|
||||
log.Printf("[AuthZ Step 2] Fetching user attributes for userID=%s", ctx.UsersID)
|
||||
userAttrs, err := repository.GetUserAttributes(ctx.UsersID)
|
||||
if err != nil {
|
||||
log.Printf("✗ Failed to get user attributes for userID=%s: %v", ctx.UserID, err)
|
||||
log.Printf("✗ Failed to get user attributes for userID=%s: %v", ctx.UsersID, err)
|
||||
return &models.AuthorizationResult{
|
||||
Allowed: false,
|
||||
Message: fmt.Sprintf("Failed to get user attributes: %v", err),
|
||||
@@ -84,10 +84,10 @@ func Authorize(ctx *models.AuthorizationContext) (*models.AuthorizationResult, e
|
||||
result.RedirectRoute = "dashboard"
|
||||
result.Message = "Access granted"
|
||||
log.Printf("✓ Authorization GRANTED for user=%s, resource=%s, action=%s (evaluated in %v)",
|
||||
ctx.UserID, ctx.Resource, ctx.Action, time.Since(startTime))
|
||||
ctx.UsersID, ctx.Resource, ctx.Action, time.Since(startTime))
|
||||
} else {
|
||||
log.Printf("✗ Authorization DENIED for user=%s, resource=%s, action=%s - Reason: %s (evaluated in %v)",
|
||||
ctx.UserID, ctx.Resource, ctx.Action, reason, time.Since(startTime))
|
||||
ctx.UsersID, ctx.Resource, ctx.Action, reason, time.Since(startTime))
|
||||
result.Message = reason
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func Authorize(ctx *models.AuthorizationContext) (*models.AuthorizationResult, e
|
||||
evalTime := time.Since(startTime)
|
||||
if evalTime > 100*time.Millisecond {
|
||||
fmt.Printf("WARN: Slow authorization evaluation: %v for user=%s, resource=%s, action=%s\n",
|
||||
evalTime, ctx.UserID, ctx.Resource, ctx.Action)
|
||||
evalTime, ctx.UsersID, ctx.Resource, ctx.Action)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
+16
-16
@@ -32,7 +32,7 @@ func TestAuthorize_PermissionNotFound(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "nonexistent",
|
||||
Action: "read",
|
||||
ResourceData: make(map[string]string),
|
||||
@@ -40,19 +40,19 @@ func TestAuthorize_PermissionNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
// Mock user query
|
||||
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
|
||||
userRows := 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", "", "Doe", "", "john@example.com",
|
||||
"EMP123", "Y", "Y", "123 Street", "09123456789", "device1",
|
||||
1, "N", "secret", "Y", time.Now(), time.Now())
|
||||
|
||||
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("user123").
|
||||
WillReturnRows(userRows)
|
||||
|
||||
// Mock permission query with role check
|
||||
mock.ExpectQuery("SELECT p.id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
|
||||
mock.ExpectQuery("SELECT p.role_permissions_id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
|
||||
WithArgs("nonexistent", "read", 1).
|
||||
WillReturnError(errors.New("permission not found"))
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestAuthorize_Success(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
ResourceData: make(map[string]string),
|
||||
@@ -82,14 +82,14 @@ func TestAuthorize_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
// Mock user query
|
||||
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
|
||||
userRows := 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", "", "Doe", "", "john@example.com",
|
||||
"EMP123", "Y", "Y", "123 Street", "09123456789", "device1",
|
||||
1, "N", "secret", "Y", time.Now(), time.Now())
|
||||
|
||||
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("user123").
|
||||
WillReturnRows(userRows)
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestAuthorize_Success(t *testing.T) {
|
||||
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
||||
AddRow("department", "engineering")
|
||||
|
||||
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(attrRows)
|
||||
|
||||
@@ -134,7 +134,7 @@ func TestAuthorize_UserAttributesError(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
ResourceData: make(map[string]string),
|
||||
@@ -142,14 +142,14 @@ func TestAuthorize_UserAttributesError(t *testing.T) {
|
||||
}
|
||||
|
||||
// Mock user query
|
||||
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
|
||||
userRows := 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", "", "Doe", "", "john@example.com",
|
||||
"EMP123", "Y", "Y", "123 Street", "09123456789", "device1",
|
||||
1, "N", "secret", "Y", time.Now(), time.Now())
|
||||
|
||||
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("user123").
|
||||
WillReturnRows(userRows)
|
||||
|
||||
@@ -162,7 +162,7 @@ func TestAuthorize_UserAttributesError(t *testing.T) {
|
||||
WillReturnRows(permRows)
|
||||
|
||||
// Mock user attributes query with error
|
||||
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").
|
||||
WillReturnError(errors.New("database error"))
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestAuthorize_PolicyAttributesError(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
ResourceData: make(map[string]string),
|
||||
@@ -189,14 +189,14 @@ func TestAuthorize_PolicyAttributesError(t *testing.T) {
|
||||
}
|
||||
|
||||
// Mock user query
|
||||
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
|
||||
userRows := 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", "", "Doe", "", "john@example.com",
|
||||
"EMP123", "Y", "Y", "123 Street", "09123456789", "device1",
|
||||
1, "N", "secret", "Y", time.Now(), time.Now())
|
||||
|
||||
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("user123").
|
||||
WillReturnRows(userRows)
|
||||
|
||||
@@ -212,7 +212,7 @@ func TestAuthorize_PolicyAttributesError(t *testing.T) {
|
||||
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
||||
AddRow("department", "engineering")
|
||||
|
||||
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(attrRows)
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ func refreshCache(s *models.CachedAuthorizationService) {
|
||||
s.LastCacheRefresh = time.Now()
|
||||
cacheMutex.Unlock()
|
||||
|
||||
log.Printf("✓ Cache refreshed: %d policy groups cached", len(policies))
|
||||
// log.Printf("✓ Cache refreshed: %d policy groups cached", len(policies))
|
||||
|
||||
// Store policies in Redis for distributed access (non-blocking)
|
||||
// Permissions are now cached on-demand with role awareness
|
||||
@@ -178,7 +178,7 @@ func refreshCache(s *models.CachedAuthorizationService) {
|
||||
redisclient.RDB.Set(ctx, redisKey, policiesJSON, cacheTTL)
|
||||
}
|
||||
|
||||
log.Printf("INFO: Policy cache synced to Redis - %d policy groups", len(policies))
|
||||
// log.Printf("INFO: Policy cache synced to Redis - %d policy groups", len(policies))
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -230,13 +230,17 @@ func NewCachedAuthorizationService() *models.CachedAuthorizationService {
|
||||
func AuthorizeWithCache(s *models.CachedAuthorizationService, ctx *models.AuthorizationContext) (*models.AuthorizationResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
log.Printf("[AuthZ Cached] Starting authorization check for user=%s, resource=%s, action=%s", ctx.UserID, ctx.Resource, ctx.Action)
|
||||
log.Printf("[CACHE-ENTRY] AuthorizeWithCache() called - UsersID=%s, Resource=%s, Action=%s, RoleID=%d",
|
||||
ctx.UsersID, ctx.Resource, ctx.Action, ctx.RoleID)
|
||||
log.Printf("[CACHE-ENTRY] Full context: %+v", ctx)
|
||||
|
||||
log.Printf("[AuthZ Cached] Starting authorization check for user=%s, resource=%s, action=%s", ctx.UsersID, ctx.Resource, ctx.Action)
|
||||
|
||||
// Step 0: Get user to retrieve role_id (needed for role-based permission lookup)
|
||||
log.Printf("[AuthZ Step 0] Fetching user details for userID=%s", ctx.UserID)
|
||||
user, err := repository.GetUserByID(ctx.UserID)
|
||||
log.Printf("[AuthZ Step 0] Fetching user details for userID=%s", ctx.UsersID)
|
||||
user, err := repository.GetUserByID(ctx.UsersID)
|
||||
if err != nil {
|
||||
log.Printf("✗ User not found for userID=%s: %v", ctx.UserID, err)
|
||||
log.Printf("✗ User not found for userID=%s: %v", ctx.UsersID, err)
|
||||
return &models.AuthorizationResult{
|
||||
Allowed: false,
|
||||
Message: fmt.Sprintf("User not found: %v", err),
|
||||
@@ -246,34 +250,34 @@ func AuthorizeWithCache(s *models.CachedAuthorizationService, ctx *models.Author
|
||||
|
||||
// Step 1: Check if the user's role has the permission (not just if permission exists)
|
||||
// Use role-aware cache key: roleID:resource:action
|
||||
cacheKey := fmt.Sprintf("%d:%s:%s", user.RoleID, ctx.Resource, ctx.Action)
|
||||
cacheKey := fmt.Sprintf("%d:%s:%s", ctx.RoleID, ctx.Resource, ctx.Action)
|
||||
log.Printf("[AuthZ Step 1] Looking up permission in cache with role: %s", cacheKey)
|
||||
permission, exists := getPermissionFromCache(s, cacheKey)
|
||||
|
||||
if !exists {
|
||||
// Cache miss - try database lookup with role check
|
||||
log.Printf("[AuthZ Step 1] Cache miss - querying database for role_id=%d, resource=%s, action=%s", user.RoleID, ctx.Resource, ctx.Action)
|
||||
permission, err = repository.GetPermissionByResourceActionAndRole(ctx.Resource, ctx.Action, user.RoleID)
|
||||
log.Printf("[AuthZ Step 1] Cache miss - querying database for role_id=%d, resource=%s, action=%s", ctx.RoleID, ctx.Resource, ctx.Action)
|
||||
permission, err = repository.GetPermissionByResourceActionAndRole(ctx.Resource, ctx.Action, ctx.RoleID)
|
||||
if err != nil {
|
||||
log.Printf("✗ Permission not found or not granted to role_id=%d for resource=%s, action=%s: %v", user.RoleID, ctx.Resource, ctx.Action, err)
|
||||
log.Printf("✗ [AuthZ Step 1] Permission not found or not granted to role_id=%d for resource=%s, action=%s: %v", ctx.RoleID, ctx.Resource, ctx.Action, err)
|
||||
return &models.AuthorizationResult{
|
||||
Allowed: false,
|
||||
Message: "Permission not granted to your role",
|
||||
}, nil
|
||||
}
|
||||
log.Printf("[AuthZ Step 1] Permission found in DB: ID=%d, Name=%s", permission.ID, permission.PermissionName)
|
||||
log.Printf("✓ [AuthZ Step 1] Permission found in DB: ID=%d, Name=%s", permission.ID, permission.PermissionName)
|
||||
|
||||
// Cache the result for future use
|
||||
storePermissionInCache(s, cacheKey, permission)
|
||||
} else {
|
||||
log.Printf("[AuthZ Step 1] Permission found in cache: ID=%d, Name=%s", permission.ID, permission.PermissionName)
|
||||
log.Printf("✓ [AuthZ Step 1] Permission found in cache: ID=%d, Name=%s", permission.ID, permission.PermissionName)
|
||||
}
|
||||
|
||||
// Step 2: Get user attributes (with distributed cache)
|
||||
log.Printf("[AuthZ Step 2] Fetching user attributes for userID=%s", ctx.UserID)
|
||||
userAttrs, err := getCachedUserAttributes(s, ctx.UserID)
|
||||
log.Printf("[AuthZ Step 2] Fetching user attributes for userID=%s", ctx.UsersID)
|
||||
userAttrs, err := getCachedUserAttributes(s, ctx.UsersID)
|
||||
if err != nil {
|
||||
log.Printf("✗ Failed to get user attributes for userID=%s: %v", ctx.UserID, err)
|
||||
log.Printf("✗ Failed to get user attributes for userID=%s: %v", ctx.UsersID, err)
|
||||
return &models.AuthorizationResult{
|
||||
Allowed: false,
|
||||
Message: "Failed to get user attributes",
|
||||
@@ -311,11 +315,11 @@ func AuthorizeWithCache(s *models.CachedAuthorizationService, ctx *models.Author
|
||||
if allowed {
|
||||
result.Message = "Access granted"
|
||||
log.Printf("✓ Authorization GRANTED for user=%s, resource=%s, action=%s (evaluated in %v)",
|
||||
ctx.UserID, ctx.Resource, ctx.Action, time.Since(startTime))
|
||||
ctx.UsersID, ctx.Resource, ctx.Action, time.Since(startTime))
|
||||
} else {
|
||||
result.Message = reason
|
||||
log.Printf("✗ Authorization DENIED for user=%s, resource=%s, action=%s - Reason: %s (evaluated in %v)",
|
||||
ctx.UserID, ctx.Resource, ctx.Action, reason, time.Since(startTime))
|
||||
ctx.UsersID, ctx.Resource, ctx.Action, reason, time.Since(startTime))
|
||||
}
|
||||
|
||||
// Performance monitoring
|
||||
@@ -323,12 +327,12 @@ func AuthorizeWithCache(s *models.CachedAuthorizationService, ctx *models.Author
|
||||
|
||||
if evalTime < 50*time.Millisecond {
|
||||
log.Print("Cached authorization evaluation time: ", evalTime,
|
||||
" for user=", ctx.UserID, ", resource=", ctx.Resource, ", action=", ctx.Action)
|
||||
" for user=", ctx.UsersID, ", resource=", ctx.Resource, ", action=", ctx.Action)
|
||||
}
|
||||
|
||||
if evalTime > 50*time.Millisecond {
|
||||
log.Print("WARN: Slow cached authorization evaluation: ", evalTime,
|
||||
" for user=", ctx.UserID, ", resource=", ctx.Resource, ", action=", ctx.Action)
|
||||
" for user=", ctx.UsersID, ", resource=", ctx.Resource, ", action=", ctx.Action)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -104,7 +104,7 @@ func TestGetCachedUserAttributes_CacheMiss(t *testing.T) {
|
||||
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
||||
AddRow("department", "engineering")
|
||||
|
||||
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(attrRows)
|
||||
|
||||
@@ -219,14 +219,14 @@ func TestAuthorizeWithCache_Success(t *testing.T) {
|
||||
service.PolicyCache[1] = []models.PolicyAttribute{}
|
||||
|
||||
// Mock user query (needed to get role_id)
|
||||
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
|
||||
userRows := 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", "", "Doe", "", "john@example.com",
|
||||
"EMP123", "Y", "Y", "123 Street", "09123456789", "device1",
|
||||
1, "N", "secret", "Y", time.Now(), time.Now())
|
||||
|
||||
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("user123").
|
||||
WillReturnRows(userRows)
|
||||
|
||||
@@ -234,12 +234,12 @@ func TestAuthorizeWithCache_Success(t *testing.T) {
|
||||
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
||||
AddRow("department", "engineering")
|
||||
|
||||
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(attrRows)
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
ResourceData: make(map[string]string),
|
||||
@@ -268,20 +268,20 @@ func TestAuthorizeWithCache_PermissionNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "nonexistent",
|
||||
Action: "read",
|
||||
}
|
||||
|
||||
// Mock user query
|
||||
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_initial", "last_name", "suffix", "email_address",
|
||||
userRows := 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", "", "Doe", "", "john@example.com",
|
||||
"EMP123", "Y", "Y", "123 Street", "09123456789", "device1",
|
||||
1, "N", "secret", "Y", time.Now(), time.Now())
|
||||
|
||||
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("user123").
|
||||
WillReturnRows(userRows)
|
||||
|
||||
|
||||
@@ -135,8 +135,7 @@ func evaluatePolicy(policyAttribute models.PolicyAttribute, ctx *models.Authoriz
|
||||
log.Print("Role ID!!!!!: ", ctx.RoleID)
|
||||
if policyAttribute.AttributeType == "user" &&
|
||||
policyAttribute.AttributeName == "region" &&
|
||||
(ctx.RoleID == "1" || ctx.RoleID == "2" || ctx.RoleID == "Super Admin" ||
|
||||
ctx.RoleID == "System Admin") {
|
||||
(ctx.RoleID == 1 || ctx.RoleID == 2) {
|
||||
fmt.Printf("[POLICY EVALUATION] Type: %s, Attribute: %s\n", policyAttribute.AttributeType, policyAttribute.AttributeName)
|
||||
fmt.Printf(" Skipped for roleID: %s (Super | System Admin bypass)\n\n", ctx.RoleID)
|
||||
return true, ""
|
||||
|
||||
@@ -657,7 +657,7 @@ func TestEvaluatePoliciesComplexConditions(t *testing.T) {
|
||||
|
||||
func TestResolveVariablesAllAttributeTypes(t *testing.T) {
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
UserAttributes: map[string]string{
|
||||
@@ -719,7 +719,7 @@ func TestEvaluatePolicies_UserRegionMatchesResourceRegion(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "U0000000001",
|
||||
UsersID: "U0000000001",
|
||||
Resource: "personnel",
|
||||
Action: "assign_role",
|
||||
UserAttributes: map[string]string{
|
||||
@@ -752,7 +752,7 @@ func TestEvaluatePolicies_MissingResourceAttribute(t *testing.T) {
|
||||
// The policy should fail because the placeholder cannot be resolved
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "U0000000001",
|
||||
UsersID: "U0000000001",
|
||||
Resource: "personnel",
|
||||
Action: "assign_role",
|
||||
UserAttributes: map[string]string{
|
||||
@@ -880,7 +880,7 @@ func TestEvaluatePolicies_RegionBypassForAdminRoles(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
roleID string
|
||||
roleID int
|
||||
userRegion string
|
||||
resourceRegion string
|
||||
shouldBeAllowed bool
|
||||
@@ -888,7 +888,7 @@ func TestEvaluatePolicies_RegionBypassForAdminRoles(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "roleID 1 bypasses region check",
|
||||
roleID: "1",
|
||||
roleID: 1,
|
||||
userRegion: "02",
|
||||
resourceRegion: "01",
|
||||
shouldBeAllowed: true,
|
||||
@@ -896,7 +896,7 @@ func TestEvaluatePolicies_RegionBypassForAdminRoles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "roleID 2 bypasses region check",
|
||||
roleID: "2",
|
||||
roleID: 2,
|
||||
userRegion: "03",
|
||||
resourceRegion: "01",
|
||||
shouldBeAllowed: true,
|
||||
@@ -904,7 +904,7 @@ func TestEvaluatePolicies_RegionBypassForAdminRoles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "other roleID respects region check",
|
||||
roleID: "3",
|
||||
roleID: 3,
|
||||
userRegion: "02",
|
||||
resourceRegion: "01",
|
||||
shouldBeAllowed: false,
|
||||
@@ -912,7 +912,7 @@ func TestEvaluatePolicies_RegionBypassForAdminRoles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Super Admin role bypasses region check",
|
||||
roleID: "Super Admin",
|
||||
roleID: 1,
|
||||
userRegion: "02",
|
||||
resourceRegion: "01",
|
||||
shouldBeAllowed: true,
|
||||
@@ -920,7 +920,7 @@ func TestEvaluatePolicies_RegionBypassForAdminRoles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Admin role does not bypass region check",
|
||||
roleID: "Admin",
|
||||
roleID: 2,
|
||||
userRegion: "03",
|
||||
resourceRegion: "01",
|
||||
shouldBeAllowed: false,
|
||||
@@ -931,7 +931,7 @@ func TestEvaluatePolicies_RegionBypassForAdminRoles(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "U0000000001",
|
||||
UsersID: "U0000000001",
|
||||
Resource: "personnel",
|
||||
Action: "assign_role",
|
||||
RoleID: tt.roleID,
|
||||
|
||||
Reference in New Issue
Block a user