Files
datahub/backend/test/Cases/Unit/Middleware/PermissionMiddlewareTest.php
2026-03-12 16:26:34 +08:00

355 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
// ========== 安全修复测试 ==========
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 {
// 恢复路由记录(使用 Db::table 保留原 id
Db::table('routes')->insert($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 {
// 恢复路由记录(使用 Db::table 保留原 id
Db::table('routes')->insert($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();
}
}