$type, 'valid_types' => implode(', ', array_map(fn ($case) => $case->value, PermissionType::cases())), ]); } $permissionType = PermissionType::from($type); // Step 2: 권한 식별자 동적 파라미터 치환 $permissions = explode('|', $permission); $resolvedPermissions = array_map( fn ($perm) => $this->resolvePermissionIdentifier($perm, $request), $permissions ); // Step 3: 권한 확인 $user = $request->user(); if ($user) { $hasPermission = count($resolvedPermissions) === 1 ? $user->hasPermission($resolvedPermissions[0], $permissionType) : $user->hasPermissions($resolvedPermissions, filter_var($requireAll ?? 'true', FILTER_VALIDATE_BOOLEAN), $permissionType); if (! $hasPermission) { return ResponseHelper::forbidden('auth.permission_denied', [ 'required_permissions' => implode(', ', $resolvedPermissions), ]); } } else { $hasPermission = count($resolvedPermissions) === 1 ? $this->checkGuestPermission($resolvedPermissions[0], $permissionType) : collect($resolvedPermissions)->contains(fn ($perm) => $this->checkGuestPermission($perm, $permissionType)); if (! $hasPermission) { return ResponseHelper::unauthorized('auth.guest_permission_denied', [ 'required_permissions' => implode(', ', $resolvedPermissions), ]); } } // Step 4: scope_type 스코프 체크 (인증된 사용자 + 상세 엔드포인트만) if ($user) { foreach ($resolvedPermissions as $identifier) { $resourceRouteKey = PermissionHelper::getResourceRouteKey($identifier); // resource_route_key가 없으면 스코프 체크 불필요 if (! $resourceRouteKey) { continue; } // 라우트에서 모델 resolve $model = $request->route($resourceRouteKey); // 모델이 없으면 목록 엔드포인트 → 스코프 체크 스킵 if (! $model instanceof Model) { continue; } // 스코프 접근 체크 if (! PermissionHelper::checkScopeAccess($model, $identifier, $user)) { return ResponseHelper::forbidden('auth.scope_denied'); } } } return $next($request); } /** * 비회원(guest) role의 권한을 확인합니다. * * @param string $permission 권한 식별자 * @param PermissionType $type 권한 타입 * @return bool 권한 존재 여부 */ protected function checkGuestPermission(string $permission, PermissionType $type): bool { $guestRole = $this->getGuestRole(); if (! $guestRole) { return false; } // 적재해 둔 권한 컬렉션에서 판정한다. permissions() 로 빌더를 다시 열면 // 캐시해 둔 의미가 사라져 판정마다 쿼리가 나간다. return GuestRoleResolver::hasPermission($permission, $type); } /** * guest role을 캐싱하여 조회합니다. * * @return Role|null guest 역할 */ protected function getGuestRole(): ?Role { return GuestRoleResolver::resolve(); } /** * guest role 캐시를 무효화합니다. * * RolePermissionSeeder 실행 후 자동 호출 — 시드 직후 권한 변경이 즉시 반영되도록 보장. * 운영 환경에서는 권한 재시드 (코어 업데이트 / 확장 install) 시점에 캐시 정합 유지. * 테스트 환경에서는 RefreshDatabase 트랜잭션 rollback 후 새 시드의 stale id 회귀 방지. * * @return void */ public static function clearGuestRoleCache(): void { GuestRoleResolver::flush(); } /** * 권한 식별자의 동적 파라미터를 URL 파라미터 값으로 치환합니다. * * 예시: sirsoft-board.{slug}.posts.create + {slug: 'notice'} * → sirsoft-board.notice.posts.create * * @param string $permission 권한 식별자 (예: sirsoft-board.{slug}.posts.create) * @param Request $request HTTP 요청 객체 * @return string 치환된 권한 식별자 */ protected function resolvePermissionIdentifier(string $permission, Request $request): string { // {slug}, {id} 등의 플레이스홀더 찾기 preg_match_all('/\{(\w+)\}/', $permission, $matches); if (empty($matches[1])) { return $permission; // 동적 파라미터 없음 } $routeParams = $request->route()->parameters(); $resolvedPermission = $permission; foreach ($matches[1] as $paramName) { if (! isset($routeParams[$paramName])) { // URL 파라미터가 없는 경우 경고 로그 Log::warning('Permission parameter not found in route', [ 'permission' => $permission, 'param' => $paramName, 'route' => $request->route()->getName(), 'available_params' => array_keys($routeParams), ]); // 원본 반환 (권한 체크 실패로 이어짐) return $permission; } $resolvedPermission = str_replace( "{{$paramName}}", $routeParams[$paramName], $resolvedPermission ); } return $resolvedPermission; } }