Skip to content

権限管理 (技術詳細) ​

マダミス.jp 店舗管理アプリ(javascript/mdms_jp/shop)における、ロール定義・全ページ別の利用可能機能・権限判定の実装をまとめる。

画面操作の目線でのまとめは 権限管理 を参照。

対象範囲は /shops/[id]/* および /shopGroups/[id]/* の全ページと、関連する GraphQL mutation。

ロール定義 ​

enum 上の定義 ​

javascript/mdms_jp/backend/prisma/schema.prisma

prisma
enum ShopMemberRole {
  ADMIN
  MANAGER
  STAFF
}

UI 上(権限管理画面)でユーザーに割り当てられる区分は 「管理者(ADMIN)」「運営責任者(MANAGER)」「一般スタッフ(STAFF)」の 3 つ。STAFF(一般スタッフ)が最小権限のロールで、MANAGER の UI 上の表示名は「運営責任者」。

実態としての権限レベル ​

enum は 3 値だが、実装上は以下の 5 段階 + 1(運営マスター、ctx.requester.isAdmin) が存在する。ランク判定は authorization.ts の SHOP_ROLE_RANK(ADMIN:3 / MANAGER:2 / STAFF:1、非メンバーは 0)で行い、各関所は「しきい値以上か」で判定する。

レベル識別フラグ意味
グループ管理者ShopGroupMember.isGroupAdmin = trueグループ全店舗で自動的に ADMIN 扱い + 店舗グループ自体の編集権限。新店舗が追加されても自動的に ADMIN 同等。
管理者(ADMIN)ShopMember.role = 'ADMIN'その店舗の管理者。
運営責任者(MANAGER)ShopMember.role = 'MANAGER'その店舗の運営責任者。日常運用 + 部屋・価格・店舗情報の編集 + 一般スタッフ招待・スタッフ紐付け。
一般スタッフ(STAFF)ShopMember.role = 'STAFF'その店舗の最小権限ロール。閲覧中心 + 公演・予約・顧客対応 + 自分に紐付いたスタッフ枠の編集 + 顧客メモ編集。
閲覧専用 ShopGroupMemberShopMember レコードなし & isGroupAdmin = false招待受諾直後やレガシー状態で発生しうる中間状態。顧客名簿などの閲覧系のみ通る。
運営マスターctx.requester.isAdmin = trueすべての権限判定を通る。

ShopGroupMember.isGroupAdmin のコメント(schema.prisma:216-217)

これが true の場合は、ShopGroup 自体の編集権限があり、かつ新規店舗が追加されても自動的に admin 同等の権限が与えられる。また、ShopMember が作成されていなかったとしても、role=ADMIN かのように振る舞う

ShopGroup・ShopGroupMember・ShopMember のリレーション ​

text
User
 └─ ShopGroupMember (shopGroupId × userId で unique)
     ├─ isGroupAdmin: Boolean
     └─ ShopMember[] (shopGroupMemberId × shopId で unique)
         └─ role: ADMIN | MANAGER | STAFF
  • 1 ユーザー × 1 店舗グループにつき ShopGroupMember が 1 件である
  • そのユーザーがグループ内のどの店舗に所属するかは ShopMember レコードで表現する
  • isGroupAdmin = true の場合、ShopMember レコードがなくてもすべての店舗で ADMIN として振る舞う

各権限の割り当て手順 ​

権限がどのように付与されるかを、種類別に整理する。

最初の店舗・グループの作成 ​

新しい店舗グループ(ShopGroup)と店舗(Shop)の作成は createShopWithGroup mutation(backend/src/schema/shop/mutations/create_shop_with_group.ts)で行う。実行には 運営マスターの権限が必須(ctx.requester.isAdmin)。

この mutation は店舗グループと店舗だけを作成し、ShopGroupMember を作らない。つまり、店舗作成直後の状態では誰も権限を持たない。最初のグループ管理者は次項「招待リンクによる加入」で割り当てる。

既存グループへの 2 店舗目以降の追加は createShopInGroup mutation を使う(これも運営マスター専用)。

グループ管理者の割り当て ​

経路操作必要権限備考
招待リンクによる加入/shopGroups/[id]/members または /shops/[id]/members で「招待リンクを作成」→ shopGroupId 指定で発行されたリンクを /invite/[token] で受諾リンク発行: isShopGroupAdmin / 受諾: ログインのみ受諾時に 自動で isGroupAdmin: true の ShopGroupMember が作成される(verify_invitation_link.ts)
既存メンバーの昇格/shopGroups/[id]/members で対象メンバーの「グループ管理者」チェックボックスを ON → 保存isShopGroupAdmin(既存のグループ管理者のみ実行可)updateShopGroupMember mutation で isGroupAdmin: true に更新(update_shop_group_member.ts:48)。同時に shopMembers[].role は 強制的に 'ADMIN' に上書きされる(同 85 行)
DB 直接操作サポート・開発者が psql などで ShopGroupMember.isGroupAdmin = true をセットDB アクセス権限初回のグループ管理者の設定など

管理者(ADMIN)の割り当て ​

ShopMember.role = 'ADMIN' を持つ状態。以下のいずれかで作られる。

経路操作必要権限備考
権限管理画面(グループ)/shopGroups/[id]/members で対象メンバーの店舗別ロール選択を「管理者」に変更 → 保存isShopGroupAdmin(グループ管理者)updateShopGroupMember の shopMembers[].role で指定(update_shop_group_member.ts:84-86)
権限管理画面(店舗)/shops/[id]/members で対象メンバーのロールを「管理者」に変更isShopAdminupdateShopMember mutation(backend/src/schema/shop_member/mutations/update_shop_group_member.ts)を使用。その店舗の管理者になるだけで、isGroupAdmin は変わらない

招待リンクで直接 ADMIN / MANAGER として加入させる経路は 無い。店舗の招待リンク経由は必ず STAFF(一般スタッフ)として登録される。運営責任者・管理者への昇格は加入後に管理者がロール変更する。

運営責任者(MANAGER)の割り当て ​

ShopMember.role = 'MANAGER' を持つ状態。

経路操作必要権限備考
招待リンクで加入した一般スタッフの昇格/shops/[id]/members で「招待リンクを作成」→ 該当店舗のリンクを /invite/[token] で受諾したあと、管理者がロールを変更リンク発行: isShopManager / 受諾: ログインのみ / 昇格: isShopAdmin受諾時に ShopGroupMember(isGroupAdmin: false)と ShopMember (role: 'STAFF') が同時作成される。運営責任者(MANAGER)への昇格は加入後に管理者がロール変更(updateShopMember)で行う
権限管理画面で降格・追加店舗付与/shopGroups/[id]/members で対象メンバーの店舗別ロールを「運営責任者」に設定 → 保存isShopGroupAdmin(グループ管理者)updateShopGroupMember。グループ管理者のチェックが ON のときは ADMIN に強制上書きされるため、MANAGER にしたい場合はグループ管理者のチェックを OFF のままにする
権限管理画面(店舗)/shops/[id]/members で対象メンバーのロールを「運営責任者」に変更isShopAdminupdateShopMember mutation

閲覧専用 ShopGroupMember の割り当て ​

ShopGroupMember は存在するが isGroupAdmin = false かつ ShopMember が一件も無い状態。明示的な UI 経路は無い。以下のような経緯で発生しうる。

  • 招待リンクは店舗か店舗グループのどちらかに紐付いた状態で発行されるため、通常運用では発生しないはずである
  • ただし createShopGroupMember mutation(backend/src/schema/shop_group_member/mutations/create_shop_group_member.ts)は userId と shopGroupId だけを受け取り isGroupAdmin: false の ShopGroupMember を直接作成するため、これが呼ばれた直後(まだ updateShopGroupMember でロールを付ける前)の中間状態として発生する
  • updateShopGroupMember で全 ShopMember を外しつつグループ管理者のチェックも OFF にすれば手動で作れる

運営マスターの割り当て ​

ctx.requester.isAdmin は マダミス.jp 管轄外 で、UZU 側の運営マスターのフラグ。マダミス.jp 店舗管理 UI からは付与・剥奪できない。バックエンドの Context 構築時点で決まる。

権限の剥奪 ​

操作mutation必要権限
グループからメンバーを完全削除deleteShopGroupMemberisShopGroupAdmin
グループ管理者を降格updateShopGroupMember(isGroupAdmin: false で保存)isShopGroupAdmin
ある店舗からだけ外すupdateShopGroupMember の shopMembers 配列から該当 shopId を除外して保存isShopGroupAdmin
ロール変更(ADMIN ↔ MANAGER)updateShopGroupMember または updateShopMemberisShopGroupAdmin / isShopAdmin

権限判定の実装 ​

バックエンドの認可ヘルパー ​

javascript/mdms_jp/backend/src/logic/authorization.ts に集約されている。

権限判定は SHOP_ROLE_RANK(ADMIN:3 / MANAGER:2 / STAFF:1、非メンバー 0)によるランク比較で行う。isShopAdmin / isShopManager / isShopStaff はいずれも findShopRole でロールを引き、関数名と同じしきい値(それぞれ ADMIN / MANAGER / STAFF)以上かを返す。isShopStaff は STAFF 以上 = その店舗のメンバー全員。上位ロールが下位の権限を必ず内包する(例: ADMIN は当然 isShopManager も true)ため、ロールを増やしてもしきい値の行だけで表現できる。

関数戻り値許可条件
findShopGroupMember({ ctx, userId, shopId })ShopGroupMember | nullisGroupAdmin = true または当該 shopId の ShopMember が存在
canViewShopGroupSensitiveData({ ctx, shopGroupId })boolean運営マスター / isGroupAdmin = true / 同 group 配下に ShopMember を 1 件以上保有
findShopRole({ ctx, userId, shopId })'ADMIN' | 'MANAGER' | 'STAFF' | nullisGroupAdmin = true なら 'ADMIN' を返す。それ以外は ShopMember.role
canAccessReservation(ctx, reservationId)boolean運営マスター / 予約者本人(customer.userId)/ 当該店舗のメンバー
canAccessCustomer(ctx, customerId)boolean運営マスター / 顧客本人 / 顧客が属する店舗グループのメンバー(閲覧用。一般スタッフ STAFF でも true)
canManageCustomer({ ctx, customerId })boolean運営マスター / isGroupAdmin / 顧客の属するグループ配下で MANAGER 以上。本人特例なし・一般スタッフ STAFF のみは false(顧客情報編集用)
canAccessUserContact(ctx, userContactId)boolean運営マスター / 本人 / そのユーザーの customer が属するグループのメンバー
isShopAdmin(ctx, shopId)boolean運営マスター / ランク >= ADMIN(isGroupAdmin / ShopMember.role = 'ADMIN')
isShopManager(ctx, shopId)boolean運営マスター / ランク >= MANAGER(ADMIN・グループ管理者も当然 true)
isShopStaff(ctx, shopId)boolean運営マスター / ランク >= STAFF(= その店舗のメンバーであれば true)。閲覧系の関所
canManageShopStaff({ ctx, shopStaffId })boolean運営マスター / 当該店舗で MANAGER 以上は全スタッフ枠 / それ未満は ShopMember.shopStaffId で自分に紐付いた枠のみ
isShopGroupAdmin(ctx, shopGroupId)boolean運営マスター / ShopGroupMember.isGroupAdmin = true

これらは Mutation のリゾルバーの冒頭で呼ばれ、false なら ForbiddenError('この操作には ... 権限が必要です')を投げる。ctx.requester が無い場合は UnauthenticatedError。

フロント側のロール取得経路 ​

GraphQL の Shop 型に以下のフィールドが定義されている(javascript/mdms_jp/backend/src/schema/shop/object.ts)。

フィールド行解決ロジック
Shop.myShopRole: ShopMemberRole337-350findShopRole を呼ぶ。グループ管理者なら 'ADMIN'。
Shop.hasPermission: Boolean!323-335findShopGroupMember の結果を boolean 化。

フロントは currentUser.belongingShops[].myShopRole を Apollo のキャッシュから取り出して UI 制御に使う。

フロント側のレイアウトガード ​

/shops/[id]/* のレイアウト(javascript/mdms_jp/shop/src/app/shops/[id]/layout.tsx)は、所属チェックに加えてページキー単位のロールガード を持つ。usePathname() でページキー(/shops/[id]/<key>)を取り出し、共有 CONST SHOP_MENU_MIN_ROLE の最小ロールと自分の myShopRole を hasShopRole で比較する。

ts
const pageKey = pathname.split('/')[3];
const minRole = pageKey ? SHOP_MENU_MIN_ROLE[pageKey] : undefined;
if (minRole && !currentUser.isSallyAdmin) {
  if (!hasShopRole(belongingShop?.myShopRole ?? undefined, minRole)) {
    return <div>このページにアクセスする権限がありません</div>;
  }
}

運営マスターは全ページ許可。CONST に無いキー(トップ等)は制約なし。共有 CONST は src/features/auth/shopMenuPermissions.ts で、サイドバーと同一ソース。

WARNING

このページガードはあくまで UX 用であり、真の関所はバックエンドの認可である。

/shopGroups/[id]/* のレイアウト(javascript/mdms_jp/shop/src/app/shopGroups/[id]/layout.tsx)は、isGroupAdminOf(src/features/auth/shopGroupAdmin.ts)でグループ管理者(運営マスターを含む)かどうかを判定し、グループ管理者でなければ「この店舗グループを管理する権限がありません」を返す。顧客詳細(/shopGroups/[id]/customers/[customerId])だけは店舗の予約画面からも開く共有画面のためガードの対象外で、顧客の個人情報はバックエンドの canAccessCustomer が守る。サイドメニュー(ShopGroupSideMenu.tsx)はロールによる出し分けをせず、アクセス制御はこのレイアウトに任せる。

サイドバーとページガードの共有 CONST ​

ページキー → 最小ロールの対応表 SHOP_MENU_MIN_ROLE(src/features/auth/shopMenuPermissions.ts)を唯一の定義とし、サイドバー(shopSideMenu.tsx)とページガード(layout.tsx)が同じ CONST を参照する。

ts
// shopMenuPermissions.ts
export const SHOP_MENU_MIN_ROLE: Record<string, ShopMemberRole | undefined> = {
  bookingSlots: 'STAFF', catalogs: 'STAFF', staffs: 'STAFF',
  customers: 'STAFF', 'phone-blocklist': 'STAFF', coupons: 'STAFF',
  rooms: 'MANAGER', 'pricing-rules': 'MANAGER', overview: 'MANAGER', members: 'MANAGER',
  settings: 'ADMIN', billings: 'ADMIN',
};

サイドバーは canShowMenu(path) で hasShopRole(myShopRole, SHOP_MENU_MIN_ROLE[path]) || isSallyAdmin を判定し、表に無いキーは非表示にする(判定できないときは閉じる側に倒す)。一方ページガードは、表に無いキーには制限を課さない(判定できないときは開く側に倒す)。coupons だけは事前決済が有効な店舗のみメニューに載せる条件と AND で評価する。これにより一般スタッフ(STAFF)からは rooms / pricing-rules / overview / members が非表示になる。

ページ別の必要権限 ​

ルート・認証系 ​

URL機能表示制御(フロント)主要操作 / 必要権限(バックエンド)ファイル
/所属店舗一覧ログインのみ(Query のみ)app/page.tsx
/google-auth-callbackGoogle OAuth コールバックログインのみOAuth リダイレクトapp/google-auth-callback/page.tsx
/invite/[token]招待リンク受諾ログインのみverifyInvitationLink(認証のみ)app/invite/[token]/page.tsx

店舗単位(/shops/[id]/*) ​

凡例: ⚠️ = 課題あり(リンク先の節を参照)

URL機能フロントの表示制御主要操作(Mutation)必要権限
/shops/[id]/bookingSlots公演一覧STAFF 以上に表示createPublicBookingSlot / createPrivateBookingSlot / cancelBookingSlot / deleteBookingSlot / duplicateBookingSlots / publishDraftBookingSlots / restoreBookingSlot / ShopEventSlot CRUDisShopManager ⚠️ 公演系 mutation のしきい値と運用方針のずれ
/shops/[id]/bookingSlots/[slotId]公演詳細編集STAFF 以上に表示updatePublicBookingSlot / updatePrivateBookingSlot / assignBookingSlot / confirmPrivateBookingSlot / rejectPrivateBookingSlot / sendBookingSlotNotificationisShopManager
/shops/[id]/catalogsカタログ一覧STAFF 以上に表示createShopCatalog / deleteShopCatalog / reorderShopCatalogisShopManager(reorder は対象から shopId を導出し、導出できなければ拒否する)
/shops/[id]/catalogs/[shopCatalogId]カタログ詳細編集STAFF 以上に表示updateShopCatalog / upsertShopCatalogPrice / deleteShopCatalogPriceisShopManager
/shops/[id]/catalogs/archivedアーカイブ済カタログSTAFF 以上に表示(一覧のみ)-
/shops/[id]/staffsスタッフ管理STAFF 以上に表示createShopStaff / deleteShopStaffisShopManager
reorderShopStaffisShopManager(対象から shopId を導出し、導出できなければ拒否する)
updateShopStaffcanManageShopStaff(MANAGER 以上は全枠 / STAFF は自分の枠のみ)
/shops/[id]/staffs/[staffId]スタッフ詳細(スキル編集)STAFF 以上に表示updateShopStaff / createShopStaffCatalogSkill / deleteShopStaffCatalogSkillcanManageShopStaff
/shops/[id]/customers顧客名簿STAFF 以上に表示paginatedCustomers(Query)findShopGroupMember(任意のメンバー)
顧客メモ編集(updateCustomerMemo)canAccessCustomer(一般スタッフ STAFF も可)
顧客情報編集(updateCustomer: 氏名・電話・メール)canManageCustomer(MANAGER 以上。本人特例なし)
/shops/[id]/members権限管理(店舗)MANAGER 以上に表示createInvitationLink(shop)isShopManager
updateShopMember(スタッフ紐付けのみ / ロール据え置き)isShopManager
updateShopMember(ロール変更 / role='ADMIN')isShopAdmin(isGroupAdmin には触れない)
deleteShopGroupMember(削除ボタン)isShopGroupAdmin(グループ管理者のみ)
/shops/[id]/phone-blocklist電話番号ブロックリストSTAFF 以上に表示(一覧閲覧)。登録・削除ボタンは MANAGER 以上のみ表示addPhoneToBlocklist / removePhoneFromBlocklistisShopManager(= MANAGER 以上)
/shops/[id]/pricing-rules価格設定ルールMANAGER 以上に表示query / createShopPricingRule / updateShopPricingRule / deleteShopPricingRule / updateShopPricingRulesPriorityisShopManager(query も同じ)
/shops/[id]/rooms部屋管理MANAGER 以上に表示createShopRoom / updateShopRoom / deleteShopRoom / reorderShopRoomsisShopManager
/shops/[id]/couponsクーポン(事前決済 ON 時のみメニュー表示)STAFF 以上かつ事前決済 ONquery(coupon / coupons)isShopStaff(一般スタッフを含む全メンバーが閲覧可)
createCoupon / updateCouponDescription / updateCouponDiscount / deleteCouponisShopManager
/shops/[id]/billings売上・手数料ADMIN のみ表示monthlyShopBilling の生成運営マスター専用 ⚠️ 運営マスター専用の monthlyShopBilling の生成
/shops/[id]/overview店舗情報(基本情報編集)MANAGER 以上に表示updateShop(handleUpdateShop)isShopManager
/shops/[id]/settingsその他設定(Google Calendar 連携)ADMIN のみ表示updateShopHide* / generateAuthUrl / saveAuthToken / registerCalendar / disconnectCalendar / disconnectAccount / syncAllEventsisShopAdmin

グループ単位(/shopGroups/[id]/*) ​

フロントの表示制御はいずれもフロント側のレイアウトガードによる。

凡例: ⚠️ = 課題あり(リンク先の節を参照)

URL機能フロントの表示制御主要操作(Mutation)必要権限
/shopGroups/[id]/shopsグループ内店舗一覧グループ管理者のみcreateShopInGroup運営マスター専用
/shopGroups/[id]/customersグループ顧客一覧グループ管理者のみ(Query、Customer 型は canAccessCustomer で守られる)-
/shopGroups/[id]/customers/[customerId]顧客詳細ガード対象外(店舗の予約画面からも開く)updateCustomercanManageCustomer
/shopGroups/[id]/overviewグループ情報グループ管理者のみupdateShopGroup / 法人情報 CRUDisShopGroupAdmin(グループ管理者)
/shopGroups/[id]/billingsグループ売上グループ管理者のみmonthlyShopBilling の生成運営マスター専用 ⚠️ 運営マスター専用の monthlyShopBilling の生成
/shopGroups/[id]/members権限管理(グループ)グループ管理者のみcreateShopGroupMember / updateShopGroupMember / deleteShopGroupMember / createInvitationLink / reorderShopGroupMemberisShopGroupAdmin(グループ管理者)。reorderShopGroupMember は input に shopGroupId が無いため対象から導出し、取得件数と ids の件数の一致・全行が同じ shopGroupId であることを確かめ、満たさなければ拒否する

ロール別の制限 ​

一般スタッフ(STAFF)の制限 ​

一般スタッフは閲覧中心の最小権限。バックエンドの mutation で ForbiddenError になる主な操作は次のとおり。

  • カタログ作成・編集・削除・並び替え(isShopManager)
  • カタログ価格(upsert/deleteShopCatalogPrice)(isShopManager)
  • スタッフ追加・削除・並び替え(isShopManager)、他人のスタッフ枠の編集・スキル設定(canManageShopStaff。自分に紐付いた枠のみ可)
  • 部屋 CRUD・並び替え(isShopManager)
  • 価格設定ルール CRUD・query(isShopManager)
  • 店舗情報(handleUpdateShop)(isShopManager)
  • 電話番号ブロックリストの登録・削除(addPhoneToBlocklist / removePhoneFromBlocklist)(isShopManager。一覧閲覧は可)
  • 顧客情報編集(氏名・電話・メール)(canManageCustomer。メモのみ編集可)
  • 権限管理メニュー(サイドバー非表示・ページガードで遮断)、招待リンク発行・スタッフ紐付け(isShopManager)
  • 部屋・価格設定・店舗情報メニュー(サイドバー非表示・ページガードで遮断)
  • その他設定・売上(isShopAdmin / 運営マスター)

一般スタッフが実行できる主なもの(= 上位ロールも当然可能)は次のとおり。

  • 公演(BookingSlot)系の CRUD・スタッフ割当(マトリクスは「公演 = 一般スタッフ STAFF 閲覧 + 操作可」)
  • 予約のキャンセル・人数変更・クーポン適用(canAccessReservation)
  • クーポン閲覧(isShopStaff)、電話ブロックリスト閲覧(登録・削除は運営責任者以上)
  • 顧客名簿閲覧・顧客メモ編集(findShopGroupMember / canAccessCustomer)
  • 自分に紐付いたスタッフ枠の編集・スキル設定(canManageShopStaff)
  • 支払い返金・同期

INFO

公演(BookingSlot)系 mutation の認可しきい値は isShopManager(= MANAGER 以上)だが、マトリクス上は一般スタッフにも公演操作を開放する方針(現場運用)。一般スタッフ(STAFF)のしきい値との整合は残課題である(公演系 mutation のしきい値と運用方針のずれ)。

運営責任者(MANAGER)の制限 ​

運営責任者は日常運用 + 店舗構成・設定の編集まで可能。管理者との差は以下の 3 点のみ。

  • その他設定(updateShopHide* / Google カレンダー連携) … isShopAdmin 必須
  • 売上・手数料ページ … 閲覧含め管理者・運営マスターのみ(サイドバー非表示)
  • メンバーのロール変更・削除 … updateShopMember のロール変更経路は isShopAdmin、deleteShopGroupMember は isShopGroupAdmin。運営責任者は スタッフ紐付け(ロール据え置き)のみ isShopManager で可能(updateShopMember はロールが変わるかどうかで必要権限を分ける)

加えてグループレベルの操作(店舗グループの編集 / 法人情報 / グループメンバー CRUD)は isShopGroupAdmin 必須で運営責任者も不可。

管理者(ADMIN)の制限 ​

ShopMember.role = 'ADMIN'(= isGroupAdmin: false)のユーザーでもできない操作は次のとおり。

  • ShopGroupMember CRUD: createShopGroupMember / updateShopGroupMember / deleteShopGroupMember / reorderShopGroupMember(isShopGroupAdmin 必須)
  • 店舗グループの編集(updateShopGroup、isShopGroupAdmin)
  • 法人情報 CRUD(isShopGroupAdmin)
  • グループレベルの月次集計(monthlyShopBilling の生成は運営マスター専用)

管理者でも実行できる権限管理操作(混同しやすいため明記)は次のとおり。

  • 招待リンクの発行(createInvitationLink。店舗指定は isShopManager)
  • 加入済みメンバーのロール変更(updateShopMember、isShopAdmin)。店舗で ADMIN にしてもその店舗の管理者になるだけで、isGroupAdmin には触れない

「ShopMember.role = 'ADMIN' かつ isGroupAdmin = false」の状態は、店舗権限管理画面(/shops/[id]/members)で「管理者」に変更した場合と、グループ権限管理画面(/shopGroups/[id]/members)でグループ管理者のチェックを OFF にしたまま店舗ロールを「管理者」に設定した場合に生じる。

enum の外にある権限 ​

  • グループ管理者(ShopGroupMember.isGroupAdmin: Boolean)— enum 上は別ロールではないが、実態としては最上位。フロントの「権限管理」画面では「グループ管理者」ラベルで表示される(features/members/members.tsx:286)
  • 閲覧専用 ShopGroupMember — ShopMember を持たず isGroupAdmin = false。招待リンクの種別ミスや旧来データで発生しうる中間状態。canViewShopGroupSensitiveData では false になり、顧客名簿などの個人情報は閲覧できない
  • 運営マスター(ctx.requester.isAdmin)— UZU 側の運営マスター。フロントでは isSallyAdmin で参照され、すべての判定を通る(shopSideMenu.tsx:114 など)

設計上の問題点・セキュリティリスク ​

公演系 mutation のしきい値と運用方針のずれ ​

公演系 mutation(booking_slot/* / public_booking_slot/* / private_booking_slot/* / shop_event_slot/*)は isShopManager(= MANAGER 以上)で判定する。一方、マトリクス上は一般スタッフ(STAFF)にも公演の操作を開放する方針で、公演ページ自体は一般スタッフも開ける。一般スタッフのしきい値をどう揃えるかは決まっていない。

運営マスター専用の reservationByCampaignId Query ​

backend/src/schema/reservation/query.ts:10-25 で ctx.requester?.isAdmin を要求。マーケティング集計用途のため意図的だが、ドキュメント上は明記が必要。

運営マスター専用の monthlyShopBilling の生成 ​

backend/src/schema/monthly_shop_billing/mutation.ts:16:

ts
authorize: (_parent, _args, ctx) => ctx.requester?.isAdmin ?? false,

UI 上、店舗管理側の「売上・手数料」ページから呼べる生成系の操作は、グループ管理者であっても自分で実行できない。実運用では別 cron・サポート対応の想定と思われるが、ボタンを出している以上ロールを揃えるか UI から外すか整理が必要。

shop / shopByDisplayId / shopGroup 系 Query の認可 ​

  • Shop 型の members / shopGroupMembers フィールドは authorize: isShopManager で、権限管理(メンバー一覧 + ロール)は運営責任者以上でないと取得できない。reservationNotificationEmail は isShopAdmin で判定する
  • Shop.shopEventSlots フィールドの読み取りは isShopStaff(= その店舗のメンバーであること)。公演ページ(/shops/[id]/bookingSlots)は一般スタッフも閲覧可のため、MANAGER 以上にすると一般スタッフが公演ページを開いた時にこのフィールドだけ ForbiddenError になりページ全体が表示できなくなる
  • BookingSlot.operationRecord(公演の作成者・最終更新者)は isShopStaff で判定し、その公演を操作した人の氏名に限って一般スタッフにも返す。ShopGroupMember.displayName 自体の読み取りは運営責任者以上(canManageShopGroupMemberDisplayName)で、氏名を返す経路はこの 2 つ。BookingSlot はお客様向けサイトからも取得されるため、権限がない場合は ForbiddenError ではなく null を返す(エラーにすると公演ごと取得できなくなる)。実装は backend/src/logic/bookingSlotOperator.ts。氏名の公開範囲を見直すときは両方の経路を確認する
  • monthlyShopBillings は isShopAdmin で判定する
  • 未対応(要レビュー): shop / shopByDisplayId Query 自体や shopGroup Query は認可指定なし(公開店舗ページのため意図的な部分もある)。catalogs / staffs / bookingSlots の読み取りフィールドは「メンバーかどうかすら見ていない」(一般スタッフ STAFF も閲覧可という方針と方向は一致するが、最小ロールの強制はしていない)
  • 未対応(優先): 財務系 query の accountingSummaries(accountancy/query.ts)は無認可で、任意ユーザーがプラットフォーム全体の売上を取得できる

残課題 ​

  1. 財務系 query の無認可(優先): accountingSummaries を認可で塞ぐ(shop / shopByDisplayId / shopGroup 系 Query の認可)
  2. 公演系 mutation のしきい値: 一般スタッフ(STAFF)に公演操作を開放する方針と isShopManager のずれを揃える(公演系 mutation のしきい値と運用方針のずれ)
  3. 読み取りフィールドの網羅的な認可: catalogs / staffs / bookingSlots 等の読み取りを最小ロールで強制する(一般スタッフ STAFF = 閲覧可で方向は一致するため実害は薄い)
  4. updateShopMember の API 直叩きによる越境: staffId の所属検証がなく、id だけを条件に更新する。通常 UI では踏まない
  5. スタッフとアカウントの紐付け UI の改善: 紐付けは権限管理画面の編集モーダルのみ(運営責任者も使える)。スタッフページでの未紐付け警告・アカウント選択ドロップダウンは整っていない

変更履歴 ​

日付変更PR
2026-06-19最小権限ロール STAFF(一般スタッフ)を新設し、それまでの STAFF(運営スタッフ)を MANAGER(運営責任者)にリネーム。Menu.onlyShopAdmin を廃止し、SHOP_MENU_MIN_ROLE に一本化#21677