- Validator::make를 FormRequest로 분리 (6개 생성) - 하드코딩 한글 문자열을 i18n 키로 교체 - RoleMenuPermission 데드코드 제거 - Role 모델 SpatieRole 상속으로 일원화 - 권한 변경 후 캐시 무효화 추가 (AccessService::bumpVersion) - 미문서화 8개 Swagger 엔드포인트 추가 - 역할/권한 라우트에 perm.map+permission 미들웨어 추가
489 lines
16 KiB
PHP
489 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Authz;
|
|
|
|
use App\Models\Permissions\Role;
|
|
use Spatie\Permission\Models\Permission;
|
|
use Spatie\Permission\PermissionRegistrar;
|
|
|
|
class RolePermissionService
|
|
{
|
|
protected static string $guard = 'api';
|
|
|
|
/** 현재 테넌트 컨텍스트로 팀 고정 */
|
|
protected static function setTeam(int $tenantId): void
|
|
{
|
|
app(PermissionRegistrar::class)->setPermissionsTeamId($tenantId);
|
|
}
|
|
|
|
/** 권한 캐시 무효화 */
|
|
protected static function invalidateCache(int $tenantId): void
|
|
{
|
|
AccessService::bumpVersion($tenantId);
|
|
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
|
}
|
|
|
|
/** 역할 로드 (테넌트/가드 검증) */
|
|
protected static function loadRoleOrError(int $roleId, int $tenantId): ?Role
|
|
{
|
|
$role = Role::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('guard_name', self::$guard)
|
|
->find($roleId);
|
|
|
|
return $role;
|
|
}
|
|
|
|
/** A) permission_names[] → 그대로 사용
|
|
* B) menus[] + actions[] → "menu:{id}.{act}" 배열로 변환(필요 시 Permission 생성)
|
|
*/
|
|
protected static function resolvePermissionNames(int $tenantId, array $params): array
|
|
{
|
|
$names = [];
|
|
|
|
if (! empty($params['permission_names']) && is_array($params['permission_names'])) {
|
|
foreach ($params['permission_names'] as $n) {
|
|
if (is_string($n) && $n !== '') {
|
|
$names[] = trim($n);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! empty($params['menus']) && is_array($params['menus']) &&
|
|
! empty($params['actions']) && is_array($params['actions'])) {
|
|
|
|
$allowed = config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve']);
|
|
$acts = array_values(array_unique(array_filter(array_map('trim', $params['actions']))));
|
|
$acts = array_intersect($acts, $allowed);
|
|
|
|
$menuIds = array_values(array_unique(array_map('intval', $params['menus'])));
|
|
|
|
foreach ($menuIds as $mid) {
|
|
foreach ($acts as $act) {
|
|
$names[] = "menu:{$mid}.{$act}";
|
|
}
|
|
}
|
|
}
|
|
|
|
// 빈/중복 제거
|
|
$names = array_values(array_unique(array_filter($names)));
|
|
|
|
// 존재하지 않는 Permission은 생성(tenant+guard 포함)
|
|
foreach ($names as $permName) {
|
|
Permission::firstOrCreate([
|
|
'tenant_id' => $tenantId,
|
|
'guard_name' => self::$guard,
|
|
'name' => $permName,
|
|
]);
|
|
}
|
|
|
|
return $names;
|
|
}
|
|
|
|
/** 역할의 퍼미션 목록 */
|
|
public static function list(int $roleId)
|
|
{
|
|
$tenantId = (int) app('tenant_id');
|
|
|
|
$role = self::loadRoleOrError($roleId, $tenantId);
|
|
if (! $role) {
|
|
return ['error' => __('error.role.not_found'), 'code' => 404];
|
|
}
|
|
|
|
self::setTeam($tenantId);
|
|
|
|
$perms = $role->permissions()
|
|
->where('tenant_id', $tenantId)
|
|
->where('guard_name', self::$guard)
|
|
->orderBy('name')
|
|
->get(['id', 'tenant_id', 'name', 'guard_name', 'created_at', 'updated_at']);
|
|
|
|
return $perms;
|
|
}
|
|
|
|
/** 부여 (중복 무시) */
|
|
public static function grant(int $roleId, array $params = [])
|
|
{
|
|
$tenantId = (int) app('tenant_id');
|
|
|
|
$role = self::loadRoleOrError($roleId, $tenantId);
|
|
if (! $role) {
|
|
return ['error' => __('error.role.not_found'), 'code' => 404];
|
|
}
|
|
|
|
self::setTeam($tenantId);
|
|
|
|
$names = self::resolvePermissionNames($tenantId, $params);
|
|
if (empty($names)) {
|
|
return ['error' => __('error.role.no_valid_permissions'), 'code' => 422];
|
|
}
|
|
|
|
$role->givePermissionTo($names);
|
|
|
|
self::invalidateCache($tenantId);
|
|
|
|
return 'success';
|
|
}
|
|
|
|
/** 회수 (없는 건 무시) */
|
|
public static function revoke(int $roleId, array $params = [])
|
|
{
|
|
$tenantId = (int) app('tenant_id');
|
|
|
|
$role = self::loadRoleOrError($roleId, $tenantId);
|
|
if (! $role) {
|
|
return ['error' => __('error.role.not_found'), 'code' => 404];
|
|
}
|
|
|
|
self::setTeam($tenantId);
|
|
|
|
$names = self::resolvePermissionNames($tenantId, $params);
|
|
if (empty($names)) {
|
|
return ['error' => __('error.role.no_valid_permissions'), 'code' => 422];
|
|
}
|
|
|
|
$role->revokePermissionTo($names);
|
|
|
|
self::invalidateCache($tenantId);
|
|
|
|
return 'success';
|
|
}
|
|
|
|
/** 동기화(완전 교체) */
|
|
public static function sync(int $roleId, array $params = [])
|
|
{
|
|
$tenantId = (int) app('tenant_id');
|
|
|
|
$role = self::loadRoleOrError($roleId, $tenantId);
|
|
if (! $role) {
|
|
return ['error' => __('error.role.not_found'), 'code' => 404];
|
|
}
|
|
|
|
self::setTeam($tenantId);
|
|
|
|
$names = self::resolvePermissionNames($tenantId, $params);
|
|
$role->syncPermissions($names);
|
|
|
|
self::invalidateCache($tenantId);
|
|
|
|
return 'success';
|
|
}
|
|
|
|
/** 권한 유형 목록 */
|
|
protected static function getPermissionTypes(): array
|
|
{
|
|
return config('authz.menu_actions', ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage']);
|
|
}
|
|
|
|
/** 역할의 권한 매트릭스 조회 */
|
|
public static function matrix(int $roleId)
|
|
{
|
|
$tenantId = (int) app('tenant_id');
|
|
|
|
$role = self::loadRoleOrError($roleId, $tenantId);
|
|
if (! $role) {
|
|
return ['error' => __('error.role.not_found'), 'code' => 404];
|
|
}
|
|
|
|
self::setTeam($tenantId);
|
|
|
|
// 역할에 부여된 권한 조회
|
|
$rolePermissions = \Illuminate\Support\Facades\DB::table('role_has_permissions')
|
|
->join('permissions', 'role_has_permissions.permission_id', '=', 'permissions.id')
|
|
->where('role_has_permissions.role_id', $roleId)
|
|
->where('permissions.guard_name', self::$guard)
|
|
->where('permissions.name', 'like', 'menu:%')
|
|
->pluck('permissions.name')
|
|
->toArray();
|
|
|
|
$permissions = [];
|
|
foreach ($rolePermissions 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 [
|
|
'role' => [
|
|
'id' => $role->id,
|
|
'name' => $role->name,
|
|
'description' => $role->description,
|
|
],
|
|
'permission_types' => self::getPermissionTypes(),
|
|
'permissions' => $permissions,
|
|
];
|
|
}
|
|
|
|
/** 메뉴 트리 조회 (권한 매트릭스 표시용) */
|
|
public static function menus()
|
|
{
|
|
$tenantId = (int) app('tenant_id');
|
|
|
|
$menus = \App\Models\Commons\Menu::where('tenant_id', $tenantId)
|
|
->where('is_active', true)
|
|
->orderBy('sort_order', 'asc')
|
|
->orderBy('id', 'asc')
|
|
->get(['id', 'parent_id', 'name', 'url', 'icon', 'sort_order', 'is_active']);
|
|
|
|
$flatMenus = self::flattenMenuTree($menus->toArray(), null, 0);
|
|
|
|
return [
|
|
'menus' => $flatMenus,
|
|
'permission_types' => self::getPermissionTypes(),
|
|
];
|
|
}
|
|
|
|
/** 트리 구조를 플랫한 배열로 변환 (depth 정보 포함) */
|
|
protected static function flattenMenuTree(array $menus, ?int $parentId = null, int $depth = 0): array
|
|
{
|
|
$result = [];
|
|
|
|
$filteredMenus = array_filter($menus, fn ($m) => $m['parent_id'] === $parentId);
|
|
usort($filteredMenus, fn ($a, $b) => ($a['sort_order'] ?? 0) <=> ($b['sort_order'] ?? 0));
|
|
|
|
foreach ($filteredMenus as $menu) {
|
|
$menu['depth'] = $depth;
|
|
$menu['has_children'] = count(array_filter($menus, fn ($m) => $m['parent_id'] === $menu['id'])) > 0;
|
|
$result[] = $menu;
|
|
|
|
$children = self::flattenMenuTree($menus, $menu['id'], $depth + 1);
|
|
$result = array_merge($result, $children);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/** 특정 메뉴의 특정 권한 토글 */
|
|
public static function toggle(int $roleId, array $params = [])
|
|
{
|
|
$tenantId = (int) app('tenant_id');
|
|
|
|
$role = self::loadRoleOrError($roleId, $tenantId);
|
|
if (! $role) {
|
|
return ['error' => __('error.role.not_found'), 'code' => 404];
|
|
}
|
|
|
|
$menuId = (int) $params['menu_id'];
|
|
$permissionType = $params['permission_type'];
|
|
|
|
self::setTeam($tenantId);
|
|
|
|
$permissionName = "menu:{$menuId}.{$permissionType}";
|
|
|
|
// 권한 생성 또는 조회
|
|
$permission = Permission::firstOrCreate([
|
|
'name' => $permissionName,
|
|
'guard_name' => self::$guard,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
|
|
// 현재 권한 상태 확인
|
|
$exists = \Illuminate\Support\Facades\DB::table('role_has_permissions')
|
|
->where('role_id', $roleId)
|
|
->where('permission_id', $permission->id)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
\Illuminate\Support\Facades\DB::table('role_has_permissions')
|
|
->where('role_id', $roleId)
|
|
->where('permission_id', $permission->id)
|
|
->delete();
|
|
$newValue = false;
|
|
} else {
|
|
\Illuminate\Support\Facades\DB::table('role_has_permissions')->insert([
|
|
'role_id' => $roleId,
|
|
'permission_id' => $permission->id,
|
|
]);
|
|
$newValue = true;
|
|
}
|
|
|
|
// 하위 메뉴에 권한 전파
|
|
self::propagateToChildren($roleId, $menuId, $permissionType, $newValue, $tenantId);
|
|
|
|
self::invalidateCache($tenantId);
|
|
|
|
return [
|
|
'menu_id' => $menuId,
|
|
'permission_type' => $permissionType,
|
|
'granted' => $newValue,
|
|
];
|
|
}
|
|
|
|
/** 하위 메뉴에 권한 전파 */
|
|
protected static function propagateToChildren(int $roleId, int $parentMenuId, string $permissionType, bool $value, int $tenantId): void
|
|
{
|
|
$children = \App\Models\Commons\Menu::where('parent_id', $parentMenuId)
|
|
->where('tenant_id', $tenantId)
|
|
->get();
|
|
|
|
foreach ($children as $child) {
|
|
$permissionName = "menu:{$child->id}.{$permissionType}";
|
|
$permission = Permission::firstOrCreate([
|
|
'name' => $permissionName,
|
|
'guard_name' => self::$guard,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
|
|
if ($value) {
|
|
$exists = \Illuminate\Support\Facades\DB::table('role_has_permissions')
|
|
->where('role_id', $roleId)
|
|
->where('permission_id', $permission->id)
|
|
->exists();
|
|
|
|
if (! $exists) {
|
|
\Illuminate\Support\Facades\DB::table('role_has_permissions')->insert([
|
|
'role_id' => $roleId,
|
|
'permission_id' => $permission->id,
|
|
]);
|
|
}
|
|
} else {
|
|
\Illuminate\Support\Facades\DB::table('role_has_permissions')
|
|
->where('role_id', $roleId)
|
|
->where('permission_id', $permission->id)
|
|
->delete();
|
|
}
|
|
|
|
self::propagateToChildren($roleId, $child->id, $permissionType, $value, $tenantId);
|
|
}
|
|
}
|
|
|
|
/** 모든 권한 허용 */
|
|
public static function allowAll(int $roleId)
|
|
{
|
|
$tenantId = (int) app('tenant_id');
|
|
|
|
$role = self::loadRoleOrError($roleId, $tenantId);
|
|
if (! $role) {
|
|
return ['error' => __('error.role.not_found'), 'code' => 404];
|
|
}
|
|
|
|
self::setTeam($tenantId);
|
|
|
|
$menus = \App\Models\Commons\Menu::where('tenant_id', $tenantId)
|
|
->where('is_active', true)
|
|
->get();
|
|
|
|
$permissionTypes = self::getPermissionTypes();
|
|
|
|
foreach ($menus as $menu) {
|
|
foreach ($permissionTypes as $type) {
|
|
$permissionName = "menu:{$menu->id}.{$type}";
|
|
$permission = Permission::firstOrCreate([
|
|
'name' => $permissionName,
|
|
'guard_name' => self::$guard,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
|
|
$exists = \Illuminate\Support\Facades\DB::table('role_has_permissions')
|
|
->where('role_id', $roleId)
|
|
->where('permission_id', $permission->id)
|
|
->exists();
|
|
|
|
if (! $exists) {
|
|
\Illuminate\Support\Facades\DB::table('role_has_permissions')->insert([
|
|
'role_id' => $roleId,
|
|
'permission_id' => $permission->id,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
self::invalidateCache($tenantId);
|
|
|
|
return 'success';
|
|
}
|
|
|
|
/** 모든 권한 거부 */
|
|
public static function denyAll(int $roleId)
|
|
{
|
|
$tenantId = (int) app('tenant_id');
|
|
|
|
$role = self::loadRoleOrError($roleId, $tenantId);
|
|
if (! $role) {
|
|
return ['error' => __('error.role.not_found'), 'code' => 404];
|
|
}
|
|
|
|
self::setTeam($tenantId);
|
|
|
|
$menus = \App\Models\Commons\Menu::where('tenant_id', $tenantId)
|
|
->where('is_active', true)
|
|
->get();
|
|
|
|
$permissionTypes = self::getPermissionTypes();
|
|
|
|
foreach ($menus as $menu) {
|
|
foreach ($permissionTypes as $type) {
|
|
$permissionName = "menu:{$menu->id}.{$type}";
|
|
$permission = Permission::where('name', $permissionName)
|
|
->where('guard_name', self::$guard)
|
|
->where('tenant_id', $tenantId)
|
|
->first();
|
|
|
|
if ($permission) {
|
|
\Illuminate\Support\Facades\DB::table('role_has_permissions')
|
|
->where('role_id', $roleId)
|
|
->where('permission_id', $permission->id)
|
|
->delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
self::invalidateCache($tenantId);
|
|
|
|
return 'success';
|
|
}
|
|
|
|
/** 기본 권한으로 초기화 (view만 허용) */
|
|
public static function reset(int $roleId)
|
|
{
|
|
$tenantId = (int) app('tenant_id');
|
|
|
|
$role = self::loadRoleOrError($roleId, $tenantId);
|
|
if (! $role) {
|
|
return ['error' => __('error.role.not_found'), 'code' => 404];
|
|
}
|
|
|
|
self::setTeam($tenantId);
|
|
|
|
// 1. 먼저 모든 권한 제거
|
|
self::denyAll($roleId);
|
|
|
|
// 2. view 권한만 허용
|
|
$menus = \App\Models\Commons\Menu::where('tenant_id', $tenantId)
|
|
->where('is_active', true)
|
|
->get();
|
|
|
|
foreach ($menus as $menu) {
|
|
$permissionName = "menu:{$menu->id}.view";
|
|
$permission = Permission::firstOrCreate([
|
|
'name' => $permissionName,
|
|
'guard_name' => self::$guard,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
|
|
$exists = \Illuminate\Support\Facades\DB::table('role_has_permissions')
|
|
->where('role_id', $roleId)
|
|
->where('permission_id', $permission->id)
|
|
->exists();
|
|
|
|
if (! $exists) {
|
|
\Illuminate\Support\Facades\DB::table('role_has_permissions')->insert([
|
|
'role_id' => $roleId,
|
|
'permission_id' => $permission->id,
|
|
]);
|
|
}
|
|
}
|
|
|
|
self::invalidateCache($tenantId);
|
|
|
|
return 'success';
|
|
}
|
|
}
|