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

#はじめに
[前回投稿](http://qiita.com/nagase/items/38ba71b710a0a6926950)の続きで、今度は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: ユーザ０１
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: ユーザ０１
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.local`に`testuser`というユーザーアカウントを作成しています。

```go: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)については[こちら](https://support.microsoft.com/ja-jp/help/305144/how-to-use-the-useraccountcontrol-flags-to-manipulate-user-account-properties)等を参考に、環境に合わせて変更する必要があります。
ちなみに今回指定した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](https://qiita-image-store.s3.amazonaws.com/0/94476/74f0f90b-cbfb-f08d-0329-14d3f54b06a0.png)