コンテンツにスキップ

ロールと店舗アサイン

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 に従う。


店舗スコープの roleoperator_store_link を管理し、オペレーターの所属・ロール・実効権限を制御する。

本SRSは「誰がどの店舗に、どの role で所属しているか」を扱う。認証要素そのものは SRS-TEN-002 が、operator → staff の業務リンクは SRS-CUS-004 が所有する。


  • 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 設定ミスを検証できる

Section titled “3.1 主シナリオ(招待 → 受諾 → link 作成)”
  1. 管理者が role を選んで招待を発行する
  2. operator_invitation が pending になる
  3. 招待先が SRS-TEN-002 フローで本人確認と Passkey 登録を完了する
  4. 同一トランザクションで operator_store_link(operator_id, store_id, role_id) を作成する
  5. accepted_ataccepted_operator_id を更新する
  6. operator は active_store_id = store_id でログイン完了する
  • AF-1 既存 operator に追加店舗を付与する。新規 identity は作らない
  • AF-2 管理者が custom role を作成する。is_preset=falserolerole_permission を作る
  • AF-3 管理者が既存 link の role_id を変更する
  • AF-4 管理者が所属を revoke する
  • AF-5 管理者が effective permission viewer で role 由来の権限を確認する。Phase 1 は override 配列が常に空
  • 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

  • オペレーター一覧画面
  • 招待一覧画面
  • role 一覧画面
  • custom role 作成 / 編集ダイアログ
  • operator detail drawer
  • effective permission viewer
  • 一覧は「所属済み」と「招待中」を分けて表示する
  • operator row は role 変更、revoke、viewer を持つ
  • preset role は閲覧のみ。編集ボタンを出さない
  • custom role は name と permission 集合を編集できる
  • viewer は role permissions, effective permissions を表示する。Phase 1 は override 配列を常に空で返す
  • custom role key は store 内 unique、owner|manager|staff|receptionist と衝突禁止
  • custom role name は 1〜100 文字
  • revoke は確認ダイアログ必須
  • self-link 操作は UI でも disable、API でも拒否

MethodPath用途permission
GET/api/admin/rolesrole 一覧admin:role:read
POST/api/admin/rolescustom role 作成admin:role:read(Phase 1 は owner のみ alpha 機能、UI 非公開でも可)
PATCH/api/admin/roles/:roleIdcustom role 更新同上
GET/api/admin/operators/:id/effective-permissions実効権限ビューアadmin:operator:read
POST/api/admin/operators/:id/assign-rolelink role 変更admin:operator_store_link:write
POST/api/admin/operators/:id/revoke店舗所属 revokeadmin:operator_store_link:write

custom role 機能は Phase 1 では UI 非公開。API は存在するが owner のみ。

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 } }
code意味
RBAC.LAST_OWNER_REQUIRED最後の owner 維持ルール違反
RBAC.SELF_LINK_MUTATION_FORBIDDEN自分自身の link 操作禁止
RBAC.PRESET_ROLE_IMMUTABLEpreset role 編集禁止
RBAC.ROLE_KEY_CONFLICTcustom role key 衝突
RBAC.OPERATOR_NOT_LINKED当該 store 未所属
RBAC.INVITATION_ROLE_INVALID受諾時に role 不正

operator_store_link(中間テーブル、複合PK)

Section titled “operator_store_link(中間テーブル、複合PK)”
カラム制約
operator_iduuidNOT NULL, FK → operator(id)
store_iduuidNOT NULL, FK → store(id)
role_iduuidNOT NULL, FK (store_id, role_id) → role(store_id, id)
created_attimestamptzNOT 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 で詳述)。

本テーブル自体は TEN-002 所有だが、本SRSが state machine を所有する。 pending / accepted / revoked / expired は status 列ではなく accepted_at / revoked_at / expires_at から導出する。

  • Phase 1 では作らない(DB スキーマすら不在)
  • admin:operator_override:* 系 permission key は Phase 1 では予約しない
  • viewer は role-only 計算結果を返す

0013_operator_membership_and_staff_link.sql で以下を 1 ファイルに統合する(CUS-004 と共有)。

  1. operator_store_link 作成、RLS ENABLE + FORCE、GRANT SELECT, INSERT, UPDATE, DELETE ON operator_store_link TO app
  2. operator_staff_link 作成(CUS-004 が canonical schema 所有)
  3. 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 追加で完結させる。


Section titled “7.1 招待 → 受諾 → link の状態機械”
状態条件遷移先
pendingaccepted_at IS NULL AND revoked_at IS NULL AND expires_at > now()accepted, revoked, expired
acceptedaccepted_at IS NOT NULLなし
revokedrevoked_at IS NOT NULLなし
expiredaccepted_at IS NULL AND revoked_at IS NULL AND expires_at <= now()なし

ルール:

  • pending -> acceptedoperator_store_link 作成は必須
  • accepted になった招待は再送しない。新規招待行を作る
  • expired は自動状態であり復活させない
  • 自分自身への role change を禁止する
  • 自分自身の revoke を禁止する
  • 理由はセッション継続中の権限面・ロックアウト面が汚いから。Phase 1 では単純禁止が正しい
  • 各 store は常に preset role key = 'owner' の active link を 1 件以上持つ必要がある
  • custom role に全権限を積んでも「owner の代替」とは見なさない
  • owner 数が 1 件の時、その link の revoke と role change は禁止する
  • owner, manager, staff, receptionist は name, key, permission set を編集不可
  • preset role は seed と migration backfill の基準点なので、運用時変更を許すと全店整合が壊れる
  • is_preset=false role のみ作成 / 編集可
  • delete は Phase 1 では提供しない
  • custom role への permission 付与は role_permission を丸ごと置換する
  • Phase 1 では UI 非公開、API のみ owner で利用可
  • Phase 1 の実効権限 = role_permission のみ
  • override_feature_enabled=false
  • overrides=[]
  • viewer API は admin:operator:read で守る
  • assign-role, revoke, custom-role.create, custom-role.updateoperator_action_log に記録する
  • actor_kind='operator'operator_id を NOT NULL で記録
  • viewer は read 系なので通常監査対象外だが、sensitive 列が Y のため必要に応じて admin:audit:read 系で別追跡
  • permission resolver は packages/auth
  • Hono middleware は apps/api/src/middleware/require-permission.ts
  • requireOperatorStoreLinkWrite のような特殊 middleware も apps/api 側に置く

  • operator list p95 300ms 以下
  • effective permission viewer p95 200ms 以下
  • role 変更は次 request から即時反映
  • list API は current store scope のみ。全店横断 API は Phase 1 で作らない

key用途初期付与
admin:role:readrole 閲覧owner / manager / staff / receptionist
admin:operator:readoperator/link/effective permission 閲覧owner / manager
admin:operator_store_link:writelink の role 変更・revokeowner / manager

custom role の admin:role:write は Phase 1 では予約せず、UI 非公開 API は admin:role:read 保有 + owner role チェックで暫定動作する。Phase 2 で admin:role:write 正式追加(OQ)。

  • operator_store_linkstore_id 基準で tenant RLS
  • rolerole_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 403 RBAC.PRESET_ROLE_IMMUTABLE
  • GWT-10 custom role 作成: Given owner / When custom role 作成 / Then is_preset=false role と 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

  • 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

  • なし(招待メール送信は TEN-002 job を利用)

#内容扱い
OQ-TEN-003-01admin:role:write を Phase 2 で正式追加Phase 2 検討
OQ-TEN-003-02custom role の delete / archivePhase 2 で再設計

VersionDateAuthorChange
0.12026-05-05Codex / yudai初版ドラフト
0.22026-05-05yudai (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 共有