ラベル Ruby の投稿を表示しています。 すべての投稿を表示
ラベル Ruby の投稿を表示しています。 すべての投稿を表示

2024年10月9日水曜日

Rails の Devise で認証を実現したプロジェクトに、 Pundit で認可を追加する

前提

Rails の Devise で認証を実現する からの続き。

ロールが adminuser で取得できるリソースを分ける。

アカウントに role カラムを追加

マイグレーションファイルを作成

rails generate migration add_role_to_accounts

マイグレーションファイルで、 role カラムを追加

db/migrate/20241008113604_add_role_to_accounts.rb:

class AddRoleToAccounts < ActiveRecord::Migration[7.2]
  def change
    add_column , , , "user"
  end
end

マイグレーション実行

rails db:migrate

管理者は、 DB から直接 role カラムを修正することとし、ログインのビューは変更しない。

リソースの作成

一般ユーザーも触れるリソースの追加

rails generate scaffold AllWelcomeResource name:string
rails db:migrate

管理者のみ触れるリソースの追加

rails generate scaffold AdminOnlyResource name:string
rails db:migrate

トップ画面の更新

各リソースの index へ行けるように、トップ画面にリンクを作成。

app/views/top/index.html.erb:

<ul>
  <li>一般歓迎リソース</li>
  <li>
    <ul>
      <li>
        <%= link_to :all_welcome_resources, all_welcome_resources_path %>
      </li>
    </ul>
  </li>
  <li>管理者リソース</li>
  <li>
    <ul>
      <li>
        <%= link_to :admin_only_resources, admin_only_resources_path %>
      </li>
    </ul>
  </li>
</ul>

Pundit ジェムのインストール

bundle add pundit
rails generate pundit:install

AdminOnlyResource への認可処理追加

ざっくり手順は、以下。

  1. コントローラーに認可処理に必要なボイラープレートを記載
  2. ポリシーファイルに index, show, create, new, update, edit, destroy の 7 種類に対する認可ポリシーを記述する。

コントローラーにボイラープレートを記載

ApplicationController

今回は、 users テーブルではなく accounts テーブルを使って認証を行っているため、ログイン済みユーザーの取得には current_account 関数を使う必要がある。

Pundit のデフォルトでは、 current_user 関数を使う用になっているため、この定義を上書きする。

app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  include Pundit    # この 4 行を追加
  def pundit_user   # この 4 行を追加
    current_account # この 4 行を追加
  end               # この 4 行を追加

  before_action 
  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
  allow_browser 
end

AdminOnlyResourceController

app/controllers/admin_only_resources_controller.rb:

class AdminOnlyResourcesController < ApplicationController
  include Pundit                                                      # この 2 行を追加
  rescue_from Pundit::NotAuthorizedError,   # この 2 行を追加

  before_action , %i[ show edit update destroy ]

  # GET /admin_only_resources or /admin_only_resources.json
  def index
    authorize AdminOnlyResource # この行を追加
    @admin_only_resources = AdminOnlyResource.all
  end

  # GET /admin_only_resources/1 or /admin_only_resources/1.json
  def show
    authorize AdminOnlyResource # この行を追加
  end

  # GET /admin_only_resources/new
  def new
    authorize AdminOnlyResource # この行を追加
    @admin_only_resource = AdminOnlyResource.new
  end

  # GET /admin_only_resources/1/edit
  def edit
    authorize AdminOnlyResource # この行を追加
  end

  # POST /admin_only_resources or /admin_only_resources.json
  def create
    authorize AdminOnlyResource # この行を追加
    @admin_only_resource = AdminOnlyResource.new(admin_only_resource_params)

    respond_to do |format|
      if @admin_only_resource.save
        format.html { redirect_to @admin_only_resource, "Admin only resource was successfully created." }
        format.json { render , , @admin_only_resource }
      else
        format.html { render ,  }
        format.json { render @admin_only_resource.errors,  }
      end
    end
  end

  # PATCH/PUT /admin_only_resources/1 or /admin_only_resources/1.json
  def update
    authorize AdminOnlyResource # この行を追加
    respond_to do |format|
      if @admin_only_resource.update(admin_only_resource_params)
        format.html { redirect_to @admin_only_resource, "Admin only resource was successfully updated." }
        format.json { render , , @admin_only_resource }
      else
        format.html { render ,  }
        format.json { render @admin_only_resource.errors,  }
      end
    end
  end

  # DELETE /admin_only_resources/1 or /admin_only_resources/1.json
  def destroy
    authorize AdminOnlyResource # この行を追加
    @admin_only_resource.destroy!

    respond_to do |format|
      format.html { redirect_to admin_only_resources_path, , "Admin only resource was successfully destroyed." }
      format.json { head  }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_admin_only_resource
      @admin_only_resource = AdminOnlyResource.find(params[])
    end

    # Only allow a list of trusted parameters through.
    def admin_only_resource_params
      params.require().permit()
    end

    def user_not_authorized                                             # この 4 行を追加
      flash[] = "You are not authorized to perform this action."  # この 4 行を追加
      redirect_to(request.referer || root_path)                         # この 4 行を追加
    end                                                                 # この 4 行を追加
end
  • include Pundit: Pundit の機能を使えるようにする
  • rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized: 認証失敗時にトップページへリダイレクト
  • authorize <モデル名>: 認可処理、認可に失敗すると、 Pundit::NotAuthorizedError が発生する

ポリシーファイルに index, show, create, new, update, edit, destroy の 7 種類に対する認可ポリシーを記述する。

今回は、 AdminOnlyResource のポリシーを作成するので、 app/policies/admin_only_resource_policy.rb にポリシーを記述する。

ポリシーファイルの生成

app/policies/admin_only_resource_policy.rb:

class AdminOnlyResourcePolicy < ApplicationPolicy
  def index?
    user.role == "admin"
  end
  def show?
    user.role == "admin"
  end
  def new?
    user.role == "admin"
  end
  def edit?
    user.role == "admin"
  end
  def create?
    user.role == "admin"
  end
  def update?
    user.role == "admin"
  end
  def destroy?
    user.role == "admin"
  end
end

これで、 roleadmin のユーザー以外が見れないようになる。

A5SQL で role を user にしたり admin にしたりして試してみよう。

以上。

参考資料

2024年9月29日日曜日

Rails の Devise で認証を実現する

プロジェクト作成

rails new app --javascript importmap --css tailwind --asset-pipeline sprockets -d postgresql --no-api

データベース設定

config/database.yml

今回はただの学習目的なので、全定義を default に書いてしまう。

# PostgreSQL. Versions 9.3 and up are supported.
#
# Install the pg driver:
#   gem install pg
# On macOS with Homebrew:
#   gem install pg -- --with-pg-config=/usr/local/bin/pg_config
# On Windows:
#   gem install pg
#       Choose the win32 build.
#       Install PostgreSQL and put its /bin directory on your path.
#
# Configure Using Gemfile
# gem "pg"
#
default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  database: public
  host: postgres
  username: admin
  password: password

development:
  <<: *default

  # The specified database role being used to connect to PostgreSQL.
  # To create additional roles in PostgreSQL see `$ createuser --help`.
  # When left blank, PostgreSQL will use the default role. This is
  # the same name as the operating system user running Rails.
  #username: app

  # The password associated with the PostgreSQL role (username).
  #password:

  # Connect on a TCP socket. Omitted by default since the client uses a
  # domain socket that doesn't need configuration. Windows does not have
  # domain sockets, so uncomment these lines.
  #host: localhost

  # The TCP port the server listens on. Defaults to 5432.
  # If your server runs on a different port number, change accordingly.
  #port: 5432

  # Schema search path. The server defaults to $user,public
  #schema_search_path: myapp,sharedapp,public

  # Minimum log levels, in increasing order:
  #   debug5, debug4, debug3, debug2, debug1,
  #   log, notice, warning, error, fatal, and panic
  # Defaults to warning.
  #min_messages: notice

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: test

# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
# Instead, provide the password or a full connection URL as an environment
# variable when you boot the app. For example:
#
#   DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
#
# If the connection URL is provided in the special DATABASE_URL environment
# variable, Rails will automatically merge its configuration values on top of
# the values provided in this file. Alternatively, you can specify a connection
# URL environment variable explicitly:
#
#   production:
#     url: <%= ENV["MY_APP_DATABASE_URL"] %>
#
# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
# for a full overview on how database connection configuration can be specified.
#
production:
  <<: *default
  database: production

トップページの作成

controller 作成

rails generate controller top

view 作成

view ファイル作成

app/view/top/index.html.erb

<h1>Welcome top page</h1>

ルーティング変更

config/routes.rb

...(snip)
  # Defines the root path route ("/")
  # root "posts#index"
  root "top#index"
...(snip)

動作確認

BINDING=0.0.0.0 ./bin/dev

ホスト PC のブラウザで、 http://localhost:3000 にアクセスすると、 Welcome top paga とだけ記載されたページが表示される。

OK.

devise で認証をする

devise gem のインストール

Gemfile に gem "devise" を追加し、 bundle install する。

プロジェクトに devise をインストール

実行後、ガイドが表示されるので、基本的にガイドに従えば OK。

$ rails generate devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Depending on your application's configuration some manual setup may be required:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

     * Required for all applications. *

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"
     
     * Not required for API-only Applications *

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

     * Not required for API-only Applications *

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views
       
     * Not required *

===============================================================================

「1.」は定義済み。「2.」も定義済み。

「3.」の実施

app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for(:title) || "App" %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= yield :head %>

    <link rel="manifest" href="/manifest.json">
    <link rel="icon" href="/icon.png" type="image/png">
    <link rel="icon" href="/icon.svg" type="image/svg+xml">
    <link rel="apple-touch-icon" href="/icon.png">
    <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <main class="container mx-auto mt-28 px-5 flex">
    <div>
      <p class="notice"><%= notice %></p> <!-- この行を追加 -->
      <p class="alert"><%= alert %></p> <!-- この行を追加 -->
    </div>
      <%= yield %>
    </main>
  </body>
</html>

「4.」はログインの View を改造したい場合に行う。 今回は firststep という事で、デフォルトのままやってみる。

devise 用モデルの作成

firststep とは関係のない諸々のわけがあって、今回は accounts テーブルにログイン情報を保存するように設定していく。

rails generate devise Accounts
rails db:migrate

devise 用コントローラーの作成

firststep とは関係のない諸々のわけがあって、今回は accounts テーブルにログイン情報を保存するように設定していく。

rails generate devise:controllers account

ページ表示時に強制的にログインを促す

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  before_action  # この行を追加
  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
  allow_browser 
end

この場合、ApplicationController に記載したので、全ページにログインが必要という事になる。 特定ページのみであれば、そのページの controller に仕込むことになるっぽい?

動作確認

ホスト PC のブラウザで、 http://localhost:3000 にアクセスすると、ログイン画面が表示される。

右上の Sign up を押下し、ユーザー登録を済ませると、 Welcome! You have signed up successfully. という言葉とともに、トップページが表示される。

(スタイルを何も定義していないので一行に表示されてしまうが、今回は無視する)

2020年8月30日日曜日

CLI アプリケーションな gem を作った

CLI アプリケーションな gem を作った

テストでは test-unitsimplecov を使用。

手順を記録していなかったので、思い出しながらまとめ直し。

ひな形作成

$ bundle gem newgem
Creating gem 'newgem'...
Do you want to generate tests with your gem?
Type 'rspec' or 'minitest' to generate those test files now and in the future. rspec/minitest/(none): minitest
Do you want to license your code permissively under the MIT license?
This means that any other developer or company will be legally allowed to use your code for free as long as they admit you created it. You can read more about the MIT license at https://choosealicense.com/licenses/mit. y/(n): n
Do you want to include a code of conduct in gems you generate?
Codes of conduct can increase contributions to your project by contributors who prefer collaborative, safe spaces. You can read more about the code of conduct at contributor-covenant.org. Having a code of conduct means agreeing to the responsibility of enforcing it, so be sure that you are prepared to do that. Be sure that your email address is specified as a contact in the generated code of conduct so that people know who to contact in case of a violation. For suggestions about how to enforce codes of conduct, see https://bit.ly/coc-enforcement. y/(n): n
      create  newgem/Gemfile
      create  newgem/lib/newgem.rb
      create  newgem/lib/newgem/version.rb
      create  newgem/newgem.gemspec
      create  newgem/Rakefile
      create  newgem/README.md
      create  newgem/bin/console
      create  newgem/bin/setup
      create  newgem/.gitignore
      create  newgem/.travis.yml
      create  newgem/test/test_helper.rb
      create  newgem/test/newgem_test.rb
Initializing git repo in /work/newgem
Gem 'newgem' was successfully created. For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html

基本情報更新

  1. newgem/newgem.gemspec を更新
    • spec.author
    • spec.email
    • spec.summary
    • spec.description
    • spec.homepage
    • spec.metadata["source_code_uri"]
    • spec.metadata["changelog_uri"]
  2. spec.metadata["allowed_push_host" = "TODO: Set to 'http://mygemserver.com'" を削除
    • rubygems.org に push できるようにするため

test-unit and simplecov を使うための設定

  1. newgem/Gemfile から minitest の行を削除
  2. newgem/newgem.gemspec に依存関係定義を追加
    • spec.add_development_dependency 'test-unit'
    • spec.add_development_dependency 'simplecov'
  3. newgem/test/test_helper.rb の更新
    • よくわからないけどこんな感じになった

      $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
      
      require 'simplecov'
      require "test/unit"
      
      SimpleCov.start do
        enable_coverage :branch
        add_filter 'test'
      end
      
      require "newgem"
  4. newgem/test/newgem_test.rb の更新
    • 更新というか、テストに合わせて新規作成する感じ。ひな形は以下のような感じ。

      require "test_helper"
      
      require_relative '../lib/test/target/file.rb'
      
      class TestTargetClass < Test::Unit::TestCase
        sub_test_case "method_name" do
          test "test perspective" do
      
            expected = "dummy"
            actual = ...(snip)
      
            assert_equal(expected, actual)
          end
        end
      end

開発手順

依存パッケージ取得

bundle install

テスト

$ bundle exec rake test

ビルド

$ bundle exec rake build

リリース

$ gem signin
$ bundle exec rake release

多分こんな感じ…。 以上。

参考資料