From ae1831e61f47e647bb0e128f7b811ab9ae209882 Mon Sep 17 00:00:00 2001 From: F04C Date: Tue, 3 Feb 2026 16:35:16 +0800 Subject: [PATCH] feat: standardize field names and add flexible role_id handling for JWT compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- handlers/authorize.go | 47 ++++++++++--- handlers/authorize_test.go | 58 ++++++++-------- middleware/jwt.go | 15 ++-- middleware/jwt_test.go | 52 +++++++------- middleware/rate_limiter.go | 2 +- models/authorize.go | 87 ++++++++++++++++++++++-- models/rbac.go | 61 +++++++++++++++-- repository/permission_repository.go | 57 ++++++++-------- repository/permission_repository_test.go | 22 +++--- services/authorize.go | 18 ++--- services/authorize_test.go | 32 ++++----- services/cached_authorization.go | 42 ++++++------ services/cached_authorization_test.go | 16 ++--- services/policy_evaluator.go | 3 +- services/policy_evaluator_test.go | 20 +++--- 15 files changed, 348 insertions(+), 184 deletions(-) diff --git a/handlers/authorize.go b/handlers/authorize.go index c97d53d..6345aab 100644 --- a/handlers/authorize.go +++ b/handlers/authorize.go @@ -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 +} diff --git a/handlers/authorize_test.go b/handlers/authorize_test.go index 505bc03..86f3ac4 100644 --- a/handlers/authorize_test.go +++ b/handlers/authorize_test.go @@ -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{ diff --git a/middleware/jwt.go b/middleware/jwt.go index a1c7328..ceb998f 100644 --- a/middleware/jwt.go +++ b/middleware/jwt.go @@ -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 } diff --git a/middleware/jwt_test.go b/middleware/jwt_test.go index 843f6a1..ff497c0 100644 --- a/middleware/jwt_test.go +++ b/middleware/jwt_test.go @@ -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)), }, diff --git a/middleware/rate_limiter.go b/middleware/rate_limiter.go index 930a352..b565ce4 100644 --- a/middleware/rate_limiter.go +++ b/middleware/rate_limiter.go @@ -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 diff --git a/models/authorize.go b/models/authorize.go index 8953e8e..9e54df6 100644 --- a/models/authorize.go +++ b/models/authorize.go @@ -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 diff --git a/models/rbac.go b/models/rbac.go index b833083..b37cbb8 100644 --- a/models/rbac.go +++ b/models/rbac.go @@ -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"` diff --git a/repository/permission_repository.go b/repository/permission_repository.go index 32ff68d..06c12a3 100644 --- a/repository/permission_repository.go +++ b/repository/permission_repository.go @@ -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) diff --git a/repository/permission_repository_test.go b/repository/permission_repository_test.go index 0bab75a..8bc3854 100644 --- a/repository/permission_repository_test.go +++ b/repository/permission_repository_test.go @@ -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")) diff --git a/services/authorize.go b/services/authorize.go index ddcada7..479a808 100644 --- a/services/authorize.go +++ b/services/authorize.go @@ -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 diff --git a/services/authorize_test.go b/services/authorize_test.go index f4eaa78..0d7ab98 100644 --- a/services/authorize_test.go +++ b/services/authorize_test.go @@ -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) diff --git a/services/cached_authorization.go b/services/cached_authorization.go index afb0962..a2852ca 100644 --- a/services/cached_authorization.go +++ b/services/cached_authorization.go @@ -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 diff --git a/services/cached_authorization_test.go b/services/cached_authorization_test.go index 2fa2507..f0014df 100644 --- a/services/cached_authorization_test.go +++ b/services/cached_authorization_test.go @@ -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) diff --git a/services/policy_evaluator.go b/services/policy_evaluator.go index d1ab5bd..b88ccf3 100644 --- a/services/policy_evaluator.go +++ b/services/policy_evaluator.go @@ -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, "" diff --git a/services/policy_evaluator_test.go b/services/policy_evaluator_test.go index 9c419c6..e967cbd 100644 --- a/services/policy_evaluator_test.go +++ b/services/policy_evaluator_test.go @@ -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,