Ruby + ActiveLdapでActiveDirectoryにいるユーザーの所属グループを取得する

Ruby + ActiveLdapでActiveDirectoryにLDAP接続し、ユーザーの所属グループを取得することがあったので、その時のメモを残します。  

環境

また、ActiveLdapの書き方については公式チュートリアルがとても参考になりました。
TutorialJa - ruby-activeldap
 

ActiveDirectoryの構成

ドメインの直下にユーザーhogehogeおよびOUhogeを置いて、OUhogeの中にユーザーfuga_logonを入れました*1

example.local
| |
| `--hoge (OU)
|     `--fuga_logon (User)
`--fugafuga (User)

 
ユーザー情報については以下の通りです。

ログオンアカウント名 フルネーム(CN) 所属するグループ
fuga_logon fuga_cn hogehogeとpiyopiyo
fugafuga ふがふが hogehoge

 
また、以下を参考にLDAPインタフェースへのアクセスをログに残すようにしました。

 

Gemfile

activeldapだけでは動作しないため、LDAPクライアントとしてnet-ldapを使いました。

source 'https://rubygems.org'

gem 'activeldap'
gem 'net-ldap'

 
 

ActiveDirectoryへの接続設定

デフォルトではActiveDirectoryに対する匿名接続は許可されていないため、ActiveDirectoryのLDAPにBindするアカウント名をfugafugaとして接続するようにします。
なお、これ以降の例でも、ActiveLdap::Base.setup_connectionは同じ内容です。

require 'active_ldap'


HOST_IP_V4 = '192.168.0.1'
BIND_USER_CN = 'ふがふが'
BIND_USER_OU = ''
BIND_USER_PASSWORD = 'fugafuga'


def create_ou_string
  BIND_USER_OU.empty? ? '' : "ou=#{BIND_USER_OU},"
end


# ENV['USERDNSDOMAIN']には、example.localという形で入っている
domain = ENV['USERDNSDOMAIN'].split('.')
base = "dc=#{domain[0]},dc=#{domain[1]}"
bind_user = "cn=#{BIND_USER_CN},#{create_ou_string}#{base}"

# 今回の範囲内では、Bindするユーザーは「Domain users」グループでも構わない
ActiveLdap::Base.setup_connection host: HOST_IP_V4,
                                  port: 636,
                                  method: 'ssl',
                                  base: base,
                                  bind_dn: bind_user,
                                  password: BIND_USER_PASSWORD

 
ENV['USERDNSDOMAIN']でログインしている端末のドメインが取得できるので、それを元にbaseで使う文字列を作成します。なお、Windowsで設定されているENVの一覧は、以下のコードで調べました。

ENV.each { |env| p env}

 
ldapsを利用するため、ポートを636、methodを ssl として設定します。
Sinatra + ActiveLDAPで簡易LDAP管理インターフェースを作った - ~nabeken/diary/

 
bind_dnに指定する「接続時にBindするユーザー情報」は、ドメインコントローラーなどでldp.exeを利用して確認します。
第3回 LDAPを使ってActive Directoryを制御しよう[その1:ldpとcsvde] - 知られざるActive Directory技術の「舞台裏」

   

ldap_mapping

ActiveLdap::Baseを継承したクラスにて、 ldap_mappingメソッドを使ってマッピングをします。

class User < ActiveLdap::Base
  ldap_mapping dn_attribute: "cn", prefix: ''
end

 
dn_attributeとprefixの関係については、Rubyist Magazine27号の記事中の図が参考になりました。
ActiveLdap を使ってみよう(前編) クラス定義と ldap_mapping - Rubyist Magazine27号

また、prefixが不要な場合であっても、prefixパラメータには空文字を渡します。 Re: ActiveLdap questions - Forum: Ruby

 
 

ActiveDirectoryからユーザー情報を抽出

example.localドメイン内のユーザー情報を抽出

find()メソッドの引数として、ActiveDirectoryユーザーのフルネーム(CN)を指定します*2

class User < ActiveLdap::Base
  ldap_mapping dn_attribute: "cn", prefix: ''
end

p User.find('fuga').dn
p User.find('ふがふが').dn

 
とすると、以下のような結果が得られます。

#<ActiveLdap::DistinguishedName:0x27b28c0 @rdns=[{"cn"=>"fuga"}, {"OU"=>"hoge"}, {"dc"=>"EXAMPLE"}, {"dc"=>"local"}]>
 
#<ActiveLdap::DistinguishedName:0x27730b0 @rdns=[{"cn"=>"\u3075\u304C\u3075\u304C"}, {"dc"=>"EXAMPLE"}, {"dc"=>"local"}]>

 
なお、ユーザーを作成時にフルネーム欄へ指定した値がCNと表示名(displayName)として設定されるため、何もしなければCNと表示名は同じ値になっているかと思います。このあたりの詳細は以下のサイトに記載がありました。

 

特定のOU以下のユーザー情報を抽出 (例:OUhoge以下)
class OuUser  < ActiveLdap::Base
  ldap_mapping dn_attribute: "cn", prefix: "ou=hoge"
end

p OuUser.find('fuga').dn
p OuUser.find('ふがふが').dn

とすると、

#<ActiveLdap::DistinguishedName:0x43342b0 @rdns=[{"cn"=>"fuga"}, {"ou"=>"hoge"}, {"dc"=>"EXAMPLE"}, {"dc"=>"local"}]>
 
path/to/vendor/bundle/ruby/2.0.0/gems/activeldap-4.0.3/lib/active_ldap/operations.rb:347:in `find_one': Couldn't find OuUser: DN: 縺オ縺後・縺・ filter: ["cn", "\u3075\u304C\u3075\u304C"] (ActiveLdap::EntryNotFound)

のように、OUhoge以下に存在しないユーザーについては例外が発生します。

 

ActiveDirectoryユーザーのユーザーログオン名で検索する場合

filterオプションにsAMAccountNameを使用した条件を渡します。 なお、同一ドメイン内ではユーザーログオン名は重複しないと考え、:firstを指定しています。

class LogonUser < ActiveLdap::Base
  ldap_mapping dn_attribute: "cn", prefix: ''
end
logon_user = 'fuga'
p LogonUser.find(:first, filter: "(sAMAccountName=#{logon_user})").dn

 
とすると、

#<ActiveLdap::DistinguishedName:0x2761728 @rdns=[{"cn"=>"fuga"}, {"OU"=>"hoge"}, {"dc"=>"EXAMPLE"}, {"dc"=>"local"}]>

と、結果が返ってきます。

sAMAccountName以外にfilterオプションとして使えそうなものは、ldp.exeを使ったり、以下の記事の「オブジェクトを一括登録する場合のCSVファイルの属性」を指定してみたりします。
第8回 Active Directoryの導入後の作業 (2/3) - @IT 管理者のためのActive Directory入門

また、ActiveLdapでのfilterオプションの書き方は、以下が参考になりました。
ActiveLdap::Base#find の :filter オプション - tashenの日記

 

ユーザーの所属するグループを取得する

ユーザーの所属するグループは、find()メソッドの戻り値のmemberOf以下に入っていますので、次のコードで確認できます。

class User < ActiveLdap::Base
  ldap_mapping dn_attribute: "cn", prefix: ''
end

user = User.find('fuga')
p user.memberOf
p user.memberOf[0].rdns[0]['CN']

 
結果は、

[#<ActiveLdap::DistinguishedName:0x4428e28 @rdns=[{"CN"=>"hogehoge"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>, #<ActiveLdap::DistinguishedName:0x4428690 @rdns=[{"CN"=>"piyo"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>]
 
"hogehoge"

となります。

そのため、「ユーザーがあるグループに所属しているかどうか」は次のコードで調べられます*3

class LdapUser < ActiveLdap::Base
  # OU'hoge'内に制限するなら、prefixに 'hoge' を渡す
  ldap_mapping dn_attribute: 'cn', prefix: ''

  def member?(group)
    # 見つからない場合、Array#indexはnilを返すのを利用
    result = self.memberOf.index do |m|
               m.rdns.index { |r| r.has_value?(group) }
             end

    !result.nil?
  end
end


# ドメイン内ではユーザーログオン名は一意になるため、 :firstを指定しておく
# CNで探す場合
user1 = LdapUser.find(:first, 'fuga_cn')

p user1.memberOf

if user1.member?('hogehoge')
  puts 'CNで検索し、所属を確認しました'
else
  puts 'CNで検索し、所属を確認できませんでした'
end


# ログオン名で探す場合
user2 = LdapUser.find(:first, filter: '(sAMAccountName=fuga_logon)')

p user2.memberOf

if user2.member?('hogehoge')
  puts 'ユーザーログオン名で検索し、所属を確認しました'
else
  puts 'ユーザーログオン名で検索し、所属を確認できませんでした'
end

 
結果は、

[#<ActiveLdap::DistinguishedName:0x4351110 @rdns=[{"CN"=>"piyopiyo"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>, #<ActiveLdap::DistinguishedName:0x4350978 @rdns=[{"CN"=>"hogehoge"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>]
 
CNで検索し、所属を確認しました
 
[#<ActiveLdap::DistinguishedName:0x43783b0 @rdns=[{"CN"=>"piyopiyo"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>, #<ActiveLdap::DistinguishedName:0x4383bd0 @rdns=[{"CN"=>"hogehoge"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>]
 
ユーザーログオン名で検索し、所属を確認しました

となり、所属を確認することができました。
 
 

ソースコード

所属を確認する部分とGemfileはGistに上げました。
ActriveLdapのサンプルコード

 

参考資料

ActiveLdap関連

 

ActiveDirectory / LDAP関連

*1:直下にユーザーを置くのはあまりよい構成ではない気がします

*2:最初、引数はユーザーログオン名だと思っていて軽くハマりました

*3:より良い書き方がありそうですが...