devise_ldap_authenticatable でグループ制限したい人生だった (Rails 4)

とあるアプリ (でLDAP認証を実現するために、 cschiewek/devise_ldap_authenticatable · GitHub を使っていて幸せに暮らしていたけど、ある日突然○○グループのみ認証を通したくなったので、設定ファイルをいじったら簡単にできるやろって思ったらできなかったので、ちょこっと頑張ったお話。

devise_ldap_authenticatable でグループ認証したい人向けのお話です

先に結論から

  • config/initializers/devise_ldap_authenticatable_customizer.rb を作成
  • config/ldap.yml を編集

で完了

gemのバージョンたち

net-ldap (0.5.1)
devise (3.1.1)
devise_ldap_authenticatable (0.8.1) # 現時点で rubygems 的に最新のgemのバージョン

モンキーパッチを当てる

config/initializers/devise_ldap_authenticatable_customizer.rb

module DeviseLdapAuthenticatableCustomizer

  begin
    class Railtie < ::Rails::Railtie

      config.after_initialize do

        Devise::LDAP::Connection.class_eval do

          def initialize(params = {})
            ldap_config = YAML.load(ERB.new(File.read(::Devise.ldap_config || "#{Rails.root}/config/ldap.yml")).result)[Rails.env]
            ldap_options = params
            ldap_config["ssl"] = :simple_tls if ldap_config["ssl"] === true
            ldap_options[:encryption] = ldap_config["ssl"].to_sym if ldap_config["ssl"]

            @ldap = Net::LDAP.new(ldap_options)
            @ldap.host = ldap_config["host"]
            @ldap.port = ldap_config["port"]
            @ldap.base = ldap_config["base"]
            @attribute = ldap_config["attribute"]
            @ldap_auth_username_builder = params[:ldap_auth_username_builder]

            @group_base = ldap_config["group_base"]
            @check_group_membership = ldap_config.has_key?("check_group_membership") ? ldap_config["check_group_membership"] : ::Devise.ldap_check_group_membership
            # この行を追加
            @check_group_membership_without_admin = ldap_config.has_key?("check_group_membership_without_admin") ? ldap_config["check_group_membership_without_admin"] : ::Devise.ldap_check_group_membership_without_admin
            @required_groups = ldap_config["required_groups"]
            @required_attributes = ldap_config["require_attribute"]

            @ldap.auth ldap_config["admin_user"], ldap_config["admin_password"] if params[:admin]

            @login = params[:login]
            @password = params[:password]
            @new_password = params[:new_password]
          end

          def in_group?(group_name, group_attribute = LDAP::DEFAULT_GROUP_UNIQUE_MEMBER_LIST_KEY)
            in_group = false

            if @check_group_membership_without_admin
              group_checking_ldap = @ldap
            else
              group_checking_ldap = Connection.admin
            end

            unless ::Devise.ldap_ad_group_check
              group_checking_ldap.search(:base => group_name, :scope => Net::LDAP::SearchScope_BaseObject) do |entry|
                #元のコード
                #if entry[group_attribute].include? dn
                if entry[group_attribute].include? dn.gsub(/uid=([^,]+),.*$/,'\1') 
                  in_group = true
                  DeviseLdapAuthenticatable::Logger.send("User #{dn} IS included in group: #{group_name}")
                end
              end
            else
              # AD optimization - extension will recursively check sub-groups with one query
              # "(memberof:1.2.840.113556.1.4.1941:=group_name)"
              search_result = group_checking_ldap.search(:base => dn,
                                :filter => Net::LDAP::Filter.ex("memberof:1.2.840.113556.1.4.1941", group_name),
                                :scope => Net::LDAP::SearchScope_BaseObject)
              # Will return  the user entry if belongs to group otherwise nothing
              if search_result.length == 1 && search_result[0].dn.eql?(dn)
                in_group = true
                DeviseLdapAuthenticatable::Logger.send("User #{dn} IS included in group: #{group_name}")
              end
            end

            unless in_group
              DeviseLdapAuthenticatable::Logger.send("User #{dn} is not in group: #{group_name}")
            end

            return in_group
          end

        end
      end
    end
  rescue Exception(err_msg)
    puts (" --- error => #{err_msg} --- ")
  end
end

LDAP設定ファイル

config/ldap.yml グループとかLDAPのホストとかは適宜変えてください

## Authorizations
# Uncomment out the merging for each environment that you'd like to include.
# You can also just copy and paste the tree (do not include the "authorizations") to each
# environment if you need something different per enviornment.
authorizations: &AUTHORIZATIONS
  group_base: ou=groups,dc=test,dc=com
  ## Requires config.ldap_check_group_membership in devise.rb be true
  # Can have multiple values, must match all to be authorized
  required_groups:
    # If only a group name is given, membership will be checked against "uniqueMember"
    - cn=admins,ou=groups,dc=test,dc=com
    - cn=users,ou=groups,dc=test,dc=com
    # If an array is given, the first element will be the attribute to check against, the second the group name
    - ["moreMembers", "cn=users,ou=groups,dc=test,dc=com"]
  ## Requires config.ldap_check_attributes in devise.rb to be true
  ## Can have multiple attributes and values, must match all to be authorized
  require_attribute:
    objectClass: inetOrgPerson
    authorizationRole: postsAdmin

## Environment

development:
  host: ldap.network.kani.dc
  port: 389
  attribute: uid
  base: ou=users,dc=kani,dc=co,dc=jp
  ssl: false
  group_base: ou=groups,dc=kani,dc=co,dc=jp
  check_group_membership: true
  check_group_membership_without_admin: true
  required_groups:
    - ["memberuid", "cn=hogegroup,ou=groups,dc=kani,dc=co,dc=jp"]

test:
  host: ldap.network.kani.dc
  port: 389
  attribute: uid
  base: ou=users,dc=kani,dc=co,dc=jp
  ssl: false

production:
  host: ldap.network.kani.dc
  port: 389
  attribute: uid
  base: ou=users,dc=kani,dc=co,dc=jp
  ssl: false
  group_base: ou=groups,dc=kani,dc=co,dc=jp
  check_group_membership: true
  check_group_membership_without_admin: true
  required_groups:
    - ["memberuid", "cn=hogegroup,ou=groups,dc=kani,dc=co,dc=jp"]

解説

LDAP認証の流れ

  1. 個人アカウントでLDAP認証してオブジェクト取得
  2. そのオブジェクトを使ってグループ認証

ざっくりこんな感じ

2 を行う際、ldap.yml に check_group_membership_without_admin がないと、グループ認証する際に、入力されたユーザーではなく admin のアカウントとパスワードでバインドしてしまうので、invalid bind みたいな怒られ方をします。

そんな設定なかった

で、config に書くんですが、rubygemsから落とせる最新版 (0.8.1) にはその設定がなく、githubのmasterブランチ (0.8.3) にはありました。

ただ、認証系のgemをmasterから取ってくるのはイヤだったので、0.8.1にモンキーパッチを当てることにしました。

ポイントは2か所

  1. Devise::LDAP::Connection の initialize で ldap.yml の設定を読んでいたので、check_group_membership_without_admin を読ませる

  2. グループに属しているかチェックしている in_group? メソッドで、uid で認証して欲しかったので、正規表現で切り取って認証

手を加えたのは Devise::LDAP::Connection の中の2行だけ

これでグループ認証があなたのもとへ、やったね!