d83d941993
Run Tests / test (pull_request) Successful in 34s
- 新增主規格 role-authorization,更新 feature-test-coverage 的 middleware 描述 - 勾選手動驗證任務 5.1、5.2(23/23) - CLAUDE.md 授權規範與 openspec/config.yaml 移除「member/provider 只有 auth:sanctum」的過時說明 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.0 KiB
6.0 KiB
Design
Context
動機見 proposal.md(Why)。目前現況:
bootstrap/app.php只註冊一個角色 middleware alias:admin→App\Http\Middleware\EnsureAdmin。它在!$request->user() || !$request->user()->isAdmin()時回403 { status: false, message: "無權限存取" }App\Models\User已有isMember()、isProvider()、isAdmin(),都是直接比對role字串/api/member/*、/api/provider/*群組內,只有AuthController的 logout/refresh/profile/change-password 自己檢查角色,回應同樣是403 { status: false, message: "無權限存取" };MemberBookingController、ReviewController(store/update/destroy)、ProviderOfferController、ScheduleController、ProviderBookingController、CourseImageController、ProviderVerificationController只檢查資源所有權,不檢查角色- 前端
axios.js、coachAxios.js的攔截器只對 401 做 refresh 重試,403 直接交給呼叫端;adminAxios.js沒有呼叫/member/*、/provider/* - 部署流程(
deploy.yml)每次都會執行route:cache,路由變更隨部署生效
Goals / Non-Goals
Goals:
- 在路由層擋下所有跨角色呼叫 member/provider 專屬端點
- 角色不符時的回應與現有
EnsureAdmin、AuthController完全一致,前端不需要改
Non-Goals:
- 不檢查
is_active(停用帳號沿用既有行為,與角色 middleware 無關)、教練審核狀態(審核狀態的限制維持在既有業務邏輯,例如visibleToPublicscope) - 不移除
AuthController內既有的角色檢查(重構屬待辦 M1) - 不調整跨角色共用路由(通知、即時訊息、
/reviews/{id}/helpful、/logout、/user),包含「admin 能否存取這些路由」也不在此次決定 - 不更動
EnsureAdmin
Decisions
D1:新增 EnsureMember、EnsureProvider 兩個 middleware,而非參數化的 EnsureRole
- 做法:在
app/Http/Middleware/新增兩個 class,結構與EnsureAdmin相同,分別呼叫isMember()、isProvider();在bootstrap/app.php註冊 aliasmember、provider - 理由:既有慣例是每個角色一個 middleware(
EnsureAdmin+ aliasadmin)。若新增參數化的role:member,專案內會同時存在兩種寫法;要統一就得改EnsureAdmin的使用處,超出此次範圍 - 替代方案:
EnsureRole搭配role:member,provider。程式碼較少,但會跟既有的adminalias 形成兩種寫法並存,因此不採用
D2:403 回應沿用 { status: false, message: "無權限存取" }
- 理由:與
EnsureAdmin、AuthController::getUnauthorizedResponse()一致,前端與 Swagger 已記載的 403 不需變動 - middleware 內同時處理
$request->user()為 null 的情況(與EnsureAdmin相同的防禦寫法);實際上auth:sanctum排在前面,未登入會先回 401
D3:middleware 只比對角色,不檢查審核狀態
- 理由:未審核(
unsubmitted)、審核中(pending)、被駁回(rejected)的教練都需要進入/api/provider/verification*送審或補件,也需要在審核通過前先建立課程。若在 middleware 檢查審核狀態,這些流程會整個斷掉
D4:掛在既有群組的 middleware 陣列,順序為 ['auth:sanctum', 'member']/['auth:sanctum', 'provider']
- 理由:與 admin 群組
['auth:sanctum', 'admin']寫法一致;auth:sanctum先執行,保證未登入回 401、角色不符回 403 - 公開的 register/login 路由定義在群組外,不受影響(已核對
routes/api.php)
D5:Controller 內既有的資源所有權檢查全部保留
- 理由:CLAUDE.md 授權規範要求 middleware 補上後仍保留 Controller 內的所有權檢查;角色 middleware 只解決「角色錯」,不解決「同角色存取別人的資源」
安全確認
- 輸入驗證:此次不新增外部輸入;既有端點的
$request->validate()不變。middleware 只讀取 Sanctum 認證後的$request->user()->role,沒有讀取任何請求參數 - SQL/shell 拼接:無
- 資料庫存取:middleware 不額外查詢資料庫(
$request->user()由 Sanctum 認證時已載入);無 raw query,無 N+1 - 權限檢查層級:路由層(
member、providermiddleware),加上 Controller 內既有的資源所有權檢查(例如MemberBookingController::show比對member_id、ProviderOfferController比對provider_id) - 未授權觸發可能性:修正後,非 member 角色無法存取
/api/member/*,非 provider 角色無法存取/api/provider/*;未登入一律 401 - 各路由所掛 middleware:
/api/member/*(register、login 除外):auth:sanctum+member/api/provider/*(register、login 除外):auth:sanctum+provider- 其他路由不變
- 前端:不涉及 DOM 渲染;前端呼叫已帶各自的 Bearer token,不需改
Risks / Trade-offs
- [既有測試可能以錯誤角色呼叫 member/provider 路由,加上 middleware 後改回 403] → 實作時先跑完整
php artisan test,逐一判斷失敗的測試是「測試本身用錯角色」(修正測試)還是「真的依賴跨角色行為」(停下來回報) - [AuthController 內的角色檢查與 middleware 重複] → 行為一致、無害;移除留給待辦 M1,避免此次範圍擴大
- [admin 被擋在 member/provider 路由之外,若未來後台想借用這些端點會失敗] → 已核對前端 admin 端目前沒有呼叫;未來需要時應在
/api/admin/*新增端點 - [跨角色共用路由(通知、即時訊息)對 admin 的行為未定義] → 不在此次範圍;即時訊息的
authorizeParticipant對 admin 已回 false,通知只會讀到 admin 自己的資料
Migration Plan
- 無 migration、無新套件、無環境變數
- 部署:merge 後由
deploy.yml重建 image,並執行route:cache,新的路由 middleware 即生效 - 回滾:revert 此次 commit 再部署即可,沒有資料需要還原