ロールと店舗アサイン
ロールと店舗アサイン
Section titled “ロールと店舗アサイン”Document ID: SRS-TEN-003 Parent: SRS-ROOT-001 v0.5 Version: 0.2 Status: Draft Last Updated: 2026-05-05 Depends on: SRS-TEN-001 v0.4, SRS-TEN-002 v0.2 依存される: SRS-MST-001, SRS-MST-002, SRS-CUS-004, SRS-RES-001/002/003/004/005, SRS-CUS-001/002, SRS-REG-001/002
本書は SRS-ROOT-001 v0.5 に従う。
店舗スコープの role と operator_store_link を管理し、オペレーターの所属・ロール・実効権限を制御する。
本SRSは「誰がどの店舗に、どの role で所属しているか」を扱う。認証要素そのものは SRS-TEN-002 が、operator → staff の業務リンクは SRS-CUS-004 が所有する。
2. ユーザーストーリー
Section titled “2. ユーザーストーリー”- As a owner, I want to 店長や受付を店舗に招待・所属させたい, so that 店舗ごとの権限を明示できる
- As a manager, I want to 自店舗の staff / receptionist の所属を管理したい, so that 日常運用をオーナーに依存しない
- As a owner, I want to 最後の owner を消せないようにしたい, so that 店舗ロックアウトを防げる
- As a owner, I want to custom role を作りつつ preset role は壊したくない, so that 既定運用と例外運用を分離できる
- As a 管理者, I want to オペレーターごとの effective permission を見たい, so that role 設定ミスを検証できる
3. ユースケース
Section titled “3. ユースケース”3.1 主シナリオ(招待 → 受諾 → link 作成)
Section titled “3.1 主シナリオ(招待 → 受諾 → link 作成)”- 管理者が role を選んで招待を発行する
operator_invitationが pending になる- 招待先が SRS-TEN-002 フローで本人確認と Passkey 登録を完了する
- 同一トランザクションで
operator_store_link(operator_id, store_id, role_id)を作成する accepted_atとaccepted_operator_idを更新する- operator は
active_store_id = store_idでログイン完了する
3.2 代替フロー
Section titled “3.2 代替フロー”- AF-1 既存 operator に追加店舗を付与する。新規 identity は作らない
- AF-2 管理者が custom role を作成する。
is_preset=falseのroleとrole_permissionを作る - AF-3 管理者が既存 link の
role_idを変更する - AF-4 管理者が所属を revoke する
- AF-5 管理者が effective permission viewer で role 由来の権限を確認する。Phase 1 は override 配列が常に空
3.3 例外フロー
Section titled “3.3 例外フロー”- EF-1 同一
(operator_id, store_id)link 再作成: 200 no-op または 409。契約は no-op より 409 の方が運用で気づきやすい - EF-2 最後の owner を revoke: 422
RBAC.LAST_OWNER_REQUIRED - EF-3 最後の owner を custom role / manager に降格: 422
RBAC.LAST_OWNER_REQUIRED - EF-4 自分自身の link を role change / revoke: 422
RBAC.SELF_LINK_MUTATION_FORBIDDEN - EF-5 preset role 編集: 403
RBAC.PRESET_ROLE_IMMUTABLE - EF-6 招待受諾と link 作成の間で role が削除・無効化: 409
RBAC.INVITATION_ROLE_INVALID
4. UI仕様
Section titled “4. UI仕様”4.1 画面構成
Section titled “4.1 画面構成”- オペレーター一覧画面
- 招待一覧画面
- role 一覧画面
- custom role 作成 / 編集ダイアログ
- operator detail drawer
- effective permission viewer
4.2 要素と挙動
Section titled “4.2 要素と挙動”- 一覧は「所属済み」と「招待中」を分けて表示する
- operator row は role 変更、revoke、viewer を持つ
- preset role は閲覧のみ。編集ボタンを出さない
- custom role は name と permission 集合を編集できる
- viewer は
role permissions,effective permissionsを表示する。Phase 1 は override 配列を常に空で返す
4.3 バリデーション
Section titled “4.3 バリデーション”- custom role
keyは store 内 unique、owner|manager|staff|receptionistと衝突禁止 - custom role
nameは 1〜100 文字 - revoke は確認ダイアログ必須
- self-link 操作は UI でも disable、API でも拒否
5. API仕様
Section titled “5. API仕様”5.1 エンドポイント一覧
Section titled “5.1 エンドポイント一覧”| Method | Path | 用途 | permission |
|---|---|---|---|
| GET | /api/admin/roles | role 一覧 | admin:role:read |
| POST | /api/admin/roles | custom role 作成 | admin:role:read(Phase 1 は owner のみ alpha 機能、UI 非公開でも可) |
| PATCH | /api/admin/roles/:roleId | custom role 更新 | 同上 |
| GET | /api/admin/operators/:id/effective-permissions | 実効権限ビューア | admin:operator:read |
| POST | /api/admin/operators/:id/assign-role | link role 変更 | admin:operator_store_link:write |
| POST | /api/admin/operators/:id/revoke | 店舗所属 revoke | admin:operator_store_link:write |
custom role 機能は Phase 1 では UI 非公開。API は存在するが owner のみ。
5.2 レスポンス要約
Section titled “5.2 レスポンス要約”GET /api/admin/roles
{ data: { roles: [{ id, key, name, is_preset, permissions[] }] } }
GET /api/admin/operators/:id/effective-permissions
{ data: { operator_id, store_id, role, role_permissions[], overrides: [], effective_permissions[], override_feature_enabled: false } }
POST /api/admin/operators/:id/assign-role
- request
{ role_id } - response
{ data: { operator_id, store_id, role_id } }
5.3 エラーコード
Section titled “5.3 エラーコード”| code | 意味 |
|---|---|
RBAC.LAST_OWNER_REQUIRED | 最後の owner 維持ルール違反 |
RBAC.SELF_LINK_MUTATION_FORBIDDEN | 自分自身の link 操作禁止 |
RBAC.PRESET_ROLE_IMMUTABLE | preset role 編集禁止 |
RBAC.ROLE_KEY_CONFLICT | custom role key 衝突 |
RBAC.OPERATOR_NOT_LINKED | 当該 store 未所属 |
RBAC.INVITATION_ROLE_INVALID | 受諾時に role 不正 |
6. データモデル影響
Section titled “6. データモデル影響”6.1 スキーマ
Section titled “6.1 スキーマ”operator_store_link(中間テーブル、複合PK)
Section titled “operator_store_link(中間テーブル、複合PK)”| カラム | 型 | 制約 |
|---|---|---|
operator_id | uuid | NOT NULL, FK → operator(id) |
store_id | uuid | NOT NULL, FK → store(id) |
role_id | uuid | NOT NULL, FK (store_id, role_id) → role(store_id, id) |
created_at | timestamptz | NOT NULL |
| PRIMARY KEY | (operator_id, store_id) |
規律:
- 独立 id は持たない
- 1 operator は同一 store に 1 link だけ
- role は必ず同一 store の role に限定
UNIQUE(operator_id, store_id) 兼 PK が他テーブル参照可能となるため、operator_staff_link 等は (store_id, operator_id) に対して FK を張る(CUS-004 で詳述)。
operator_invitation 受諾状態
Section titled “operator_invitation 受諾状態”本テーブル自体は TEN-002 所有だが、本SRSが state machine を所有する。
pending / accepted / revoked / expired は status 列ではなく accepted_at / revoked_at / expires_at から導出する。
operator_permission_override
Section titled “operator_permission_override”- Phase 1 では作らない(DB スキーマすら不在)
admin:operator_override:*系 permission key は Phase 1 では予約しない- viewer は role-only 計算結果を返す
6.2 マイグレーション計画
Section titled “6.2 マイグレーション計画”0013_operator_membership_and_staff_link.sql で以下を 1 ファイルに統合する(CUS-004 と共有)。
operator_store_link作成、RLS ENABLE + FORCE、GRANT SELECT, INSERT, UPDATE, DELETE ON operator_store_link TO appoperator_staff_link作成(CUS-004 が canonical schema 所有)- permission backfill
admin:operator:read/create/update/retire(TEN-002)admin:operator_store_link:write(本SRS)admin:operator_staff_link:read/write(CUS-004)
permission backfill は 0013 1 ファイル内で INSERT ... ON CONFLICT(key) DO UPDATE SET description + 既存全 store の preset role への role_permission 追加で完結させる。
7. 業務ルール
Section titled “7. 業務ルール”7.1 招待 → 受諾 → link の状態機械
Section titled “7.1 招待 → 受諾 → link の状態機械”| 状態 | 条件 | 遷移先 |
|---|---|---|
pending | accepted_at IS NULL AND revoked_at IS NULL AND expires_at > now() | accepted, revoked, expired |
accepted | accepted_at IS NOT NULL | なし |
revoked | revoked_at IS NOT NULL | なし |
expired | accepted_at IS NULL AND revoked_at IS NULL AND expires_at <= now() | なし |
ルール:
pending -> acceptedでoperator_store_link作成は必須acceptedになった招待は再送しない。新規招待行を作るexpiredは自動状態であり復活させない
7.2 自分自身の link 操作禁止
Section titled “7.2 自分自身の link 操作禁止”- 自分自身への role change を禁止する
- 自分自身の revoke を禁止する
- 理由はセッション継続中の権限面・ロックアウト面が汚いから。Phase 1 では単純禁止が正しい
7.3 最後の owner 保護
Section titled “7.3 最後の owner 保護”- 各 store は常に
preset role key = 'owner'の active link を 1 件以上持つ必要がある - custom role に全権限を積んでも「owner の代替」とは見なさない
- owner 数が 1 件の時、その link の revoke と role change は禁止する
7.4 preset role 編集禁止
Section titled “7.4 preset role 編集禁止”owner,manager,staff,receptionistは name, key, permission set を編集不可- preset role は seed と migration backfill の基準点なので、運用時変更を許すと全店整合が壊れる
7.5 custom role
Section titled “7.5 custom role”is_preset=falserole のみ作成 / 編集可- delete は Phase 1 では提供しない
- custom role への permission 付与は
role_permissionを丸ごと置換する - Phase 1 では UI 非公開、API のみ owner で利用可
7.6 effective permission viewer
Section titled “7.6 effective permission viewer”- Phase 1 の実効権限 =
role_permissionのみ override_feature_enabled=falseoverrides=[]- viewer API は
admin:operator:readで守る
7.7 監査
Section titled “7.7 監査”assign-role,revoke,custom-role.create,custom-role.updateはoperator_action_logに記録するactor_kind='operator'でoperator_idを NOT NULL で記録- viewer は read 系なので通常監査対象外だが、
sensitive列がYのため必要に応じてadmin:audit:read系で別追跡
7.8 layer 分離
Section titled “7.8 layer 分離”- permission resolver は
packages/auth - Hono middleware は
apps/api/src/middleware/require-permission.ts requireOperatorStoreLinkWriteのような特殊 middleware もapps/api側に置く
8. 非機能要件
Section titled “8. 非機能要件”- operator list p95 300ms 以下
- effective permission viewer p95 200ms 以下
- role 変更は次 request から即時反映
- list API は current store scope のみ。全店横断 API は Phase 1 で作らない
9. セキュリティ・認可
Section titled “9. セキュリティ・認可”9.1 使用する permission キー
Section titled “9.1 使用する permission キー”| key | 用途 | 初期付与 |
|---|---|---|
admin:role:read | role 閲覧 | owner / manager / staff / receptionist |
admin:operator:read | operator/link/effective permission 閲覧 | owner / manager |
admin:operator_store_link:write | link の role 変更・revoke | owner / manager |
custom role の admin:role:write は Phase 1 では予約せず、UI 非公開 API は admin:role:read 保有 + owner role チェックで暫定動作する。Phase 2 で admin:role:write 正式追加(OQ)。
9.2 RLS
Section titled “9.2 RLS”operator_store_linkはstore_id基準で tenant RLSroleとrole_permissionは既存 TEN-001 RLS を利用operator_invitationは TEN-002 RLS を利用operator単体 read は app 層で store join を強制する
10. 受け入れ基準(Given-When-Then)
Section titled “10. 受け入れ基準(Given-When-Then)”- GWT-1 招待受諾で link 作成: Given pending invitation / When 受諾成功 / Then
(operator_id, store_id)link が 1 件作成される - GWT-2 link 重複拒否: Given 既に同一 link 存在 / When 同じ operator/store に再作成 / Then 409
- GWT-3 role 変更成功: Given owner が manager link を保持 / When receptionist role に変更 / Then
role_id更新 - GWT-4 revoke 成功: Given owner が receptionist link を選択 / When revoke / Then link 削除または失効
- GWT-5 最後の owner revoke 禁止: Given owner link が 1 件のみ / When revoke / Then 422
RBAC.LAST_OWNER_REQUIRED - GWT-6 最後の owner 降格禁止: Given owner link が 1 件のみ / When manager role に変更 / Then 422
- GWT-7 self revoke 禁止: Given 自分の link / When revoke / Then 422
RBAC.SELF_LINK_MUTATION_FORBIDDEN - GWT-8 self role change 禁止: Given 自分の link / When role change / Then 422
- GWT-9 preset role 編集拒否: Given
role.key='owner'/ When PATCH / Then 403RBAC.PRESET_ROLE_IMMUTABLE - GWT-10 custom role 作成: Given owner / When custom role 作成 / Then
is_preset=falserole とrole_permissionが作成される - GWT-11 effective viewer: Given manager / When viewer API / Then role 由来 permissions が返り
override_feature_enabled=false - GWT-12 override DB 不在: Given owner / When
operator_permission_overrideテーブルへの SELECT / Then table does not exist - GWT-13 invite expired state: Given
expires_at < now()/ When accept / Then link 作成されない - GWT-14 role key conflict: Given custom role key が preset と衝突 / When create / Then 409
RBAC.ROLE_KEY_CONFLICT
11. テスト計画
Section titled “11. テスト計画”- Unit: last-owner 判定, self-link 禁止, invitation derived status, effective permission 計算
- Integration: create role, assign role, revoke, viewer, preset immutability, RLS
- Contract: operator admin APIs
- Cross-SRS: invite accept transaction with TEN-002
12. 関連ジョブ(graphile-worker)
Section titled “12. 関連ジョブ(graphile-worker)”- なし(招待メール送信は TEN-002 job を利用)
13. Open Questions
Section titled “13. Open Questions”| # | 内容 | 扱い |
|---|---|---|
| OQ-TEN-003-01 | admin:role:write を Phase 2 で正式追加 | Phase 2 検討 |
| OQ-TEN-003-02 | custom role の delete / archive | Phase 2 で再設計 |
14. 変更履歴
Section titled “14. 変更履歴”| Version | Date | Author | Change |
|---|---|---|---|
| 0.1 | 2026-05-05 | Codex / yudai | 初版ドラフト |
| 0.2 | 2026-05-05 | yudai (with Codex co-design) | Round 2 反映: migration 番号 0013 に確定(CUS-004 と同居)、operator_permission_override の DB 不在(Phase 1)を明文化、admin:operator_override:* 予約 key を撤去、layer 分離(packages/auth = resolver、apps/api = middleware)、custom role API は Phase 1 で UI 非公開。CUS-004 と permission backfill 共有 |