ae1831e61f
- 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.
353 lines
11 KiB
Go
353 lines
11 KiB
Go
package services
|
|
|
|
import (
|
|
"authorization/db"
|
|
"authorization/models"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
)
|
|
|
|
func setupMockDBForCached(t *testing.T) (sqlmock.Sqlmock, func()) {
|
|
mockDB, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create mock database: %v", err)
|
|
}
|
|
|
|
originalDB := db.DB
|
|
db.DB = mockDB
|
|
|
|
cleanup := func() {
|
|
db.DB = originalDB
|
|
mockDB.Close()
|
|
}
|
|
|
|
return mock, cleanup
|
|
}
|
|
|
|
func TestNewCachedAuthorizationService(t *testing.T) {
|
|
mock, cleanup := setupMockDBForCached(t)
|
|
defer cleanup()
|
|
|
|
// Mock the initial cache load queries
|
|
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
|
|
AddRow(1, "read_document", "Read document", "document", "read")
|
|
|
|
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions ORDER BY id").
|
|
WillReturnRows(permRows)
|
|
|
|
policyRows := sqlmock.NewRows([]string{"id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"})
|
|
|
|
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, id").
|
|
WillReturnRows(policyRows)
|
|
|
|
service := NewCachedAuthorizationService()
|
|
|
|
if service == nil {
|
|
t.Fatal("Expected service, got nil")
|
|
}
|
|
if service.PermissionCache == nil {
|
|
t.Error("Expected PermissionCache to be initialized")
|
|
}
|
|
if service.PolicyCache == nil {
|
|
t.Error("Expected PolicyCache to be initialized")
|
|
}
|
|
if service.UserAttrCache == nil {
|
|
t.Error("Expected UserAttrCache to be initialized")
|
|
}
|
|
if service.CacheExpiry != 5*time.Second {
|
|
t.Errorf("Expected CacheExpiry 30s, got %v", service.CacheExpiry)
|
|
}
|
|
|
|
// Give time for cache to load
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
func TestGetCachedUserAttributes_CacheHit(t *testing.T) {
|
|
service := &models.CachedAuthorizationService{
|
|
UserAttrCache: make(map[string]map[string]string),
|
|
UserAttrMutex: &sync.RWMutex{},
|
|
}
|
|
|
|
// Pre-populate cache
|
|
service.UserAttrCache["user123"] = map[string]string{
|
|
"department": "engineering",
|
|
"level": "5",
|
|
}
|
|
|
|
attrs, err := getCachedUserAttributes(service, "user123")
|
|
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
if len(attrs) != 2 {
|
|
t.Errorf("Expected 2 attributes, got %d", len(attrs))
|
|
}
|
|
if attrs["department"] != "engineering" {
|
|
t.Errorf("Expected department 'engineering', got '%s'", attrs["department"])
|
|
}
|
|
}
|
|
|
|
func TestGetCachedUserAttributes_CacheMiss(t *testing.T) {
|
|
mock, cleanup := setupMockDBForCached(t)
|
|
defer cleanup()
|
|
|
|
service := &models.CachedAuthorizationService{
|
|
UserAttrCache: make(map[string]map[string]string),
|
|
UserAttrMutex: &sync.RWMutex{},
|
|
}
|
|
|
|
// Mock database query for cache miss
|
|
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
|
AddRow("department", "engineering")
|
|
|
|
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE users_id = \\?").
|
|
WithArgs("user123").
|
|
WillReturnRows(attrRows)
|
|
|
|
attrs, err := getCachedUserAttributes(service, "user123")
|
|
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
if len(attrs) != 1 {
|
|
t.Errorf("Expected 1 attribute, got %d", len(attrs))
|
|
}
|
|
|
|
// Verify it's now in cache
|
|
if _, exists := service.UserAttrCache["user123"]; !exists {
|
|
t.Error("Expected user attributes to be cached")
|
|
}
|
|
}
|
|
|
|
func TestRefreshCache(t *testing.T) {
|
|
mock, cleanup := setupMockDBForCached(t)
|
|
defer cleanup()
|
|
|
|
service := &models.CachedAuthorizationService{
|
|
PermissionCache: make(map[string]*models.Permission),
|
|
PolicyCache: make(map[int][]models.PolicyAttribute),
|
|
CacheMutex: &sync.RWMutex{},
|
|
}
|
|
|
|
// 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(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 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) {
|
|
service := &models.CachedAuthorizationService{
|
|
UserAttrCache: make(map[string]map[string]string),
|
|
UserAttrMutex: &sync.RWMutex{},
|
|
}
|
|
|
|
// Add many entries to trigger cleanup
|
|
for i := 0; i < 10001; i++ {
|
|
service.UserAttrCache[string(rune(i))] = map[string]string{"test": "value"}
|
|
}
|
|
|
|
cleanUserAttributeCache(service)
|
|
|
|
if len(service.UserAttrCache) != 0 {
|
|
t.Error("Expected user attribute cache to be cleared")
|
|
}
|
|
}
|
|
|
|
func TestCleanUserAttributeCache_SmallCache(t *testing.T) {
|
|
service := &models.CachedAuthorizationService{
|
|
UserAttrCache: make(map[string]map[string]string),
|
|
UserAttrMutex: &sync.RWMutex{},
|
|
}
|
|
|
|
// Add few entries
|
|
service.UserAttrCache["user1"] = map[string]string{"test": "value"}
|
|
service.UserAttrCache["user2"] = map[string]string{"test": "value"}
|
|
|
|
cleanUserAttributeCache(service)
|
|
|
|
if len(service.UserAttrCache) != 2 {
|
|
t.Error("Expected small cache to remain unchanged")
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeWithCache_Success(t *testing.T) {
|
|
mock, cleanup := setupMockDBForCached(t)
|
|
defer cleanup()
|
|
|
|
service := &models.CachedAuthorizationService{
|
|
PermissionCache: make(map[string]*models.Permission),
|
|
PolicyCache: make(map[int][]models.PolicyAttribute),
|
|
UserAttrCache: make(map[string]map[string]string),
|
|
CacheMutex: &sync.RWMutex{},
|
|
UserAttrMutex: &sync.RWMutex{},
|
|
}
|
|
|
|
// 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",
|
|
Action: "read",
|
|
}
|
|
|
|
// Add empty policies
|
|
service.PolicyCache[1] = []models.PolicyAttribute{}
|
|
|
|
// Mock user query (needed to get role_id)
|
|
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 users_id, first_name, middle_initial, last_name, suffix, email_address").
|
|
WithArgs("user123").
|
|
WillReturnRows(userRows)
|
|
|
|
// Mock user attributes query
|
|
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
|
AddRow("department", "engineering")
|
|
|
|
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE users_id = \\?").
|
|
WithArgs("user123").
|
|
WillReturnRows(attrRows)
|
|
|
|
ctx := &models.AuthorizationContext{
|
|
UsersID: "user123",
|
|
Resource: "document",
|
|
Action: "read",
|
|
ResourceData: make(map[string]string),
|
|
Environment: make(map[string]string),
|
|
}
|
|
|
|
result, err := AuthorizeWithCache(service, ctx)
|
|
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
if !result.Allowed {
|
|
t.Error("Expected access granted")
|
|
}
|
|
}
|
|
|
|
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),
|
|
CacheMutex: &sync.RWMutex{},
|
|
UserAttrMutex: &sync.RWMutex{},
|
|
}
|
|
|
|
ctx := &models.AuthorizationContext{
|
|
UsersID: "user123",
|
|
Resource: "nonexistent",
|
|
Action: "read",
|
|
}
|
|
|
|
// Mock user query
|
|
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 users_id, first_name, middle_initial, 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 {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
if result.Allowed {
|
|
t.Error("Expected access denied")
|
|
}
|
|
if result.Message != "Permission not granted to your role" {
|
|
t.Errorf("Expected 'Permission not granted to your role', got '%s'", result.Message)
|
|
}
|
|
}
|
|
|
|
func TestInvalidateUserCache(t *testing.T) {
|
|
service := &models.CachedAuthorizationService{
|
|
UserAttrCache: make(map[string]map[string]string),
|
|
UserAttrMutex: &sync.RWMutex{},
|
|
}
|
|
|
|
service.UserAttrCache["user123"] = map[string]string{"test": "value"}
|
|
|
|
InvalidateUserCache(service, "user123")
|
|
|
|
if _, exists := service.UserAttrCache["user123"]; exists {
|
|
t.Error("Expected user cache to be invalidated")
|
|
}
|
|
}
|
|
|
|
func TestGetCacheStats(t *testing.T) {
|
|
service := &models.CachedAuthorizationService{
|
|
PermissionCache: make(map[string]*models.Permission),
|
|
PolicyCache: make(map[int][]models.PolicyAttribute),
|
|
UserAttrCache: make(map[string]map[string]string),
|
|
CacheMutex: &sync.RWMutex{},
|
|
UserAttrMutex: &sync.RWMutex{},
|
|
LastCacheRefresh: time.Now().Add(-10 * time.Second),
|
|
}
|
|
|
|
service.PermissionCache["doc:read"] = &models.Permission{ID: 1}
|
|
service.PermissionCache["doc:write"] = &models.Permission{ID: 2}
|
|
service.PolicyCache[1] = []models.PolicyAttribute{{ID: 1}}
|
|
service.UserAttrCache["user1"] = map[string]string{"dept": "eng"}
|
|
|
|
stats := GetCacheStats(service)
|
|
|
|
if stats["permissions_cached"] != 2 {
|
|
t.Errorf("Expected 2 permissions cached, got %v", stats["permissions_cached"])
|
|
}
|
|
if stats["policies_cached"] != 1 {
|
|
t.Errorf("Expected 1 policy cached, got %v", stats["policies_cached"])
|
|
}
|
|
if stats["user_attributes_cached"] != 1 {
|
|
t.Errorf("Expected 1 user attribute cached, got %v", stats["user_attributes_cached"])
|
|
}
|
|
|
|
cacheAge := stats["cache_age_seconds"].(float64)
|
|
if cacheAge < 9 || cacheAge > 12 {
|
|
t.Errorf("Expected cache age around 10 seconds, got %v", cacheAge)
|
|
}
|
|
}
|