404 lines
13 KiB
PHP
404 lines
13 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace HyperfTest\Cases\Integration\Permission;
|
||
|
||
use App\Model\Role;
|
||
use App\Model\RoleRouteOverride;
|
||
use App\Model\Route;
|
||
use App\Model\RouteGroup;
|
||
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;
|
||
|
||
/**
|
||
* 权限中间件端到端集成测试
|
||
*
|
||
* 覆盖 AuthMiddleware → PermissionMiddleware → Controller 完整链路
|
||
*
|
||
* @internal
|
||
* @coversNothing
|
||
*/
|
||
class PermissionFlowTest 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' => 'integ_perm_' . $suffix,
|
||
'email' => 'integ_perm_' . $suffix . '@example.com',
|
||
'password' => 'Pass_' . $suffix,
|
||
'status' => 1,
|
||
'role_id' => $role->id,
|
||
], $overrides));
|
||
}
|
||
|
||
/**
|
||
* 为指定角色授权指定路由的路由组
|
||
*
|
||
* @return array{group: RouteGroup, old_group_id: ?int} 用于清理
|
||
*/
|
||
protected function authorizeRouteGroup(int $role_id, Route $route): array
|
||
{
|
||
$group = RouteGroup::query()->create([
|
||
'name' => 'integ_test_' . uniqid(),
|
||
'label' => '集成测试路由组',
|
||
]);
|
||
|
||
$old_group_id = $route->group_id;
|
||
$route->group_id = $group->id;
|
||
$route->save();
|
||
|
||
Db::table('role_route_groups')->insert([
|
||
'role_id' => $role_id,
|
||
'group_id' => $group->id,
|
||
]);
|
||
|
||
return ['group' => $group, 'old_group_id' => $old_group_id];
|
||
}
|
||
|
||
/**
|
||
* 清理路由组授权
|
||
*/
|
||
protected function cleanupRouteGroup(int $role_id, Route $route, array $auth_data): void
|
||
{
|
||
Db::table('role_route_groups')
|
||
->where('role_id', $role_id)
|
||
->where('group_id', $auth_data['group']->id)
|
||
->delete();
|
||
$route->group_id = $auth_data['old_group_id'];
|
||
$route->save();
|
||
$auth_data['group']->delete();
|
||
}
|
||
|
||
// ========== 测试用例 ==========
|
||
|
||
public function test_admin_full_access(): void
|
||
{
|
||
$user = $this->createTestUser('administrator');
|
||
|
||
// administrator 可访问全部 UserController 端点
|
||
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
||
$response->assertStatus(200);
|
||
$response->assertJsonPath('code', 0);
|
||
|
||
// 参数化路由
|
||
$response = $this->get('/api/v1/users/1', [], $this->authHeaders($user));
|
||
$this->assertContains($response->getStatusCode(), [200, 404]);
|
||
}
|
||
|
||
public function test_developer_with_group_auth_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 路由');
|
||
}
|
||
|
||
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
|
||
|
||
try {
|
||
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
||
$response->assertStatus(200);
|
||
} finally {
|
||
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
|
||
}
|
||
}
|
||
|
||
public function test_accessor_without_auth_denied(): void
|
||
{
|
||
$user = $this->createTestUser('accessor');
|
||
|
||
// accessor 无任何路由授权 → 403
|
||
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
||
$response->assertStatus(403);
|
||
}
|
||
|
||
public function test_accessor_with_override_access(): 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 路由');
|
||
}
|
||
|
||
// 添加 store scope 以通过数据范围检查
|
||
$store = Store::query()->first();
|
||
if ($store) {
|
||
UserDataScope::query()->create([
|
||
'user_id' => $user->id,
|
||
'scope_type' => 'store',
|
||
'scope_id' => $store->id,
|
||
]);
|
||
}
|
||
|
||
// 设置 override 允许访问
|
||
RoleRouteOverride::query()->create([
|
||
'role_id' => $user->role_id,
|
||
'route_id' => $route->id,
|
||
'allowed' => true,
|
||
]);
|
||
|
||
try {
|
||
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
||
$response->assertStatus(200);
|
||
} finally {
|
||
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_override_deny_overrides_group(): 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 路由');
|
||
}
|
||
|
||
// 授权路由组
|
||
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
|
||
|
||
// 设置 override 为拒绝(优先级高于 group)
|
||
RoleRouteOverride::query()->create([
|
||
'role_id' => $user->role_id,
|
||
'route_id' => $route->id,
|
||
'allowed' => false,
|
||
]);
|
||
|
||
try {
|
||
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
||
$response->assertStatus(403);
|
||
} finally {
|
||
RoleRouteOverride::query()
|
||
->where('role_id', $user->role_id)
|
||
->where('route_id', $route->id)
|
||
->delete();
|
||
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
|
||
}
|
||
}
|
||
|
||
public function test_developer_scope_param_validation(): 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 路由');
|
||
}
|
||
|
||
// 授权路由访问
|
||
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
|
||
|
||
try {
|
||
// 传不属于自己的 platform_id → 403
|
||
$response = $this->get('/api/v1/users', ['platform_id' => 999999], $this->authHeaders($user));
|
||
$response->assertStatus(403);
|
||
} finally {
|
||
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
|
||
}
|
||
}
|
||
|
||
// ========== 边缘场景 ==========
|
||
|
||
public function test_accessor_with_invalid_store_id_denied(): 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_id=1),然后传不在 bitmap 中的 store_id
|
||
$store = Store::query()->first();
|
||
if (!$store) {
|
||
$this->markTestSkipped('stores 表中无数据');
|
||
}
|
||
|
||
UserDataScope::query()->create([
|
||
'user_id' => $user->id,
|
||
'scope_type' => 'store',
|
||
'scope_id' => $store->id,
|
||
]);
|
||
|
||
try {
|
||
// 传不属于自己的 store_id → 403
|
||
$response = $this->get('/api/v1/users', ['store_id' => 999999], $this->authHeaders($user));
|
||
$response->assertStatus(403);
|
||
} finally {
|
||
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_accessor_with_invalid_company_id_denied(): 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 = Store::query()->first();
|
||
if (!$store) {
|
||
$this->markTestSkipped('stores 表中无数据');
|
||
}
|
||
|
||
UserDataScope::query()->create([
|
||
'user_id' => $user->id,
|
||
'scope_type' => 'store',
|
||
'scope_id' => $store->id,
|
||
]);
|
||
|
||
try {
|
||
// 传不属于自己的 company_id → 403
|
||
$response = $this->get('/api/v1/users', ['company_id' => 999999], $this->authHeaders($user));
|
||
$response->assertStatus(403);
|
||
} finally {
|
||
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_accessor_with_no_scope_data_gets_empty_results(): 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 路由');
|
||
}
|
||
|
||
// 授权路由访问但不设置任何 scope 数据
|
||
RoleRouteOverride::query()->create([
|
||
'role_id' => $user->role_id,
|
||
'route_id' => $route->id,
|
||
'allowed' => true,
|
||
]);
|
||
|
||
try {
|
||
// accessor 无 scope 数据 → bitmap 为空 → scope_ids=[] → 200 但无数据
|
||
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
||
$response->assertStatus(200);
|
||
$response->assertJsonPath('code', 0);
|
||
} finally {
|
||
RoleRouteOverride::query()
|
||
->where('role_id', $user->role_id)
|
||
->where('route_id', $route->id)
|
||
->delete();
|
||
}
|
||
}
|
||
|
||
public function test_developer_with_invalid_store_id_denied(): 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 路由');
|
||
}
|
||
|
||
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
|
||
|
||
try {
|
||
// developer 传不属于自己的 store_id → 403
|
||
$response = $this->get('/api/v1/users', ['store_id' => 999999], $this->authHeaders($user));
|
||
$response->assertStatus(403);
|
||
} finally {
|
||
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
|
||
}
|
||
}
|
||
|
||
public function test_developer_with_no_platforms_gets_empty_scope(): void
|
||
{
|
||
// 创建 developer 用户但不关联任何 platform
|
||
$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 路由');
|
||
}
|
||
|
||
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
|
||
|
||
try {
|
||
// developer 无维护平台 → platform_ids=[] → scope_ids=[] → 200 但无数据
|
||
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
||
$response->assertStatus(200);
|
||
$response->assertJsonPath('code', 0);
|
||
} finally {
|
||
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
|
||
}
|
||
}
|
||
|
||
public function test_ungrouped_route_denied_for_non_admin(): 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 路由');
|
||
}
|
||
|
||
// 确保路由未分组且无 override
|
||
$old_group_id = $route->group_id;
|
||
$route->group_id = null;
|
||
$route->save();
|
||
|
||
try {
|
||
// 路由已注册但未分组、无 override → 403
|
||
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
|
||
$response->assertStatus(403);
|
||
} finally {
|
||
$route->group_id = $old_group_id;
|
||
$route->save();
|
||
}
|
||
}
|
||
}
|