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'; } }