admin area

This commit is contained in:
2026-01-01 12:10:35 +01:00
parent 02c0cc44e1
commit f001df3744
68 changed files with 10324 additions and 32 deletions

View File

@@ -0,0 +1,747 @@
import { AuthorizationService } from './AuthorizationService';
import { AdminUser } from '../entities/AdminUser';
describe('AuthorizationService', () => {
describe('TDD - Test First', () => {
describe('canListUsers', () => {
it('should allow owner to list users', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
// Act
const canList = AuthorizationService.canListUsers(owner);
// Assert
expect(canList).toBe(true);
});
it('should allow admin to list users', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
// Act
const canList = AuthorizationService.canListUsers(admin);
// Assert
expect(canList).toBe(true);
});
it('should deny regular user from listing users', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canList = AuthorizationService.canListUsers(user);
// Assert
expect(canList).toBe(false);
});
it('should deny suspended admin from listing users', () => {
// Arrange
const suspendedAdmin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'suspended',
});
// Act
const canList = AuthorizationService.canListUsers(suspendedAdmin);
// Assert
expect(canList).toBe(false);
});
it('should allow owner with multiple roles to list users', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
// Act
const canList = AuthorizationService.canListUsers(owner);
// Assert
expect(canList).toBe(true);
});
});
describe('canPerformAction with manage', () => {
it('should allow owner to manage any user', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canManage = AuthorizationService.canPerformAction(owner, 'manage', targetUser);
// Assert
expect(canManage).toBe(true);
});
it('should allow admin to manage non-admin users', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canManage = AuthorizationService.canPerformAction(admin, 'manage', targetUser);
// Assert
expect(canManage).toBe(true);
});
it('should deny admin from managing other admins', () => {
// Arrange
const admin1 = AdminUser.create({
id: 'admin-1',
email: 'admin1@example.com',
displayName: 'Admin 1',
roles: ['admin'],
status: 'active',
});
const admin2 = AdminUser.create({
id: 'admin-2',
email: 'admin2@example.com',
displayName: 'Admin 2',
roles: ['admin'],
status: 'active',
});
// Act
const canManage = AuthorizationService.canPerformAction(admin1, 'manage', admin2);
// Assert
expect(canManage).toBe(false);
});
it('should deny regular user from managing anyone', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
// Act
const canManage = AuthorizationService.canPerformAction(user, 'manage', targetUser);
// Assert
expect(canManage).toBe(false);
});
it('should allow admin to manage suspended users', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const suspendedUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'suspended',
});
// Act
const canManage = AuthorizationService.canPerformAction(admin, 'manage', suspendedUser);
// Assert
expect(canManage).toBe(true);
});
});
describe('canPerformAction with modify_roles', () => {
it('should allow owner to modify roles', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canModify = AuthorizationService.canPerformAction(owner, 'modify_roles', targetUser);
// Assert
expect(canModify).toBe(true);
});
it('should deny admin from modifying roles', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canModify = AuthorizationService.canPerformAction(admin, 'modify_roles', targetUser);
// Assert
expect(canModify).toBe(false);
});
it('should deny regular user from modifying roles', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
// Act
const canModify = AuthorizationService.canPerformAction(user, 'modify_roles', targetUser);
// Assert
expect(canModify).toBe(false);
});
});
describe('canPerformAction with change_status', () => {
it('should allow owner to change any status', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canChange = AuthorizationService.canPerformAction(owner, 'change_status', targetUser);
// Assert
expect(canChange).toBe(true);
});
it('should allow admin to change non-admin status', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canChange = AuthorizationService.canPerformAction(admin, 'change_status', targetUser);
// Assert
expect(canChange).toBe(true);
});
it('should deny admin from changing admin status', () => {
// Arrange
const admin1 = AdminUser.create({
id: 'admin-1',
email: 'admin1@example.com',
displayName: 'Admin 1',
roles: ['admin'],
status: 'active',
});
const admin2 = AdminUser.create({
id: 'admin-2',
email: 'admin2@example.com',
displayName: 'Admin 2',
roles: ['admin'],
status: 'active',
});
// Act
const canChange = AuthorizationService.canPerformAction(admin1, 'change_status', admin2);
// Assert
expect(canChange).toBe(false);
});
it('should deny regular user from changing status', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
// Act
const canChange = AuthorizationService.canPerformAction(user, 'change_status', targetUser);
// Assert
expect(canChange).toBe(false);
});
});
describe('canPerformAction with delete', () => {
it('should allow owner to delete any user', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canDelete = AuthorizationService.canPerformAction(owner, 'delete', targetUser);
// Assert
expect(canDelete).toBe(true);
});
it('should allow admin to delete non-admin users', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canDelete = AuthorizationService.canPerformAction(admin, 'delete', targetUser);
// Assert
expect(canDelete).toBe(true);
});
it('should deny admin from deleting other admins', () => {
// Arrange
const admin1 = AdminUser.create({
id: 'admin-1',
email: 'admin1@example.com',
displayName: 'Admin 1',
roles: ['admin'],
status: 'active',
});
const admin2 = AdminUser.create({
id: 'admin-2',
email: 'admin2@example.com',
displayName: 'Admin 2',
roles: ['admin'],
status: 'active',
});
// Act
const canDelete = AuthorizationService.canPerformAction(admin1, 'delete', admin2);
// Assert
expect(canDelete).toBe(false);
});
it('should deny regular user from deleting anyone', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
// Act
const canDelete = AuthorizationService.canPerformAction(user, 'delete', targetUser);
// Assert
expect(canDelete).toBe(false);
});
it('should allow owner to delete suspended users', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const suspendedUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'suspended',
});
// Act
const canDelete = AuthorizationService.canPerformAction(owner, 'delete', suspendedUser);
// Assert
expect(canDelete).toBe(true);
});
});
describe('getPermissions', () => {
it('should return correct permissions for owner', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
// Act
const permissions = AuthorizationService.getPermissions(owner);
// Assert
expect(permissions).toContain('users.view');
expect(permissions).toContain('users.list');
expect(permissions).toContain('users.manage');
expect(permissions).toContain('users.roles.modify');
expect(permissions).toContain('users.status.change');
expect(permissions).toContain('users.create');
expect(permissions).toContain('users.delete');
expect(permissions).toContain('users.export');
});
it('should return correct permissions for admin', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
// Act
const permissions = AuthorizationService.getPermissions(admin);
// Assert
expect(permissions).toContain('users.view');
expect(permissions).toContain('users.list');
expect(permissions).toContain('users.manage');
expect(permissions).toContain('users.status.change');
expect(permissions).toContain('users.create');
expect(permissions).toContain('users.delete');
expect(permissions).not.toContain('users.roles.modify');
expect(permissions).not.toContain('users.export');
});
it('should return empty permissions for regular user', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const permissions = AuthorizationService.getPermissions(user);
// Assert
expect(permissions).toEqual([]);
});
it('should return empty permissions for suspended admin', () => {
// Arrange
const suspendedAdmin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'suspended',
});
// Act
const permissions = AuthorizationService.getPermissions(suspendedAdmin);
// Assert
expect(permissions).toEqual([]);
});
});
describe('hasPermission', () => {
it('should return true for owner with users.view permission', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
// Act
const hasPermission = AuthorizationService.hasPermission(owner, 'users.view');
// Assert
expect(hasPermission).toBe(true);
});
it('should return false for admin with users.roles.modify permission', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
// Act
const hasPermission = AuthorizationService.hasPermission(admin, 'users.roles.modify');
// Assert
expect(hasPermission).toBe(false);
});
it('should return false for regular user with any permission', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const hasPermission = AuthorizationService.hasPermission(user, 'users.view');
// Assert
expect(hasPermission).toBe(false);
});
});
describe('enforce', () => {
it('should not throw for authorized action', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act & Assert - Should not throw
expect(() => {
AuthorizationService.enforce(owner, 'manage', targetUser);
}).not.toThrow();
});
it('should throw for unauthorized action', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
// Act & Assert - Should throw
expect(() => {
AuthorizationService.enforce(user, 'manage', targetUser);
}).toThrow();
});
});
describe('enforcePermission', () => {
it('should not throw for authorized permission', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
// Act & Assert - Should not throw
expect(() => {
AuthorizationService.enforcePermission(owner, 'users.view');
}).not.toThrow();
});
it('should throw for unauthorized permission', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
// Act & Assert - Should throw
expect(() => {
AuthorizationService.enforcePermission(admin, 'users.roles.modify');
}).toThrow();
});
});
});
});

View File

@@ -0,0 +1,283 @@
import { AdminUser } from '../entities/AdminUser';
import { AuthorizationError } from '../errors/AdminDomainError';
/**
* Domain service for authorization checks
* Stateless service that enforces access control rules
*/
export class AuthorizationService {
/**
* Check if an actor can perform an action on a target user
*/
static canPerformAction(
actor: AdminUser,
action: 'view' | 'manage' | 'modify_roles' | 'change_status' | 'delete',
target?: AdminUser
): boolean {
// Actors must be system admins and active
if (!actor.isSystemAdmin() || !actor.isActive()) {
return false;
}
switch (action) {
case 'view':
return this.canView(actor, target);
case 'manage':
return this.canManage(actor, target);
case 'modify_roles':
return this.canModifyRoles(actor, target);
case 'change_status':
return this.canChangeStatus(actor, target);
case 'delete':
return this.canDelete(actor, target);
default:
return false;
}
}
/**
* Check if actor can view target user
*/
private static canView(actor: AdminUser, target?: AdminUser): boolean {
if (!target) {
// Viewing list - only admins can view
return actor.isSystemAdmin();
}
// Can always view self
if (actor.id.equals(target.id)) {
return true;
}
// Owner can view everyone
if (actor.hasRole('owner')) {
return true;
}
// Admin can view non-admin users
if (actor.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Check if actor can manage target user
*/
private static canManage(actor: AdminUser, target?: AdminUser): boolean {
if (!target) {
return false;
}
// Can always manage self
if (actor.id.equals(target.id)) {
return true;
}
// Owner can manage everyone
if (actor.hasRole('owner')) {
return true;
}
// Admin can manage non-admin users
if (actor.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Check if actor can modify roles of target user
*/
private static canModifyRoles(actor: AdminUser, target?: AdminUser): boolean {
if (!target) {
return false;
}
// Only owner can modify roles
if (!actor.hasRole('owner')) {
return false;
}
// Cannot modify own roles (prevents accidental lockout)
if (actor.id.equals(target.id)) {
return false;
}
return true;
}
/**
* Check if actor can change status of target user
*/
private static canChangeStatus(actor: AdminUser, target?: AdminUser): boolean {
if (!target) {
return false;
}
// Cannot change own status
if (actor.id.equals(target.id)) {
return false;
}
// Owner can change anyone's status
if (actor.hasRole('owner')) {
return true;
}
// Admin can change user status but not other admins/owners
if (actor.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Check if actor can delete target user
*/
private static canDelete(actor: AdminUser, target?: AdminUser): boolean {
if (!target) {
return false;
}
// Cannot delete self
if (actor.id.equals(target.id)) {
return false;
}
// Owner can delete anyone
if (actor.hasRole('owner')) {
return true;
}
// Admin can delete users but not admins/owners
if (actor.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Enforce authorization - throws if not authorized
*/
static enforce(
actor: AdminUser,
action: 'view' | 'manage' | 'modify_roles' | 'change_status' | 'delete',
target?: AdminUser
): void {
if (!this.canPerformAction(actor, action, target)) {
const actionLabel = action.replace('_', ' ');
const targetLabel = target ? `user ${target.email.value}` : 'user list';
throw new AuthorizationError(
`User ${actor.email.value} is not authorized to ${actionLabel} ${targetLabel}`
);
}
}
/**
* Check if actor can list users
*/
static canListUsers(actor: AdminUser): boolean {
return actor.isSystemAdmin() && actor.isActive();
}
/**
* Check if actor can create users
*/
static canCreateUsers(actor: AdminUser): boolean {
// Only owner can create users with admin roles
// Admin can create regular users
return actor.isSystemAdmin() && actor.isActive();
}
/**
* Check if actor can assign a specific role
*/
static canAssignRole(actor: AdminUser, roleToAssign: string, target?: AdminUser): boolean {
// Only owner can assign owner or admin roles
if (roleToAssign === 'owner' || roleToAssign === 'admin') {
return actor.hasRole('owner');
}
// Admin can assign user role
if (roleToAssign === 'user') {
// Admin can assign user role to non-admin users
// Owner can assign user role to anyone
if (actor.hasRole('owner')) {
return true;
}
if (actor.hasRole('admin')) {
if (!target) {
return true;
}
return !target.isSystemAdmin();
}
}
return false;
}
/**
* Get permissions for actor
*/
static getPermissions(actor: AdminUser): string[] {
const permissions: string[] = [];
if (!actor.isSystemAdmin() || !actor.isActive()) {
return permissions;
}
// Base permissions for all admins
permissions.push(
'users.view',
'users.list'
);
// Admin permissions
if (actor.hasRole('admin')) {
permissions.push(
'users.manage',
'users.status.change',
'users.create',
'users.delete'
);
}
// Owner permissions
if (actor.hasRole('owner')) {
permissions.push(
'users.manage',
'users.roles.modify',
'users.status.change',
'users.create',
'users.delete',
'users.export'
);
}
return permissions;
}
/**
* Check if actor has specific permission
*/
static hasPermission(actor: AdminUser, permission: string): boolean {
const permissions = this.getPermissions(actor);
return permissions.includes(permission);
}
/**
* Enforce permission - throws if not authorized
*/
static enforcePermission(actor: AdminUser, permission: string): void {
if (!this.hasPermission(actor, permission)) {
throw new AuthorizationError(
`User ${actor.email.value} does not have permission: ${permission}`
);
}
}
}