fix(auth)!: implement proper RBAC with role-permission checking

BREAKING CHANGE: Authorization now requires role_permissions table

Previously checked only if permission existed, now verifies user's
role has been granted the permission. Closes critical security gap
allowing any user to access any resource.

- feat: add role_permissions table schema
- feat: add GetPermissionByResourceActionAndRole repository method
- fix: update Authorize to check user role before granting access
- fix: update cache keys to include roleID
- test: update all tests for new authorization flow
This commit is contained in:
2026-01-22 14:09:37 +08:00
parent 509a502a85
commit 1a68840805
7 changed files with 261 additions and 238 deletions
+7
View File
@@ -11,6 +11,13 @@ type Permission struct {
Action string `json:"action" db:"action"`
}
// RolePermission represents the junction table linking roles to permissions
type RolePermission struct {
ID int `json:"id" db:"id"`
RoleID int `json:"role_id" db:"role_id"`
PermissionID int `json:"permission_id" db:"permission_id"`
}
// PolicyAttribute represents an ABAC policy attribute/constraint
type PolicyAttribute struct {
ID int `json:"id" db:"id"`
+7 -7
View File
@@ -7,17 +7,17 @@ import (
"fmt"
)
// GetPermissionByResourceAndAction finds a permission by resource and action
func GetPermissionByResourceAndAction(resource, action string) (*models.Permission, error) {
func GetPermissionByResourceActionAndRole(resource, action string, roleID int) (*models.Permission, error) {
query := `
SELECT id, permission_name, description, resource, action
FROM permissions
WHERE resource = ? AND action = ?
SELECT p.id, p.permission_name, p.description, p.resource, p.action
FROM permissions p
INNER JOIN role_permissions rp ON p.id = rp.permission_id
WHERE p.resource = ? AND p.action = ? AND rp.role_id = ?
LIMIT 1
`
var perm models.Permission
err := db.DB.QueryRow(query, resource, action).Scan(
err := db.DB.QueryRow(query, resource, action, roleID).Scan(
&perm.ID,
&perm.PermissionName,
&perm.Description,
@@ -27,7 +27,7 @@ func GetPermissionByResourceAndAction(resource, action string) (*models.Permissi
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("permission not found for resource=%s, action=%s", resource, action)
return nil, fmt.Errorf("permission not found or not granted to role_id=%d for resource=%s, action=%s", roleID, resource, action)
}
return nil, fmt.Errorf("error querying permission: %w", err)
}
-157
View File
@@ -28,76 +28,6 @@ func setupMockDB(t *testing.T) (sqlmock.Sqlmock, func()) {
return mock, cleanup
}
func TestGetPermissionByResourceAndActionSuccess(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
rows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "read_document", "Read document permission", "document", "read")
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("document", "read").
WillReturnRows(rows)
perm, err := GetPermissionByResourceAndAction("document", "read")
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if perm == nil {
t.Fatal("Expected permission, got nil")
}
if perm.ID != 1 {
t.Errorf("Expected ID 1, got %d", perm.ID)
}
if perm.Resource != "document" {
t.Errorf("Expected resource 'document', got '%s'", perm.Resource)
}
if perm.Action != "read" {
t.Errorf("Expected action 'read', got '%s'", perm.Action)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}
func TestGetPermissionByResourceAndActionNotFound(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("nonexistent", "read").
WillReturnError(sql.ErrNoRows)
perm, err := GetPermissionByResourceAndAction("nonexistent", "read")
if err == nil {
t.Error("Expected error for non-existent permission")
}
if perm != nil {
t.Error("Expected nil permission")
}
}
func TestGetPermissionByResourceAndActionDatabaseError(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("document", "read").
WillReturnError(errors.New("database connection failed"))
perm, err := GetPermissionByResourceAndAction("document", "read")
if err == nil {
t.Error("Expected error for database failure")
}
if perm != nil {
t.Error("Expected nil permission")
}
}
func TestGetPolicyAttributesByPermissionSuccess(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
@@ -295,71 +225,6 @@ func TestGetAllPolicyAttributesEmpty(t *testing.T) {
}
}
// Additional comprehensive test cases
func TestGetPermissionByResourceAndActionEmptyResource(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
rows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"})
// Match the exact query format with whitespace handling
mock.ExpectQuery(`SELECT id, permission_name, description, resource, action\s+FROM permissions\s+WHERE resource = \? AND action = \?\s+LIMIT 1`).
WithArgs("", "read").
WillReturnRows(rows)
perm, err := GetPermissionByResourceAndAction("", "read")
if err == nil {
t.Error("Expected error for empty resource")
}
if perm != nil {
t.Error("Expected nil permission for empty resource")
}
}
func TestGetPermissionByResourceAndActionEmptyAction(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
rows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"})
// Match the exact query format with whitespace handling
mock.ExpectQuery(`SELECT id, permission_name, description, resource, action\s+FROM permissions\s+WHERE resource = \? AND action = \?\s+LIMIT 1`).
WithArgs("document", "").
WillReturnRows(rows)
perm, err := GetPermissionByResourceAndAction("document", "")
if err == nil {
t.Error("Expected error for empty action")
}
if perm != nil {
t.Error("Expected nil permission for empty action")
}
}
func TestGetPermissionByResourceAndActionSpecialCharacters(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
rows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "special_perm", "Permission with special chars", "doc/file-v1.2", "read:write")
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("doc/file-v1.2", "read:write").
WillReturnRows(rows)
perm, err := GetPermissionByResourceAndAction("doc/file-v1.2", "read:write")
if err != nil {
t.Errorf("Expected no error for special chars, got %v", err)
}
if perm == nil {
t.Fatal("Expected permission, got nil")
}
}
func TestGetPolicyAttributesByPermissionInvalidID(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
@@ -616,25 +481,3 @@ func TestGetUserAttributesDatabaseError(t *testing.T) {
t.Error("Expected nil attributes on error")
}
}
func TestGetPermissionByResourceAndActionScanError(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
// Create row with wrong number of columns to cause scan error
rows := sqlmock.NewRows([]string{"id", "permission_name"}).
AddRow(1, "read_document")
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("document", "read").
WillReturnRows(rows)
perm, err := GetPermissionByResourceAndAction("document", "read")
if err == nil {
t.Error("Expected scan error, got nil")
}
if perm != nil {
t.Error("Expected nil permission on scan error")
}
}
+15 -5
View File
@@ -12,14 +12,24 @@ import (
func Authorize(ctx *models.AuthorizationContext) (*models.AuthorizationResult, error) {
startTime := time.Now()
// Step 1: Find the permission for the requested resource and action
log.Printf("[AuthZ Step 1] Fetching permission for resource=%s, action=%s", ctx.Resource, ctx.Action)
permission, err := repository.GetPermissionByResourceAndAction(ctx.Resource, ctx.Action)
log.Printf("[AuthZ Step 0] Fetching user details for userID=%s", ctx.UserID)
user, err := repository.GetUserByID(ctx.UserID)
if err != nil {
log.Printf("✗ Permission not found for resource=%s, action=%s: %v", ctx.Resource, ctx.Action, err)
log.Printf("✗ User not found for userID=%s: %v", ctx.UserID, err)
return &models.AuthorizationResult{
Allowed: false,
Message: fmt.Sprintf("Permission not found: %v", err),
Message: fmt.Sprintf("User not found: %v", err),
}, nil
}
log.Printf("[AuthZ Step 0] User found: role_id=%d", user.RoleID)
log.Printf("[AuthZ Step 1] Checking if role_id=%d has permission for resource=%s, action=%s", user.RoleID, ctx.Resource, ctx.Action)
permission, err := repository.GetPermissionByResourceActionAndRole(ctx.Resource, ctx.Action, user.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)
return &models.AuthorizationResult{
Allowed: false,
Message: fmt.Sprintf("Permission not granted to your role: %v", err),
}, nil
}
log.Printf("[AuthZ Step 1] Permission found: ID=%d, Name=%s", permission.ID, permission.PermissionName)
+120 -19
View File
@@ -5,6 +5,7 @@ import (
"authorization/models"
"errors"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
)
@@ -38,8 +39,23 @@ func TestAuthorize_PermissionNotFound(t *testing.T) {
Environment: make(map[string]string),
}
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("nonexistent", "read").
// Mock user query
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_name", "last_name", "suffix", "email_address",
"account_type", "emp_id", "reg", "prov", "aProv", "mun", "bgy", "is_logged_in",
"first_logged_in", "address", "contact_number", "device_id", "role_id",
"role_dps", "is_deleted", "secret_key", "is_activated", "created_at", "updated_at"}).
AddRow("user123", "John", "", "Doe", "", "john@example.com",
"regular", "EMP123", "01", "001", "001", "01", "001", "Y",
"Y", "123 Street", "09123456789", "device1", 1,
0, "N", "secret", "Y", time.Now(), time.Now())
mock.ExpectQuery("SELECT user_id, first_name, middle_name, 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").
WithArgs("nonexistent", "read", 1).
WillReturnError(errors.New("permission not found"))
result, err := Authorize(ctx)
@@ -67,12 +83,26 @@ func TestAuthorize_Success(t *testing.T) {
Environment: make(map[string]string),
}
// Mock permission query
// Mock user query
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_name", "last_name", "suffix", "email_address",
"account_type", "emp_id", "reg", "prov", "aProv", "mun", "bgy", "is_logged_in",
"first_logged_in", "address", "contact_number", "device_id", "role_id",
"role_dps", "is_deleted", "secret_key", "is_activated", "created_at", "updated_at"}).
AddRow("user123", "John", "", "Doe", "", "john@example.com",
"regular", "EMP123", "01", "001", "001", "01", "001", "Y",
"Y", "123 Street", "09123456789", "device1", 1,
0, "N", "secret", "Y", time.Now(), time.Now())
mock.ExpectQuery("SELECT user_id, first_name, middle_name, last_name, suffix, email_address").
WithArgs("user123").
WillReturnRows(userRows)
// Mock permission query with role check
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "read_document", "Read document permission", "document", "read")
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("document", "read").
mock.ExpectQuery("SELECT p.id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
WithArgs("document", "read", 1).
WillReturnRows(permRows)
// Mock user attributes query
@@ -115,12 +145,26 @@ func TestAuthorize_UserAttributesError(t *testing.T) {
Environment: make(map[string]string),
}
// Mock permission query
// Mock user query
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_name", "last_name", "suffix", "email_address",
"account_type", "emp_id", "reg", "prov", "aProv", "mun", "bgy", "is_logged_in",
"first_logged_in", "address", "contact_number", "device_id", "role_id",
"role_dps", "is_deleted", "secret_key", "is_activated", "created_at", "updated_at"}).
AddRow("user123", "John", "", "Doe", "", "john@example.com",
"regular", "EMP123", "01", "001", "001", "01", "001", "Y",
"Y", "123 Street", "09123456789", "device1", 1,
0, "N", "secret", "Y", time.Now(), time.Now())
mock.ExpectQuery("SELECT user_id, first_name, middle_name, last_name, suffix, email_address").
WithArgs("user123").
WillReturnRows(userRows)
// Mock permission query with role check
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "read_document", "Read document permission", "document", "read")
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("document", "read").
mock.ExpectQuery("SELECT p.id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
WithArgs("document", "read", 1).
WillReturnRows(permRows)
// Mock user attributes query with error
@@ -150,12 +194,26 @@ func TestAuthorize_PolicyAttributesError(t *testing.T) {
Environment: make(map[string]string),
}
// Mock permission query
// Mock user query
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_name", "last_name", "suffix", "email_address",
"account_type", "emp_id", "reg", "prov", "aProv", "mun", "bgy", "is_logged_in",
"first_logged_in", "address", "contact_number", "device_id", "role_id",
"role_dps", "is_deleted", "secret_key", "is_activated", "created_at", "updated_at"}).
AddRow("user123", "John", "", "Doe", "", "john@example.com",
"regular", "EMP123", "01", "001", "001", "01", "001", "Y",
"Y", "123 Street", "09123456789", "device1", 1,
0, "N", "secret", "Y", time.Now(), time.Now())
mock.ExpectQuery("SELECT user_id, first_name, middle_name, last_name, suffix, email_address").
WithArgs("user123").
WillReturnRows(userRows)
// Mock permission query with role check
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "read_document", "Read document permission", "document", "read")
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("document", "read").
mock.ExpectQuery("SELECT p.id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
WithArgs("document", "read", 1).
WillReturnRows(permRows)
// Mock user attributes query
@@ -185,12 +243,26 @@ func TestCheckPermission_Success(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
// Mock permission query
// Mock user query
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_name", "last_name", "suffix", "email_address",
"account_type", "emp_id", "reg", "prov", "aProv", "mun", "bgy", "is_logged_in",
"first_logged_in", "address", "contact_number", "device_id", "role_id",
"role_dps", "is_deleted", "secret_key", "is_activated", "created_at", "updated_at"}).
AddRow("user123", "John", "", "Doe", "", "john@example.com",
"regular", "EMP123", "01", "001", "001", "01", "001", "Y",
"Y", "123 Street", "09123456789", "device1", 1,
0, "N", "secret", "Y", time.Now(), time.Now())
mock.ExpectQuery("SELECT user_id, first_name, middle_name, last_name, suffix, email_address").
WithArgs("user123").
WillReturnRows(userRows)
// Mock permission query with role check
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "read_document", "Read document permission", "document", "read")
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("document", "read").
mock.ExpectQuery("SELECT p.id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
WithArgs("document", "read", 1).
WillReturnRows(permRows)
// Mock user attributes query
@@ -226,8 +298,23 @@ func TestCheckPermission_Denied(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("document", "read").
// Mock user query
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_name", "last_name", "suffix", "email_address",
"account_type", "emp_id", "reg", "prov", "aProv", "mun", "bgy", "is_logged_in",
"first_logged_in", "address", "contact_number", "device_id", "role_id",
"role_dps", "is_deleted", "secret_key", "is_activated", "created_at", "updated_at"}).
AddRow("user123", "John", "", "Doe", "", "john@example.com",
"regular", "EMP123", "01", "001", "001", "01", "001", "Y",
"Y", "123 Street", "09123456789", "device1", 1,
0, "N", "secret", "Y", time.Now(), time.Now())
mock.ExpectQuery("SELECT user_id, first_name, middle_name, last_name, suffix, email_address").
WithArgs("user123").
WillReturnRows(userRows)
// Mock permission query with role check - should fail
mock.ExpectQuery("SELECT p.id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
WithArgs("document", "read", 1).
WillReturnError(errors.New("permission not found"))
resourceData := map[string]string{"document_id": "123"}
@@ -248,12 +335,26 @@ func TestCheckPermission_NilResourceData(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
// Mock permission query
// Mock user query
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_name", "last_name", "suffix", "email_address",
"account_type", "emp_id", "reg", "prov", "aProv", "mun", "bgy", "is_logged_in",
"first_logged_in", "address", "contact_number", "device_id", "role_id",
"role_dps", "is_deleted", "secret_key", "is_activated", "created_at", "updated_at"}).
AddRow("user123", "John", "", "Doe", "", "john@example.com",
"regular", "EMP123", "01", "001", "001", "01", "001", "Y",
"Y", "123 Street", "09123456789", "device1", 1,
0, "N", "secret", "Y", time.Now(), time.Now())
mock.ExpectQuery("SELECT user_id, first_name, middle_name, last_name, suffix, email_address").
WithArgs("user123").
WillReturnRows(userRows)
// Mock permission query with role check
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "read_document", "Read document permission", "document", "read")
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
WithArgs("document", "read").
mock.ExpectQuery("SELECT p.id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
WithArgs("document", "read", 1).
WillReturnRows(permRows)
// Mock user attributes query
+61 -35
View File
@@ -97,6 +97,26 @@ func getPermissionFromCache(s *models.CachedAuthorizationService, cacheKey strin
return permission, exists
}
func storePermissionInCache(s *models.CachedAuthorizationService, cacheKey string, permission *models.Permission) {
// Store in Redis (async)
if redisclient.RDB != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
permJSON, _ := json.Marshal(permission)
key := permissionCachePrefix + cacheKey
redisclient.RDB.Set(ctx, key, permJSON, cacheTTL)
}()
}
// Store in local cache
cacheMutex := s.CacheMutex.(*sync.RWMutex)
cacheMutex.Lock()
s.PermissionCache[cacheKey] = permission
cacheMutex.Unlock()
}
// getPoliciesFromCache retrieves policies from Redis or local cache
func getPoliciesFromCache(s *models.CachedAuthorizationService, permissionID int) []models.PolicyAttribute {
// Try Redis first if available
@@ -125,48 +145,32 @@ func getPoliciesFromCache(s *models.CachedAuthorizationService, permissionID int
// refreshCache reloads permissions and policies from database and stores in Redis
func refreshCache(s *models.CachedAuthorizationService) {
// Load all permissions
permissions, err := repository.GetAllPermissions()
if err != nil {
log.Printf("ERROR: Failed to refresh permissions cache: %v", err)
return
}
// Note: We no longer pre-cache all permissions since we need role-specific lookups.
// Permissions will be cached on-demand as users access them (lazy loading).
// Load all policies
// Load all policies (these are permission-specific, not role-specific)
policies, err := repository.GetAllPolicyAttributes()
if err != nil {
log.Printf("ERROR: Failed to refresh policies cache: %v", err)
return
}
// Update local cache atomically
newPermCache := make(map[string]*models.Permission)
for i := range permissions {
perm := &permissions[i]
key := perm.Resource + ":" + perm.Action
newPermCache[key] = perm
}
// Update policy cache atomically
cacheMutex := s.CacheMutex.(*sync.RWMutex)
cacheMutex.Lock()
s.PermissionCache = newPermCache
s.PolicyCache = policies
s.LastCacheRefresh = time.Now()
cacheMutex.Unlock()
// Store in Redis for distributed access (non-blocking)
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
if redisclient.RDB != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Store permissions in Redis
for key, perm := range newPermCache {
permJSON, _ := json.Marshal(perm)
redisKey := permissionCachePrefix + key
redisclient.RDB.Set(ctx, redisKey, permJSON, cacheTTL)
}
// Store policies in Redis
for permID, policyList := range policies {
policiesJSON, _ := json.Marshal(policyList)
@@ -174,8 +178,7 @@ func refreshCache(s *models.CachedAuthorizationService) {
redisclient.RDB.Set(ctx, redisKey, policiesJSON, cacheTTL)
}
log.Printf("INFO: Cache refreshed and synced to Redis - %d permissions, %d policy groups",
len(newPermCache), len(policies))
log.Printf("INFO: Policy cache synced to Redis - %d policy groups", len(policies))
}()
}
}
@@ -229,19 +232,42 @@ func AuthorizeWithCache(s *models.CachedAuthorizationService, ctx *models.Author
log.Printf("[AuthZ Cached] Starting authorization check for user=%s, resource=%s, action=%s", ctx.UserID, ctx.Resource, ctx.Action)
// Step 1: Get permission from distributed cache
cacheKey := ctx.Resource + ":" + ctx.Action
log.Printf("[AuthZ Step 1] Looking up permission in cache: %s", cacheKey)
// 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)
if err != nil {
log.Printf("✗ User not found for userID=%s: %v", ctx.UserID, err)
return &models.AuthorizationResult{
Allowed: false,
Message: fmt.Sprintf("User not found: %v", err),
}, nil
}
log.Printf("[AuthZ Step 0] User found: role_id=%d", user.RoleID)
// 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)
log.Printf("[AuthZ Step 1] Looking up permission in cache with role: %s", cacheKey)
permission, exists := getPermissionFromCache(s, cacheKey)
if !exists {
log.Printf("✗ Permission not found in cache for resource=%s, action=%s", ctx.Resource, ctx.Action)
return &models.AuthorizationResult{
Allowed: false,
Message: "Permission not found",
}, nil
// 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)
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)
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)
// 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)
+51 -15
View File
@@ -3,6 +3,7 @@ package services
import (
"authorization/db"
"authorization/models"
"errors"
"sync"
"testing"
"time"
@@ -132,32 +133,31 @@ func TestRefreshCache(t *testing.T) {
CacheMutex: &sync.RWMutex{},
}
// Mock permission query
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "read_document", "Read document", "document", "read").
AddRow(2, "write_document", "Write document", "document", "write")
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions ORDER BY id").
WillReturnRows(permRows)
// Note: Permissions are no longer preloaded - they're cached on-demand
// Only policies are preloaded during cache refresh
// Mock policy attributes query
policyRows := sqlmock.NewRows([]string{"id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"}).
AddRow(1, "department", "user", "=", "engineering", 1)
AddRow(1, "department", "user", "=", "engineering", 1).
AddRow(2, "region", "user", "=", "01", 2)
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, id").
WillReturnRows(policyRows)
refreshCache(service)
// Verify permissions are cached
if len(service.PermissionCache) != 2 {
t.Errorf("Expected 2 permissions in cache, got %d", len(service.PermissionCache))
// Verify permissions cache is empty (lazy loading)
if len(service.PermissionCache) != 0 {
t.Errorf("Expected 0 permissions in cache (lazy loading), got %d", len(service.PermissionCache))
}
// Verify policies are cached
if len(service.PolicyCache[1]) != 1 {
t.Errorf("Expected 1 policy for permission 1, got %d", len(service.PolicyCache[1]))
}
if len(service.PolicyCache[2]) != 1 {
t.Errorf("Expected 1 policy for permission 2, got %d", len(service.PolicyCache[2]))
}
}
func TestCleanUserAttributeCache(t *testing.T) {
@@ -207,8 +207,8 @@ func TestAuthorizeWithCache_Success(t *testing.T) {
UserAttrMutex: &sync.RWMutex{},
}
// Add permission to cache
service.PermissionCache["document:read"] = &models.Permission{
// Add permission to cache with role-aware key (roleID:resource:action)
service.PermissionCache["1:document:read"] = &models.Permission{
ID: 1,
PermissionName: "read_document",
Resource: "document",
@@ -218,6 +218,20 @@ func TestAuthorizeWithCache_Success(t *testing.T) {
// Add empty policies
service.PolicyCache[1] = []models.PolicyAttribute{}
// Mock user query (needed to get role_id)
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_name", "last_name", "suffix", "email_address",
"account_type", "emp_id", "reg", "prov", "aProv", "mun", "bgy", "is_logged_in",
"first_logged_in", "address", "contact_number", "device_id", "role_id",
"role_dps", "is_deleted", "secret_key", "is_activated", "created_at", "updated_at"}).
AddRow("user123", "John", "", "Doe", "", "john@example.com",
"regular", "EMP123", "01", "001", "001", "01", "001", "Y",
"Y", "123 Street", "09123456789", "device1", 1,
0, "N", "secret", "Y", time.Now(), time.Now())
mock.ExpectQuery("SELECT user_id, first_name, middle_name, last_name, suffix, email_address").
WithArgs("user123").
WillReturnRows(userRows)
// Mock user attributes query
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
AddRow("department", "engineering")
@@ -245,6 +259,9 @@ func TestAuthorizeWithCache_Success(t *testing.T) {
}
func TestAuthorizeWithCache_PermissionNotFound(t *testing.T) {
mock, cleanup := setupMockDBForCached(t)
defer cleanup()
service := &models.CachedAuthorizationService{
PermissionCache: make(map[string]*models.Permission),
PolicyCache: make(map[int][]models.PolicyAttribute),
@@ -258,6 +275,25 @@ func TestAuthorizeWithCache_PermissionNotFound(t *testing.T) {
Action: "read",
}
// Mock user query
userRows := sqlmock.NewRows([]string{"user_id", "first_name", "middle_name", "last_name", "suffix", "email_address",
"account_type", "emp_id", "reg", "prov", "aProv", "mun", "bgy", "is_logged_in",
"first_logged_in", "address", "contact_number", "device_id", "role_id",
"role_dps", "is_deleted", "secret_key", "is_activated", "created_at", "updated_at"}).
AddRow("user123", "John", "", "Doe", "", "john@example.com",
"regular", "EMP123", "01", "001", "001", "01", "001", "Y",
"Y", "123 Street", "09123456789", "device1", 1,
0, "N", "secret", "Y", time.Now(), time.Now())
mock.ExpectQuery("SELECT user_id, first_name, middle_name, last_name, suffix, email_address").
WithArgs("user123").
WillReturnRows(userRows)
// Permission not in cache, so will query DB and fail
mock.ExpectQuery("SELECT p.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"))
result, err := AuthorizeWithCache(service, ctx)
if err != nil {
@@ -266,8 +302,8 @@ func TestAuthorizeWithCache_PermissionNotFound(t *testing.T) {
if result.Allowed {
t.Error("Expected access denied")
}
if result.Message != "Permission not found" {
t.Errorf("Expected 'Permission not found', got '%s'", result.Message)
if result.Message != "Permission not granted to your role" {
t.Errorf("Expected 'Permission not granted to your role', got '%s'", result.Message)
}
}