- routes/api.php: 8개 엔티티의 restore 라우트를 super.admin 미들웨어 밖으로 이동 - tenants, departments, users, menus, boards - pm/projects, pm/tasks, pm/issues - UserService.canAccessUser(): withTrashed() 적용하여 soft-deleted 사용자 권한 체크 가능 - UserPermissionService.canModifyUser(): withTrashed() 적용 (일관성 유지) 권한 정책: - 복원 (Restore): 일반관리자 가능 - 영구삭제 (Force Delete): 슈퍼관리자 전용 버그 수정: - 302 Found 에러 해결 (미들웨어 블로킹) - soft-deleted 사용자 복원 시 권한 체크 실패 해결 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
845 lines
33 KiB
PHP
845 lines
33 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Commons\Menu;
|
|
use App\Models\Permission;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class UserPermissionService
|
|
{
|
|
/**
|
|
* 권한 유형 목록
|
|
*/
|
|
private array $permissionTypes = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage'];
|
|
|
|
/**
|
|
* 사용자의 권한 매트릭스 조회 (역할 + 부서 + 개인 오버라이드 통합)
|
|
* 각 권한에 대해 최종 상태와 소스 정보 반환
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
* @return array 메뉴별 권한 상태 매트릭스
|
|
* [menuId][type] = ['effective' => 'allow'|'deny'|null, 'source' => 'role'|'department'|'personal'|null, 'personal' => 'allow'|'deny'|null]
|
|
*/
|
|
public function getUserPermissionMatrix(int $userId, ?int $tenantId = null, string $guardName = 'api'): array
|
|
{
|
|
$now = now();
|
|
|
|
// 1. 역할 권한 조회 (Spatie)
|
|
$rolePermissions = $this->getRolePermissions($userId, $guardName);
|
|
|
|
// 2. 부서 권한 조회 (permission_overrides with Department)
|
|
$departmentPermissions = $this->getDepartmentPermissions($userId, $tenantId, $guardName);
|
|
|
|
// 3. 개인 오버라이드 조회 (permission_overrides with User)
|
|
$personalOverrides = $this->getPersonalOverrides($userId, $tenantId, $guardName);
|
|
|
|
// 4. 통합 매트릭스 생성
|
|
$permissions = [];
|
|
|
|
// 모든 메뉴 ID 수집
|
|
$allMenuIds = array_unique(array_merge(
|
|
array_keys($rolePermissions),
|
|
array_keys($departmentPermissions),
|
|
array_keys($personalOverrides)
|
|
));
|
|
|
|
foreach ($allMenuIds as $menuId) {
|
|
if (! isset($permissions[$menuId])) {
|
|
$permissions[$menuId] = [];
|
|
}
|
|
|
|
foreach ($this->permissionTypes as $type) {
|
|
$hasRole = isset($rolePermissions[$menuId][$type]) && $rolePermissions[$menuId][$type];
|
|
$hasDept = isset($departmentPermissions[$menuId][$type]) && $departmentPermissions[$menuId][$type];
|
|
$personal = $personalOverrides[$menuId][$type] ?? null;
|
|
|
|
// 최종 권한 계산 (API AccessService 우선순위와 동일)
|
|
// 1) 개인 DENY → 거부
|
|
// 2) 역할 권한 → 허용
|
|
// 3) 부서 ALLOW → 허용
|
|
// 4) 개인 ALLOW → 허용
|
|
// 5) 기본 → 없음
|
|
|
|
$effective = null;
|
|
$source = null;
|
|
|
|
if ($personal === 'deny') {
|
|
$effective = 'deny';
|
|
$source = 'personal';
|
|
} elseif ($hasRole) {
|
|
$effective = 'allow';
|
|
$source = 'role';
|
|
} elseif ($hasDept) {
|
|
$effective = 'allow';
|
|
$source = 'department';
|
|
} elseif ($personal === 'allow') {
|
|
$effective = 'allow';
|
|
$source = 'personal';
|
|
}
|
|
|
|
$permissions[$menuId][$type] = [
|
|
'effective' => $effective,
|
|
'source' => $source,
|
|
'personal' => $personal,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $permissions;
|
|
}
|
|
|
|
/**
|
|
* 역할 권한 조회 (Spatie model_has_roles + role_has_permissions)
|
|
*/
|
|
private function getRolePermissions(int $userId, string $guardName): array
|
|
{
|
|
$rolePermissions = DB::table('model_has_roles as mhr')
|
|
->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'mhr.role_id')
|
|
->join('permissions as p', 'p.id', '=', 'rhp.permission_id')
|
|
->where('mhr.model_type', User::class)
|
|
->where('mhr.model_id', $userId)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', 'like', 'menu:%')
|
|
->pluck('p.name')
|
|
->toArray();
|
|
|
|
$result = [];
|
|
foreach ($rolePermissions as $permName) {
|
|
if (preg_match('/^menu:(\d+)\.(\w+)$/', $permName, $matches)) {
|
|
$menuId = (int) $matches[1];
|
|
$type = $matches[2];
|
|
|
|
if (! isset($result[$menuId])) {
|
|
$result[$menuId] = [];
|
|
}
|
|
$result[$menuId][$type] = true;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 부서 권한 조회 (permission_overrides with Department)
|
|
*/
|
|
private function getDepartmentPermissions(int $userId, ?int $tenantId, string $guardName): array
|
|
{
|
|
$now = now();
|
|
|
|
$query = DB::table('department_user as du')
|
|
->join('permission_overrides as po', function ($j) use ($now) {
|
|
$j->on('po.model_id', '=', 'du.department_id')
|
|
->where('po.model_type', 'App\\Models\\Tenants\\Department')
|
|
->whereNull('po.deleted_at')
|
|
->where('po.effect', 1)
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now);
|
|
})
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
|
});
|
|
})
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->whereNull('du.deleted_at')
|
|
->where('du.user_id', $userId)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', 'like', 'menu:%');
|
|
|
|
if ($tenantId) {
|
|
$query->where('du.tenant_id', $tenantId)
|
|
->where('po.tenant_id', $tenantId);
|
|
}
|
|
|
|
$deptPermissions = $query->pluck('p.name')->toArray();
|
|
|
|
$result = [];
|
|
foreach ($deptPermissions as $permName) {
|
|
if (preg_match('/^menu:(\d+)\.(\w+)$/', $permName, $matches)) {
|
|
$menuId = (int) $matches[1];
|
|
$type = $matches[2];
|
|
|
|
if (! isset($result[$menuId])) {
|
|
$result[$menuId] = [];
|
|
}
|
|
$result[$menuId][$type] = true;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 개인 오버라이드 조회 (permission_overrides with User)
|
|
*/
|
|
private function getPersonalOverrides(int $userId, ?int $tenantId, string $guardName): array
|
|
{
|
|
$now = now();
|
|
|
|
$query = DB::table('permission_overrides as po')
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->select('p.name', 'po.effect')
|
|
->where('po.model_type', User::class)
|
|
->where('po.model_id', $userId)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', 'like', 'menu:%')
|
|
->whereNull('po.deleted_at')
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now);
|
|
})
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
|
});
|
|
|
|
if ($tenantId) {
|
|
$query->where('po.tenant_id', $tenantId);
|
|
}
|
|
|
|
$userPermissions = $query->get();
|
|
|
|
$result = [];
|
|
foreach ($userPermissions as $perm) {
|
|
if (preg_match('/^menu:(\d+)\.(\w+)$/', $perm->name, $matches)) {
|
|
$menuId = (int) $matches[1];
|
|
$type = $matches[2];
|
|
|
|
if (! isset($result[$menuId])) {
|
|
$result[$menuId] = [];
|
|
}
|
|
$result[$menuId][$type] = $perm->effect == 1 ? 'allow' : 'deny';
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 특정 메뉴의 특정 권한 토글 (스마트 토글)
|
|
* - 역할/부서 권한 있음 (개인 오버라이드 없음) → 개인 DENY 추가
|
|
* - 개인 DENY → 제거 (역할/부서 권한으로 복원 또는 미설정)
|
|
* - 미설정 (권한 없음) → 개인 ALLOW 추가
|
|
* - 개인 ALLOW → 개인 DENY로 변경
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int $menuId 메뉴 ID
|
|
* @param string $permissionType 권한 유형
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
* @return string|null 토글 후 개인 오버라이드 상태 (null: 미설정, 'allow': 허용, 'deny': 거부)
|
|
*/
|
|
public function togglePermission(int $userId, int $menuId, string $permissionType, ?int $tenantId = null, string $guardName = 'api'): ?string
|
|
{
|
|
$permissionName = "menu:{$menuId}.{$permissionType}";
|
|
|
|
// 권한 생성 또는 조회
|
|
$permission = Permission::firstOrCreate(
|
|
['name' => $permissionName, 'guard_name' => $guardName],
|
|
['tenant_id' => null, 'created_by' => auth()->id()]
|
|
);
|
|
|
|
$now = now();
|
|
|
|
// 현재 개인 오버라이드 조회
|
|
$currentOverride = DB::table('permission_overrides')
|
|
->where('model_type', User::class)
|
|
->where('model_id', $userId)
|
|
->where('permission_id', $permission->id)
|
|
->where('tenant_id', $tenantId)
|
|
->whereNull('deleted_at')
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('effective_from')->orWhere('effective_from', '<=', $now);
|
|
})
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('effective_to')->orWhere('effective_to', '>=', $now);
|
|
})
|
|
->first();
|
|
|
|
// 역할/부서 권한 확인
|
|
$hasRolePermission = $this->hasRolePermission($userId, $permissionName, $guardName);
|
|
$hasDeptPermission = $this->hasDeptPermission($userId, $permissionName, $tenantId, $guardName);
|
|
$hasInheritedPermission = $hasRolePermission || $hasDeptPermission;
|
|
|
|
// 스마트 토글 로직
|
|
if ($currentOverride) {
|
|
if ($currentOverride->effect == 0) {
|
|
// 개인 DENY → 제거 (역할/부서로 복원 또는 미설정)
|
|
DB::table('permission_overrides')
|
|
->where('id', $currentOverride->id)
|
|
->update([
|
|
'deleted_at' => now(),
|
|
'deleted_by' => auth()->id(),
|
|
]);
|
|
|
|
return null;
|
|
} else {
|
|
// 개인 ALLOW → 개인 DENY
|
|
DB::table('permission_overrides')
|
|
->where('id', $currentOverride->id)
|
|
->update([
|
|
'effect' => 0, // DENY
|
|
'updated_at' => now(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
return 'deny';
|
|
}
|
|
} else {
|
|
// 개인 오버라이드 없음
|
|
if ($hasInheritedPermission) {
|
|
// 역할/부서 권한 있음 → 개인 DENY 추가 (오버라이드)
|
|
$this->createPersonalOverride($userId, $permission->id, $tenantId, 0); // DENY
|
|
|
|
return 'deny';
|
|
} else {
|
|
// 권한 없음 → 개인 ALLOW 추가
|
|
$this->createPersonalOverride($userId, $permission->id, $tenantId, 1); // ALLOW
|
|
|
|
return 'allow';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 역할 권한 존재 여부 확인
|
|
*/
|
|
private function hasRolePermission(int $userId, string $permissionName, string $guardName): bool
|
|
{
|
|
return DB::table('model_has_roles as mhr')
|
|
->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'mhr.role_id')
|
|
->join('permissions as p', 'p.id', '=', 'rhp.permission_id')
|
|
->where('mhr.model_type', User::class)
|
|
->where('mhr.model_id', $userId)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', $permissionName)
|
|
->exists();
|
|
}
|
|
|
|
/**
|
|
* 부서 권한 존재 여부 확인
|
|
*/
|
|
private function hasDeptPermission(int $userId, string $permissionName, ?int $tenantId, string $guardName): bool
|
|
{
|
|
$now = now();
|
|
|
|
$query = DB::table('department_user as du')
|
|
->join('permission_overrides as po', function ($j) use ($now) {
|
|
$j->on('po.model_id', '=', 'du.department_id')
|
|
->where('po.model_type', 'App\\Models\\Tenants\\Department')
|
|
->whereNull('po.deleted_at')
|
|
->where('po.effect', 1)
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now);
|
|
})
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
|
});
|
|
})
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->whereNull('du.deleted_at')
|
|
->where('du.user_id', $userId)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', $permissionName);
|
|
|
|
if ($tenantId) {
|
|
$query->where('du.tenant_id', $tenantId)
|
|
->where('po.tenant_id', $tenantId);
|
|
}
|
|
|
|
return $query->exists();
|
|
}
|
|
|
|
/**
|
|
* 개인 오버라이드 생성 (삭제된 레코드 복원 또는 새로 생성)
|
|
*/
|
|
private function createPersonalOverride(int $userId, int $permissionId, ?int $tenantId, int $effect): void
|
|
{
|
|
$deletedRecord = DB::table('permission_overrides')
|
|
->where('model_type', User::class)
|
|
->where('model_id', $userId)
|
|
->where('permission_id', $permissionId)
|
|
->where('tenant_id', $tenantId)
|
|
->whereNotNull('deleted_at')
|
|
->first();
|
|
|
|
if ($deletedRecord) {
|
|
DB::table('permission_overrides')
|
|
->where('id', $deletedRecord->id)
|
|
->update([
|
|
'effect' => $effect,
|
|
'deleted_at' => null,
|
|
'deleted_by' => null,
|
|
'updated_at' => now(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
} else {
|
|
DB::table('permission_overrides')->insert([
|
|
'tenant_id' => $tenantId,
|
|
'model_type' => User::class,
|
|
'model_id' => $userId,
|
|
'permission_id' => $permissionId,
|
|
'effect' => $effect,
|
|
'reason' => null,
|
|
'effective_from' => null,
|
|
'effective_to' => null,
|
|
'created_at' => now(),
|
|
'created_by' => auth()->id(),
|
|
'updated_at' => now(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 모든 권한 허용 (permission_overrides 테이블 사용)
|
|
* 모든 메뉴에 대해 ALLOW 상태로 설정 (기존 DENY 포함하여 모두 ALLOW로 변경)
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
*/
|
|
public function allowAllPermissions(int $userId, ?int $tenantId = null, string $guardName = 'api'): void
|
|
{
|
|
$query = Menu::where('is_active', 1);
|
|
if ($tenantId) {
|
|
$query->where('tenant_id', $tenantId);
|
|
}
|
|
$menus = $query->get();
|
|
|
|
$now = now();
|
|
|
|
foreach ($menus as $menu) {
|
|
foreach ($this->permissionTypes as $type) {
|
|
$permissionName = "menu:{$menu->id}.{$type}";
|
|
$permission = Permission::firstOrCreate(
|
|
['name' => $permissionName, 'guard_name' => $guardName],
|
|
['tenant_id' => null, 'created_by' => auth()->id()]
|
|
);
|
|
|
|
// 현재 유효한 오버라이드 확인 (ALLOW 또는 DENY)
|
|
$existingOverride = DB::table('permission_overrides')
|
|
->where('model_type', User::class)
|
|
->where('model_id', $userId)
|
|
->where('permission_id', $permission->id)
|
|
->where('tenant_id', $tenantId)
|
|
->whereNull('deleted_at')
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('effective_from')->orWhere('effective_from', '<=', $now);
|
|
})
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('effective_to')->orWhere('effective_to', '>=', $now);
|
|
})
|
|
->first();
|
|
|
|
if ($existingOverride) {
|
|
// 기존 오버라이드가 있으면 ALLOW로 변경
|
|
if ($existingOverride->effect != 1) {
|
|
DB::table('permission_overrides')
|
|
->where('id', $existingOverride->id)
|
|
->update([
|
|
'effect' => 1, // ALLOW
|
|
'updated_at' => now(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
}
|
|
} else {
|
|
// 삭제된 레코드가 있으면 복원, 없으면 생성
|
|
$deletedRecord = DB::table('permission_overrides')
|
|
->where('model_type', User::class)
|
|
->where('model_id', $userId)
|
|
->where('permission_id', $permission->id)
|
|
->where('tenant_id', $tenantId)
|
|
->whereNotNull('deleted_at')
|
|
->first();
|
|
|
|
if ($deletedRecord) {
|
|
DB::table('permission_overrides')
|
|
->where('id', $deletedRecord->id)
|
|
->update([
|
|
'effect' => 1, // ALLOW
|
|
'deleted_at' => null,
|
|
'deleted_by' => null,
|
|
'updated_at' => now(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
} else {
|
|
DB::table('permission_overrides')->insert([
|
|
'tenant_id' => $tenantId,
|
|
'model_type' => User::class,
|
|
'model_id' => $userId,
|
|
'permission_id' => $permission->id,
|
|
'effect' => 1, // ALLOW
|
|
'reason' => null,
|
|
'effective_from' => null,
|
|
'effective_to' => null,
|
|
'created_at' => now(),
|
|
'created_by' => auth()->id(),
|
|
'updated_at' => now(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 모든 권한 초기화 (모두 미설정으로)
|
|
* 모든 오버라이드 레코드를 soft delete하여 미설정 상태로 초기화
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
*/
|
|
public function denyAllPermissions(int $userId, ?int $tenantId = null, string $guardName = 'api'): void
|
|
{
|
|
$query = Menu::where('is_active', 1);
|
|
if ($tenantId) {
|
|
$query->where('tenant_id', $tenantId);
|
|
}
|
|
$menus = $query->get();
|
|
|
|
foreach ($menus as $menu) {
|
|
foreach ($this->permissionTypes as $type) {
|
|
$permissionName = "menu:{$menu->id}.{$type}";
|
|
$permission = Permission::where('name', $permissionName)
|
|
->where('guard_name', $guardName)
|
|
->first();
|
|
|
|
if ($permission) {
|
|
// Soft delete all overrides (ALLOW or DENY) for this user
|
|
DB::table('permission_overrides')
|
|
->where('model_type', User::class)
|
|
->where('model_id', $userId)
|
|
->where('permission_id', $permission->id)
|
|
->where('tenant_id', $tenantId)
|
|
->whereNull('deleted_at')
|
|
->update([
|
|
'deleted_at' => now(),
|
|
'deleted_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기본 권한으로 초기화 (view만 허용)
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
*/
|
|
public function resetToDefaultPermissions(int $userId, ?int $tenantId = null, string $guardName = 'api'): void
|
|
{
|
|
// 1. 먼저 모든 권한 제거
|
|
$this->denyAllPermissions($userId, $tenantId, $guardName);
|
|
|
|
// 2. view 권한만 허용
|
|
$query = Menu::where('is_active', 1);
|
|
if ($tenantId) {
|
|
$query->where('tenant_id', $tenantId);
|
|
}
|
|
$menus = $query->get();
|
|
|
|
$now = now();
|
|
|
|
foreach ($menus as $menu) {
|
|
$permissionName = "menu:{$menu->id}.view";
|
|
$permission = Permission::firstOrCreate(
|
|
['name' => $permissionName, 'guard_name' => $guardName],
|
|
['tenant_id' => null, 'created_by' => auth()->id()]
|
|
);
|
|
|
|
// 이미 유효한 ALLOW 오버라이드가 있는지 확인
|
|
$exists = DB::table('permission_overrides')
|
|
->where('model_type', User::class)
|
|
->where('model_id', $userId)
|
|
->where('permission_id', $permission->id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('effect', 1)
|
|
->whereNull('deleted_at')
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('effective_from')->orWhere('effective_from', '<=', $now);
|
|
})
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('effective_to')->orWhere('effective_to', '>=', $now);
|
|
})
|
|
->exists();
|
|
|
|
if (! $exists) {
|
|
// 기존에 삭제된 레코드가 있으면 복원, 없으면 생성
|
|
$existingRecord = DB::table('permission_overrides')
|
|
->where('model_type', User::class)
|
|
->where('model_id', $userId)
|
|
->where('permission_id', $permission->id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('effect', 1)
|
|
->first();
|
|
|
|
if ($existingRecord) {
|
|
DB::table('permission_overrides')
|
|
->where('id', $existingRecord->id)
|
|
->update([
|
|
'deleted_at' => null,
|
|
'deleted_by' => null,
|
|
'updated_at' => now(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
} else {
|
|
DB::table('permission_overrides')->insert([
|
|
'tenant_id' => $tenantId,
|
|
'model_type' => User::class,
|
|
'model_id' => $userId,
|
|
'permission_id' => $permission->id,
|
|
'effect' => 1, // ALLOW
|
|
'reason' => null,
|
|
'effective_from' => null,
|
|
'effective_to' => null,
|
|
'created_at' => now(),
|
|
'created_by' => auth()->id(),
|
|
'updated_at' => now(),
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 메뉴 트리 조회 (권한 매트릭스 표시용)
|
|
*
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @return \Illuminate\Support\Collection 메뉴 트리
|
|
*/
|
|
public function getMenuTree(?int $tenantId = null): \Illuminate\Support\Collection
|
|
{
|
|
$query = Menu::with('parent')
|
|
->where('is_active', 1);
|
|
|
|
if ($tenantId) {
|
|
$query->where('tenant_id', $tenantId);
|
|
}
|
|
|
|
$allMenus = $query->orderBy('sort_order', 'asc')
|
|
->orderBy('id', 'asc')
|
|
->get();
|
|
|
|
// depth 계산하여 플랫한 구조로 변환
|
|
return $this->flattenMenuTree($allMenus);
|
|
}
|
|
|
|
/**
|
|
* 트리 구조를 플랫한 배열로 변환 (depth 정보 포함)
|
|
*
|
|
* @param \Illuminate\Support\Collection $menus 메뉴 컬렉션
|
|
* @param int|null $parentId 부모 메뉴 ID
|
|
* @param int $depth 현재 깊이
|
|
*/
|
|
private function flattenMenuTree(\Illuminate\Support\Collection $menus, ?int $parentId = null, int $depth = 0): \Illuminate\Support\Collection
|
|
{
|
|
$result = collect();
|
|
|
|
$filteredMenus = $menus->where('parent_id', $parentId)->sortBy('sort_order');
|
|
|
|
foreach ($filteredMenus as $menu) {
|
|
$menu->depth = $depth;
|
|
|
|
// 자식 메뉴 존재 여부 확인
|
|
$menu->has_children = $menus->where('parent_id', $menu->id)->count() > 0;
|
|
|
|
$result->push($menu);
|
|
|
|
// 자식 메뉴 재귀적으로 추가
|
|
$children = $this->flattenMenuTree($menus, $menu->id, $depth + 1);
|
|
$result = $result->merge($children);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 특정 사용자의 활성 메뉴 권한 확인 (permission_overrides 테이블 사용)
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int $menuId 메뉴 ID
|
|
* @param string $permissionType 권한 유형
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
* @return bool 권한 존재 여부
|
|
*/
|
|
public function hasPermission(int $userId, int $menuId, string $permissionType, string $guardName = 'api'): bool
|
|
{
|
|
$permissionName = "menu:{$menuId}.{$permissionType}";
|
|
$now = now();
|
|
|
|
return DB::table('permission_overrides as po')
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->where('po.model_type', User::class)
|
|
->where('po.model_id', $userId)
|
|
->where('po.effect', 1)
|
|
->where('p.name', $permissionName)
|
|
->where('p.guard_name', $guardName)
|
|
->whereNull('po.deleted_at')
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now);
|
|
})
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
|
})
|
|
->exists();
|
|
}
|
|
|
|
/**
|
|
* 대상 사용자가 슈퍼관리자인지 검증 (일반관리자는 슈퍼관리자 수정 불가)
|
|
*
|
|
* @param int $targetUserId 대상 사용자 ID
|
|
* @return bool true면 수정 가능, false면 수정 불가
|
|
*/
|
|
public function canModifyUser(int $targetUserId): bool
|
|
{
|
|
// withTrashed()를 사용하여 일관성 유지
|
|
$targetUser = User::withTrashed()->find($targetUserId);
|
|
$currentUser = auth()->user();
|
|
|
|
// 대상 사용자가 슈퍼관리자이고 현재 사용자가 슈퍼관리자가 아니면 수정 불가
|
|
if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 테넌트별 사용자 목록 조회 (권한 개수 포함)
|
|
* 일반관리자는 슈퍼관리자가 목록에서 제외됨
|
|
*
|
|
* @param int $tenantId 테넌트 ID
|
|
* @return \Illuminate\Support\Collection 사용자 목록
|
|
*/
|
|
public function getUsersByTenant(int $tenantId): \Illuminate\Support\Collection
|
|
{
|
|
$currentUser = auth()->user();
|
|
|
|
$query = User::whereHas('tenants', function ($query) use ($tenantId) {
|
|
$query->where('tenants.id', $tenantId)
|
|
->where('user_tenants.is_active', true);
|
|
})
|
|
->where('is_active', true);
|
|
|
|
// 일반관리자는 슈퍼관리자를 볼 수 없음
|
|
if (! $currentUser?->is_super_admin) {
|
|
$query->where('is_super_admin', false);
|
|
}
|
|
|
|
$users = $query->orderBy('name')->get();
|
|
|
|
// 각 사용자별 권한 개수 계산
|
|
$now = now();
|
|
foreach ($users as $user) {
|
|
$permissionCounts = $this->getUserPermissionCounts($user->id, $tenantId, $now);
|
|
$user->web_permission_count = $permissionCounts['web'];
|
|
$user->api_permission_count = $permissionCounts['api'];
|
|
}
|
|
|
|
return $users;
|
|
}
|
|
|
|
/**
|
|
* 사용자별 guard별 권한 개수 조회 (역할 + 부서 + 개인 오버라이드 통합)
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int $tenantId 테넌트 ID
|
|
* @param \Carbon\Carbon $now 현재 시간
|
|
* @return array ['web' => int, 'api' => int]
|
|
*/
|
|
private function getUserPermissionCounts(int $userId, int $tenantId, $now): array
|
|
{
|
|
$result = ['web' => 0, 'api' => 0];
|
|
|
|
foreach (['web', 'api'] as $guardName) {
|
|
// 1. 역할 권한
|
|
$rolePermissions = DB::table('model_has_roles as mhr')
|
|
->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'mhr.role_id')
|
|
->join('permissions as p', 'p.id', '=', 'rhp.permission_id')
|
|
->where('mhr.model_type', User::class)
|
|
->where('mhr.model_id', $userId)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', 'like', 'menu:%')
|
|
->pluck('p.name')
|
|
->toArray();
|
|
|
|
// 2. 부서 권한
|
|
$deptPermissions = DB::table('department_user as du')
|
|
->join('permission_overrides as po', function ($j) use ($now, $tenantId) {
|
|
$j->on('po.model_id', '=', 'du.department_id')
|
|
->where('po.model_type', 'App\\Models\\Tenants\\Department')
|
|
->where('po.tenant_id', $tenantId)
|
|
->whereNull('po.deleted_at')
|
|
->where('po.effect', 1)
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now);
|
|
})
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
|
});
|
|
})
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->whereNull('du.deleted_at')
|
|
->where('du.user_id', $userId)
|
|
->where('du.tenant_id', $tenantId)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', 'like', 'menu:%')
|
|
->pluck('p.name')
|
|
->toArray();
|
|
|
|
// 3. 개인 오버라이드 (ALLOW)
|
|
$personalAllows = DB::table('permission_overrides as po')
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->where('po.model_type', User::class)
|
|
->where('po.model_id', $userId)
|
|
->where('po.tenant_id', $tenantId)
|
|
->where('po.effect', 1)
|
|
->whereNull('po.deleted_at')
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now);
|
|
})
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
|
})
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', 'like', 'menu:%')
|
|
->pluck('p.name')
|
|
->toArray();
|
|
|
|
// 4. 개인 오버라이드 (DENY) - 제외할 권한
|
|
$personalDenies = DB::table('permission_overrides as po')
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->where('po.model_type', User::class)
|
|
->where('po.model_id', $userId)
|
|
->where('po.tenant_id', $tenantId)
|
|
->where('po.effect', 0)
|
|
->whereNull('po.deleted_at')
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now);
|
|
})
|
|
->where(function ($w) use ($now) {
|
|
$w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now);
|
|
})
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', 'like', 'menu:%')
|
|
->pluck('p.name')
|
|
->toArray();
|
|
|
|
// 통합: (역할 OR 부서 OR 개인ALLOW) - 개인DENY
|
|
$allAllowed = array_unique(array_merge($rolePermissions, $deptPermissions, $personalAllows));
|
|
$effectivePermissions = array_diff($allAllowed, $personalDenies);
|
|
|
|
$result[$guardName] = count($effectivePermissions);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|