2026-03-09 14:15:44 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
namespace HyperfTest\Cases\Unit\Middleware;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Model\Role;
|
|
|
|
|
|
use App\Model\Route;
|
|
|
|
|
|
use App\Model\RouteGroup;
|
|
|
|
|
|
use App\Model\RoleRouteOverride;
|
|
|
|
|
|
use App\Model\Store;
|
|
|
|
|
|
use App\Model\User;
|
|
|
|
|
|
use App\Model\UserDataScope;
|
|
|
|
|
|
use Hyperf\DbConnection\Db;
|
|
|
|
|
|
use HyperfTest\TestCase;
|
|
|
|
|
|
use Qbhy\HyperfAuth\AuthManager;
|
|
|
|
|
|
|
|
|
|
|
|
use function Hyperf\Support\make;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @internal
|
|
|
|
|
|
* @coversNothing
|
|
|
|
|
|
*/
|
|
|
|
|
|
class PermissionMiddlewareTest extends TestCase
|
|
|
|
|
|
{
|
|
|
|
|
|
protected function getAuthToken(User $user): string
|
|
|
|
|
|
{
|
|
|
|
|
|
$auth = make(AuthManager::class);
|
|
|
|
|
|
return $auth->guard('jwt')->login($user);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected function authHeaders(User $user): array
|
|
|
|
|
|
{
|
|
|
|
|
|
return ['Authorization' => 'Bearer ' . $this->getAuthToken($user)];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected function createTestUser(string $role_name, array $overrides = []): User
|
|
|
|
|
|
{
|
|
|
|
|
|
$role = Role::query()->where('name', $role_name)->firstOrFail();
|
|
|
|
|
|
$suffix = bin2hex(random_bytes(4));
|
|
|
|
|
|
|
|
|
|
|
|
return User::query()->create(array_merge([
|
|
|
|
|
|
'username' => 'perm_test_' . $suffix,
|
|
|
|
|
|
'email' => 'perm_test_' . $suffix . '@example.com',
|
|
|
|
|
|
'password' => 'Pass_' . $suffix,
|
|
|
|
|
|
'status' => 1,
|
|
|
|
|
|
'role_id' => $role->id,
|
|
|
|
|
|
], $overrides));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== Step 1: 路由访问检查 ==========
|
|
|
|
|
|
|
|
|
|
|
|
public function test_administrator_bypasses_route_check(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $this->createTestUser('administrator');
|
|
|
|
|
|
|
|
|
|
|
|
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
|
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(200);
|
|
|
|
|
|
$response->assertJsonPath('code', 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function test_non_admin_without_route_group_returns_403(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $this->createTestUser('accessor');
|
|
|
|
|
|
|
|
|
|
|
|
// accessor 没有路由组授权,应返回 403
|
|
|
|
|
|
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
|
|
|
|
|
|
|
|
|
|
|
$response->assertStatus(403);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function test_route_group_authorization_allows_access(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $this->createTestUser('developer');
|
|
|
|
|
|
|
|
|
|
|
|
$route = Route::query()
|
|
|
|
|
|
->where('method', 'GET')
|
|
|
|
|
|
->where('path', '/api/v1/users')
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if (!$route) {
|
|
|
|
|
|
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建路由组并授权
|
|
|
|
|
|
$group = RouteGroup::query()->create([
|
|
|
|
|
|
'name' => 'test_user_mgmt_' . uniqid(),
|
|
|
|
|
|
'label' => '用户管理测试',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$old_group_id = $route->group_id;
|
|
|
|
|
|
$route->group_id = $group->id;
|
|
|
|
|
|
$route->save();
|
|
|
|
|
|
|
|
|
|
|
|
Db::table('role_route_groups')->insert([
|
|
|
|
|
|
'role_id' => $user->role_id,
|
|
|
|
|
|
'group_id' => $group->id,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
|
|
|
|
|
$response->assertStatus(200);
|
|
|
|
|
|
|
|
|
|
|
|
// 清理
|
|
|
|
|
|
Db::table('role_route_groups')
|
|
|
|
|
|
->where('role_id', $user->role_id)
|
|
|
|
|
|
->where('group_id', $group->id)
|
|
|
|
|
|
->delete();
|
|
|
|
|
|
$route->group_id = $old_group_id;
|
|
|
|
|
|
$route->save();
|
|
|
|
|
|
$group->delete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function test_override_allows_access_bypassing_group(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $this->createTestUser('accessor');
|
|
|
|
|
|
|
|
|
|
|
|
$route = Route::query()
|
|
|
|
|
|
->where('method', 'GET')
|
|
|
|
|
|
->where('path', '/api/v1/users')
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if (!$route) {
|
|
|
|
|
|
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置 override 为允许(无需路由组)
|
|
|
|
|
|
RoleRouteOverride::query()->create([
|
|
|
|
|
|
'role_id' => $user->role_id,
|
|
|
|
|
|
'route_id' => $route->id,
|
|
|
|
|
|
'allowed' => true,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
|
|
|
|
|
$response->assertStatus(200);
|
|
|
|
|
|
|
|
|
|
|
|
// 清理
|
|
|
|
|
|
RoleRouteOverride::query()
|
|
|
|
|
|
->where('role_id', $user->role_id)
|
|
|
|
|
|
->where('route_id', $route->id)
|
|
|
|
|
|
->delete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function test_override_deny_overrides_group_auth(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $this->createTestUser('developer');
|
|
|
|
|
|
|
|
|
|
|
|
$route = Route::query()
|
|
|
|
|
|
->where('method', 'GET')
|
|
|
|
|
|
->where('path', '/api/v1/users')
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if (!$route) {
|
|
|
|
|
|
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 先授权路由组
|
|
|
|
|
|
$group = RouteGroup::query()->create([
|
|
|
|
|
|
'name' => 'test_override_deny_' . uniqid(),
|
|
|
|
|
|
'label' => '覆盖拒绝测试',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$old_group_id = $route->group_id;
|
|
|
|
|
|
$route->group_id = $group->id;
|
|
|
|
|
|
$route->save();
|
|
|
|
|
|
|
|
|
|
|
|
Db::table('role_route_groups')->insert([
|
|
|
|
|
|
'role_id' => $user->role_id,
|
|
|
|
|
|
'group_id' => $group->id,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置 override 为拒绝(优先级高于 group)
|
|
|
|
|
|
RoleRouteOverride::query()->create([
|
|
|
|
|
|
'role_id' => $user->role_id,
|
|
|
|
|
|
'route_id' => $route->id,
|
|
|
|
|
|
'allowed' => false,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
|
|
|
|
|
$response->assertStatus(403);
|
|
|
|
|
|
|
|
|
|
|
|
// 清理
|
|
|
|
|
|
RoleRouteOverride::query()
|
|
|
|
|
|
->where('role_id', $user->role_id)
|
|
|
|
|
|
->where('route_id', $route->id)
|
|
|
|
|
|
->delete();
|
|
|
|
|
|
Db::table('role_route_groups')
|
|
|
|
|
|
->where('role_id', $user->role_id)
|
|
|
|
|
|
->where('group_id', $group->id)
|
|
|
|
|
|
->delete();
|
|
|
|
|
|
$route->group_id = $old_group_id;
|
|
|
|
|
|
$route->save();
|
|
|
|
|
|
$group->delete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== Step 2: 数据范围检查 ==========
|
|
|
|
|
|
|
|
|
|
|
|
public function test_user_without_role_returns_403(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$suffix = bin2hex(random_bytes(4));
|
|
|
|
|
|
$user = User::query()->create([
|
|
|
|
|
|
'username' => 'norole_' . $suffix,
|
|
|
|
|
|
'email' => 'norole_' . $suffix . '@example.com',
|
|
|
|
|
|
'password' => 'Pass_' . $suffix,
|
|
|
|
|
|
'status' => 1,
|
|
|
|
|
|
'role_id' => null,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
|
|
|
|
|
$response->assertStatus(403);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function test_accessor_with_store_scope_and_override(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $this->createTestUser('accessor');
|
|
|
|
|
|
|
|
|
|
|
|
$route = Route::query()
|
|
|
|
|
|
->where('method', 'GET')
|
|
|
|
|
|
->where('path', '/api/v1/users')
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if (!$route) {
|
|
|
|
|
|
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 授权路由访问
|
|
|
|
|
|
RoleRouteOverride::query()->create([
|
|
|
|
|
|
'role_id' => $user->role_id,
|
|
|
|
|
|
'route_id' => $route->id,
|
|
|
|
|
|
'allowed' => true,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加 store scope
|
|
|
|
|
|
$store = Store::query()->first();
|
|
|
|
|
|
if ($store) {
|
|
|
|
|
|
UserDataScope::query()->create([
|
|
|
|
|
|
'user_id' => $user->id,
|
|
|
|
|
|
'scope_type' => 'store',
|
|
|
|
|
|
'scope_id' => $store->id,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
|
|
|
|
|
$response->assertStatus(200);
|
|
|
|
|
|
|
|
|
|
|
|
// 清理
|
|
|
|
|
|
RoleRouteOverride::query()
|
|
|
|
|
|
->where('role_id', $user->role_id)
|
|
|
|
|
|
->where('route_id', $route->id)
|
|
|
|
|
|
->delete();
|
|
|
|
|
|
UserDataScope::query()->where('user_id', $user->id)->delete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function test_administrator_scope_type_is_all(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $this->createTestUser('administrator');
|
|
|
|
|
|
|
|
|
|
|
|
// administrator 应有 scope_type='all',可以正常访问
|
|
|
|
|
|
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
|
|
|
|
|
$response->assertStatus(200);
|
|
|
|
|
|
$response->assertJsonPath('code', 0);
|
|
|
|
|
|
}
|
2026-03-12 14:04:32 +08:00
|
|
|
|
|
|
|
|
|
|
// ========== 安全修复测试 ==========
|
|
|
|
|
|
|
|
|
|
|
|
public function test_unregistered_route_denied_for_non_admin(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $this->createTestUser('developer');
|
|
|
|
|
|
|
|
|
|
|
|
// 从 routes 表删除 GET /api/v1/users 记录,模拟未注册场景
|
|
|
|
|
|
$route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first();
|
|
|
|
|
|
if (!$route) {
|
|
|
|
|
|
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$route_data = $route->toArray();
|
|
|
|
|
|
$route->delete();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 白名单模式:routes 表中无记录 → 403
|
|
|
|
|
|
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
|
|
|
|
|
$response->assertStatus(403);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
// 恢复路由记录
|
|
|
|
|
|
Route::query()->create($route_data);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function test_admin_allowed_on_unregistered_route(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $this->createTestUser('administrator');
|
|
|
|
|
|
|
|
|
|
|
|
// 从 routes 表删除 GET /api/v1/users 记录
|
|
|
|
|
|
$route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first();
|
|
|
|
|
|
if (!$route) {
|
|
|
|
|
|
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$route_data = $route->toArray();
|
|
|
|
|
|
$route->delete();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// administrator 跳过路由检查,即使路由未在 routes 表中也应正常访问
|
|
|
|
|
|
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
|
|
|
|
|
$response->assertStatus(200);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
// 恢复路由记录
|
|
|
|
|
|
Route::query()->create($route_data);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function test_parametric_route_matches_template_path(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$user = $this->createTestUser('developer');
|
|
|
|
|
|
|
|
|
|
|
|
$route = Route::query()
|
|
|
|
|
|
->where('method', 'GET')
|
|
|
|
|
|
->where('path', '/api/v1/users/{id}')
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if (!$route) {
|
|
|
|
|
|
$this->markTestSkipped('routes 表中无 GET /api/v1/users/{id} 路由');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建路由组并授权
|
|
|
|
|
|
$group = RouteGroup::query()->create([
|
|
|
|
|
|
'name' => 'test_param_route_' . uniqid(),
|
|
|
|
|
|
'label' => '参数化路由测试',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$old_group_id = $route->group_id;
|
|
|
|
|
|
$route->group_id = $group->id;
|
|
|
|
|
|
$route->save();
|
|
|
|
|
|
|
|
|
|
|
|
Db::table('role_route_groups')->insert([
|
|
|
|
|
|
'role_id' => $user->role_id,
|
|
|
|
|
|
'group_id' => $group->id,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 访问参数化路由 /api/v1/users/1(应匹配模板路径 /api/v1/users/{id})
|
|
|
|
|
|
$response = $this->get('/api/v1/users/1', [], $this->authHeaders($user));
|
|
|
|
|
|
// 路由检查应通过(200 或 404 取决于用户是否存在,但不应是 403)
|
|
|
|
|
|
$this->assertNotEquals(403, $response->getStatusCode());
|
|
|
|
|
|
|
|
|
|
|
|
// 清理
|
|
|
|
|
|
Db::table('role_route_groups')
|
|
|
|
|
|
->where('role_id', $user->role_id)
|
|
|
|
|
|
->where('group_id', $group->id)
|
|
|
|
|
|
->delete();
|
|
|
|
|
|
$route->group_id = $old_group_id;
|
|
|
|
|
|
$route->save();
|
|
|
|
|
|
$group->delete();
|
|
|
|
|
|
}
|
2026-03-09 14:15:44 +08:00
|
|
|
|
}
|