（備忘録）管理者権限やセキュリティetc...

# 管理者権限の付与方法
## 手順
### 1.gemのインストール
gemfileに記述し`bundle install`

```Gemfile.
gem 'devise'
gem 'cancancan'
gem 'rails_admin'
```

### ２.deviseのセットアップ
deviseのインストールと、userモデルの作成

```
rails g devise:install
rails g devise user
rails db:migrate
```
`http://<host>:3000/users/sign_up`にアクセスして、メールアドレス等入力して登録。

### 3.cancanのセットアップ
```
rails g cancan:ability
```
### 4. rails_adminのセットアップ
```
rails g rails_admin:install
```
install時、パスを聞かれるが、今回はデフォルトの/adminで表示させるので、そのままEnter。

### 5.rails_admin.rb を編集（コメントアウトを外す）
```rails_admin.rb
# == Devise ==
config.authenticate_with do
  warden.authenticate! scope: :user
end
  config.current_user_method(&:current_user)

# == Cancan ==
config.authorize_with :cancan
```
### 6.adminの判定の追加
- rails_adminでの管理画面に入れるユーザーを管理者のみに限定する設定
- userテーブルにadminを追加し、このフラグがtrueのユーザーのみ管理者として扱う
  - 出来上がったマイグレーションファイルを編集（falseにすることで自動的に管理者設定されない。）

```
rails g migration AddAdminToUser admin:boolean
```

```2021~~~~~_add_column_to_user.rb
# == マイグレーションファイル
class AddAdminToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end
```
```ターミナル.
rails db:migrate
```

- 次に先ほど登録したユーザーを管理者に設定。
- 既にあるユーザーに権限をつけることも可
- コントローラーやビューでadminがtrueの場合とfailseの場合で分岐して、各処理を書く

```
 # 既存のユーザーへの管理者権限の付与
rails c
[1] pry(main)> user = User.find(1)  //idが1のレコードを取得（今作成したuserのid）

[2] pry(main)> user.admin = true

[3] pry(main)> user.save
```
- 最後にcancancanの権限設定（管理者権限を簡単に定義しておくことのできるgem）
app/models/ability.rb

```ability.rb
class Ability
  include CanCan::Ability
  def initialize(user)
    if user.try(:admin?) # userがnillだった場合（ログインしていなかった場合）にエラーになるのを防ぐため
       can :access, :rails_admin　# 管理者ページへのアクセスを許可
       can :manage, :all         # 全ての権限を付与
   end
  end
end
```

- ログイン済みで、adminがtrueのユーザーは、全てのモデルのCRUDが行えて（can :manage, :all）、rails_adminの管理画面にアクセス権限（can :access, :rails_admin）があるという設定。

### ⭐️controllerでの判断（一例）
下記のように記載することで、indexにはadminカラムがtrueのユーザーのみがリンク可能。
adminカラムがfalseのユーザーは/に指定したページへとリダイレクトされる。

```usersController.rb
class UsersController < ApplicationController
  before_action user_admin, only: [:index]

  def index
      @users = User.all
  end

  private
    def user_admin
       @users = User.all
       if  current_user.admin == false
           redirect_to root_path
       else
           render action: "index"
       end
    end
end
```

## ユーザーのログイン状態でルーティング先を変更

- ログイン状態であればリクエストのコンテンツを表示
- ログイン状態でなければログインページ、もしくはサインアップのページに飛ぶ

/app/controllers/application_controller.rb

```application_controller.rb
before_action :authenticate_user! #protect_from_forgery with: :exceptionより後におく。
```

- アプリ全体の仕様にする場合にはapplicationに記載。
- 実装の確認のために、ログアウトのリンクを仕込んでおく。

/app/views/layouts/application.html.erb

```application.html.erb
# 以下を適当なところに追記
<% if user_signed_in? %>
  <p><%= link_to "ログアウト", destroy_user_session_path, method: :delete %></p>
<% end %>

```

- ログイン時はリクエストしたページへ通し、ログオフ時はログイン画面へ。

## メール認証
- サインアップしたユーザーアカウントは、一旦仮登録としてメールを飛ばし、返信を持って本登録させる。

### 1.Userモデルにconfirmableを追加。
- 各種認証時に「確認作業」が定義できるようになる。Eメール認証はそのやり方のうちの一つ（くらい）。

/app/models/user.rb

```
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :confirmable #最後のやつを追加
```

- Userテーブルに確認にまつわる項目を追加しなければいけないのでマイグレーションファイルを作成。

```
rails g migration add_confirmable_to_devise
```

マイグレーションファイルを次のように編集。
/db/migrate/yyyymmddxxxxxx_add_confirmable_to_devise.rb

```
class AddConfirmableToDevise < ActiveRecord::Migration[5.1]
  # Note: You can't use change, as User.update_all will fail in the down migration
  def up
    add_column :users, :confirmation_token, :string
    add_column :users, :confirmed_at, :datetime
    add_column :users, :confirmation_sent_at, :datetime
    add_column :users, :unconfirmed_email, :string # Only if using reconfirmable
    add_index :users, :confirmation_token, unique: true
    # User.reset_column_information # Need for some types of updates, but not for update_all.
    # To avoid a short time window between running the migration and updating all existing
    # users as confirmed, do the following
    User.all.update_all confirmed_at: DateTime.now
    # All existing user accounts should be able to log in after this.
  end

  def down
    remove_columns :users, :confirmation_token, :confirmed_at, :confirmation_sent_at
    remove_columns :users, :unconfirmed_email # Only if using reconfirmable
  end
end
```

```terminal
rake db:migrate
```
- Eメール送信のための設定完了。
- SMTPサーバの設定が必要。githubにアップしたくはないので、letter-opener_webを使用。
  - gemのインストール。
     - SMTP（Simple Mail Transfer Protocol）サーバとは、メールの送信元と宛先の間で、メールを送受信または中継することを主な目的としたアプリケーション

Gemfile

```
group :development do #開発環境のみ
  gem 'letter_opener' # 追加
  gem 'letter_opener_web' # 追加
end
```

```
bundle install --path vender/bundle
```
設定ファイルを編集

```
config.action_mailer.perform_caching = true # falseをtrueに修正
config.action_mailer.default_url_options = { host: 'localhost:3000' } # 追加
config.action_mailer.delivery_method = :letter_opener_web # 追加
```

以下をルーティング設定に追加。
/config/routes.rb

```
if Rails.env.development? #開発環境の場合
  mount LetterOpenerWeb::Engine, at: "/letter_opener"
end
```
- localhost:3000/letter_openerを開くと送信されたメールをブラウザで確認できる。

## cancancanについて
- 権限があるかはcan?メソッドで判定することが出来る。

※具体例

```
class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new  # ログインしていない場合は、からユーザーを用意し判定に用いる

    # default parmission
    cannot :buy, Product

    if user.sys_admin?
      can :manage, :all
    end

    if user.product_manager?
      can :manage, Stockpile, owner: user # 自分がオーナーの倉庫には全権限を持つ
      can :read, Stockpile                 # そうでなくても、読み取り権限を持つ

      # 自分の倉庫にある製品に対してすべての権限を持つ
      can :manage, Product, stockpile: {owner: user}
      # ただし、新規登録、削除はできない
      cannot [:create, :destroy], Product
    end

    if user.customer?
      # 複数のモデルに権限を付与できる
      can :read, [Stockpile, Product]

      # 独自権限も作れる
      can :buy, Procuct, stockpile: nil # 倉庫から出されている製品を買える
    end
  end
end
```

read：参照
create：登録
update：更新
destroy：削除
manage：すべて
基本の権限には上記のものがあります。
「manage / すべて」というのは`「read+crete+update+destroy」`というわけではない点に注意。
上の例では「buy / 購入」という権限を独自に作られているが、
manage権限ではbuyも可能名ため、デフォルトの権限付与で、製品の購入を禁じ、
もし`user.customer? = true`ならば購入可能になるように加工。
※あるモデルのある権限にcanとcannotが重なって付与された場合、最後に適応されたほうが有効になる。

```
# current_userがproduct_managerの場合
can? :manage, Stockpile   #=> true
can? :read, Stockpile   #=> true
can? :update, Stockpile   #=> true

# 1. あるモデルに権限Xを持っているか判定する
@stockpile = Stockpile.find_by(owner: user)
can? :manage, @stockpile  #=> true

# 2. あるレコードに権限Xを持っているか判定する
@stockpile_2 = Stockpile.where("owner_id != ?", user.id).first
can? :manage, @stockpile_2  #=> false
can? :read, @stockpile_2    #=> true
can? :update, @stockpile_2  #=> false

# 3. あるモデルから権限を持っているレコードを取得する
Stockpile.accessible_by(current_ability).to_sql
#=> SELECT "stockpiles".*
#     FROM "stockpiles"
#    WHERE "stockpiles"."owner_id" = 1
```

ということが出来ます。
3の方法では、index権限での検索がデフォルトのようですが、

```
Stockpile.accessible_by(current_ability, :destroy)
Stockpile.accessible_by(current_ability, :my_permission)
```
### 権限のAND条件
```
# 自身が登録した食べ物に参照権限がある
can :show, Product, type: 'food', created_by: user.id

# 一年前〜半年前に登録された食べ物の削除権限がある
can :destroy, Product, type: 'food', created_at: (1.years.ago..6.months.ago)

# どう検索されるか確認
Stockpile.accessible_by(current_ability, :show)
#  WHERE "stockpiles"."type" = 'food' AND "stockpiles"."created_by" = 1
Stockpile.accessible_by(current_ability, :destroy)
#  WHERE "stockpiles"."type" = 'food' AND "stockpiles"."created_at" BETWEEN '2014-12-1' AND '2014-6-1'
```

### 権限のOR条件
```
# 食べ物、または飲み物の閲覧権限がある
can :read, Product, type: 'food'
can :read, Product, type: 'drink'

# どう検索されるか確認
Stockpile.accessible_by(current_ability)
#  WHERE "stockpiles"."type" = 'food' OR "stockpiles"."type" = 'drink'
```

## deviseで定義されているモジュールについて

|モジュール名|内容|
|:--|:--|
|DatabaseAuthenticatable|データベースに保存されたパスワードが正しいか検証をします。同時にパスワードの暗号化も行います。
|
|Recoverable|パスワードをリセットします。|
|Trackable|ログインした回数、最終ログイン日時、前回ログイン日時、最終ログインIP、前回ログインIPを保存する。|
|Confirmable|新規登録時にメール認証機能をつけます。|
|Lockable|ログインに何度も失敗すると、アカウントをロックします。何回失敗するとロックするかはこのLockable内で指定します。|


以上がマイグレーションファイルに設定されており、初期状態だとコメントアウトされているものもあるので、
使いたい場合はコメントアウトを外す必要がある。

### その他に用意されているモジュール
- マイグレーションファイルに記述されている以外のモジュール

|モジュール名|内容|
|:--|:--|
|Timeoutable|ログインしたままの状態で一定時間経つと自動でログアウトさせる。デフォルトは30分。|

- 有効期限を設定する場合は`config/initializers/devise.rb`に下記を追記。

```
# 1ヶ月と指定
config.timeout_in = 1.month
```

次にrailsの有効期限を設定します。
`config/initializers/session_store.rb`を下記のように編集します。


```
TechReviewSite::Application.config.session_store :cookie_store, key: '自分のアプリの設定_session', expire_after: 1.month'
```

こうすることにより1ヶ月はログインセッションが保持され、
1ヶ月以内にアクセスすればさらに1ヶ月セッション時間が保持される。

|モジュール名|内容|
|:--|:--|
|Validatable|Emailやパスワードのバリデーションを追加します。|
|Omniauthable|twitterやFacebookなどのSNS認証を追加します。|

### モジュールの使い方
`app/models/users.rb` で下記のように指定します。

```
devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :validatable
```

### deviseのビューについて
投稿フォームのデザインはdeviseで用意されています。
ただシンプルなので自分のアプリに合ったデザインに変更するのが良いでしょう。
その際は下記のコマンドを入力します。

```
rails g devise:views
```
各フォルダがどのビューに対応しているか確認してみましょう。

|フォルダ名|ファイル名|内容|
|:--|:--|:--|
|confirmations	|new	|パスワード再発行時のフォーム
|mailer	        |後述	|送信されるメールの内容（下記に詳細）
|passwords	|edit	|パスワード編集フォーム
|passwords	|new	|パスワードを忘れた時のフォーム
|registrations	|edit	|ユーザー情報編集フォーム
|registrations	|new	|新規登録のフォーム
|sessions	|new	|ログインフォーム
|shared	        |_link	|各フォームに表示されるリンクの部分テンプレート
|unlocks	|new	|アカウントロック通知メールを送信するフォーム

## Devise Securityについて
deviseでは過去に使用されたパスワードの制限や多重ログイン禁止などの
エンタープライズ用のセキュリティ要件までは取り扱っていない。
よりセキュアなRailsアプリを構築するために使用される。

### 追加できる機能
|名前|内容|
|:--|:--|
|:password_expirable|設定された時間が過ぎるとパスワードが自動的に失効。
|:secure_validatable|Devise本家の:validatableよりも、さらに強いEmail、パスワード検証。|
|:password_archivable|過去に使われたパスワードをold_passwordsテーブルに格納し、履歴確認。	|
|:session_limitable|いちアカウントが一度に1つのセッションしか使えないことを保証する。（＝多重ログインの禁止）|
|:expirable|アクティブでない状態がXX日間続いたユーザアカウントを失効させる。|
|:security_questionable|秘密の質問機能を可能に。|
|:captcha support|さらに追加的な機能として、sign_up、sign_in、recover、unlockに対して`captcha`を導入することができる。| 
|:paranoid_verification|ユーザがアプリを使うための識別コードを管理者が発行できる|

### インストール

`Gemfile`に`devise-security`を追加して`bundle install`

```
gem 'devise-security'
```

### Config設定
次にジェネレータを実行します。

```
rails generate devise_security:install
```
- `config/initializers/devise-security.rb`に設定ファイルが作成されるので、
それを適宜欲しい機能に応じて修正する。

config/initializers/devise-security.rb

```
Devise.setup do |config|

  # パスワード有効期限（例. ３ヶ月）
  # config.expire_password_after = 3.months

  # パスワードは英文字（大文字・小文字）と数字、記号を最低1字ずつ含む
  # config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }

  # アーカイブする旧パスワードの数
  # config.password_archiving_count = 5

  # 古いパスワードを拒否する？(true, false, count)
  # config.deny_old_passwords = true

  # recoverフォームにcaptchaを導入する？
  # config.captcha_for_recover = true

  # sign upフォームにcaptchaを導入する？
  # config.captcha_for_sign_up = true

  # sign inフォームにcaptchaを導入する？
  # config.captcha_for_sign_in = true

  # unlockフォームにcaptchaを導入する？
  # config.captcha_for_unlock = true

  # recoverフォームに秘密の質問を導入する？
  # captcha_for_recoverが自動的にtrueになります。
  # config.security_question_for_recover = false

  # unlockフォームに秘密の質問を導入する？
  # captcha_for_unlockが自動的にtrueになります。
  # config.security_question_for_unlock = false

  # confirmationフォームに秘密の質問を導入する？
  # captcha_for_confirmationが自動的にtrueになります。
  # config.security_question_for_confirmation = false

  # ==> Configuration for :expirable
  # 最後のアクティブから失効になるまでの時間
  # config.expire_after = 90.days
end
```

#### DBスキーマの修正
各機能に応じてDBスキーマを修正します。
詳細はDevise SecurityのREADMEを見てください。
`Session limitable`の例

```
def change
    add_column :resources, :unique_session_id, :string, limit: 20
end
```
### リソースに機能を付与
```
devise :database_authenticatable, ..., :session_limitable
```

### ※herokuにアップする場合
herokuへのプッシュ後、

```
heroku rake db:migrate
heroku run rails c
> user = User.find(1)
> user.update_attribute(:admin_flg, true)
```

## 参照URL
https://qiita.com/umanoda/items/679419ce30d1996628ed
https://qiita.com/NZTK/items/7868b2897ff1c46ee338
https://qiita.com/shun-kusano/items/1a65040aae3e0db72294
https://qiita.com/7sgg_m/items/178e0d90fc55c5716474
https://pikawaka.com/rails/devise#devise%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%96%B9%E6%B3%95
https://qiita.com/tsukakei/items/7c4701270cacedcf5ab7
https://github.com/devise-security/devise-security