見積・請求のデータは、取引先名や金額が並ぶ社内の大事な情報です。第1回で作った取引先・品目のマスタは、今のところURLを知っていれば誰でも開けて、消せてしまいます。この第2回では、Ruby on Rails 8標準の認証ジェネレータでログインを付け、さらに「見るだけの人」「登録できる人」「削除もできる人」を分ける権限を入れます。
前回の第1回:取引先と品目のマスタを作るの終わりで「ログインがないので、公開サーバーには置かないでください」と書きました。この回でその宿題を片づけます。
「社内システムだから、パスワードさえあれば誰でも同じように使えれば十分では? 権限まで分ける必要はありますか?」
先に結論です。ログインだけでは足りず、「誰が何をできるか」の線引きまで最初に決めておくのが安全です。Rails 8には認証の土台(ログイン・ログアウト・セッション管理)を作る bin/rails generate authentication が標準で入っており、外部ライブラリなしで作れます。一方で、権限の分け方は会社ごとに違うため、自分で作る部分になります。
この記事では、認証ジェネレータの出力を読み解き、3段階の権限(閲覧のみ/担当者/管理者)を実装して、自動テストで固めるところまでを解説します。
認証ジェネレータが作るもの、作らないもの
Rails 8の認証ジェネレータは、ログインに必要な最小限の部品を一式つくります。公式のRails 8紹介記事によると、Session と User のモデル、SessionsController、Authentication コンセルン(=複数の画面で共有する処理のまとまり)などが生成され、一方で新規ユーザーの登録画面(サインアップ)は「アプリごとに作りが違うため」あえて含まれません。
📰 出典:Ruby on Rails「Rails 8.0: No PaaS Required」
生成されるものを、役割ごとに整理します。
| 生成物 | 役割 |
|---|---|
User モデル | メールアドレスとパスワード(暗号化して保存)を持つ |
Session モデル | 「どの端末でログイン中か」を1行ずつ記録する(IPアドレス・ブラウザ情報つき) |
Authentication コンセルン | 全画面で「ログイン済みか」を確認し、未ログインならログイン画面へ送る |
SessionsController | ログイン・ログアウトの処理。連続した失敗を制限する仕組み(レート制限)つき |
| マイグレーション | users と sessions テーブルの作成 |
なお、生成時に「パスワード再設定」の機能(メール送信つき)も一緒に作られます。この連載ではメール送信を第6回で扱うため、今回は再設定の部品を取り除いて進めます(後から足す前提です)。
実装手順
1. ジェネレータを実行する
bin/rails generate authentication
bin/rails db:migrate
パスワードを安全に保存する has_secure_password が使う bcrypt というライブラリが、Gemfile で有効になります。これは、パスワードそのものではなく「元に戻せない形(ハッシュ)」に変換して保存する仕組みです。万一データベースが漏れても、パスワードがそのまま読まれることはありません。
2. 全画面を「ログイン必須」にする
app/controllers/application_controller.rb に Authentication が差し込まれます。これで、すべての画面が既定でログイン必須になります。
class ApplicationController < ActionController::Base
include Authentication
include Authorization # 次の手順で作る権限チェック
# ...
end
「基本は全部ログイン必須で、開けてよい画面だけ例外にする」という向きが重要です。逆(基本は誰でも開けて、守る画面だけ指定する)だと、新しい画面を作るたびに守り忘れが起きます。ログイン画面だけが例外で、SessionsController に次の1行があります。
# app/controllers/sessions_controller.rb
allow_unauthenticated_access only: %i[ new create ]
3. ユーザーに「権限」を持たせる
users テーブルに role(権限)の列を足します。
# db/migrate/20261002124428_add_role_to_users.rb
class AddRoleToUsers < ActiveRecord::Migration[8.1]
def change
add_column :users, :role, :string, null: false, default: "viewer"
end
end
既定値を「閲覧のみ(viewer)」にしているのがポイントです。権限の設定を忘れたユーザーが、うっかり強い権限を持つことを防げます。
モデルでは、Railsの enum で3つの権限を定義し、「何ができるか」をメソッドにします。
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
has_many :sessions, dependent: :destroy
# viewer: 閲覧のみ / staff: 登録・更新ができる / admin: 削除もできる
enum :role, { viewer: "viewer", staff: "staff", admin: "admin" }, default: :viewer
normalizes :email_address, with: ->(e) { e.strip.downcase }
validates :email_address, presence: true, uniqueness: true
validates :password, length: { minimum: 8 }, allow_nil: true
def can_edit? = staff? || admin?
def can_destroy? = admin?
end
「権限の名前」ではなく「できること(can_edit?)」で判定する形にしておくと、後から「経理担当」のような権限を足しても、画面側の判定を書き換えずに済みます。
4. 権限チェックを1か所にまとめる
各画面に if を散らさず、コンセルンにまとめます。
# app/controllers/concerns/authorization.rb
module Authorization
extend ActiveSupport::Concern
class_methods do
# 例: require_permission :can_edit?, except: %i[ index show ]
def require_permission(permission, **options)
before_action(**options) { forbid unless Current.user.public_send(permission) }
end
end
private
def forbid
redirect_back_or_to root_path, alert: "この操作を行う権限がありません。", status: :see_other
end
end
そのうえで、取引先・品目の画面に次の2行を足すだけで制限できます。
# app/controllers/customers_controller.rb(items_controller.rb も同様)
require_permission :can_edit?, except: %i[ index show ]
require_permission :can_destroy?, only: :destroy
Current.user は、リクエストごとに「いまログインしている人」を引き出すRailsの仕組み(Current)を使っています。Current モデルに delegate :user, to: :session を1行足すだけで使えるようになります。
5. 画面も権限に合わせる
ボタンを隠すだけでは安全になりませんが(URLを直接開かれると防げません)、押せないボタンが並ぶと使う人が混乱します。制限の本体は手順4のサーバー側で、画面の出し分けは親切のためと考えてください。
<%# app/views/customers/index.html.erb %>
<% if Current.user.can_edit? %><%= link_to "新規登録", new_customer_path %><% end %>
ログイン画面は、メールアドレスとパスワードの2項目だけのシンプルな形にします。
<%# app/views/sessions/new.html.erb %>
<%= form_with url: session_path, class: "login-form" do |form| %>
<p>
<%= form.label :email_address, "メールアドレス" %>
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username" %>
</p>
<p>
<%= form.label :password, "パスワード" %>
<%= form.password_field :password, required: true, autocomplete: "current-password", maxlength: 72 %>
</p>
<%= form.submit "ログイン" %>
<% end %>
ログイン失敗の文言は「メールアドレスまたはパスワードが違います」とし、どちらが違うのかは教えません。「このメールアドレスは登録されていない」と分かると、第三者が登録済みのアドレスを探る手がかりになるためです。
動作確認:自動テストで権限の線引きを固める
権限の仕組みは、画面を目で確認するだけだと必ず抜けが出ます。そこで、「誰が何をできて、何をできないか」を自動テストにしておきます。
# test/controllers/authorization_test.rb(抜粋)
test "閲覧のみの権限は一覧を見られるが登録はできない" do
sign_in_as users(:viewer)
get customers_url
assert_response :success
assert_select "a", text: "新規登録", count: 0
assert_no_difference "Customer.count" do
post customers_url, params: { customer: { name: "新規商事" } }
end
assert_redirected_to root_path
end
test "担当者は登録・更新できるが削除はできない" do
sign_in_as users(:staff)
# ... 品目を登録できること、削除は拒否されること
end
ログインそのものについても、「正しい組み合わせで入れる」「パスワードが違うと入れない」「未ログインで業務画面を開くとログイン画面へ移り、ログイン後に元の画面へ戻る」をテストにしています。
検証は次のコマンドで行い、今回の変更後は30件のテストがすべて通り、RuboCop(コードの書き方の自動チェック)も指摘なしでした。
cd blogs/it_hacchu/series/rails-mitsumori/code
bin/rails test && bundle exec rubocop
※ この回はブラウザでの目視確認ではなく、自動テストで動作を確認しています(開発環境での確認)。メール送信が必要なパスワード再設定は、第6回以降に扱うため未実装です。
開発用の確認アカウントは db/seeds.rb に、開発環境(Rails.env.development?)の場合だけ作る形で入れています。本番環境に共通のパスワードのアカウントを残さないためで、本番の最初の管理者は、サーバー上で一度だけコマンドを実行して作る想定です(第9回で扱います)。
つまずきやすい点・セキュリティ上の注意
- サインアップ画面は作っていない:社内システムでは、管理者がユーザーを登録するのが一般的です。誰でも自分で登録できる形にすると、外部の人が入り込めてしまいます
- パスワードの長さ:今回は8文字以上のチェックを入れました。
maxlength: 72は bcrypt が扱える長さの上限に合わせたものです - レート制限は入口の対策のひとつ:ジェネレータが入れる連続失敗の制限は有効ですが、「これで絶対に破られない」という意味ではありません。本番では、アクセスの制限やログの監視も併せて検討します
- 権限を増やすときは、先にテストを書く:「管理者だけが使える画面」を増やすたびに、閲覧のみ・担当者で拒否されるテストを足しておくと、守り忘れに気づけます
- この連載で省略していること:二段階認証、パスワード再設定、ログイン履歴の画面、アカウントのロックなどは扱いません。必要な場合は別途の費用になります
発注者向けメモ:ログインと権限を頼むときの確認点
ログイン機能は「どこまで作るか」で費用が大きく変わります。開発会社に頼むときは、次を確認してください。
- ☐ 誰がシステムを使うか(社員だけか、取引先も入るか)を一覧にして伝えたか
- ☐ 権限の種類(見るだけ/登録できる/削除できる/承認できる など)を、実際の業務に沿って決めたか
- ☐ ユーザーの追加・退職者の無効化を、誰が・どの画面でするか決めたか
- ☐ パスワードを忘れたときの再設定の流れ(メール送信を含む)が見積に入っているか
- ☐ 二段階認証やシングルサインオン(=会社の共通IDでログインする仕組み)が必要か、必要なら見積に含まれているか
- ☐ 「いつ・誰が・何を変更したか」の記録(操作ログ)が必要か
開発会社への質問例
- 「権限は何段階を想定していますか。見るだけの人と、削除できる人を分けられますか」
- 「ログイン機能は自社で作りますか、既存の仕組み(認証サービスなど)を使いますか。それぞれの運用上の違いは何ですか」
- 「退職者のアカウントを止める手順は、画面からできますか。それとも開発会社への依頼が必要ですか」
- 「ログインに失敗し続けた場合や、パスワードが漏れた場合の対策は、どこまで含まれていますか」
工数が増えやすいのは、権限の種類が多い場合と、取引先の画面ごとに見せる範囲が違う場合(「A社の担当者はA社のデータだけ見える」など)、そして二段階認証やシングルサインオンです。最初のリリースでは3段階程度の権限で始め、使いながら増やす進め方も現実的です。
「権限の分け方は、あとから変えると画面もテストも直すことになります。最初に『誰が何をするか』を一緒に書き出しておくのが、いちばん安くつきます」
まとめと次回予告
第2回では、Rails 8標準の認証ジェネレータでログインを付け、権限(閲覧のみ/担当者/管理者)をサーバー側で制限し、自動テストで線引きを固めました。外部のライブラリやサービスを使わずに、業務システムに必要な入口の守りの土台ができました。
次回の第3回は「見積書と明細を作る」です。見積書(親)と明細行(子)の関係をモデルで表し、Hotwire(=画面の一部だけを書き換える仕組み)で、明細行をその場で追加できる画面を作ります。
この連載の記事一覧
この記事は連載「Railsで作る見積・請求管理システム」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【Railsで作る見積・請求管理システム 第0回】Rails 8の雛形を動かして、Excel見積からの脱却を始める
- 【Railsで作る見積・請求管理システム 第1回】取引先と品目のマスタを作る(scaffoldと入力チェック)
- 【Railsで作る見積・請求管理システム 第2回】ログインを付ける(Rails 8標準の認証と権限の分け方)(この記事)
- 【Railsで作る見積・請求管理システム 第3回】見積書と明細を作る(Hotwireで明細行をその場で追加)
- 【Railsで作る見積・請求管理システム 第4回】金額・消費税・端数処理を正しく計算する(税率ごとに1回だけ丸める)
- 【Railsで作る見積・請求管理システム 第5回】見積書をPDFで出力する(Prawnで日本語フォントを扱う)
- 【Railsで作る見積・請求管理システム 第6回】見積書をメールで送る(Solid Queueで送信を非同期にする)
- 【Railsで作る見積・請求管理システム 第7回】受注から請求書へ変換する(状態遷移と請求番号、インボイスの記載事項)
- 【Railsで作る見積・請求管理システム 第8回】入金管理と督促の一覧を作る(一部入金・期限超過の抽出)










コメント
コメント一覧 (2件)
[…] 前回の第2回:ログインを付けるで、ログインと3段階の権限(閲覧のみ/担当者/管理者)を入れました。今回の見積書の画面も、その権限の仕組みにそのまま乗せます。 […]
[…] […]