option('skip-route-coverage')) { $issues = array_merge($issues, $this->checkProtectedAdminRouteCoverage()); } if (! (bool) $this->option('skip-resource-bindings')) { $issues = array_merge($issues, $this->checkPermissionResourceBindings()); } if ($issues === []) { $this->info('Admin authorization audit passed.'); return self::SUCCESS; } $this->error(sprintf('Admin authorization audit found %d issue(s).', count($issues))); foreach ($issues as $issue) { $this->line(sprintf('- [%s] %s', $issue['type'], $issue['message'])); } return self::FAILURE; } /** * @return list */ private function checkProtectedAdminRouteCoverage(): array { $protectedRoutes = $this->protectedAdminRoutes(); if ($protectedRoutes === []) { return [[ 'type' => 'route_coverage', 'message' => 'No protected admin routes were discovered from the current route collection.', ]]; } $resourceRouteNames = DB::table('admin_api_resources') ->where('status', 1) ->pluck('route_name') ->filter(static fn ($routeName): bool => is_string($routeName) && $routeName !== '') ->map(fn (string $routeName): string => $this->normalizeAdminRouteName($routeName)) ->all(); $resourceRouteNameSet = array_fill_keys($resourceRouteNames, true); $issues = []; foreach ($protectedRoutes as $route) { if (! isset($resourceRouteNameSet[$route['name']])) { $issues[] = [ 'type' => 'route_coverage', 'message' => sprintf( 'Protected route `%s %s` (`%s`) has no active row in `admin_api_resources`.', $route['method'], $route['uri'], $route['name'], ), ]; } } return $issues; } /** * @return list */ private function checkPermissionResourceBindings(): array { $rows = DB::table('admin_api_resources as ar') ->leftJoin('admin_api_resource_bindings as arb', 'arb.api_resource_id', '=', 'ar.id') ->select('ar.code', 'ar.route_name', DB::raw('count(arb.id) as binding_count')) ->where('ar.status', 1) ->where('ar.auth_mode', 'permission_required') ->groupBy('ar.id', 'ar.code', 'ar.route_name') ->havingRaw('count(arb.id) = 0') ->orderBy('ar.code') ->get(); $issues = []; foreach ($rows as $row) { $issues[] = [ 'type' => 'resource_bindings', 'message' => sprintf( 'API resource `%s` (`%s`) requires permission but has no row in `admin_api_resource_bindings`.', (string) $row->code, (string) $row->route_name, ), ]; } return $issues; } /** * @return list */ private function protectedAdminRoutes(): array { $routes = []; /** @var IlluminateRoute $route */ foreach (Route::getRoutes() as $route) { $middleware = $route->gatherMiddleware(); if (! in_array('lottery.admin', $middleware, true) && ! in_array('App\Http\Middleware\EnsureAdminApi', $middleware, true) ) { continue; } $name = $route->getName(); if (! is_string($name) || $name === '') { continue; } $methods = array_values(array_filter( $route->methods(), static fn (string $method): bool => $method !== 'HEAD', )); $routes[] = [ 'name' => $this->normalizeAdminRouteName($name), 'method' => $methods[0] ?? 'GET', 'uri' => '/'.ltrim($route->uri(), '/'), ]; } usort($routes, static fn (array $a, array $b): int => [$a['uri'], $a['method']] <=> [$b['uri'], $b['method']]); return $routes; } private function normalizeAdminRouteName(string $routeName): string { return preg_replace('/^(api\.v1\.admin\.)+/', 'api.v1.admin.', $routeName) ?? $routeName; } }