Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Railsのdefaultsを用いた権限によるアクセス制御


この記事は、Sansan Advent Calendar 2025、16日目の記事です。

はじめに

こんにちは、技術本部Digitization部Entry Engineeringグループgeesチームでインターンをしている輿石です。
geesチームは、名刺データ化システムを開発しています。
本記事では、もともと権限によるアクセス制御が存在しなかった内部向けの名刺データ化管理システムに対して、Railsのdefaultsを用いて「エンドポイントごとに必要な権限を宣言的に管理する仕組み」を導入した話をまとめます。

ルーティングのdefaultsに「必要な権限情報」を付与し、その情報をもとにアクセス制御を行う仕組みを設計・実装しました。これにより、
・各エンドポイントに必要な権限がルーティングから一目で分かる
・権限仕様の追加や変更がしやすい
といった状態を実現しました。

この記事では、この権限導入の背景から、defaultsを使った設計、実装の詳細までを順に説明します。

既存システムの前提

今回対象としたのは、geesチームが保守している内部向けの管理画面で、サーバーサイドレンダリング構成の Rails アプリケーションです。テンプレートエンジンにはHamlを採用しており、クライアントサイドからAPIを叩く形ではなく、サーバーサイドで画面を組み立てる従来型のWebアプリケーションとして動いています。

権限管理は、アプリケーション内では完結させず、OIDCで認証・認可を実現しています。権限の付与・管理はすべて認証基盤側で行い、アプリケーション側は「付与済みの権限一覧をどう解釈して画面や機能へのアクセスを制御するか」に専念する構成です。

やったこと

概要

以前は権限チェックがなく、全ユーザー共通のアクセス・機能制御だったものを、認証基盤から取得した権限を元に自作したロジックでアクセス制御を行うようにしました。
要件は以下の2点です。

  • ページ単位での認可
  • アクセス権のないページへのリンクを表示しないこと

この要件を満たすため、認可モデルを定義し、共通の認可ロジックを実装しています。ページ単位で制御する方針のため、routes.rbに権限情報を集約することで一元管理できると考え、defaultsオプションを用いる構成としました。実際の認可は、共通のbefore_actionguarded_link_toによって行います。

routes.rbで一元管理する理由

権限の必要条件をルーティングに集約すると、画面追加時に権限設定の抜け漏れを防ぎやすいです。権限キーの一覧もroutes.rbを見れば把握でき、どのページに権限が設定されていないかも目視しやすく、レビュー時のチェックポイントが明確になります。

ルーティングでdefaultsを付与する実装例

routes.rbのdefaultsオプションに、required_permissionsパラメータを渡して権限管理できるようにしました。Rails のdefaults公式ガイド)はルーティング定義に静的なパラメータを指定でき、URL 生成時にparamsに含まれるためcontroller/view両方で参照できます。
defaults は、ルートの定義時にハッシュ化されてparamsに格納されるため、外部入力では上書きされず、セキュリティが担保されています。

# config/routes.rb
namespace :admin do
  resources :users, only: :index, defaults: {required_permissions: 'general:admin'} do
    post :ban,  defaults: {required_permissions: 'general:admin'}
    post :gray, defaults: {required_permissions: 'general:admin'}
  end
end

ApplicationControllerでの認可フック

before_actiondefaultsrequired_permissionsとユーザー権限を突き合わせ、許可されない場合は 403 を返すようにしました。権限ロジックを各controllerに書かずに共通コールバックへ寄せ、ApplicationControllerに置くことで全ルートで自動適用されるようにしています。

before_action :authorize_permission

def authorize_permission
  required = params[:required_permissions]
  return if required.blank?
  # required_permissions に対してユーザーの権限を検証する
  return if authorizer.allowed?(current_user, required)

  render "errors/forbidden", status: :forbidden
end

Hamlビューでのリンク制御

view 側の権限チェックをばらばらに書かず、従来のlink_toをラップしたguarded_link_toに集約して表示時にも権限を確認するようにしました。APIと動作は同じにしてあるので、既存リンクを置き換えるだけで導入できます。表示段階で弾くことで、ユーザー体験も崩れにくくなります。

def guarded_link_to(name = nil, options = nil, required_permissions: nil, **html_options, &block)
  return unless authorizer.allowed?(current_user, required_permissions)
  link_to(name, options, html_options, &block)
end
%li
  = guarded_link_to "ユーザー一覧", admin_users_path, required_permissions: 'general:admin'

渡されたrequired_permissionsauthorizerで判定し、許可されない場合はリンク自体を描画しません。内部では素のlink_toを呼ぶだけなので、呼び出し元のAPIはそのままです。

権限定義とAuthorizerの役割

Authorizerで権限定義を一元化し、コントローラとヘルパー双方から同じ判定ロジックを呼び出すようにしました。単体テストで許可・不許可の境界を固め、仕様がずれにくくなるようにしています。

なぜdefaultsを使ったのか

Railsのdefaultsとは

ルーティングに静的パラメータを埋め込む仕組みで、URL生成時に paramsに載り、controller/viewの両方から参照できます。外部入力では上書きされません。Rails Guides とdeepwikiを参考に実際のソースコードから実装を確認して採用しました。

paramsに置くメリット

controllerviewの両方から同じキーで参照できるため、条件分岐の漏れを減らせます。パスヘルパーがdefaultsを自動で含むので、各画面で同じ値を見にいくことができるので同じパスに対して異なる権限をつけることがないです。

セキュリティ面

defaultsオプションではクエリパラメータなどの上書きがないため、外部からの改ざんは避けられます。

代替案

各画面のコントローラごとに、必要な権限をチェックするbefore_actionを個別に定義していく方法も考えましたが、画面追加時にbefore_actionの書き忘れが起こりやすく、レビューでも検出しづらいことを懸念しました。defaultsを使わない方法(別テーブルや定数で持つなど)も検討しましたが、ルートと権限が離れると誰がどこで参照するか散らばるため、routesへの埋め込みを選びました。

導入で得た学び

スレッドセーフ

当初は気づかなかったのですが、権限情報をクラス変数で実装していたため、マルチスレッドで処理されるRailsの各リクエスト間でその情報が共有されてしまっていました。そのため、現在はスレッド変数を使うように変更しています。
この点については、より良い方法やRailsの内部実装も含めて、今後さらに調査していきたいと考えています。

影響範囲の可視

routesに権限キーを置くと、修正対象のビューやルートの量がすぐ把握でき、作業計画を立てやすかったです。どの画面がどの権限に紐づくかをroutesで一覧化できました。

運用メリット

ルート追加時にdefaultsを書かないとレビューで即座に気づけるようになり、権限漏れの初期発見につながりました。影響範囲がroutesに集約されたことで、レビュー観点がシンプルになりました。

やってみて

元々before_actionで実装することを先輩から提案されていましたが、見通しが悪くセルフレビューや今後の保守性を考えたときに他の方法を考えました。
その際にRailsの公式ドキュメントや実装を見てRailsのルーティングについて少し詳しくなれたり、考慮が漏れていた部分をサポートいただけてよい経験になりました。

まとめ

Railsのdefaultsを使って権限情報をroutes.rbに持たせることで、ページごとの認可を宣言的に管理できるようにしました。ルーティングを見れば「どの画面がどの権限で守られているか」がすぐに分かるようになり、画面追加時の権限漏れにも気づきやすくなりました。

また、共通のbefore_actionguarded_link_toに権限判定を集約したことで、コントローラとビューでバラバラに条件分岐を書く必要がなくなり、コードの見通しがかなり良くなりました。実装の途中ではクラス変数を使ったことでスレッドセーフ性の問題にぶつかり、スレッドローカル変数に切り替える対応を通じて、Rails の実行モデルについても理解を深めることができました。

権限まわりの仕様を「どこにどう書くか」を意識して整理し直したことで、単に機能を追加しただけでなく、今後も運用しやすい形に近づけられたと感じています。

Sansan技術本部ではカジュアル面談を実施しています

Sansan技術本部では中途の方向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話します。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください

© Sansan, Inc.