OpenLDAP が抱えるパスワードを C# & .NET を使って変更してみる。

# 目的
OpenLDAP が抱えるパスワードを変更するときは ldappasswd を使うわけだが、「自作プログラムからそれをやるときに、OpenLDAP for Windows に同梱されているコマンドをどうしても叩きたくない！」という偏屈者のための記事。

「簡単にできるやろ」と高を括ってたが、半日くらいかかってしまったんで記事にする。

# とりあえずコード全体

```cs
using System;
using System.DirectoryServices.Protocols;
using System.Net;
using System.Text.RegularExpressions;

namespace LdapPasswdLib
{
    public static class LdapPasswordChanger
    {
        public const int LDAPS_DEFAULT_PORT = 636;

        public const string LDAP_EXOP_MODIFY_PASSWD = "1.3.6.1.4.1.4203.1.11.1";
        public const int LDAP_TAG_EXOP_MODIFY_PASSWD_ID = 0x80;
        public const int LDAP_TAG_EXOP_MODIFY_PASSWD_OLD = 0x81;
        public const int LDAP_TAG_EXOP_MODIFY_PASSWD_NEW = 0x82;

        /// <summary>
        /// LDAP でパスワードを変更する。
        /// </summary>
        /// <param name="accountDN">アカウントの DN</param>
        /// <param name="oldPasswd">現在のパスワード</param>
        /// <param name="newPasswd">変更後のパスワード</param>
        /// <param name="ldapServer">LDAP サーバーのホスト名または IP アドレス</param>
        /// <param name="isTls">LDAPS にする場合 true。LDAP のままにする場合 false。</param>
        public static void ChangePassword(string accountDN, string oldPasswd, string newPasswd, string ldapServer, bool isTls)
        {
            if (accountDN == null || oldPasswd == null || newPasswd == null || ldapServer == null)
                    throw new ArgumentNullException();

            if (accountDN.Length < 1 || oldPasswd.Length < 1 || newPasswd.Length < 1 || ldapServer.Length < 1)
                    throw new ArgumentException();

            ldapServer = SpecifyServerPort(ldapServer, isTls);

            LdapConnection ldapConnection = new LdapConnection(ldapServer)
            {
                Credential = new NetworkCredential(accountDN, oldPasswd),
                AuthType = AuthType.Basic,
                Timeout = new TimeSpan(0, 0, 10)
            };
            ldapConnection.SessionOptions.ProtocolVersion = 3;
            ldapConnection.SessionOptions.SecureSocketLayer = isTls;

            // https://tools.ietf.org/html/rfc3062
            var ber = BerConverter.Encode("{tststs}",
                    LDAP_TAG_EXOP_MODIFY_PASSWD_ID, accountDN,
                    LDAP_TAG_EXOP_MODIFY_PASSWD_OLD, oldPasswd,
                    LDAP_TAG_EXOP_MODIFY_PASSWD_NEW, newPasswd);
            var modifyPasswdRequest = new ExtendedRequest(LDAP_EXOP_MODIFY_PASSWD, ber);
            try
            {
                // パスワードを変更するユーザーでバインドする
                ldapConnection.Bind();

                // パスワード変更要求を送信
                ldapConnection.SendRequest(modifyPasswdRequest);

                // 応答が「成功」か確認
                if (modifyPasswdResponse.ResultCode != ResultCode.Success)
                        throw new Exception("Could not change password. (" + modifyPasswdResponse.ResultCode + ")");
            }
            finally
            {
                if (ldapConnection != null)
                {
                    try
                    {
                        ldapConnection.Dispose();
                    }
                    catch (Exception e)
                    {
                        System.Diagnostics.Debug.WriteLine(e);
                    }
                }
            }
        }

        /// <summary>
        /// TCP ポート指定なし、かつ、LDAPS のときはポート指定を追加する。
        /// </summary>
        /// <param name="ldapServer">LDAP サーバー指定</param>
        private static string SpecifyServerPort(string ldapServer, bool isTls)
        {
            if (isTls && !Regex.IsMatch(ldapServer, ":[1-9][0-9]*$")) return ldapServer + ":" + LDAPS_DEFAULT_PORT;
            else return ldapServer;
        }
    }
}
```

# 肝はここ

苦労したのはどこかというと、ここ。

```cs
var ber = BerConverter.Encode("{tststs}",
        LDAP_TAG_EXOP_MODIFY_PASSWD_ID, accountDN,
        LDAP_TAG_EXOP_MODIFY_PASSWD_OLD, oldPasswd,
        LDAP_TAG_EXOP_MODIFY_PASSWD_NEW, newPasswd);
var modifyPasswdRequest = new ExtendedRequest(LDAP_EXOP_MODIFY_PASSWD, ber);
```

`BerConverter` と `ExtendedRequest` の公式ドキュメントには、当然使用例の記載がなく (一見さんお断り感)。一番参考になったのは、[xUnit で書かれた BerConverter のテスト](https://sourcelens.com.au/Consulting/Brw/Ru/z_dir_zcrez_dir_zsrcz_dir_zcorefxz_dir_zsrcz_dir_zSystemq_dot_qDirectoryServicesq_dot_qProtocolsz_dir_ztestsz_dir_zBerConverterTestsq_dot_qcs)。ExtendedRequest の使い方は勘… (一応、[stackoverflow に情報はあった](https://stackoverflow.com/questions/1001369/what-can-you-do-with-system-directoryservices-protocols-extendedrequest))

あと、ここで使ってる定数 `LDAP_TAG_～` は、[openldap のソース コード](https://github.com/openldap/openldap)のヘッダーから頂いてきた。

識者が読めば判るのかもだが、俺は [RFC 3062](https://tools.ietf.org/html/rfc3062) を読んだだけではこのコードが書ける気はしない…。結局、openldap の ldappasswd のコードを移植しただけ。

# 結論

皆は ldappasswd コマンドを使おう。