midPoint からActive Directoryにプロビジョニングする（ネストグループ編）

[midPoint by OpenStandia Advent Calendar 2024](https://qiita.com/advent-calendar/2024/midpoint-by-openstandia) の19日目は、[15日目の記事](https://qiita.com/wadahiro/items/d2f5822c8fbb2e883333)で構築したActive Directory（以下、AD）互換環境であるSamba4に対して、ネストグループをプロビジョニングします。

既に、16日目と17日目の記事でそれぞれADへのユーザーとグループプロビジョニングについて解説済みです。

- [16日目「midPoint からActive Directoryにプロビジョニングする（ユーザー編）」](https://qiita.com/wadahiro/private/b5c9fa2d69c5cae0a8b9)
- [17日目「midPoint からActive Directoryにプロビジョニングする（グループ編）」](https://qiita.com/wadahiro/private/662f7e9f993094f0b83f)

そして昨日の18日目の記事で、ユーザーとグループ間のメンバーシップのプロビジョニングについても解説済みです。

- [18日目「midPoint からActive Directoryにプロビジョニングする（グループメンバーシップ編）」](https://qiita.com/wadahiro/private/1d08f72a974566f42069)

今回は、グループとグループ間のメンバーシップのプロビジョニング、つまり**ネストグループ**のプロビジョニングについて解説します。

# ネストグループとは

始めに、ADのネストグループについて補足しておきます。ADではグループの`member`属性に別のグループの`dn`値を格納することで、グループのネストを表現できます。グループはあるOU配下にフラットに存在したとしても、`member`属性で関係性を結んでおくことで、ネスト構造を表現できるようになっています。また、`member`に設定された子グループ側は、親グループの`dn`値をリードオンリーの`memberOf`属性で持ち、相互に辿れるようになっています。

このようにグループをネスト構造にしておくことで、LDAP検索時に[LDAP_MATCHING_RULE_IN_CHAINというExtensible Match Rule](https://ldapwiki.com/wiki/Wiki.jsp?page=LDAP_MATCHING_RULE_IN_CHAIN)を指定すると、ネストグループ内を再帰的に検索することもできます。ADを参照するアプリケーションによっては、この機能を利用して特定グループ配下のサブグループも含めて何らかの処理対象にすることができるようになっています。もちろん、LDAP_MATCHING_RULE_IN_CHAINを使わなくても、LDAP検索を再帰的に繰り返して階層を辿っていくことも可能です。

一方、midPointは組織オブジェクトは親組織への参照をアサインで持っており、組織ツリーを表現できます。そのツリー構成を、ADのネストグループとしてプロビジョニングするのが今回のテーマです。下図は、midPoint内の組織情報と連携先であるADのグループの関係性を示したものです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/a63c08c0-d9b1-325d-a1ec-b8ec39278836.png)

AD側のグループとグループ間の関連に注目すると、結局のところ[昨日の記事](https://qiita.com/wadahiro/private/1d08f72a974566f42069)と同じで、単にグループの`member`属性に属するメンバーの`dn`値を入れているに過ぎません。つまり、midPoint的にはこれは、**グループとグループ間のアソシエーションを設定し、そのアソシエーションのアウトバウンドマッピングを設定する**というだけであり、昨日実施したユーザーとグループ間の設定とほとんど同じです。

では早速、アソシエーションの設定を行っていきます。

# アソシエーションの設定（グループとグループ間）

グループとグループ間のアソシーションを設定します。サブジェクトとなる「Group」オブジェクトタイプの設定でアソシーションを設定します。

「AD」リソースの詳細画面より「スキーマ処理」メニューを開き、既に作成済みのオブジェクトタイプである「Group」をクリックして設定画面を開き、ユーザーとグループ間と同様にアソシーションを設定していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/e75945cd-504d-aebb-331a-935018f733e4.png)


設定手順は[昨日の記事](https://qiita.com/wadahiro/private/1d08f72a974566f42069)と同じなので、設定値だけここでは示します。ADにおけるグループのネストは、ユーザーとグループと同じで、単に`member`と`memberOf`属性で関係性を表現しているだけなので、設定値として同じになります。

アソシーションリストでは、以下の値を設定します。


- **参照**：`group`
- **種類**：`エンタイトルメント`
- **用途**：`default`
- **方向**：`オブジェクトからサブジェクト`
- **アソシエーション属性**：`member`
- **値属性**：`dn`


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/473ed1ad-121e-17c3-2997-677a44caf1ec.png)

詳細設定では以下を追加設定します。

- **アソシエーション属性のショートカット**：`memberOf`
- **値属性のショートカット**：`dn`
- **明示的な参照整合性**：`False`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/b2912c72-906d-18e7-566d-d2fd02833d50.png)


# アソシーションのアウトバウンドマッピングの設定

グループとグループ間のアソシエーションを設定したら、後はそのアソシエーションのアウトバウンドマッピングを設定するだけです。[昨日の記事](https://qiita.com/wadahiro/private/1d08f72a974566f42069)のユーザーとグループ間のプロビジョニングと同様に、midPoint上で対応する組織と組織間がアサインで結ばれた（親子関係が作られた）タイミングで実施するとよさそうです。よって、昨日実施したユーザーとグループ間と同じ考え方で、**「Company Org」アーキタイプからorder=2でADリソース（種類：エンタイトルメント、用途：default）にインデュースメントを設定**することで実現します（紫色で加筆した部分）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/386d105e-edaf-4313-4103-bde9c1cf77c4.png)


しかしながら、昨日と同じ手順では画面からリソースに対するインデュースメント設定の追加ができません。どうも、既に「Comapny Org」アーキタイプでADリソース（種類：エンタイトルメント、用途：default）に対してインデュースメントが存在するため、同じADリソース（種類：エンタイトルメント、用途：default）に対するインデュースメントの追加に対応していないようです。

回避策として、「インデュースメント ＞ インデュース・エンタイトルメント」メニューを開いてこちらから設定します。この画面は（恐らく）リソースのインデュースメント画面が刷新されるまで使われていた、レガシーな設定UIと思われます。既に設定済みの、ADリソース（種類：アカウント、用途：default）へのアソシエーションのアウトバウンドマッピング付きインデュースメントのみが表示されています（アソシエーション設定のない他のインデュースメントはこの画面では非表示となります）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/2fb26859-f9ea-6831-55fd-8d56f162fc6b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/a2d43a02-88c5-6785-1717-62cede4063ed.png)をクリックして、「オブジェクトの選択」ダイアログを表示します。「AD」にチェックを入れて、以下のようにパラメータを設定して「追加」ボタンをクリックします。

- **種類**：`エンタイトルメント`
- **用途**：`default`
- **アソシエーション**：`ref: group`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/1e4fe6c6-b581-37ee-f6bf-6947912eb925.png)

行が追加されます（緑色の箇所）。ここで一旦「保存」ボタンをクリックして保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/e5375e9a-ea92-0fef-62ee-005a993b4d34.png)


再度「Company Org」アーキタイプの編集画面を開き、「RAWデータの編集」ボタンをクリックしてRAW編集画面を表示します。アソシエーションのアウトバウンドマッピングは現状、XMLで直接設定する必要があります。ADリソース（種類：エンタイトルメント、用途：default）に対し`<association>〜</association>`タグが設定されているインデュースメント設定箇所を探し、以下のように追記して保存します。

```diff_xml
    <inducement id="...">
        <construction>
+           <strength>weak</strength>
            <resourceRef oid="..." relation="org:default" type="c:ResourceType">
                <!-- AD -->
            </resourceRef>
            <kind>entitlement</kind>
            <intent>default</intent>
            <association id="...">
                <ref>group</ref>
+               <outbound>
+                   <expression>
+                       <associationFromLink>
+                           <projectionDiscriminator>
+                               <kind>entitlement</kind>
+                               <intent>default</intent>
+                           </projectionDiscriminator>
+                       </associationFromLink>
+                   </expression>
+               </outbound>
            </association>
        </construction>
+       <focusType>c:OrgType</focusType>
+       <order>2</order>
    </inducement>
```

`<focusType>c:OrgType</focusType>`として、**組織オブジェクトに限定**している点に注意してください。これで保存します。


# プロビジョニングの確認

例によって、まずは組織を1件リコンサイルのプレビューを実施して、設定が正しいかどうか確認します。問題なければ保存し、ADに想定通りプロビジョニングされたかどうか確認します。最後にHRシステムから組織の取り込みようタスクである、「HR recon orgs」リコンシリエーションタスクを実行して、今回設定したアソシエーションのアウトバウンドマッピングを再評価させ、残りのネストグループもプロビジョニングします。


## 1件リコンサイルによる確認

「001」組織はルート組織であり、親を持たないのでネストグループのプロビジョニングは発生しません。その子組織である「002」組織の詳細画面を開き、リコンサイルのプレビューを確認します。この組織は「001」組織を親に持ちますので、「001」ADグループのメンバーとして所属するようにADにプロビジョニングされます。内容を確認し、想定通りのADグループ情報がプレビューに表示されていたら、保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/09e70b73-7f61-7f56-a50e-b244632216dc.png)

保存後、該当組織のADのプロジェクション詳細を確認してみます。すると、ADの「memberOf」属性に親グループのDNが設定されていることが分かります。また、アソシーションも設定されていることも分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/e123cb66-c7fe-8834-7207-2c4bdd99eb22.png)

## リコンシリエーションタスクによる一括プロビジョニング

最後に、「HR recon orgs」リコンシリエーションタスクを実行してマッピングを再評価させ、残りの組織にも適用します。

タスク実行完了後、ADにLDAP検索してネストグループがプロビジョニングされているか確認してみましょう。`docker compose exec ad bash -c "ldapsearch -D cn=Administrator,cn=Users,dc=ad,dc=example,dc=com -w p@ssw0rd -b ou=Groups,ou=IDM,dc=ad,dc=example,dc=com member memberOf"`コマンドをコンソールより実行します。

以下のようにグループのmemberとmemberOfにグループのDN値が設定されていることが分かります（ユーザーもメンバーに混ざっているので注意）。

```console
$ docker compose exec ad bash -c "ldapsearch -D cn=Administrator,cn=Users,dc=ad,dc=example,dc=com -w p@ssw0rd -b ou=Groups,ou=IDM,dc=ad,dc=example,dc=com member memberOf"
# extended LDIF
#
# LDAPv3
# base <ou=Groups,ou=IDM,dc=ad,dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: member memberOf
#

# 008, Groups, IDM, ad.example.com
dn: CN=008,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1005,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1009,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1004,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=004,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com

# 003, Groups, IDM, ad.example.com
dn: CN=003,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=001,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=007,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com

# 009, Groups, IDM, ad.example.com
dn: CN=009,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1002,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1005,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1010,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1004,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=004,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com

# Groups, IDM, ad.example.com
dn: OU=Groups,OU=IDM,DC=ad,DC=example,DC=com

# 001, Groups, IDM, ad.example.com
dn: CN=001,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=002,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=003,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=004,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com

# 007, Groups, IDM, ad.example.com
dn: CN=007,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1009,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1003,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1001,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1008,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=003,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com

# 006, Groups, IDM, ad.example.com
dn: CN=006,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=002,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1002,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=011,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1003,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1010,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1007,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1001,OU=Users,OU=IDM,DC=ad,DC=example,DC=com

# 011, Groups, IDM, ad.example.com
dn: CN=011,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=006,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com

# 002, Groups, IDM, ad.example.com
dn: CN=002,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=001,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=006,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=005,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com

# 005, Groups, IDM, ad.example.com
dn: CN=005,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=002,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1002,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1009,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=010,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1006,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
member: CN=1001,OU=Users,OU=IDM,DC=ad,DC=example,DC=com

# 010, Groups, IDM, ad.example.com
dn: CN=010,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=005,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com

# 004, Groups, IDM, ad.example.com
dn: CN=004,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=001,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=009,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
member: CN=008,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com

# search result
search: 2
result: 0 Success

# numResponses: 13
# numEntries: 12
```



# まとめ

18日目では、midPointからADへのネストグループのプロビジョニング設定について解説しました。midPointではエンタイトルメント管理をアソシエーションというもので汎用的に扱う仕組みになっているため、ネストグループに関しても同様の設定を適用することで実現できることが分かったかと思います。

次回は、マルチアカウントという少し高度な機能を紹介したいと思います。名前の通り、midPointの1オブジェクトに対して接続先システム側で複数のアカウントを管理するための機能ですが、これはアカウントに限定されるわけではなく、ADでいうとグループにも適用できます。例えば、1つの組織に対応してプロビジョニングするADグループを複数定義することも可能になります。源泉システムから取り込まれた組織情報に対して、連動して作成したいADグループが複数あることはよくあるユースケースですので、是非紹介したいと思います。