Help us understand the problem. What is going on with this article?

Go言語からActive Directoryにアクセスしてみる(登録編)

More than 3 years have passed since last update.

はじめに

前回投稿の続きで、今度はActive Directoryにエントリ(ユーザーアカウント)を追加してみたいと思います。

前提条件

Active Directoryを外部からLDAP経由で操作する場合、ユーザーパスワード(unicodePwd)周りについて、以下のような制約があります。

  • パスワードを設定/変更する場合、LDAP over SSL(LDAPS)で接続する必要があります。
  • パスワードが未設定の場合、アカウントオプション(userAccountControl)を設定することは出来ません。
  • ダブルクォーテーション付のパスフレーズを、UTF-16(リトルエンディアン、BOM無)に変換して設定する必要があります。

また、AD側のLDAPS設定方法ですが、今回は投稿内容の趣旨から若干逸れますので省略します。

エントリ内のユーザーアカウント確認

サンプルコードを書く前に、dsquery コマンドを実行して既存のユーザー情報を覗いてみます。

>dsquery * "cn=user01,cn=Users,dc=mydomain,dc=local", -attr *
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
cn: user01
description: ユーザ01
distinguishedName: CN=user01,CN=Users,DC=mydomain,DC=local
instanceType: 4
whenCreated: 12/09/2005 08:15:38
whenChanged: 06/27/2016 10:19:58
displayName: ユーザ01
uSNCreated: 8298
memberOf: CN=Users,CN=Builtin,DC=mydomain,DC=local
uSNChanged: 8298
name: user01
objectGUID: {3844A2B1-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
userAccountControl: 66048
badPwdCount: 0
codePage: 0
countryCode: 0
badPasswordTime: 131368051478279796
lastLogon: 131402372151448047
pwdLastSet: 127785897385156250
primaryGroupID: 513
objectSid: S-1-5-21-2910603885-XXXXXXXXXX-XXXXXXXXXX-XXXX
accountExpires: 9223372036854775807
logonCount: 58
sAMAccountName: user01
sAMAccountType: 805306368
userPrincipalName: user01@mydomain.local
objectCategory: CN=Person,CN=Schema,CN=Configuration,DC=mydomain,DC=local
dSCorePropagationData: 01/01/1601 00:00:00
ADsPath: LDAP://DC1.mydomain.local/CN=user01,CN=Users,DC=mydomain,DC=local

上記のように、指定したユーザー(DN: cn=user01,cn=Users,dc=mydomain,dc=local)の属性情報が、LDIFファイル形式で表示されました。
この属性の中から、最低限必要と思われる属性のみを指定して、新しくユーザーを登録してみたいと思います。

サンプルコード

あくまでもサンプルコードですので、「動けば良い」レベルで書いています。

このコードでは、ドメインmydomain.localtestuserというユーザーアカウントを作成しています。

ldapadd.go
package main

import (
    "fmt"
    "log"
    "gopkg.in/ldap.v2"
    "golang.org/x/text/encoding/unicode"
    "crypto/tls"
)

const (
    LDAPSV = "dc1.mydomain.local"
    PROTO  = "tcp"
    PORTNO = 636
    BINDDN = "cn=Administrator,cn=Users,dc=mydomain,dc=local"
    BINDPW = "XXXXXXXX"
)

func main() {
    /*
    LDAPサーバへ接続 (LDAPS)
      >InsecureSkipVerify=trueにより、SSLがオレオレ証明書でも接続可能
    */
    tlsConfig := &tls.Config{InsecureSkipVerify: true}
    l, err := ldap.DialTLS(PROTO, fmt.Sprintf("%s:%d", LDAPSV, PORTNO), tlsConfig)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Ldap server connected.")
    defer func() {
        l.Close()
        fmt.Println("Ldap server disconnected.")
    }()

    //LDAPサーバ認証(バインド)
    err = l.Bind(BINDDN, BINDPW)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Ldap server logged in.")

    /*
    ユーザーパスワード変換(ASCII => UTF16LE)
      >パスワード(ダブルクォーテーション付)をUTF16リトルエンディアン(BOM無)に変換
    */
    utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
    u16pass, err := utf16.NewEncoder().String("\"p@ssw0rd\"")
    if err != nil {
        log.Fatal(err)
    }

    //追加リクエスト作成
    addRequest := ldap.NewAddRequest("cn=testuser,cn=Users,dc=mydomain,dc=local")
    addRequest.Attribute("cn", []string{"testuser"})
    addRequest.Attribute("displayName", []string{"テストユーザ"})
    addRequest.Attribute("objectCategory", []string{"CN=Person,CN=Schema,CN=Configuration,DC=mydomain,DC=local"})
    addRequest.Attribute("objectClass", []string{"top", "person", "organizationalPerson", "user"})
    addRequest.Attribute("sAMAccountName", []string{"testuser"})
    addRequest.Attribute("userPrincipalName", []string{"testuser@mydomain.local"})
    addRequest.Attribute("unicodePwd", []string{u16pass})
    addRequest.Attribute("userAccountControl", []string{"66048"})

    //追加リクエストを基にエントリ追加
    err = l.Add(addRequest)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Ldap server entry added.")
}

多少汚いコード(!)ですが、何をやっているかはお解り頂けるかと思います。

軽く解説

LDAPサーバへの接続

前回の検索編はLDAPでの接続でしたが、今回は予めADサーバをLDAPSに対応するよう設定し、LDAP over SSL(port=636)で接続しています。
TLS設定(tlsConfig)でInsecureSkipVerify: trueとあるのは、サーバ証明書に「オレオレ証明書」を使っていることによる証明書エラーを回避する為です。

    tlsConfig := &tls.Config{InsecureSkipVerify: true}
    l, err := ldap.DialTLS(PROTO, fmt.Sprintf("%s:%d", LDAPSV, PORTNO), tlsConfig)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Ldap server connected.")
    defer func() {
        l.Close()
        fmt.Println("Ldap server disconnected.")
    }()

ユーザーパスワード変換

この箇所で"p@ssw0rd"というASCII文字列を、UTF-16LE(BOM無)に変換する処理を行っています。
余談ですが、WindowsでUnicodeというと、UTF-16LEを指すそうな。

    utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
    u16pass, err := utf16.NewEncoder().String("\"p@ssw0rd\"")
    if err != nil {
        log.Fatal(err)
    }

追加リクエスト作成

まずはDN(cn=testuser,cn=Users,dc=mydomain,dc=local)を引数に、リクエストで用いるインスタンスを作成し、その後必要な属性値をひとつずつセットしています。
尚、アカウント制御の属性(userAccountControl)についてはこちら等を参考に、環境に合わせて変更する必要があります。
ちなみに今回指定した10進66048ですが、16進で表すと0x10200となります。

    addRequest := ldap.NewAddRequest("cn=testuser,cn=Users,dc=mydomain,dc=local")
    addRequest.Attribute("cn", []string{"testuser"})
    addRequest.Attribute("displayName", []string{"テストユーザ"})
    addRequest.Attribute("objectCategory", []string{"CN=Person,CN=Schema,CN=Configuration,DC=mydomain,DC=local"})
    addRequest.Attribute("objectClass", []string{"top", "person", "organizationalPerson", "user"})
    addRequest.Attribute("sAMAccountName", []string{"testuser"})
    addRequest.Attribute("userPrincipalName", []string{"testuser@mydomain.local"})
    addRequest.Attribute("unicodePwd", []string{u16pass})
    addRequest.Attribute("userAccountControl", []string{"66048"})

サンプルコードでは、属性情報をハードコーディングしていますが、実際にはLDIF、CSV等のテキストファイルやRDBMSを読み込み、順次処理するようなロジックを書くことになると思います。

実行してみる

サンプルコードを実行すると、エラーが無ければ以下のようなメッセージとともに正常終了します。

>go run ldapadd.go
Ldap server connected.
Ldap server logged in.
Ldap server entry added.
Ldap server disconnected.

確認してみる

ADサーバ側で dsqueryコマンドを実行して、testuser が無事に登録されていることを確認します。

>dsquery * "cn=testuser,cn=Users,dc=mydomain,dc=local" -attr *
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
cn: testuser
distinguishedName: CN=testuser,CN=Users,DC=mydomain,DC=local
instanceType: 4
whenCreated: 06/23/2017 03:02:24
whenChanged: 06/23/2017 03:08:55
displayName: テストユーザ
uSNCreated: 24753
uSNChanged: 24757
name: testuser
objectGUID: {D7844598-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
userAccountControl: 66048
badPwdCount: 0
codePage: 0
countryCode: 0
badPasswordTime: 0
lastLogoff: 0
lastLogon: 0
pwdLastSet: 131426605443450970
primaryGroupID: 513
objectSid: S-1-5-21-2873641411-XXXXXXXXXX-XXXXXXXXXX-XXXX
accountExpires: 9223372036854775807
logonCount: 0
sAMAccountName: testuser
sAMAccountType: 805306368
userPrincipalName: testuser@mydomain.local
objectCategory: CN=Person,CN=Schema,CN=Configuration,DC=mydomain,DC=local
dSCorePropagationData: 01/01/1601 00:00:00
lastLogonTimestamp: 131426609359530970
ADsPath: LDAP://DC1.mydomain.local/CN=testuser,CN=Users,DC=mydomain,DC=local

もちろん、AD管理ツールからも確認できます。
account.png

nagase
田舎のインフラ屋です。 最近はフルスタック屋です。 アラフィフのオッサンです。 良く使うもの:AWS、GCP、コンテナ、Python、IoT
https://kahomusen-holdings.co.jp
gooday
北部九州を中心に展開するホームセンター「GooDay(グッデイ)」の運営
https://www.gooday.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away