【Java】ActiveDirectoryの参照/更新

# はじめに
ADのユーザーをメンテナンスする機能を提供するため、ユーザー一覧を取得したり、更新できるようにしたい。
JavaではJNDIを使ってADへアクセスできる。

# ADの参照

## 単一ユーザーの取得
AD認証などで、ユーザーIDを指定して検索する場合は、下記URL参照。

https://www.earthlink.co.jp/engineerblog/intra-mart-engineerblog/3336/

## ユーザー一覧の取得
特定のOUに所属するユーザーの一覧など、結果が複数件あることが予想される検索の場合、LDAPのページサイズを意識する必要がある。
WindowsのActive Directoryの場合、おそらく1,000件が結果の最大サイズ。
それを超える件数を取得したい場合は、ページングをしながら検索を繰り返す。

```java:ページングしながらユーザー一覧を取得するサンプル
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://server"); // ADサーバーのアドレス
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "userid@domain"); // ADサーバーにログオンするためのユーザーID@ドメイン
env.put(Context.SECURITY_CREDENTIALS, "password"); // パスワード

final int PAGE_SIZE = 1000;

InitialLdapContext context = null;
try {
	context = new InitialLdapContext(env, null);

	// 検索条件の指定
	String name = "OU=aaa, DC=bbb, DC=ccc";
	String filter = "(objectCategory=user)";

	SearchControls control = new SearchControls();
	control.setSearchScope(SearchControls.SUBTREE_SCOPE);

	byte[] cookie = null;

	do {
		context.setRequestControls(new Control[] { new PagedResultsControl(PAGE_SIZE, cookie,
				Control.CRITICAL) });

		// 検索実行
		NamingEnumeration<SearchResult> results = context.search(name, filter, control);

		// 結果取得
		while (results.hasMore()) {
			SearchResult result = results.next();
			AdUserBean user = new AdUserBean(result);

			System.out.println(user);
		}

		// 次のページがあるかチェック
		cookie = null;

		if (context.getResponseControls() != null) {
			Optional<Control> found = Arrays.stream(context.getResponseControls())
					.filter(c -> c instanceof PagedResultsResponseControl).findFirst();

			if (found.isPresent()) {
				PagedResultsResponseControl prrc = (PagedResultsResponseControl) found.get();
				cookie = prrc.getCookie();
			}
		}

	} while (cookie != null);

} finally {
	try {
		if (context != null) {
			context.close();
		}
	} catch (NamingException e) {
	}
}
```

```java:AdUserBean
public class AdUserBean {

	private String cn = null;
	private String name = null;
	/** 名前 */
	private String displayName = null;
	/** ユーザーID＋ドメイン */
	private String userPrincipalName = null;
	/** LDAP識別名 */
	private String distinguishedName = null;
	/** メールアドレス */
	private String mail = null;
	/** ポケットベル */
	private String pager = null;

	/**
	 * コンストラクタ
	 */
	public AdUserBean() {

	}


	/**
	 * コンストラクタ
	 *
	 * Active Directory検索結果から内容を取得する
	 *
	 * @param result
	 * @throws NamingException
	 */
	public AdUserBean(SearchResult result) throws NamingException {
		NamingEnumeration<? extends Attribute> attributes = result.getAttributes().getAll();

		while (attributes.hasMoreElements()) {
			Attribute attribute = attributes.next();
			Object value = attribute.get();

			if ("name".equals(attribute.getID())) {
				if (value instanceof String) {
					name = (String) value;
				}

			} else if ("displayName".equals(attribute.getID())) {
				if (value instanceof String) {
					displayName = (String) value;
				}

			} else if ("cn".equals(attribute.getID())) {
				if (value instanceof String) {
					cn = (String) value;
				}

			} else if ("userPrincipalName".equals(attribute.getID())) {
				if (value instanceof String) {
					userPrincipalName = (String) value;
				}

			} else if ("pager".equals(attribute.getID())) {
				if (value instanceof String) {
					pager = (String) value;
				}

			} else if ("mail".equals(attribute.getID())) {
				if (value instanceof String) {
					mail = (String) value;
				}

			} else if ("distinguishedName".equals(attribute.getID())) {
				if (value instanceof String) {
					distinguishedName = (String) value;
				}

			}
		}

	}

// 以下略
}

```

## `hasMore`と`hasMoreElements`の違い
`search()`メソッドの戻り値である`NamingEnumeration`クラスには、普通の`Enumeration`クラスにある`hasMoreElements`メソッドと、独自の`hasMore`メソッドの2種類がある。

```java
while (results.hasMore()) {
	SearchResult result = results.next();
	// 略
}
```

`hasMore`メソッドを使うと、例えばページサイズを超えた件数を取得しようとしたときに`NamingException`がスローされる。
https://docs.oracle.com/javase/jp/11/docs/api/java.naming/javax/naming/NamingEnumeration.html

ただし、先のサンプルのようにページングしながらADを検索する場合は、コード側で指定したページサイズ＞サーバー側のページサイズ となっていてもエラーにならず、サーバー側のページサイズで検索結果が返される。

## 属性値の取り方
Active Directoryの属性には、単一の値を持つ項目と複数の値を持つ項目がある。
それぞれ取得方法が異なるので注意。

```java:属性値の取得部分を抜粋
/** ポケットベル */
private String pager = null;
/** ポケットベル(その他) */
private List<String> otherPager = null;

public AdUserBean(SearchResult result) throws NamingException {
	NamingEnumeration<? extends Attribute> attributes = result.getAttributes().getAll();

	while (attributes.hasMoreElements()) {
		Attribute attribute = attributes.next();
		Object value = attribute.get();

		if ("pager".equals(attribute.getID())) {
			// ポケットベル(単一の項目)
			if (value instanceof String) {
				pager = (String) value;
			}

		} else if ("otherPager".equals(attribute.getID())) {
			// ポケットベル(その他)　(複数の項目)
			NamingEnumeration<?> values = attribute.getAll();

			while (values.hasMore()) {
				if (value instanceof String) {
					if (otherPager == null) {
						otherPager = new ArrayList<String>();
					}

					value = values.next();

					otherPager.add((String) value);
				}
			}

		}
	}
}
```

# ユーザー属性の変更

```java:ユーザー属性の変更
private static void update() throws NamingException {

	Hashtable<String, String> env = new Hashtable<String, String>();
	env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
	env.put(Context.PROVIDER_URL, "ldap://server"); // ADサーバーのアドレス
	env.put(Context.SECURITY_AUTHENTICATION, "simple");
	env.put(Context.SECURITY_PRINCIPAL, "user@domain"); // ADサーバーにログオンするためのユーザーID@ドメイン
	env.put(Context.SECURITY_CREDENTIALS, "password"); // パスワード

	DirContext context = null;
	try {
		context = new InitialDirContext(env);

		Attributes attrs = new BasicAttributes();
		attrs.put(new BasicAttribute("pager", "123456"));
		attrs.put(new BasicAttribute("telephoneNumber", "1234567"));

		String name = "CN=username, OU=xxx, DC=yyy, DC=zzz";

		context.modifyAttributes(name, DirContext.REPLACE_ATTRIBUTE, attrs);

	} finally {
		try {
			if (context != null) {
				context.close();
			}
		} catch (NamingException e) {
		}
	}
}
```

`modifyAttributes`の引数に指定するDNは、そのユーザー(オブジェクト)が属するOUなどを正確に指定しないといけない。OUの階層を省略したりすると、「オブジェクトが見つからない」とエラーになる。


# 属性や検索フィルターの参考

メールアドレスは単体項目だが、電話番号は複数指定可能な項目。

Active Directory のユーザーの属性についての説明(他にも属性の種類ごとに説明ページが分かれている)
https://docs.microsoft.com/en-us/windows/win32/ad/naming-properties

Active Directory の属性一覧
https://docs.microsoft.com/en-us/windows/win32/adschema/attributes-all

LDAP の検索フィルター構文
http://software.fujitsu.com/jp/manual/manualfiles/M050000/B1WN4911/01/idmgr07/idmgr447.htm
https://www.ibm.com/support/knowledgecenter/ja/SSYJ99_8.5.0/admin-system/rbug_ldapfltrxprns.html