feat(authz): redirect deleted accounts to /login

detect soft-deleted users during authorization lookup
return a dedicated deleted-user result from auth services
redirect deleted accounts to /login in the handler
update repository, service, and handler tests for the new flow
This commit is contained in:
2026-05-08 09:04:28 +08:00
parent db5eba572f
commit e6bbaeffa6
8 changed files with 193 additions and 47 deletions
+9
View File
@@ -3,6 +3,7 @@ package services
import (
"authorization/models"
"authorization/repository"
"errors"
"fmt"
"log"
"time"
@@ -15,6 +16,14 @@ func Authorize(ctx *models.AuthorizationContext) (*models.AuthorizationResult, e
log.Printf("[AuthZ Step 0] Fetching user details for userID=%s", ctx.UsersID)
user, err := repository.GetUserByID(ctx.UsersID)
if err != nil {
if errors.Is(err, repository.ErrUserDeleted) {
log.Printf("✗ Deleted user attempted authorization for userID=%s", ctx.UsersID)
return &models.AuthorizationResult{
Allowed: false,
RedirectRoute: "/login",
Message: "Account deleted",
}, nil
}
log.Printf("✗ User not found for userID=%s: %v", ctx.UsersID, err)
return &models.AuthorizationResult{
Allowed: false,
+53 -20
View File
@@ -40,15 +40,15 @@ func TestAuthorize_PermissionNotFound(t *testing.T) {
}
// Mock user query
userRows := sqlmock.NewRows([]string{"users_id", "email_address"}).
AddRow("user123", "john@example.com")
userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}).
AddRow("user123", "john@example.com", 1, "0")
mock.ExpectQuery("SELECT users_id, email_address").
WithArgs("user123").
WillReturnRows(userRows)
// Mock permission query with role check
mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM uess_user_management.permissions p INNER JOIN uess_user_management.role_permissions rp").
WithArgs("nonexistent", "read", 1).
WillReturnError(errors.New("permission not found"))
@@ -79,8 +79,8 @@ func TestAuthorize_Success(t *testing.T) {
}
// Mock user query
userRows := sqlmock.NewRows([]string{"users_id", "email_address"}).
AddRow("user123", "john@example.com")
userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}).
AddRow("user123", "john@example.com", 1, "0")
mock.ExpectQuery("SELECT users_id, email_address").
WithArgs("user123").
@@ -90,7 +90,7 @@ func TestAuthorize_Success(t *testing.T) {
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "read_document", "Read document permission", "document", "read")
mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM uess_user_management.permissions p INNER JOIN uess_user_management.role_permissions rp").
WithArgs("document", "read", 1).
WillReturnRows(permRows)
@@ -98,14 +98,14 @@ 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 users_id = \\?").
WithArgs("user123").
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM uess_user_management.user_attributes WHERE users_id = \\?").
WithArgs("user123", "user123").
WillReturnRows(attrRows)
// Mock policy attributes query (empty for this test)
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 WHERE permission_id = \\?").
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM uess_user_management.policy_attributes WHERE permission_id = \\?").
WithArgs(1).
WillReturnRows(policyRows)
@@ -136,8 +136,8 @@ func TestAuthorize_UserAttributesError(t *testing.T) {
}
// Mock user query
userRows := sqlmock.NewRows([]string{"users_id", "email_address"}).
AddRow("user123", "john@example.com")
userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}).
AddRow("user123", "john@example.com", 1, "0")
mock.ExpectQuery("SELECT users_id, email_address").
WithArgs("user123").
@@ -147,13 +147,13 @@ func TestAuthorize_UserAttributesError(t *testing.T) {
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "read_document", "Read document permission", "document", "read")
mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM uess_user_management.permissions p INNER JOIN uess_user_management.role_permissions rp").
WithArgs("document", "read", 1).
WillReturnRows(permRows)
// Mock user attributes query with error
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE users_id = \\?").
WithArgs("user123").
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM uess_user_management.user_attributes WHERE users_id = \\?").
WithArgs("user123", "user123").
WillReturnError(errors.New("database error"))
result, err := Authorize(ctx)
@@ -180,8 +180,8 @@ func TestAuthorize_PolicyAttributesError(t *testing.T) {
}
// Mock user query
userRows := sqlmock.NewRows([]string{"users_id", "email_address"}).
AddRow("user123", "john@example.com")
userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}).
AddRow("user123", "john@example.com", 1, "0")
mock.ExpectQuery("SELECT users_id, email_address").
WithArgs("user123").
@@ -191,7 +191,7 @@ func TestAuthorize_PolicyAttributesError(t *testing.T) {
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
AddRow(1, "read_document", "Read document permission", "document", "read")
mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM uess_user_management.permissions p INNER JOIN uess_user_management.role_permissions rp").
WithArgs("document", "read", 1).
WillReturnRows(permRows)
@@ -199,12 +199,12 @@ 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 users_id = \\?").
WithArgs("user123").
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM uess_user_management.user_attributes WHERE users_id = \\?").
WithArgs("user123", "user123").
WillReturnRows(attrRows)
// Mock policy attributes query with error
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes WHERE permission_id = \\?").
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM uess_user_management.policy_attributes WHERE permission_id = \\?").
WithArgs(1).
WillReturnError(errors.New("database error"))
@@ -252,3 +252,36 @@ func TestGetRoleCandidates_Priority(t *testing.T) {
}
})
}
func TestAuthorize_DeletedUserRedirectsToLogin(t *testing.T) {
mock, cleanup := setupMockDB(t)
defer cleanup()
ctx := &models.AuthorizationContext{
UsersID: "deleted-user",
Resource: "document",
Action: "read",
RoleID: 1,
ResourceData: make(map[string]string),
Environment: make(map[string]string),
}
userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}).
AddRow("deleted-user", "deleted@example.com", 1, "1")
mock.ExpectQuery("SELECT users_id, email_address, role_id, is_deleted").
WithArgs("deleted-user").
WillReturnRows(userRows)
result, err := Authorize(ctx)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result.Allowed {
t.Error("Expected access denied")
}
if result.RedirectRoute != "/login" {
t.Errorf("Expected redirect to /login, got %q", result.RedirectRoute)
}
}
+9
View File
@@ -6,6 +6,7 @@ import (
"authorization/repository"
"context"
"encoding/json"
"errors"
"fmt"
"log"
"sync"
@@ -240,6 +241,14 @@ func AuthorizeWithCache(s *models.CachedAuthorizationService, ctx *models.Author
log.Printf("[AuthZ Step 0] Fetching user details for userID=%s", ctx.UsersID)
user, err := repository.GetUserByID(ctx.UsersID)
if err != nil {
if errors.Is(err, repository.ErrUserDeleted) {
log.Printf("✗ Deleted user attempted authorization for userID=%s", ctx.UsersID)
return &models.AuthorizationResult{
Allowed: false,
RedirectRoute: "/login",
Message: "Account deleted",
}, nil
}
log.Printf("✗ User not found for userID=%s: %v", ctx.UsersID, err)
return &models.AuthorizationResult{
Allowed: false,
+49 -9
View File
@@ -104,8 +104,8 @@ 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 users_id = \\?").
WithArgs("user123").
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM uess_user_management.user_attributes WHERE users_id = \\?").
WithArgs("user123", "user123").
WillReturnRows(attrRows)
attrs, err := getCachedUserAttributes(service, "user123")
@@ -219,8 +219,8 @@ func TestAuthorizeWithCache_Success(t *testing.T) {
service.PolicyCache[1] = []models.PolicyAttribute{}
// Mock user query
userRows := sqlmock.NewRows([]string{"users_id", "email_address"}).
AddRow("user123", "john@example.com")
userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}).
AddRow("user123", "john@example.com", 1, "0")
mock.ExpectQuery("SELECT users_id, email_address").
WithArgs("user123").
@@ -230,8 +230,8 @@ 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 users_id = \\?").
WithArgs("user123").
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM uess_user_management.user_attributes WHERE users_id = \\?").
WithArgs("user123", "user123").
WillReturnRows(attrRows)
ctx := &models.AuthorizationContext{
@@ -268,18 +268,19 @@ func TestAuthorizeWithCache_PermissionNotFound(t *testing.T) {
UsersID: "user123",
Resource: "nonexistent",
Action: "read",
RoleID: 1,
}
// Mock user query
userRows := sqlmock.NewRows([]string{"users_id", "email_address"}).
AddRow("user123", "john@example.com")
userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}).
AddRow("user123", "john@example.com", 1, "0")
mock.ExpectQuery("SELECT users_id, email_address").
WithArgs("user123").
WillReturnRows(userRows)
// Permission not in cache, so will query DB and fail
mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM permissions p INNER JOIN role_permissions rp").
mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM uess_user_management.permissions p INNER JOIN uess_user_management.role_permissions rp").
WithArgs("nonexistent", "read", 1).
WillReturnError(errors.New("permission not found"))
@@ -296,6 +297,45 @@ func TestAuthorizeWithCache_PermissionNotFound(t *testing.T) {
}
}
func TestAuthorizeWithCache_DeletedUserRedirectsToLogin(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{},
}
userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}).
AddRow("deleted-user", "deleted@example.com", 1, "1")
mock.ExpectQuery("SELECT users_id, email_address, role_id, is_deleted").
WithArgs("deleted-user").
WillReturnRows(userRows)
ctx := &models.AuthorizationContext{
UsersID: "deleted-user",
Resource: "document",
Action: "read",
RoleID: 1,
}
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.RedirectRoute != "/login" {
t.Errorf("Expected redirect to /login, got %q", result.RedirectRoute)
}
}
func TestInvalidateUserCache(t *testing.T) {
service := &models.CachedAuthorizationService{
UserAttrCache: make(map[string]map[string]string),