権限に応じたナビゲーションは分かりやすさを高めますが、セキュリティはバックエンドに置くべきです。役割、ポリシー、安全なUIの隠蔽のシンプルなパターンを紹介します。

「ボタンを隠せ」と言うとき、人は通常二つのどちらかを意味します:機能を使えないユーザーのためにUIをすっきりさせること、あるいは誤用を止めること。フロントエンドで現実的に達成できるのは前者だけです。
権限対応のナビゲーションメニューは主にUXのツールです。アプリを開いたときにユーザーがすぐに何ができるか分かるようにし、クリックごとに「アクセス拒否」画面に遭遇するのを防ぎます。また「どこで請求書を承認するの?」や「なぜこのページでエラーになるの?」といった混乱を減らしてサポート負荷も下げます。
UIを隠すことはセキュリティではありません。明確化です。
好奇心のある同僚なら例えば:
つまり、権限対応メニューが実際に解決するのは正直な案内です。インターフェースをユーザーの職務、役割、文脈に合わせ、何が利用できないかが明らかになるようにします。
良い終着点は次の通りです:
例:小さなCRMではSales担当はLeadsやTasksを見られるがUser Managementは見られないべきです。もしそれでもユーザー管理のURLを貼り付けたら、ページはフェイルクローズし、サーバーはユーザー一覧やロール変更の試行をブロックし続けるべきです。
表示はインターフェースが見せるものです。認可はリクエストがサーバーに到達したときにシステムが実際に許可することです。
権限対応メニューは混乱を減らします。誰かがBillingやAdminを見られないのであれば、それらを隠すことでアプリがすっきりしサポート件数が減ります。しかしボタンを隠すことは施錠ではありません。人は開発者ツール、古いブックマーク、コピーしたリクエストなどで裏のエンドポイントにアクセスできます。
実用的なルール:どんな体験を提供したいかを決め、それに従ってバックエンドで必ずルールを強制してください。
アクションをどのように提示するかを決めるとき、ほとんどの場合は次の三つのパターンで事足ります:
「見ることはできるが編集できない」はよくあるケースで、明示的に設計する価値があります。読み取り用と変更用で二つの権限として扱ってください。メニューでは読み取り権限がある人に顧客詳細を見せ、編集権限がある人にのみEditを見せる。またページ内ではフィールドを読み取り専用にし、編集コントロールは権限で遮断しながらページ自体は読み込むといった扱いが考えられます。
最も重要なのは、最終的な結果はバックエンドが決めるということです。UIがすべての管理アクションを隠していても、サーバーは敏感なリクエストごとに権限をチェックし、試行があれば明確な「許可されていない」レスポンスを返す必要があります。
権限対応メニューを最速で出す方法は、チームが一文で説明できるモデルから始めることです。説明できないものは正しく保てません。
ロールはグルーピングのために使い、意味そのものにしないでください。AdminやSupportは有用なバケツです。しかしロールが増殖してくると(例:Admin-West-Coast-ReadOnly)、UIは迷路になりバックエンドは推測の温床になります。
代わりに、行動ベースで小さな権限(例:invoice.create や customer.export)を真の情報源にすることを優先してください。新機能は通常新しいアクションを追加するので、ジョブタイトルを増やすよりこちらの方がスケールします。
そして文脈のためにポリシー(ルール)を追加してください。ここで「自分のレコードだけ編集できる」や「$5,000未満の請求書のみ承認できる」といった条件を扱います。ポリシーは条件だけ異なる多数のほぼ重複した権限を作る必要を防ぎます。
維持可能なレイヤリングは次のようになります:
ネーミングは多くの人が思うより重要です。UIがExport Customersと言っているのにAPIが download_all_clients_v2 を使っていると、いつか間違ったものを隠したり正しいものをブロックしたりします。名前は人間に分かりやすく、一貫してフロントとバックで共有してください:
noun.verb(または resource.action)を使う例:CRMではSalesロールに lead.create と lead.update を含め、ポリシーで更新はユーザー所有のリードに限るとすると、メニューは分かりやすく保たれバックエンドは厳格です。
権限対応メニューは見た目が良いのは確かですが、バックエンドが主導でないと意味がありません。UIはヒント、サーバーは裁定者だと考えてください。
まず保護対象を紙に書き出してください。ページではなくアクションです。顧客リストの表示は顧客のエクスポートや削除とは別です。これが、セキュリティ劇場に陥らない権限対応ナビゲーションの背骨になります。
canEditCustomers、canDeleteCustomers、canExport のような真偽値、あるいはコンパクトな権限文字列のリストを返す。最小限に保つ。重要な小ルール:クライアント提供のロールや権限フラグを決して信頼しないでください。UIはcapabilitiesに基づいてボタンを隠せますが、APIは不正なリクエストを拒否し続けなければなりません。
権限対応メニューは人が何をできるかを見つけやすくするためのもので、セキュリティを装うためのものではありません。フロントエンドはガイドレール、バックエンドがロックです。
ボタンごとに権限チェックを散らすのではなく、各メニュー項目に必要な権限を含めた一つの設定からナビゲーションを定義し、それをレンダリングする方が良いです。これによりルールが読みやすくなり、UIの隅の見落としを減らせます。
シンプルなパターンの例:
const menu = [
{ label: "Contacts", path: "/contacts", requires: "contacts.read" },
{ label: "Export", action: "contacts.export", requires: "contacts.export" },
{ label: "Admin", path: "/admin", requires: "admin.access" },
];
const visibleMenu = menu.filter(item => userPerms.includes(item.requires));
管理セクション(Admin)全体を隠す方が、管理ページリンクごとにチェックを振りまくより少ない箇所で済みます。
ユーザーが永続的に許可されないなら隠す。ユーザーに権限はあるが文脈が足りないなら無効化する。
例:Delete contact はコンタクトが選択されるまで無効にする。同じ権限だが文脈が不足しているだけです。無効化する場合は近くに短い「理由」メッセージを入れてください(ツールチップ、補助テキスト、インラインノートなど):「削除するコンタクトを選択してください」。
守るべきルールセット:
メニュー項目を隠すことは人の集中を助けますが、保護にはなりません。リクエストは再生、編集、UI外からのトリガーが可能なので、バックエンドが最終的な裁定者である必要があります。
良いルール:データを変更するすべてのアクションには、全リクエストが通る一箇所の認可チェックを用意すること。ミドルウェア、ハンドララッパー、あるいは各エンドポイントの先頭で呼ぶ小さなポリシーレイヤーなど、どれか一つのアプローチを選んで統一してください。さもないとパスを見逃します。
認可は入力検証とは分離してください。まず「このユーザーはこれをして良いか?」を決め、その後にペイロードを検証します。先に検証してしまうと、存在すべきでない人にレコードIDの存在などを漏らす可能性があります。
スケールするパターン:
Can(user, "invoice.delete", invoice))。フロントエンドとログの双方に役立つステータスコードを使ってください:
401 Unauthorized : 呼び出し元がログインしていないとき。403 Forbidden : ログインはしているが許可されていないとき。404 Not Found を偽装に使う場合は注意してください。リソースの存在を隠すのに便利ですが、ランダムに混ぜるとデバッグが難しくなります。リソースタイプごとに一貫したルールを決めてください。
同じ認可がボタンからのクリック、モバイルアプリ、スクリプト、直接API呼び出しのいずれから来ても実行されることを確認してください。
最後に、拒否された試行はデバッグと監査のためにログに記録してください。ただしログは安全に保ち、誰が、どのアクション、どの高レベルのリソースタイプかを記録し、機密フィールドや全ペイロード、シークレットは避けてください。
多くの権限バグは、メニューが想定していなかったやり方でユーザーが操作したときに出ます。だから権限対応メニューは有用ですが、それを迂回する経路も設計しておくことが重要です。
メニューでBillingを隠していても、ユーザーは保存済みURLを貼り付けたりブラウザ履歴から開いたりできます。すべてのページロードを新しいリクエストとして扱い、現在のユーザーの権限を取得し、画面自体が保護されたデータの読み込みを拒否するようにしてください。フレンドリーな「アクセス権がありません」という表示で良いですが、本当の保護はバックエンドが何も返さないことです。
誰でも開発者ツール、スクリプト、別クライアントからAPIを呼べます。だから管理画面だけでなく全エンドポイントをチェックしてください。見落としがちなリスクはバルク操作です:単一の /items/bulk-update が非管理者に見えないフィールドまで変更させてしまう可能性があります。
ロールがセッション中に変わることもあります。管理者が権限を剥奪した後も、ユーザーが古いトークンやキャッシュされたメニュー状態を持っているかもしれません。短寿命トークンやサーバーサイドの権限参照を使い、401/403 が返ったら権限をリフレッシュしてUIを更新する設計にしてください。
共有端末は別の罠です:メニュー状態のキャッシュがアカウント間で漏れることがあります。メニュー表示はユーザーIDでキー化して保存するか、そもそも永続化しないでください。
リリース前に行うべき5つのテスト:
内部用のCRMを想像してください。ロールはSales、Support、Adminの三つ。全員がサインインし、アプリは左側メニューを表示しますが、メニューは便利機能に過ぎません。本当の安全はサーバーが許可するかどうかです。
読みやすい単純な権限セットの例:
UIはまずバックエンドに現在ユーザーが許可されたアクション(多くは権限文字列のリスト)とユーザーIDやチームなどの基本コンテキストを要求します。その結果からメニューを作ります。billing.view がなければBillingは見えません。leads.export があればLeads画面にExportボタンが見えます。自分のリードだけ編集できる場合、Editボタンは見えるが自分のものでないときは無効化するか明確なメッセージを出すべきです。
ここが重要です:すべてのアクションエンドポイントは同じルールを強制します。
例:Salesはリードを作成でき、所有しているリードは編集できる。Supportはチケットを見て割り当てできるがBillingには触れない。AdminはユーザーとBillingを管理できる。
誰かがリード削除を試みたら、バックエンドは次をチェックします:
leads.delete があるか?lead.owner_id == user.id か?Supportユーザーが手動で削除エンドポイントを呼んでも、バックエンドはForbiddenを返します。隠されたメニュー項目が保護ではなく、バックエンドの決定が保護なのです。
権限対応メニューで最も大きな罠は、メニューが正しく見えるだけで仕事が終わったと考えることです。ボタンを隠すことは混乱を減らしますが、リスクを減らすわけではありません。
よく見られるミス:
isAdmin フラグで一括管理する。早く感じるが広がると例外が増え、誰もアクセスルールを説明できなくなる。role、isAdmin、permissions を受け入れてはいけません。自前のセッションやトークンからIDとアクセスを導出し、サーバー側で権限を調べてください。具体例:リードのExportメニューを非管理者に隠していても、エクスポートエンドポイントが権限チェックをしていなければ、リクエストを推測したユーザーや同僚からコピーしたリクエストでファイルをダウンロードされてしまいます。
権限対応メニューを採用する前に、ユーザーが実際に何ができるかに焦点を当てた最終確認を行ってください。UIだけでなくエンドポイントを直接呼ぶテストも行い、サーバーが真の基準であることを確認します。
チェックリスト:
ギャップを見つける実践的な方法:危険なボタン(ユーザー削除、CSVエクスポート、課金変更など)を一つ選び、エンドツーエンドでトレースしてください。該当メニューは適切に隠されているか、APIは不正な呼び出しを拒否するか、UIは403を受け取っても優雅に回復するかを確認します。
小さく始めてください。初日から完璧なアクセスマトリクスは不要です。最も重要なアクション(view/create/edit/delete/export/manage users)だけを選び、既存ロールにマップして先に進みます。新機能を出すときは、その機能が導入する新しいアクションのみを追加してください。
画面を作る前に、ページではなくアクションの一覧を素早く作る習慣をつけてください。Invoicesのようなメニュー項目は多くのアクション(一覧表示、詳細表示、作成、返金、エクスポート)を隠していることがあります。先にそれらを洗い出すとUIとバックエンドのルールが明確になり、ページ単位でゲートを掛けて肝心のエンドポイントを保護し忘れるミスを避けられます。
権限ルールをリファクタするときは、他のリスクの高い変更と同様にセーフティネットを用意してください。スナップショットを取れば、ロールが必要なアクセスを失ったり不適切に増えたりした場合でも、プロダクション修正よりロールバックの方が早くなります。
シンプルなリリース手順:
もしKoder.aiのようなチャットベースのプラットフォームで構築しているなら(Koder.ai、koder.ai)、この構造は同じです:権限とポリシーを一箇所で定義し、UIはサーバーからcapabilitiesを読み取り、バックエンドのチェックをすべてのハンドラで必須にしてください。
権限対応メニューは主に明確さを提供します。ユーザーが実際にできることに集中できるようにし、行き止まりのクリックを減らし、「なぜこれが見えるの?」というサポート件数を減らします。
しかしセキュリティはバックエンドで強制されなければなりません。ユーザーはUIの表示に関係なく、ディープリンク、ブックマーク、直接のAPI呼び出しなどを試みることができます。
機能がその役割にとって実質的に発見されるべきでない場合は非表示にします。
ユーザーに権限があるが現時点で文脈が足りない(例:レコード未選択、フォームが無効、データ読み込み中)の場合は無効化します。無効化する場合は、壊れて見えないように短い説明を付けてください。
表示は認可ではないからです。ユーザーはURLを貼り付けたり、管理画面の古いブックマークを使ったり、UI外でAPIを呼び出したりできます。
UIはガイダンスとして扱い、バックエンドをすべての機密リクエストに対する最終決定者として扱ってください。
サーバーはログイン時やセッション更新時に小さな「キャパビリティ」レスポンスを返すべきです。それはサーバー側の権限チェックに基づいています。UIはその情報からメニューやボタンを描画します。
ブラウザから渡される isAdmin のようなクライアント提供のフラグを信頼してはいけません。認可は認証されたIDからサーバー側で算出してください。
アクションをインベントリ化することから始めてください。ページではなく、機能ごとの具体的な操作(read/create/update/delete/export/invite/変更課金など)を分けます。
その後、各アクションをバックエンドハンドラ(またはミドルウェア/ラッパー)で実行前に必ずチェックします。メニューは同じ権限名に紐づけてUIとAPIの整合性を保ちます。
実務的なデフォルトは次の通りです:ロールはバケット(集合)として使い、権限を真の情報源にします。権限は小さく、アクションベース(例:invoice.create)に保ち、ロールに付与してください。
もしロールが地域や所有権のような条件で増え始めるなら、その条件はポリシーに移して、無限のロール変種を避けてください。
「自分のレコードだけ編集できる」や「一定金額以下のみ承認できる」などの文脈ルールにはポリシーを使ってください。これにより権限リスト自体は安定したまま、現実の制約を表現できます。
バックエンドはリソースコンテキスト(所有者IDや組織IDなど)を使ってポリシーを評価するべきで、UIの想定に頼らないでください。
常にではありませんが、機密情報を露出するような読み取りはゲートするべきです。エクスポート、監査ログ、給与データ、管理者ユーザー一覧など、UIが通常表示しない情報を返すエンドポイントはチェック対象です。
良い基準は:すべての書き込みは必ずチェックし、機密性の高い読み取りもチェックすることです。
バルクエンドポイントは見落としやすく、一度に多くのレコードやフィールドを変更できます。UIではブロックされているユーザーが /items/bulk-update に直接アクセスすると危険です。
バルク操作自体の権限をチェックし、そのロールで変更を許可されているフィールドのみを受け付けるように検証してください。そうしないと、UIで見えないフィールドが編集されてしまいます。
権限はセッション中に変わる可能性があると仮定してください。APIが 401 または 403 を返したら、UIはそれを通常の状態として扱い:キャパビリティを再取得し、メニューを更新し、明確なメッセージを表示します。
また、共有端末ではメニューのキャッシュが別アカウントに漏れないように、キャッシュはユーザーIDでキー化するか、そもそも永続化しないでください。