star-cloud/tests/Feature/Admin/RoleHierarchyTest.php
sky121113 201663741d [FEAT] 帳號與角色多層級隔離(主/子帳號樹狀 + 角色權限/機台授權子集把關)
1. 主帳號不變量:storeAccount 的 is_admin 改補位(每家公司恰一主帳號)、destroyAccount 禁刪最後主帳號、roles 新增 is_company_admin 旗標保護公司管理員角色不被刪除
2. 帳號樹狀層級:users 新增 parent_id/level(主帳號 level 0、子帳號最多兩層),新增 visibleTo/canManageAccount,帳號清單與管理端點(accounts/update/destroy/toggle、機台授權選帳號、補貨指派)一律套層級可見範圍與越權防護
3. 角色指派子集:storeAccount/updateAccount 指派角色與帳號 Modal 下拉一律限制為「權限為操作者子集」,杜絕權限提升
4. 機台授權子集:MachinePermissionController 僅允許在操作者本身可授權機台範圍內增刪,保留範圍外既有授權,並修正 machine_access 全域 scope 對讀寫的干擾
5. 角色層級隔離:roles 新增 created_by,子帳號只看/只管/只能指派自己建立的角色(主帳號看全公司),edit/update/destroy 加 canBeManagedBy 守衛
6. 補強:MachineSetting 機台權限人員清單補租戶隔離、修正角色下拉 N+1、三語系新增提示字串
7. 測試:新增 AccountHierarchyTest/AccountSubsetGuardTest/RoleHierarchyTest,Admin 套件 55 測試 0 回歸

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:03:43 +08:00

118 lines
4.5 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
namespace Tests\Feature\Admin;
use App\Models\System\Company;
use App\Models\System\Role;
use App\Models\System\User;
use Spatie\Permission\Models\Permission;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* 第五批:角色層級隔離測試。
* - 子帳號只看得到 / 只能管理 自己建立的角色(看不到上層/旁線建的)。
* - 主帳號看得到同公司全部角色。
* - storeRole 寫入 created_byupdate/destroy 受 canBeManagedBy 守衛。
*/
class RoleHierarchyTest extends TestCase
{
use RefreshDatabase;
protected Company $company;
protected User $main;
protected User $subA;
protected User $subB;
protected function setUp(): void
{
parent::setUp();
Permission::create(['name' => 'menu.data-config.sub-accounts', 'guard_name' => 'web']);
$this->company = Company::create(['name' => 'Test Co', 'code' => 'TEST']);
$role = Role::create(['name' => 'tenant-mgr', 'company_id' => $this->company->id, 'is_system' => false, 'guard_name' => 'web']);
$role->syncPermissions(['menu.data-config.sub-accounts']);
$this->main = User::factory()->create(['company_id' => $this->company->id, 'is_admin' => true, 'level' => 0, 'parent_id' => null]);
$this->main->assignRole($role);
$this->subA = User::factory()->create(['company_id' => $this->company->id, 'is_admin' => false, 'level' => 1, 'parent_id' => $this->main->id]);
$this->subA->assignRole($role);
$this->subB = User::factory()->create(['company_id' => $this->company->id, 'is_admin' => false, 'level' => 1, 'parent_id' => $this->main->id]);
$this->subB->assignRole($role);
}
private function makeRole(string $name, User $creator): Role
{
return Role::create([
'name' => $name, 'company_id' => $this->company->id, 'is_system' => false,
'guard_name' => 'web', 'created_by' => $creator->id,
]);
}
public function test_role_visible_to_isolates_by_creator(): void
{
$roleMain = $this->makeRole('role-main', $this->main);
$roleA = $this->makeRole('role-a', $this->subA);
$roleB = $this->makeRole('role-b', $this->subB);
// 子帳號 A 只看得到自己建立的角色
$this->assertSame([$roleA->id], Role::visibleTo($this->subA)->pluck('id')->all());
// 主帳號看得到同公司全部角色(含 tenant-mgr 與三個新建的)
$mainVisible = Role::visibleTo($this->main)->pluck('id')->all();
foreach ([$roleMain->id, $roleA->id, $roleB->id] as $id) {
$this->assertContains($id, $mainVisible);
}
}
public function test_can_be_managed_by_creator_only_for_sub_account(): void
{
$roleMain = $this->makeRole('role-main', $this->main);
$roleA = $this->makeRole('role-a', $this->subA);
$this->assertTrue($roleA->canBeManagedBy($this->subA)); // 自己建的
$this->assertFalse($roleMain->canBeManagedBy($this->subA)); // 上層建的
$this->assertTrue($roleMain->canBeManagedBy($this->main)); // 主帳號管全公司
$this->assertTrue($roleA->canBeManagedBy($this->main)); // 主帳號管全公司
}
public function test_store_role_sets_created_by(): void
{
$this->actingAs($this->subA)->post(route('admin.data-config.sub-account-roles.store'), [
'name' => 'a-created-role',
'permissions' => ['menu.data-config.sub-accounts'],
])->assertSessionHas('success');
$role = Role::where('name', 'a-created-role')->where('company_id', $this->company->id)->first();
$this->assertNotNull($role);
$this->assertSame($this->subA->id, $role->created_by);
}
public function test_sub_account_cannot_update_others_role(): void
{
$roleB = $this->makeRole('role-b', $this->subB);
// A 嘗試改 B 建的角色 → 被擋
$this->actingAs($this->subA)->put(route('admin.data-config.sub-account-roles.update', $roleB->id), [
'name' => 'hijacked',
'permissions' => [],
])->assertSessionHas('error');
$this->assertDatabaseHas('roles', ['id' => $roleB->id, 'name' => 'role-b']);
}
public function test_sub_account_cannot_delete_others_role(): void
{
$roleMain = $this->makeRole('role-main', $this->main);
$this->actingAs($this->subA)->delete(route('admin.data-config.sub-account-roles.destroy', $roleMain->id))
->assertSessionHas('error');
$this->assertDatabaseHas('roles', ['id' => $roleMain->id]);
}
}