- 新增后台 RBAC 相关文档,提供权限目录与维护命令说明。 - 移除不必要的角色资源同步检查,简化权限审计命令。 - 更新权限描述与同步逻辑,确保一致性与可维护性。 - 统一权限注册表,替换过时的权限别名,增强代码可读性。
157 lines
5.2 KiB
PHP
157 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Illuminate\Routing\Route as IlluminateRoute;
|
|
|
|
final class AuditAdminAuthorizationCommand extends Command
|
|
{
|
|
protected $signature = 'lottery:admin-auth-audit
|
|
{--skip-route-coverage : 跳过受保护后台路由是否已注册 API 资源的检查}
|
|
{--skip-resource-bindings : 跳过 permission_required 资源是否绑定动作权限的检查}';
|
|
|
|
protected $description = '检查后台权限配置是否存在路由覆盖缺失或资源绑定缺失';
|
|
|
|
public function handle(): int
|
|
{
|
|
$issues = [];
|
|
|
|
if (! (bool) $this->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<array{type: string, message: string}>
|
|
*/
|
|
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<array{type: string, message: string}>
|
|
*/
|
|
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<array{name: string, method: string, uri: string}>
|
|
*/
|
|
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;
|
|
}
|
|
}
|