[DOCS] 更新 RBAC 實作規範與角色初始化流程建議
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 55s

This commit is contained in:
2026-03-19 17:18:21 +08:00
parent 5548bb1cc9
commit f00fc940a9
13 changed files with 474 additions and 74 deletions

View File

@@ -32,7 +32,12 @@ class CompanyController extends Controller
$per_page = $request->input('per_page', 10);
$companies = $query->latest()->paginate($per_page)->withQueryString();
return view('admin.companies.index', compact('companies'));
// 取得可供選擇的客戶角色範本 (is_system = 0, company_id = null)
$template_roles = \App\Models\System\Role::where('is_system', 0)
->whereNull('company_id')
->get();
return view('admin.companies.index', compact('companies', 'template_roles'));
}
/**
@@ -54,6 +59,7 @@ class CompanyController extends Controller
'admin_username' => 'nullable|string|max:255|unique:users,username',
'admin_password' => 'nullable|string|min:8',
'admin_name' => 'nullable|string|max:255',
'admin_role' => 'nullable|string|exists:roles,name',
]);
DB::transaction(function () use ($validated) {
@@ -79,8 +85,30 @@ class CompanyController extends Controller
'status' => 1,
]);
// 綁定客戶管理員角色
$user->assignRole('tenant-admin');
// 角色初始化與克隆邏輯 (優先使用選擇的角色,否則使用預設)
$selected_role_name = $validated['admin_role'] ?? '通用客戶角色範本';
$role_to_assign = '管理員';
$template_role = \App\Models\System\Role::where('name', $selected_role_name)
->whereNull('company_id')
->where('is_system', 0)
->first();
if ($template_role) {
// 克隆範本為該公司的「管理員」
$clonedRole = \App\Models\System\Role::query()->create([
'name' => '管理員',
'guard_name' => 'web',
'company_id' => $company->id,
'is_system' => false,
]);
$clonedRole->syncPermissions($template_role->permissions);
} else {
// 如果找不到選定的角色範本,退而求其次嘗試指派現有角色 (通常不應發生)
$role_to_assign = $selected_role_name;
}
$user->assignRole($role_to_assign);
}
});

View File

@@ -16,10 +16,7 @@ class PermissionController extends Controller
// 租戶隔離:租戶只能看到自己公司的角色 + 系統角色 (company_id is null)
if (!$user->isSystemAdmin()) {
$query->where(function($q) use ($user) {
$q->where('company_id', $user->company_id)
->orWhereNull('company_id');
});
$query->where('company_id', $user->company_id);
}
// 搜尋:角色名稱
@@ -58,15 +55,21 @@ class PermissionController extends Controller
*/
public function storeRole(Request $request)
{
$is_system = auth()->user()->isSystemAdmin() && $request->boolean('is_system');
$company_id = $is_system ? null : auth()->user()->company_id;
$validated = $request->validate([
'name' => 'required|string|max:255|unique:roles,name',
'name' => [
'required', 'string', 'max:255',
\Illuminate\Validation\Rule::unique('roles', 'name')->where(function ($query) use ($company_id) {
return $query->where('company_id', $company_id);
})
],
'permissions' => 'nullable|array',
'permissions.*' => 'string|exists:permissions,name',
]);
$is_system = auth()->user()->isSystemAdmin() && $request->boolean('is_system');
$role = \App\Models\System\Role::create([
$role = \App\Models\System\Role::query()->create([
'name' => $validated['name'],
'guard_name' => 'web',
'company_id' => $is_system ? null : auth()->user()->company_id,
@@ -92,8 +95,18 @@ class PermissionController extends Controller
{
$role = \App\Models\System\Role::findOrFail($id);
$is_system = $role->is_system;
$company_id = $role->company_id;
$validated = $request->validate([
'name' => 'required|string|max:255|unique:roles,name,' . $id,
'name' => [
'required', 'string', 'max:255',
\Illuminate\Validation\Rule::unique('roles', 'name')
->ignore($id)
->where(function ($query) use ($company_id) {
return $query->where('company_id', $company_id);
})
],
'permissions' => 'nullable|array',
'permissions.*' => 'string|exists:permissions,name',
]);
@@ -177,10 +190,7 @@ class PermissionController extends Controller
$companies = auth()->user()->isSystemAdmin() ? \App\Models\System\Company::all() : collect();
$roles_query = \App\Models\System\Role::where('name', '!=', 'super-admin');
if (!auth()->user()->isSystemAdmin()) {
$roles_query->where(function($q) {
$q->where('company_id', auth()->user()->company_id)
->orWhereNull('company_id');
});
$roles_query->where('company_id', auth()->user()->company_id);
}
$roles = $roles_query->get();
@@ -198,7 +208,7 @@ class PermissionController extends Controller
$validated = $request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username',
'email' => 'nullable|email|max:255|unique:users,email',
'email' => 'required|email|max:255|unique:users,email',
'password' => 'required|string|min:8',
'role' => 'required|string',
'status' => 'required|boolean',
@@ -206,17 +216,73 @@ class PermissionController extends Controller
'phone' => 'nullable|string|max:20',
]);
$company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
// 查找角色:優先尋找該公司的角色,若無則尋找全域範本
$role = \App\Models\System\Role::where('name', $validated['role'])
->where(function($q) use ($company_id) {
$q->where('company_id', $company_id)->orWhereNull('company_id');
})
->first();
if (!$role) {
return redirect()->back()->with('error', __('Role not found.'));
}
// 驗證角色與公司的匹配性 (RBAC Safeguard)
if ($company_id !== null) {
// 如果是租戶帳號,不能選各項系統角色 (is_system = 1)
if ($role->is_system) {
return redirect()->back()->with('error', __('System roles cannot be assigned to tenant accounts.'));
}
// 如果角色有特定的 company_id必須匹配
if ($role->company_id !== null && $role->company_id != $company_id) {
return redirect()->back()->with('error', __('This role belongs to another company and cannot be assigned.'));
}
} else {
// 如果是系統層級帳號,只能選系統角色 (is_system = 1)
if (!$role->is_system) {
return redirect()->back()->with('error', __('Only system roles can be assigned to platform administrative accounts.'));
}
}
// 角色初始化與克隆邏輯 (只有 super-admin 在幫空白公司開帳號時觸發)
$role_to_assign = $validated['role'];
$company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
if ($company_id && $role && !$role->is_system && $role->company_id === null) {
// 檢查該公司是否已有名為「管理員」的角色
$existingRole = \App\Models\System\Role::where('company_id', $company_id)
->where('name', '管理員')
->first();
if (!$existingRole) {
// 克隆範本為該公司的「管理員」
$clonedRole = \App\Models\System\Role::query()->create([
'name' => '管理員',
'guard_name' => 'web',
'company_id' => $company_id,
'is_system' => false,
]);
$clonedRole->syncPermissions($role->permissions);
$role_to_assign = '管理員';
} else {
// 如果已存在名為「管理員」的角色,則直接使用它
$role_to_assign = '管理員';
}
}
$user = \App\Models\System\User::create([
'name' => $validated['name'],
'username' => $validated['username'],
'email' => $validated['email'],
'password' => \Illuminate\Support\Facades\Hash::make($validated['password']),
'status' => $validated['status'],
'company_id' => auth()->user()->isSystemAdmin() ? $validated['company_id'] : auth()->user()->company_id,
'company_id' => $company_id,
'phone' => $validated['phone'],
]);
$user->assignRole($validated['role']);
$user->assignRole($role_to_assign);
return redirect()->back()->with('success', __('Account created successfully.'));
}
@@ -235,7 +301,7 @@ class PermissionController extends Controller
$validated = $request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username,' . $id,
'email' => 'nullable|email|max:255|unique:users,email,' . $id,
'email' => 'required|email|max:255|unique:users,email,' . $id,
'password' => 'nullable|string|min:8',
'role' => 'required|string',
'status' => 'required|boolean',
@@ -243,6 +309,35 @@ class PermissionController extends Controller
'phone' => 'nullable|string|max:20',
]);
$target_company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
// 查找角色:優先尋找該公司的角色,若無則尋找全域範本
$roleObj = \App\Models\System\Role::where('name', $validated['role'])
->where(function($q) use ($target_company_id) {
$q->where('company_id', $target_company_id)->orWhereNull('company_id');
})
->first();
if (!$roleObj) {
return redirect()->back()->with('error', __('Role not found.'));
}
// 驗證角色與公司的匹配性 (RBAC Safeguard)
if ($user->id !== auth()->id()) { // 排除編輯自己 (super-admin 有特殊邏輯)
if ($target_company_id !== null) {
if ($roleObj->is_system) {
return redirect()->back()->with('error', __('System roles cannot be assigned to tenant accounts.'));
}
if ($roleObj->company_id !== null && $roleObj->company_id != $target_company_id) {
return redirect()->back()->with('error', __('This role belongs to another company and cannot be assigned.'));
}
} else {
if (!$roleObj->is_system) {
return redirect()->back()->with('error', __('Only system roles can be assigned to platform administrative accounts.'));
}
}
}
$updateData = [
'name' => $validated['name'],
'username' => $validated['username'],
@@ -265,13 +360,37 @@ class PermissionController extends Controller
$updateData['password'] = \Illuminate\Support\Facades\Hash::make($validated['password']);
}
// 角色初始化與克隆邏輯
$role_to_assign = $validated['role'];
$target_company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
if ($target_company_id && $roleObj && !$roleObj->is_system && $roleObj->company_id === null) {
// 檢查該公司是否已有名為「管理員」的角色
$existingRole = \App\Models\System\Role::where('company_id', $target_company_id)
->where('name', '管理員')
->first();
if (!$existingRole) {
$clonedRole = \App\Models\System\Role::query()->create([
'name' => '管理員',
'guard_name' => 'web',
'company_id' => $target_company_id,
'is_system' => false,
]);
$clonedRole->syncPermissions($roleObj->permissions);
$role_to_assign = '管理員';
} else {
$role_to_assign = '管理員';
}
}
$user->update($updateData);
// 如果是編輯自己且原本是超級管理員,強制保留 super-admin 角色
if ($user->id === auth()->id() && auth()->user()->isSystemAdmin()) {
$user->syncRoles(['super-admin']);
} else {
$user->syncRoles([$validated['role']]);
$user->syncRoles([$role_to_assign]);
}
return redirect()->back()->with('success', __('Account updated successfully.'));
@@ -292,6 +411,12 @@ class PermissionController extends Controller
return redirect()->back()->with('error', __('You cannot delete your own account.'));
}
// 為了解決軟刪除導致的唯一索引佔用問題,刪除前先重命名唯一欄位
$timestamp = now()->getTimestamp();
$user->username = $user->username . '.deleted.' . $timestamp;
$user->email = $user->email . '.deleted.' . $timestamp;
$user->save();
$user->delete();
return redirect()->back()->with('success', __('Account deleted successfully.'));