- 역할/부서/개인 권한을 통합하여 최종 유효 권한 표시 - 권한 소스별 색상 구분 UI (보라=역할, 파랑=부서, 녹색=개인허용, 빨강=개인거부) - 스마트 토글 로직 (상속된 권한 오버라이드 지원) - UserPermissionService: getRolePermissions(), getDepartmentPermissions(), getPersonalOverrides() - 사용자 ID 뱃지 스타일 개선
550 lines
22 KiB
PHP
550 lines
22 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Commons\Menu;
|
|
use App\Models\Permission;
|
|
use App\Models\Tenants\Department;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class DepartmentPermissionService
|
|
{
|
|
/**
|
|
* 권한 유형 목록
|
|
*/
|
|
private array $permissionTypes = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage'];
|
|
|
|
/**
|
|
* 부서의 권한 매트릭스 조회 (permission_overrides 테이블 사용)
|
|
*
|
|
* @param int $departmentId 부서 ID
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
* @return array 메뉴별 권한 상태 매트릭스
|
|
*/
|
|
public function getDepartmentPermissionMatrix(int $departmentId, ?int $tenantId = null, string $guardName = 'api'): array
|
|
{
|
|
$now = now();
|
|
|
|
$query = DB::table('permission_overrides as po')
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->where('po.model_type', Department::class)
|
|
->where('po.model_id', $departmentId)
|
|
->where('po.effect', 1) // ALLOW만 조회
|
|
->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);
|
|
}
|
|
|
|
$departmentPermissions = $query->pluck('p.name')->toArray();
|
|
|
|
$permissions = [];
|
|
foreach ($departmentPermissions as $permName) {
|
|
if (preg_match('/^menu:(\d+)\.(\w+)$/', $permName, $matches)) {
|
|
$menuId = (int) $matches[1];
|
|
$type = $matches[2];
|
|
|
|
if (! isset($permissions[$menuId])) {
|
|
$permissions[$menuId] = [];
|
|
}
|
|
|
|
$permissions[$menuId][$type] = true;
|
|
}
|
|
}
|
|
|
|
return $permissions;
|
|
}
|
|
|
|
/**
|
|
* 특정 메뉴의 특정 권한 토글 (permission_overrides 테이블 사용)
|
|
*
|
|
* @param int $departmentId 부서 ID
|
|
* @param int $menuId 메뉴 ID
|
|
* @param string $permissionType 권한 유형
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
* @return bool 토글 후 상태 (true: 허용, false: 거부)
|
|
*/
|
|
public function togglePermission(int $departmentId, int $menuId, string $permissionType, ?int $tenantId = null, string $guardName = 'api'): bool
|
|
{
|
|
$permissionName = "menu:{$menuId}.{$permissionType}";
|
|
|
|
// 권한 생성 또는 조회
|
|
$permission = Permission::firstOrCreate(
|
|
['name' => $permissionName, 'guard_name' => $guardName],
|
|
['tenant_id' => null, 'created_by' => auth()->id()]
|
|
);
|
|
|
|
$now = now();
|
|
|
|
// 현재 권한 상태 확인 (유효한 ALLOW 오버라이드가 있는지)
|
|
$exists = DB::table('permission_overrides')
|
|
->where('model_type', Department::class)
|
|
->where('model_id', $departmentId)
|
|
->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) {
|
|
// 권한 제거 (soft delete)
|
|
DB::table('permission_overrides')
|
|
->where('model_type', Department::class)
|
|
->where('model_id', $departmentId)
|
|
->where('permission_id', $permission->id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('effect', 1)
|
|
->update([
|
|
'deleted_at' => now(),
|
|
'deleted_by' => auth()->id(),
|
|
]);
|
|
|
|
$newValue = false;
|
|
} else {
|
|
// 기존에 삭제된 레코드가 있으면 복원, 없으면 생성
|
|
$existingRecord = DB::table('permission_overrides')
|
|
->where('model_type', Department::class)
|
|
->where('model_id', $departmentId)
|
|
->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' => Department::class,
|
|
'model_id' => $departmentId,
|
|
'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(),
|
|
]);
|
|
}
|
|
|
|
$newValue = true;
|
|
}
|
|
|
|
// 하위 부서에 권한 전파
|
|
$this->propagateToChildren($departmentId, $menuId, $permissionType, $newValue, $tenantId, $guardName);
|
|
|
|
return $newValue;
|
|
}
|
|
|
|
/**
|
|
* 하위 부서에 권한 전파 (permission_overrides 테이블 사용)
|
|
*
|
|
* @param int $parentDepartmentId 부모 부서 ID
|
|
* @param int $menuId 메뉴 ID
|
|
* @param string $permissionType 권한 유형
|
|
* @param bool $value 권한 값 (true: 허용, false: 거부)
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
*/
|
|
protected function propagateToChildren(int $parentDepartmentId, int $menuId, string $permissionType, bool $value, ?int $tenantId = null, string $guardName = 'api'): void
|
|
{
|
|
$children = Department::where('parent_id', $parentDepartmentId)->get();
|
|
$now = now();
|
|
|
|
foreach ($children as $child) {
|
|
$permissionName = "menu:{$menuId}.{$permissionType}";
|
|
$permission = Permission::firstOrCreate(
|
|
['name' => $permissionName, 'guard_name' => $guardName],
|
|
['tenant_id' => null, 'created_by' => auth()->id()]
|
|
);
|
|
|
|
// 현재 권한 상태 확인 (유효한 ALLOW 오버라이드가 있는지)
|
|
$exists = DB::table('permission_overrides')
|
|
->where('model_type', Department::class)
|
|
->where('model_id', $child->id)
|
|
->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 ($value) {
|
|
// 권한 추가
|
|
if (! $exists) {
|
|
// 기존에 삭제된 레코드가 있으면 복원, 없으면 생성
|
|
$existingRecord = DB::table('permission_overrides')
|
|
->where('model_type', Department::class)
|
|
->where('model_id', $child->id)
|
|
->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' => Department::class,
|
|
'model_id' => $child->id,
|
|
'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(),
|
|
]);
|
|
}
|
|
}
|
|
} else {
|
|
// 권한 제거 (soft delete)
|
|
if ($exists) {
|
|
DB::table('permission_overrides')
|
|
->where('model_type', Department::class)
|
|
->where('model_id', $child->id)
|
|
->where('permission_id', $permission->id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('effect', 1)
|
|
->update([
|
|
'deleted_at' => now(),
|
|
'deleted_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// 재귀적으로 하위 부서 처리
|
|
$this->propagateToChildren($child->id, $menuId, $permissionType, $value, $tenantId, $guardName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 모든 권한 허용 (permission_overrides 테이블 사용)
|
|
*
|
|
* @param int $departmentId 부서 ID
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
*/
|
|
public function allowAllPermissions(int $departmentId, ?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 오버라이드가 있는지 확인
|
|
$exists = DB::table('permission_overrides')
|
|
->where('model_type', Department::class)
|
|
->where('model_id', $departmentId)
|
|
->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', Department::class)
|
|
->where('model_id', $departmentId)
|
|
->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' => Department::class,
|
|
'model_id' => $departmentId,
|
|
'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(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 모든 권한 거부 (permission_overrides 테이블에서 삭제)
|
|
*
|
|
* @param int $departmentId 부서 ID
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
*/
|
|
public function denyAllPermissions(int $departmentId, ?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 ALLOW overrides for this department
|
|
DB::table('permission_overrides')
|
|
->where('model_type', Department::class)
|
|
->where('model_id', $departmentId)
|
|
->where('permission_id', $permission->id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('effect', 1)
|
|
->whereNull('deleted_at')
|
|
->update([
|
|
'deleted_at' => now(),
|
|
'deleted_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기본 권한으로 초기화 (view만 허용)
|
|
*
|
|
* @param int $departmentId 부서 ID
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
*/
|
|
public function resetToDefaultPermissions(int $departmentId, ?int $tenantId = null, string $guardName = 'api'): void
|
|
{
|
|
// 1. 먼저 모든 권한 제거
|
|
$this->denyAllPermissions($departmentId, $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', Department::class)
|
|
->where('model_id', $departmentId)
|
|
->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', Department::class)
|
|
->where('model_id', $departmentId)
|
|
->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' => Department::class,
|
|
'model_id' => $departmentId,
|
|
'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 $departmentId 부서 ID
|
|
* @param int $menuId 메뉴 ID
|
|
* @param string $permissionType 권한 유형
|
|
* @param string $guardName Guard 이름 (api 또는 web)
|
|
* @return bool 권한 존재 여부
|
|
*/
|
|
public function hasPermission(int $departmentId, 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', Department::class)
|
|
->where('po.model_id', $departmentId)
|
|
->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();
|
|
}
|
|
}
|