Rails 7 on Windows で devise を用いた ActiveDirectory 認証のテスト

### 環境
* Windows Server 2022 Standard
* Windows 10 Pro
* Ruby 3.0.4[x64-migw32]
* Rails 7.0.4.3
* Devise 4.9.2
* Devise_ldap_authenticatable 0.8.7

### その1：ActiveDirectoryドメイン構築

####  前提
- Windows 10 Pro(22H2)上の Hyper-V 環境（ホスト環境）

#### 手順
1. 「Windows Server 2022 Standard」の評価版をダウンロードして、ホスト環境にインストールする(ホスト環境のスペック次第だが、少なくとも2CPU、RAM 8G 以上にするのがよい。以下同じ)。ホスト名を「dc1」とする。
2. 「Windows 10 Enterprise」の評価版をダウンロードして、ホスト環境にインストールする。ホスト名を「client1」とする。
3. 「dc1」上にActiveDirecotry環境を構築する。サーバマネージャで役割として「ActiveDirectoryドメインサービス」を追加した後、ドメインコントローラに昇格させる。新規フォレストを「home.example.jp」とする。同一ネットワーク内に委任可能なDNSはないため、DNSもインストールされる。
4. 「client1」のDNSに「dc1.home.example.jp」のIPアドレスを設定する。今回はドメインには参加させたが、参加させなくてもよいかもしれない。

#### 参考
- [Windows Server 2022 - Microsoft Evaluation Center](https://www.microsoft.com/ja-jp/evalcenter/evaluate-windows-server-2022)
- [Windows 10 Enterprise - Microsoft Evaluation Center](https://www.microsoft.com/ja-jp/evalcenter/evaluate-windows-10-enterprise)
- [【仮想環境】評価版 Windows Server 2022 のインストール【番外編】 – たまテク](https://tamnology.com/windows-server-2022-install)
- [Windows Server 2022でActive Directoryを構築する (新規ドメイン) - ITなんでも屋さん](https://anything-it.info/windows-server-2022-ad-install)

###  その2：Rails 環境構築

Rails の開発環境を構築する。CランタイムがUCRTでなくMSVCRTであるRubyの最新版を使う。

####  前提
- 上記で構築したWindows 10 Enterprise 評価版「client1」

#### 手順
1. ブラウザで github の「[winget-cli](https://github.com/microsoft/winget-cli)」のサイトに行く。
1. 右側の「Releases」の「Latest」の「Windows Package Manager...」を選択する。
1. 一番下の「Assets」からファイル名末尾が「.msixbundle」のファイルをダウンロードしてインストールする。
1. 管理者権限でPowerShellを起動して、wingetの動作確認後、Ruby3.0環境を構築する[^1]。
   ```ps1
   winget
   winget install git.git
   winget install RubyInstallerTeam.RubyWithDevKit.3.0
   winget install sqlite.sqlite
   winget install microsoft.visualstudiocode
   ```
1. ソースにCを含むgemをコンパイルできるように「MSYS2」「MINGW」をセットアップする。管理者権限でコマンドプロンプトを起動して`ridk install`を実行する。デフォルトの[1,3]の選択のまま、Enterでインストールを実行する。
   ```cmd
   C:\Windows\system32>ridk install
    _____       _           _____           _        _ _         ___
   |  __ \     | |         |_   _|         | |      | | |       |__ \
   | |__) |   _| |__  _   _  | |  _ __  ___| |_ __ _| | | ___ _ __ ) |
   |  _  / | | | '_ \| | | | | | | '_ \/ __| __/ _` | | |/ _ \ '__/ /
   | | \ \ |_| | |_) | |_| |_| |_| | | \__ \ || (_| | | |  __/ | / /_
   |_|  \_\__,_|_.__/ \__, |_____|_| |_|___/\__\__,_|_|_|\___|_||____|
                       __/ |           _
                      |___/          _|_ _  __   | | o __  _| _     _
                                      | (_) |    |^| | | |(_|(_)\^/_>
   
      1 - MSYS2 base installation
      2 - MSYS2 system update (optional)
      3 - MSYS2 and MINGW development toolchain
   
   Which components shall be installed? If unsure press ENTER [1,3]
   ```
1. 環境変数反映のため、新しくPowerShellを起動する。gemの実行を確認後、Railsをバージョンを確認してインストールする。ここで想定しているのは Rails 7.0系なので、最新版が7.0系でない場合は、バージョンを指定する。ドキュメントはインストールしない。
   ```ps1
   gem
   gem search \Arails\z -a
   gem install rails -N
   rails
   ```
1. 「Railsをはじめよう」のチュートリアル「blog」の2つめのモデル追加前まで作成したアプリを作成して、起動する。直接関係しないが、Visual Studio では、$home\source\repos がデフォルトのソース置き場になるので、それに倣う。Railsは、minimalオプションで最小構成にする。
   ```ps1
   cd $home
   mkdir source\repos
   cd source\repos
   rails new blog --minimal
   cd blog
   rails g scaffold Article title:string body:text
   rails db:migrate
   (Get-Content .\config\routes.rb)|%{$_ -replace '# root','root'}|Set-Content .\config\routes.rb
   'Article.create(title: "Hello Rails", body: "I am on Rails!")'|Add-Content .\db\seeds.rb
   rails db:seed
   rails s
   ```
1. ブラウザで「http://localhost:3000」 にアクセスし、CRUDの挙動を確認する。

[^1]: PowerShellが文字化けする場合は、英文フォントに強制変更されているためなので、ウィンドウ枠の右クリックプロパティから、フォントを「MSゴシック」等に変更する。

####  参考
- [GitHub - microsoft-winget-cli- Windows Package Manager CLI (aka winget)](https://github.com/microsoft/winget-cli)
- [winget を用いて，Windows での主要なソフトウェアのインストールと設定（winget を使用）（Windows 上）](https://www.kkaneko.jp/tools/win/install.html)
- [Getting Started with Rails — Ruby on Rails Guides](https://guides.rubyonrails.org/getting_started.html)
- [Rails をはじめよう - Railsガイド](https://railsguides.jp/getting_started.html)

### その3：Devise による認証機能の追加
ここは先人の記録に感謝。参考にあげたサイトを参照しなければ、実現できたとは思えない。

####  前提
* 上記で構築したRailsアプリ「blog」

#### 手順
1. Gem のインストール
   ```ruby:Gemfile
   gem 'devise'
   gem 'devise_ldap_authenticatable'
   ```
   ```
   bundle install
   ```
1. 設定ファイルの初期化、ユーザーモデルの作成と修正
   ここではモデル名として「User」を指定している。
   ```ps1
   rails g devise:install
   rails g devise User
   rails g devise_ldap_authenticatable:install
   ```
   ```diff_ruby:app/model/user.rb を修正、有効にする機能だけを残す
   class User < ApplicationRecord
     # Include default devise modules. Others available are:
     # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
   -  devise :ldap_authenticatable, :registerable,
   -         :recoverable, :rememberable, :validatable
   +   devise :ldap_authenticatable, :rememberable, :trackable
   end
   ```
   ```diff_ruby:db/migrate/yyyyMMddhhmmss_devise_create_users.rb を修正、デフォルトが emailログインなので修正する
   class DeviseCreateUsers < ActiveRecord::Migration[7.0]
     def change
       create_table :users do |t|
   +     ## LDAP authenticatable
   +     t.string :login, null: false, default: "", unique: true
         ## Database authenticatable
   -      t.string :email,              null: false, default: ""
   -      t.string :encrypted_password, null: false, default: ""

         ## Recoverable
   -      t.string   :reset_password_token
   -      t.datetime :reset_password_sent_at

         ## Rememberable
         t.datetime :remember_created_at

         ## Trackable
   -      # t.integer  :sign_in_count, default: 0, null: false
   +      t.integer  :sign_in_count, default: 0, null: false
   -      # t.datetime :current_sign_in_at
   +      t.datetime :current_sign_in_at
   -      # t.datetime :last_sign_in_at
   +      t.datetime :last_sign_in_at
   -      # t.string   :current_sign_in_ip
   +      t.string   :current_sign_in_ip
   -      # t.string   :last_sign_in_ip
   +      t.string   :last_sign_in_ip

         t.timestamps null: false
       end

   +    add_index :users, :login,                unique: true
   -    add_index :users, :email,                unique: true
   -    add_index :users, :reset_password_token, unique: true
       # add_index :users, :confirmation_token,   unique: true
       # add_index :users, :unlock_token,         unique: true
     end
   end
   ```
   ```ps1:マイグレーション
   rails db:migrate
   ```
1. 認証設定
   ```diff_ruby:config/initializers/devise.rb を修正（修正箇所を抜粋。使用したバージョンではログアウト処理でエラーになるので、HTTPメソッドを修正）
   # ==> LDAP Configuration
   # config.ldap_logger = true
   -# config.ldap_create_user = false
   +config.ldap_create_user = true
   -# config.ldap_update_password = true
   +config.ldap_update_password = false
   # config.ldap_config = "#{Rails.root}/config/ldap.yml"
   # config.ldap_check_group_membership = false
   # config.ldap_check_group_membership_without_admin = false
   # config.ldap_check_attributes = false
   # config.ldap_check_attributes_presence = false
   -# config.ldap_use_admin_to_bind = false
   +config.ldap_use_admin_to_bind = true
   # config.ldap_ad_group_check = false

   -# config.authentication_keys = [:email]
   +config.authentication_keys = [:login]

   # The default HTTP method used to sign out a resource. Default is :delete.
   -config.sign_out_via = :delete
   +config.sign_out_via = :get
   ```
   sAMAccountName は ユーザーログオン名(Windows2000より前)。base は補足の方法での確認を推奨。admin_user はLDAPアクセス用のドメインユーザーを作成して使用する。ドメイン管理者「Administrator」だとうまくいかないかも？
   ```diff_ruby:config/ldap.yml を修正
   development:
   -  host: localhost
   +  host: dc1.home.example.jp
     port: 389
   -  attribute: cn
   +  attribute: sAMAccountName
   -  base: ou=people,dc=test,dc=com
   +  base: CN=Users,DC=home,DC=example,DC=jp
   -  admin_user: cn=admin,dc=test,dc=com
   +  admin_user: "user_for_ldap"
   -  admin_password: admin_password
   +  admin_password: "user_for_ldap_password"
     ssl: false
     # <<: *AUTHORIZATIONS
   ```
1. ログイン用のviewを作成
   ```
   rails g devise:views
   ```
   ```diff_ruby:app/views/devise/session/new.html.erb を修正
   <div class="field">
   -  <%= f.label :email %><br />
   +  <%= f.label :login %><br />
   -  <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
   +  <%= f.text_field :login, autofocus: true %>
   </div>

   <div class="field">
     <%= f.label :password %><br />
   -  <%= f.password_field :password, autocomplete: "current-password" %>
   +  <%= f.password_field :password, autocomplete: "off" %>
   </div>
   ```
1. ルーティング修正、確認
   ```diff_ruby:config/routes.rb を修正
   Rails.application.routes.draw do
   -  devise_for :users
   +  devise_for :users, only:[:sessions]
     resources :articles
     # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
   
     # Defines the root path route ("/")
     root "articles#index"
   end
   ```
   ```ps1:ルーティング確認、/users で始まるURIは、new_user_session, user_session, destroy_user_session のみ
   rails routes
   ```
1. 記事の更新操作はログインユーザーのみに設定
   ```diff_ruby:app/conftollers/article_controller.rb を修正
   class ArticlesController < ApplicationController
     before_action :set_article, only: %i[ show edit update destroy ]
   +  before_action :authenticate_user!, only: %i[new create edit update destory]
   ```
1. ログアウトのリンクをレイアウトに追加。ログインしているときのみ有効。
   ```diff_ruby:layout.html.erbなど
   <%= link_to_if user_signed_in?, "Logout", destroy_user_session_path, method: :delete %>
   ```
1. サーバを起動して、新規作成、更新、削除時には、ログインが要求されること、ログイン情報の入力で編集画面に遷移できることを確認
   ```
   rails s
   ```

####  参考
- [GitHub - cschiewek-devise_ldap_authenticatable- Devise Module for LDAP](https://github.com/cschiewek/devise_ldap_authenticatable)
- [devise+ActiveDirectory(LDAP)認証 - JunkBoxWiki](https://junkbox.wicurio.com/index.php?devise%2BActiveDirectory%28LDAP%29%E8%AA%8D%E8%A8%BC)
- [devise で ActiveDirectory 認証 - Qiita](https://qiita.com/dai0916/items/6c12bf99310d7161dff1)
- [[Rails]No route matches [GET] --users-sign_out”の対処法 - Qiita](https://qiita.com/ken_sasaki2/items/ef0802ed0d8257753a6d)

### その4：認証を組み込んだテスト
#### 手順

1. テスト用dbの作成
   ```
   rails db:migate RAILS_ENV=test
   ```
1. テストを実行してみる。Scaffoldで作成されていたテストがあり、エラーになる。test開始時にロードされるuserのfixtureがユニークでないため。ユーザー情報はfixtureからロードしないので、削除。
   ```
   rails t
   ```
   ```diff_ruby:test/fixture/user.yml を修正
   -one: {}
   -two: {}
   ```
1. テストを再度実行してみる。認証を必要と設定した、new, edit, update, create が失敗する。
   ```
   rails t
   ```
1. ヘルパーを追加した上で、認証を必要とする操作のまえにログインするようにテストを修正する。なお「sign_in」しても、認証サーバにログイン要求が飛ぶのではなく、Devise上でログイン済みの扱いにしているだけの模様。
   ```diff_ruby:test/controllers/articles_controller_test.rb を修正
   class ArticlesControllerTest < ActionDispatch::IntegrationTest
   +  include Devise::Test::IntegrationHelpers    
     setup do
       @article = articles(:one)
   +    @user = User.create(login:'alice', password:'inwonderland')
     end
   
     test "should get index" do
       get articles_url
       assert_response :success
     end

     test "should get new" do
   +   sign_in @user
       get new_article_url
       assert_response :success
   +   sign_out @user
     end

     test "should create article" do
   +   sign_in @user
       assert_difference("Article.count") do
         post articles_url, params: { article: { body: @article.body, title: @article.title } }
       end
   +   sign_out @user

       assert_redirected_to article_url(Article.last)
     end

     test "should show article" do
       get article_url(@article)
       assert_response :success
     end

     test "should get edit" do
   +   sign_in @user
       get edit_article_url(@article)
       assert_response :success
   +   sign_out @user
     end

     test "should update article" do
   +   sign_in @user
       patch article_url(@article), params: { article: { body: @article.body, title: @article.title } }
       assert_redirected_to article_url(@article)
   +   sign_out @user
     end

     test "should destroy article" do
   +   sign_in @user
       assert_difference("Article.count", -1) do
         delete article_url(@article)
       end
   +   sign_out @user

       assert_redirected_to articles_url
     end
   end
   ```
1. テストを再度実行する。failuresとerrorsがゼロになっていることを確認する。
   ```
   rails t
   ```

#### 参考
- [GitHub - heartcombo-devise- Flexible authentication solution for Rails with Warden-](https://github.com/heartcombo/devise)

### 補足：設定する認証情報の確認方法

#### ドメインコントローラ上で確認
1. 「Active Directory ユーザーとコンピュータ」を起動
1.  メニューの表示から「拡張機能」を有効にする
1.  オブジェクトのプロパティを表示すると、タブに「属性エディタ―」があり、「distinguishedName」、「sAMAccountName」等が確認できる

#### ドメインに参加した Windows 10上で PowerShellで確認
1. 設定 -> オプション機能 -> 機能の追加、から「RSAT: Active Directory Domain Services 及びライトウェイトディレクトリサービスツール」をインストールする
1. PowerShellで、`Import-Module ActiryDirecotry` した後、`Get-ADUser`が使えるようになる。「DistinguishedName」、「SamAccountName」等が確認できる。

#### 参考
- [【Active Directory】様々な属性を表示・編集できる「属性エディター」タブを表示する - なべりうむ](https://naberium.com/2022/08/10/%E3%80%90active-directory%E3%80%91%E6%A7%98%E3%80%85%E3%81%AA%E5%B1%9E%E6%80%A7%E3%82%92%E8%A1%A8%E7%A4%BA%E3%83%BB%E7%B7%A8%E9%9B%86%E3%81%A7%E3%81%8D%E3%82%8B%E3%80%8C%E5%B1%9E%E6%80%A7%E3%82%A8/)
- [リモート サーバー管理ツール - Windows Server - Microsoft Learn](https://learn.microsoft.com/ja-jp/troubleshoot/windows-server/system-management-components/remote-server-administration-tools)