- 메뉴별 권한 분석 기능 (접근 가능/불가 사용자 목록) - 사용자 역추적 기능 (역할/부서/개인별 권한 추적) - CSV 내보내기 기능 - 트리 구조 시각화 (└─ 연결선, 폴더/문서 아이콘) - 중복 메뉴 표시 문제 해결 (테넌트별 메뉴만 표시)
526 lines
19 KiB
PHP
526 lines
19 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Commons\Menu;
|
|
use App\Models\Tenants\Department;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class PermissionAnalyzeService
|
|
{
|
|
/**
|
|
* 권한 유형 목록
|
|
*/
|
|
private array $permissionTypes = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage'];
|
|
|
|
/**
|
|
* 메뉴 트리 조회 (권한 분석용)
|
|
*
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string|null $search 검색어
|
|
* @param bool $onlyAccessible 접근 가능한 메뉴만 필터링
|
|
* @return Collection 메뉴 트리
|
|
*/
|
|
public function getMenuTree(?int $tenantId = null, ?string $search = null, bool $onlyAccessible = false): Collection
|
|
{
|
|
$query = Menu::with('parent')
|
|
->where('is_active', 1);
|
|
|
|
if ($tenantId) {
|
|
// 특정 테넌트: 해당 테넌트 메뉴만 (각 테넌트별로 메뉴가 복제되어 있음)
|
|
$query->where('tenant_id', $tenantId);
|
|
} else {
|
|
// 전체 보기: 공통 메뉴만
|
|
$query->whereNull('tenant_id');
|
|
}
|
|
|
|
if ($search) {
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('url', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
$allMenus = $query->orderBy('sort_order', 'asc')
|
|
->orderBy('id', 'asc')
|
|
->get();
|
|
|
|
return $this->flattenMenuTree($allMenus);
|
|
}
|
|
|
|
/**
|
|
* 트리 구조를 플랫한 배열로 변환 (depth 정보 포함)
|
|
*/
|
|
private function flattenMenuTree(Collection $menus, ?int $parentId = null, int $depth = 0): 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;
|
|
}
|
|
|
|
/**
|
|
* 특정 메뉴의 권한 분석 (접근 가능/불가능 사용자 목록)
|
|
*
|
|
* @param int $menuId 메뉴 ID
|
|
* @param string $permissionType 권한 유형 (view, create, update, delete, approve)
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름
|
|
* @return array 분석 결과
|
|
*/
|
|
public function analyzeMenuPermission(int $menuId, string $permissionType = 'view', ?int $tenantId = null, string $guardName = 'api'): array
|
|
{
|
|
$menu = Menu::find($menuId);
|
|
if (! $menu) {
|
|
return ['error' => '메뉴를 찾을 수 없습니다.'];
|
|
}
|
|
|
|
$permissionName = "menu:{$menuId}.{$permissionType}";
|
|
|
|
// 테넌트별 사용자 목록 조회
|
|
$users = $this->getUsersByTenant($tenantId);
|
|
|
|
$accessAllowed = [];
|
|
$accessDenied = [];
|
|
|
|
foreach ($users as $user) {
|
|
$analysis = $this->analyzeUserPermission($user->id, $menuId, $permissionType, $tenantId, $guardName);
|
|
|
|
$userInfo = [
|
|
'id' => $user->id,
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'departments' => $this->getUserDepartments($user->id, $tenantId),
|
|
'roles' => $this->getUserRoles($user->id, $tenantId),
|
|
'personal_override' => $analysis['personal'],
|
|
'source' => $analysis['source'],
|
|
'effective' => $analysis['effective'],
|
|
];
|
|
|
|
if ($analysis['effective'] === 'allow') {
|
|
$accessAllowed[] = $userInfo;
|
|
} else {
|
|
$accessDenied[] = $userInfo;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'menu' => [
|
|
'id' => $menu->id,
|
|
'name' => $menu->name,
|
|
'url' => $menu->url,
|
|
],
|
|
'permission_type' => $permissionType,
|
|
'permission_rule' => 'ALLOW = 부서 OR 역할 OR (개인 ALLOW) → 개인 DENY 최우선',
|
|
'access_allowed' => $accessAllowed,
|
|
'access_denied' => $accessDenied,
|
|
'summary' => [
|
|
'total_users' => count($users),
|
|
'allowed_count' => count($accessAllowed),
|
|
'denied_count' => count($accessDenied),
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 특정 사용자의 특정 메뉴 권한 분석
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int $menuId 메뉴 ID
|
|
* @param string $permissionType 권한 유형
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름
|
|
* @return array 분석 결과
|
|
*/
|
|
public function analyzeUserPermission(int $userId, int $menuId, string $permissionType = 'view', ?int $tenantId = null, string $guardName = 'api'): array
|
|
{
|
|
$permissionName = "menu:{$menuId}.{$permissionType}";
|
|
$now = now();
|
|
|
|
// 1. 역할 권한 확인
|
|
$hasRolePermission = $this->checkRolePermission($userId, $permissionName, $guardName);
|
|
|
|
// 2. 부서 권한 확인
|
|
$hasDeptPermission = $this->checkDepartmentPermission($userId, $permissionName, $tenantId, $guardName);
|
|
|
|
// 3. 개인 오버라이드 확인
|
|
$personalOverride = $this->checkPersonalOverride($userId, $permissionName, $tenantId, $guardName);
|
|
|
|
// 최종 권한 계산 (AccessService와 동일한 우선순위)
|
|
// 1) 개인 DENY → 거부
|
|
// 2) 역할 권한 → 허용
|
|
// 3) 부서 ALLOW → 허용
|
|
// 4) 개인 ALLOW → 허용
|
|
// 5) 기본 → 없음 (거부)
|
|
|
|
$effective = null;
|
|
$source = null;
|
|
|
|
if ($personalOverride === 'deny') {
|
|
$effective = 'deny';
|
|
$source = 'personal';
|
|
} elseif ($hasRolePermission) {
|
|
$effective = 'allow';
|
|
$source = 'role';
|
|
} elseif ($hasDeptPermission) {
|
|
$effective = 'allow';
|
|
$source = 'department';
|
|
} elseif ($personalOverride === 'allow') {
|
|
$effective = 'allow';
|
|
$source = 'personal';
|
|
} else {
|
|
$effective = 'deny';
|
|
$source = null;
|
|
}
|
|
|
|
return [
|
|
'effective' => $effective,
|
|
'source' => $source,
|
|
'role' => $hasRolePermission,
|
|
'department' => $hasDeptPermission,
|
|
'personal' => $personalOverride,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 역할 권한 확인
|
|
*/
|
|
private function checkRolePermission(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 checkDepartmentPermission(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', Department::class)
|
|
->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 checkPersonalOverride(int $userId, string $permissionName, ?int $tenantId, string $guardName): ?string
|
|
{
|
|
$now = now();
|
|
|
|
$query = DB::table('permission_overrides as po')
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->select('po.effect')
|
|
->where('po.model_type', User::class)
|
|
->where('po.model_id', $userId)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', $permissionName)
|
|
->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);
|
|
}
|
|
|
|
$override = $query->first();
|
|
|
|
if (! $override) {
|
|
return null;
|
|
}
|
|
|
|
return $override->effect == 1 ? 'allow' : 'deny';
|
|
}
|
|
|
|
/**
|
|
* 테넌트별 사용자 목록 조회
|
|
*/
|
|
private function getUsersByTenant(?int $tenantId): Collection
|
|
{
|
|
$query = User::where('is_active', true);
|
|
|
|
if ($tenantId) {
|
|
$query->whereHas('tenants', function ($q) use ($tenantId) {
|
|
$q->where('tenants.id', $tenantId)
|
|
->where('user_tenants.is_active', true);
|
|
});
|
|
}
|
|
|
|
return $query->orderBy('name')->get();
|
|
}
|
|
|
|
/**
|
|
* 사용자의 부서 목록 조회
|
|
*/
|
|
private function getUserDepartments(int $userId, ?int $tenantId): array
|
|
{
|
|
$query = DB::table('department_user as du')
|
|
->join('departments as d', 'd.id', '=', 'du.department_id')
|
|
->select('d.id', 'd.name', 'd.code', 'du.is_primary')
|
|
->where('du.user_id', $userId)
|
|
->whereNull('du.deleted_at')
|
|
->whereNull('d.deleted_at');
|
|
|
|
if ($tenantId) {
|
|
$query->where('du.tenant_id', $tenantId);
|
|
}
|
|
|
|
return $query->get()->map(function ($dept) {
|
|
return [
|
|
'id' => $dept->id,
|
|
'name' => $dept->name,
|
|
'code' => $dept->code,
|
|
'is_primary' => $dept->is_primary,
|
|
];
|
|
})->toArray();
|
|
}
|
|
|
|
/**
|
|
* 사용자의 역할 목록 조회
|
|
*/
|
|
private function getUserRoles(int $userId, ?int $tenantId): array
|
|
{
|
|
$query = DB::table('user_roles as ur')
|
|
->join('roles as r', 'r.id', '=', 'ur.role_id')
|
|
->select('r.id', 'r.name', 'r.display_name')
|
|
->where('ur.user_id', $userId)
|
|
->whereNull('ur.deleted_at');
|
|
|
|
if ($tenantId) {
|
|
$query->where('ur.tenant_id', $tenantId);
|
|
}
|
|
|
|
return $query->get()->map(function ($role) {
|
|
return [
|
|
'id' => $role->id,
|
|
'name' => $role->name,
|
|
'display_name' => $role->display_name,
|
|
];
|
|
})->toArray();
|
|
}
|
|
|
|
/**
|
|
* 사용자 역추적 - 특정 권한을 가진 모든 사용자 검색
|
|
*
|
|
* @param int $menuId 메뉴 ID
|
|
* @param string $permissionType 권한 유형
|
|
* @param int|null $tenantId 테넌트 ID
|
|
* @param string $guardName Guard 이름
|
|
* @return array 역추적 결과
|
|
*/
|
|
public function traceUsersWithPermission(int $menuId, string $permissionType = 'view', ?int $tenantId = null, string $guardName = 'api'): array
|
|
{
|
|
$permissionName = "menu:{$menuId}.{$permissionType}";
|
|
|
|
// 역할로 권한이 있는 사용자
|
|
$usersFromRole = 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')
|
|
->join('users as u', 'u.id', '=', 'mhr.model_id')
|
|
->join('roles as r', 'r.id', '=', 'mhr.role_id')
|
|
->select('u.id as user_id', 'u.name as user_name', 'u.email', 'r.id as role_id', 'r.name as role_name', 'r.display_name as role_display_name')
|
|
->where('mhr.model_type', User::class)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', $permissionName)
|
|
->where('u.is_active', true)
|
|
->get();
|
|
|
|
// 부서로 권한이 있는 사용자
|
|
$now = now();
|
|
$usersFromDepartment = 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', Department::class)
|
|
->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')
|
|
->join('users as u', 'u.id', '=', 'du.user_id')
|
|
->join('departments as d', 'd.id', '=', 'du.department_id')
|
|
->select('u.id as user_id', 'u.name as user_name', 'u.email', 'd.id as department_id', 'd.name as department_name', 'd.code as department_code')
|
|
->whereNull('du.deleted_at')
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', $permissionName)
|
|
->where('u.is_active', true);
|
|
|
|
if ($tenantId) {
|
|
$usersFromDepartment->where('du.tenant_id', $tenantId)
|
|
->where('po.tenant_id', $tenantId);
|
|
}
|
|
|
|
$usersFromDepartment = $usersFromDepartment->get();
|
|
|
|
// 개인 ALLOW 오버라이드가 있는 사용자
|
|
$usersFromPersonal = DB::table('permission_overrides as po')
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->join('users as u', 'u.id', '=', 'po.model_id')
|
|
->select('u.id as user_id', 'u.name as user_name', 'u.email', 'po.effect')
|
|
->where('po.model_type', User::class)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', $permissionName)
|
|
->where('po.effect', 1)
|
|
->whereNull('po.deleted_at')
|
|
->where('u.is_active', true)
|
|
->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) {
|
|
$usersFromPersonal->where('po.tenant_id', $tenantId);
|
|
}
|
|
|
|
$usersFromPersonal = $usersFromPersonal->get();
|
|
|
|
// 개인 DENY 오버라이드가 있는 사용자
|
|
$usersWithDeny = DB::table('permission_overrides as po')
|
|
->join('permissions as p', 'p.id', '=', 'po.permission_id')
|
|
->join('users as u', 'u.id', '=', 'po.model_id')
|
|
->select('u.id as user_id', 'u.name as user_name', 'u.email')
|
|
->where('po.model_type', User::class)
|
|
->where('p.guard_name', $guardName)
|
|
->where('p.name', $permissionName)
|
|
->where('po.effect', 0)
|
|
->whereNull('po.deleted_at')
|
|
->where('u.is_active', true)
|
|
->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) {
|
|
$usersWithDeny->where('po.tenant_id', $tenantId);
|
|
}
|
|
|
|
$usersWithDeny = $usersWithDeny->get();
|
|
|
|
return [
|
|
'by_role' => $usersFromRole->map(function ($item) {
|
|
return [
|
|
'user_id' => $item->user_id,
|
|
'user_name' => $item->user_name,
|
|
'email' => $item->email,
|
|
'role' => [
|
|
'id' => $item->role_id,
|
|
'name' => $item->role_name,
|
|
'display_name' => $item->role_display_name,
|
|
],
|
|
];
|
|
})->toArray(),
|
|
'by_department' => $usersFromDepartment->map(function ($item) {
|
|
return [
|
|
'user_id' => $item->user_id,
|
|
'user_name' => $item->user_name,
|
|
'email' => $item->email,
|
|
'department' => [
|
|
'id' => $item->department_id,
|
|
'name' => $item->department_name,
|
|
'code' => $item->department_code,
|
|
],
|
|
];
|
|
})->toArray(),
|
|
'by_personal' => $usersFromPersonal->map(function ($item) {
|
|
return [
|
|
'user_id' => $item->user_id,
|
|
'user_name' => $item->user_name,
|
|
'email' => $item->email,
|
|
];
|
|
})->toArray(),
|
|
'denied_users' => $usersWithDeny->map(function ($item) {
|
|
return [
|
|
'user_id' => $item->user_id,
|
|
'user_name' => $item->user_name,
|
|
'email' => $item->email,
|
|
];
|
|
})->toArray(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 권한 유형 목록 반환
|
|
*/
|
|
public function getPermissionTypes(): array
|
|
{
|
|
return $this->permissionTypes;
|
|
}
|
|
|
|
/**
|
|
* CSV 내보내기용 데이터 생성
|
|
*/
|
|
public function exportToCsv(int $menuId, string $permissionType = 'view', ?int $tenantId = null): string
|
|
{
|
|
$analysis = $this->analyzeMenuPermission($menuId, $permissionType, $tenantId);
|
|
|
|
$csv = "사용자,이메일,부서,역할,개인모드,최종,근거\n";
|
|
|
|
foreach ($analysis['access_allowed'] as $user) {
|
|
$departments = collect($user['departments'])->pluck('name')->join(', ');
|
|
$roles = collect($user['roles'])->pluck('display_name')->join(', ');
|
|
$csv .= "\"{$user['name']}\",\"{$user['email']}\",\"{$departments}\",\"{$roles}\",\"{$user['personal_override']}\",\"허용\",\"{$user['source']}\"\n";
|
|
}
|
|
|
|
foreach ($analysis['access_denied'] as $user) {
|
|
$departments = collect($user['departments'])->pluck('name')->join(', ');
|
|
$roles = collect($user['roles'])->pluck('display_name')->join(', ');
|
|
$csv .= "\"{$user['name']}\",\"{$user['email']}\",\"{$departments}\",\"{$roles}\",\"{$user['personal_override']}\",\"거부\",\"{$user['source']}\"\n";
|
|
}
|
|
|
|
return $csv;
|
|
}
|
|
}
|