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:
@@ -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,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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user