RELEASE NOTE

管理者とメンバーのロール組織の設定は管理者だけが変更。権限を確かめる仕組みも設計から解説

これまでの OpenGreenTrack では、組織の全員が同じ操作をできました。10 月 23〜24 日の変更で、組織の設定を変えられる人を「管理者」に絞りました。日々の入力や算定は、これまでどおり全員が行えます。この記事では使い方に加えて、ロールの考え方と、権限を確かめる仕組みの設計を説明します。

  • ロールと権限
  • 組織の設定
  • セキュリティ
  • 設計
管理者から見たメンバー管理のカード。メンバーごとにロールのプルダウンと削除ボタンがあり、算定 花子を管理者に変える下書きの状態で「変更を保存」ボタンが出ている

この版のポイント

  1. ロールは「管理者」と「メンバー」の 2 つ。組織の設定(企業情報・算定年度・招待・メンバー)は管理者だけが変更
  2. データの入力・算定・拠点や排出係数の管理・レポートの出力は、これまでどおり全員が使える
  3. 権限は画面ではなく、サーバーとデータベースで確かめる。画面の出し分けは使いやすさのため
  4. 最後の管理者の降格・削除・退会は、データベースのトリガーまで含めた 3 か所で止める

01CONCEPT

考え方:「組織の設定」と「日々の算定業務」を分ける

権限を細かく分けすぎると、誰が何をできるのかが分かりにくくなります。算定を誰が担うかは組織によって違うため、データの入力・算定・マスタの編集は全員に任せ、取り返しのつきにくい「組織の設定」だけを管理者に絞りました。

日々の算定業務

  • データ入力・算定
  • 拠点の管理
  • 排出係数・IDEA の取込
  • 削減目標
  • レポートの出力

組織の設定

  • 企業情報の編集
  • 算定年度の追加・削除
  • メンバーの招待・削除
  • ロールの変更
管理者
日々の算定業務も、組織の設定も行える
メンバー
日々の算定業務を行える
組織の設定は見るだけ(招待・削除・ロールの変更は出ない)
図解ロールごとに届く範囲。管理者は両方を、メンバーは日々の算定業務を行い、組織の設定は内容を見るだけです。
ロールごとにできること
操作管理者メンバー
企業情報の編集○見るだけ
算定年度の追加・削除○見るだけ
メンバーの招待・削除・ロールの変更○×
データ入力・算定・拠点・排出係数・IDEA の取込・削減目標・レポート○○
  • admin管理者組織を作った人(初期セットアップ)。管理者が変更したとき
  • loggerメンバー招待の既定値。新しくメンバーにするときに書く値
  • viewerメンバー以前からある値。新しくは書かない

判定は「role が admin か」の 1 か所だけ(isAdmin)。logger と viewer を個別に判定するコードは書かない

図解データベースに入る値と画面での表示。判定は「admin かどうか」だけで行い、それ以外はすべてメンバーとして扱います。

データベースには、以前から admin・logger・viewer の 3 つの値がありました。新しく member という値を足すと、既存の行を書き換えるデータの更新が必要になります。OpenGreenTrack はマイグレーションにデータの更新を書かない決まり(スキーマとデータを分ける)なので、値はそのまま残し、表示と判定のほうを「管理者かどうか」の 2 つにそろえました。

02HOW TO

ロールを変える・ロールを選んで招待する

STEPSメンバーのロールを変える

  1. 管理者で「企業・メンバー設定」を開く

    「設定 → 企業・メンバー設定 → メンバー管理」を開きます。

  2. ロールのプルダウンで選び直す

    変えたいメンバーの行で「管理者」か「メンバー」を選びます。この時点ではまだ下書きです(冒頭の画面)。

  3. 「変更を保存」を押す

    自分をメンバーに変える場合は、保存の前に確認が出ます。管理者が 0 人になる下書きは保存できません。

ユーザー招待のモーダル。メールアドレスの下にロールのプルダウンがあり、既定はメンバー
招待のときにロールを選べます(既定はメンバー)。初期セットアップで組織を作った人は管理者になります。
メンバーから見たメンバー管理のカード。ロールは管理者・メンバーのバッジで表示され、招待や削除のボタンはなく「メンバーの招待・削除・ロールの変更は、管理者のみできます」と書かれている
メンバーから見たメンバー管理。ロールは見えますが、変更の操作は出ません。
メンバーから見た企業情報のカード。企業名や本社所在地などが読み取り専用で表示され、右上に「管理者のみ変更できます」と書かれている
企業情報と算定年度も、メンバーには内容だけを見せます。

03DESIGN

設計:権限はどこで確かめているか

画面でボタンを隠すだけでは、ブラウザから直接サーバーの処理を呼ばれたときに防げません。そこで、画面の出し分けは使いやすさのためと割り切り、実際の守りはサーバーとデータベースの 2 か所に置いています。

層誰が操作できるか最後の管理者を守る
画面使いやすさのため管理者にだけ編集・招待・ロールの操作を出す。メンバーには「管理者のみ変更できます」と添える管理者が 0 人になる下書きは保存させない。1 人だけなら 2 人以上を勧める
サーバー守りrequireAdmin() が操作のたびにデータベースからロールを読み直す。管理用の接続で書く処理(算定年度・招待・ロール)はここで止める最後の管理者の退会を断る(409)
データベース最後の守り企業情報の更新と招待は RLS(current_user_is_admin())で管理者に限る。ロールを書き換えるのは管理用の接続専用の関数だけトリガーが、組織の管理者が 0 人になる更新・削除を拒否する(P2081)
図解3 つの層で確かめる内容。どれか 1 つを通り抜けても、その下の層で止まります。

サーバーでは、操作のたびにデータベースからロールを読み直します(requireAdmin())。ログインしたときの情報を使い回さないので、管理者からメンバーに変えられた人は、次の操作からすぐに組織の設定を変えられなくなります。算定年度の追加やロールの変更のように、データベースの行単位の権限(RLS)を通らない管理用の接続で書く処理は、ここで止めるのが唯一の守りになります。

データベースの権限は、ロールを入れる前から「差し込み口」を用意していました。企業情報の更新と招待のポリシーは最初から current_user_is_admin() という関数を参照していて、その中身は「ログインしているか」を返すだけでした。今回はこの関数の中身を「ロールが admin か」に差し替えたので、ポリシーそのものは 1 行も書き換えずに管理者だけになりました。

差し替えた関数(要点だけ抜き出したもの)
-- 以前:ログインしていれば true(ロールを入れるまでの仮の中身)
-- 今回:ログイン中の利用者のロールが admin なら true
create or replace function current_user_is_admin()
returns boolean language sql stable security definer set search_path = ''
as $$
  select exists (
    select 1 from public.profiles p
    where p.id = auth.uid() and p.role = 'admin'
  )
$$;

04DESIGN

設計:ロールを変える処理の流れ

メンバー管理の「変更を保存」では、ロールの変更とメンバーの削除をまとめて受け取ります。サーバーで検証して順番を決め、1 件ずつデータベースの関数(RPC)で反映します。ロールを書き換えられるのはこの関数だけで、管理用の接続からしか呼べません。

  1. 1画面「変更を保存」管理者が 0 人になる下書きは保存させない。自分をメンバーにするときは確認を出す
  2. 2サーバー管理者か確かめるrequireAdmin() がデータベースからロールを読み直す
  3. 3サーバー変更を検証する同じ組織のメンバーか、同じ人への変更が重複していないか、自分を削除していないか
  4. 4サーバー反映の順番を決める管理者への昇格 → メンバーへの降格 → 削除
  5. 5データベース関数 change_member_role組織ごとのロックを取り、操作する人が今も管理者か・対象が同じ組織か・値が admin か logger かを確かめる
  6. 6データベーストリガー更新の前に、組織の管理者が 0 人にならないかを数える
  7. 7データベース更新と記録ロールを書き換え、監査ログ(member.role.update)を同じトランザクションで残す

順番の理由:「花子さんを管理者にして、自分(太郎)はメンバーに戻る」を 1 回で保存したとき

降格を先にすると
  1. 太郎をメンバーに → 管理者 0 人
  2. トリガーが拒否して止まる
昇格を先にすると(OpenGreenTrack)
  1. 花子を管理者に → 管理者 2 人
  2. 太郎をメンバーに → 管理者 1 人で完了
図解保存から反映までの流れ。最後の管理者の確認は、画面・サーバー・データベースのそれぞれで行います。

反映の順番を「管理者への昇格 → メンバーへの降格 → 削除」に固定しているのは、「別の人を管理者にして、自分はメンバーに戻る」を 1 回の保存で行えるようにするためです。先に自分を降格すると、その瞬間に管理者が 0 人になって止まってしまいます。

05DESIGN

設計:同時に操作しても、管理者は 0 人にならない

管理者が 0 人になると、誰もロールを変えられず、画面からは元に戻せません。最後の管理者をメンバーに変える・削除する・最後の管理者が退会する、のどれもできないようにしたうえで、2 人の管理者がほぼ同時に互いを降格するような競合にも備えています。

管理者が 2 人(A と B)の組織で、A は「B をメンバーに」、B は「A をメンバーに」をほぼ同時に保存した

ロックが無かったら(仮の話)
  1. A の処理:自分以外の管理者を数える → B がいる → 実行
  2. B の処理:同時に数える → A がいる → 実行
  3. どちらも成功して、管理者が 0 人になる
組織ごとのロック(OpenGreenTrack)
  1. A の処理がロックを取る。B の処理は待つ
  2. A の処理:B をメンバーにして確定、ロックを外す
  3. B の処理:自分がもう管理者ではないので拒否される

ロックは「組織の管理者の人数を数える」処理にだけ組織ごとに取るので、ほかの組織の操作は待たされない

図解組織ごとのロック。管理者の人数を数える処理を 1 つずつ順に行うので、後から来た操作は先の結果を見て判断します。

このチェックはデータベースのトリガーに置いているので、画面やサーバーを通らない経路(アカウントの削除から連鎖する削除など)にも効きます。ただし、組織そのものを削除するときは残す管理者がいないので止めません。

06SECURITY

Next.js 16.4.2 への更新ほか

  • Next.js を 16.3.4 から 16.4.2 に上げ、critical と判定されていた脆弱性(16.0.0〜16.3.7 が対象)を解消しました。next/og でのリモートコード実行、SSG・ISR のキャッシュ汚染、画像最適化(/_next/image)での SSRF などが含まれます。
  • CI の Secret Scan を、その実行で取り出したコミットの履歴だけを調べるように直しました。

07CHANGELOG

変更の一覧

  • 新機能・改善組織の設定(企業情報・算定年度・招待)を管理者だけが変えられるように#182
  • 新機能・改善メンバー管理で管理者・メンバーのロールを変えられるようにし、最後の管理者を守る#180
  • 修正CI の Secret Scan を、その実行で取り出したコミットの履歴だけにする#181
  • 保守Next.js を 16.4.2 に上げて脆弱性を解消#162

TRY OPENGREENTRACK

コードもロードマップも、GitHub で公開しています。

README を読んで動かす 導入・共創について相談する