security(openspec): 新增 add-role-middleware change 規劃
Run Tests / test (pull_request) Successful in 36s
Run Tests / test (pull_request) Successful in 36s
待辦 H1:member/provider 路由群組補上角色 middleware。 新增 role-authorization 規格,並修改 feature-test-coverage 的 「預約列表端點測試覆蓋」。spec-reviewer 與 design-hardener 均 PASS。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 預約列表端點測試覆蓋
|
||||
測試套件 SHALL 驗證下列三個路由的資料隔離行為:
|
||||
- `GET /api/member/bookings`(middleware: `auth:sanctum` + `member`,非 member 角色於 middleware 層強制 403;隔離靠 `where member_id = auth()->id()`)
|
||||
- `GET /api/provider/bookings`(middleware: `auth:sanctum` + `provider`,非 provider 角色於 middleware 層強制 403;隔離靠 `whereHas schedule.provider_id`)
|
||||
- `GET /api/admin/bookings`(middleware: `auth:sanctum` + `admin`,EnsureAdmin 於 middleware 層強制 403)
|
||||
|
||||
#### Scenario: Member 只能看到自己的預約
|
||||
- **WHEN** Member A 呼叫 `GET /api/member/bookings`,系統中同時存在 Member A 與 Member B 各一筆預約
|
||||
- **THEN** 回傳 data 只包含 Member A 的 booking id;Member B 的 booking id 不在 data 中
|
||||
|
||||
#### Scenario: Provider 只能看到屬於自己課程的預約
|
||||
- **WHEN** Provider A 呼叫 `GET /api/provider/bookings`,系統中有屬於 Provider A 與 Provider B 課程各一筆預約
|
||||
- **THEN** 回傳 data 只包含 Provider A 課程的 booking id;Provider B 課程的 booking id 不在 data 中
|
||||
|
||||
#### Scenario: Admin 可查看所有預約
|
||||
- **WHEN** Admin 呼叫 `GET /api/admin/bookings`,系統中有 2 筆不同 Member 的預約
|
||||
- **THEN** 回傳 data 包含兩筆 booking id
|
||||
|
||||
#### Scenario: 未認證請求被拒絕
|
||||
- **WHEN** 未帶 token 呼叫 `GET /api/member/bookings`
|
||||
- **THEN** 回傳 HTTP 401
|
||||
@@ -0,0 +1,68 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
依使用者角色限制 member、provider 專屬 API 路由群組的存取,避免已登入的其他角色跨角色呼叫課程、時段、預約等業務端點。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Member 專屬路由僅限 member 角色存取
|
||||
`/api/member/*` 路由群組(不含公開的 `POST /api/member/register`、`POST /api/member/login`)SHALL 只允許 `role = member` 的已登入使用者存取。其他角色 MUST 收到 HTTP 403,回應為 `{ "status": false, "message": "無權限存取" }`,且請求 MUST NOT 產生任何資料變更。未帶 token 的請求 MUST 收到 HTTP 401。
|
||||
|
||||
#### Scenario: member 可存取 member 路由
|
||||
- **WHEN** 一個 `role = member` 的使用者,以該使用者的 token 呼叫 `GET /api/member/profile` 與 `GET /api/member/bookings`
|
||||
- **THEN** 回傳 HTTP 200,`status` 為 `true`
|
||||
|
||||
#### Scenario: provider 呼叫 member 路由被拒絕
|
||||
- **WHEN** 一個 `role = provider` 的使用者,以該使用者的 token 呼叫 `GET /api/member/profile`、`GET /api/member/bookings`、`POST /api/member/bookings`、`POST /api/member/reviews`
|
||||
- **THEN** 每個請求都回傳 HTTP 403,body 為 `{ "status": false, "message": "無權限存取" }`
|
||||
|
||||
#### Scenario: admin 呼叫 member 路由被拒絕
|
||||
- **WHEN** 一個 `role = admin` 的使用者,以該使用者的 token 呼叫 `GET /api/member/profile`、`GET /api/member/bookings`、`POST /api/member/bookings`
|
||||
- **THEN** 每個請求都回傳 HTTP 403,body 為 `{ "status": false, "message": "無權限存取" }`
|
||||
|
||||
#### Scenario: 非 member 建立預約不會產生資料
|
||||
- **WHEN** 一個可預約的課程時段,以及一個 `role = provider` 的使用者,以該使用者的 token 呼叫 `POST /api/member/bookings` 並帶入合法的時段 id
|
||||
- **THEN** 回傳 HTTP 403,`bookings` 資料表筆數不變,該時段的 `current_participants` 不變
|
||||
|
||||
#### Scenario: 未登入呼叫 member 路由
|
||||
- **WHEN** 不帶 token 呼叫 `GET /api/member/profile`、`GET /api/member/bookings`
|
||||
- **THEN** 回傳 HTTP 401
|
||||
|
||||
### Requirement: Provider 專屬路由僅限 provider 角色存取
|
||||
`/api/provider/*` 路由群組(不含公開的 `POST /api/provider/register`、`POST /api/provider/login`)SHALL 只允許 `role = provider` 的已登入使用者存取,且 MUST NOT 以教練審核狀態作為進入條件。其他角色 MUST 收到 HTTP 403,回應為 `{ "status": false, "message": "無權限存取" }`,且請求 MUST NOT 產生任何資料變更。未帶 token 的請求 MUST 收到 HTTP 401。
|
||||
|
||||
#### Scenario: provider 可存取 provider 路由
|
||||
- **WHEN** 一個 `role = provider` 的使用者,以該使用者的 token 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`、`GET /api/provider/schedules`、`GET /api/provider/bookings`
|
||||
- **THEN** 回傳 HTTP 200,`status` 為 `true`
|
||||
|
||||
#### Scenario: 未通過審核的 provider 仍可進入 provider 路由
|
||||
- **WHEN** 一個 `role = provider`、`verification_status` 不是 approved(例如 unsubmitted 或 pending)的使用者,以該使用者的 token 呼叫 `GET /api/provider/verification`
|
||||
- **THEN** 回傳 HTTP 200,不會因角色 middleware 回 403
|
||||
|
||||
#### Scenario: member 呼叫 provider 路由被拒絕
|
||||
- **WHEN** 一個 `role = member` 的使用者,以該使用者的 token 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`、`POST /api/provider/offers`、`POST /api/provider/schedules`、`GET /api/provider/bookings`、`GET /api/provider/verification`
|
||||
- **THEN** 每個請求都回傳 HTTP 403,body 為 `{ "status": false, "message": "無權限存取" }`
|
||||
|
||||
#### Scenario: admin 呼叫 provider 路由被拒絕
|
||||
- **WHEN** 一個 `role = admin` 的使用者,以該使用者的 token 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`、`POST /api/provider/offers`、`GET /api/provider/bookings`
|
||||
- **THEN** 每個請求都回傳 HTTP 403,body 為 `{ "status": false, "message": "無權限存取" }`
|
||||
|
||||
#### Scenario: 非 provider 建立課程不會產生資料
|
||||
- **WHEN** 一個 `role = member` 的使用者,以該使用者的 token 呼叫 `POST /api/provider/offers` 並帶入通過驗證的完整課程資料
|
||||
- **THEN** 回傳 HTTP 403,`diving_offers` 資料表筆數不變
|
||||
|
||||
#### Scenario: 未登入呼叫 provider 路由
|
||||
- **WHEN** 不帶 token 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`
|
||||
- **THEN** 回傳 HTTP 401
|
||||
|
||||
### Requirement: 跨角色共用路由不受角色限制影響
|
||||
通知(`/api/notifications/*`)與預約即時訊息(`/api/bookings/{booking}/messages*`、`/api/bookings/messages/unread-counts`)路由 SHALL 維持既有行為:member 與 provider 皆可存取,存取權限由既有的資源歸屬檢查決定,不套用 member 或 provider 角色限制。
|
||||
|
||||
#### Scenario: member 與 provider 皆可讀取通知
|
||||
- **WHEN** 一個 `role = member` 與一個 `role = provider` 的使用者,分別以各自的 token 呼叫 `GET /api/notifications`
|
||||
- **THEN** 兩者皆回傳 HTTP 200
|
||||
|
||||
#### Scenario: member 與 provider 皆可讀取未讀訊息數
|
||||
- **WHEN** 一個 `role = member` 與一個 `role = provider` 的使用者,分別以各自的 token 呼叫 `GET /api/bookings/messages/unread-counts`
|
||||
- **THEN** 兩者皆回傳 HTTP 200
|
||||
Reference in New Issue
Block a user