midPoint by OpenStandia Advent Calendar 2024 の18日目は、15日目の記事で構築したActive Directory(以下、AD)互換環境であるSamba4に対して、ユーザーとグループ間のメンバーシップをプロビジョニングします。
既に、16日目と17日目の記事でそれぞれADへのユーザーとグループプロビジョニングについて解説済みです。
- 16日目「midPoint からActive Directoryにプロビジョニングする(ユーザー編)」
- 17日目「midPoint からActive Directoryにプロビジョニングする(グループ編)」
ただし、まだグループメンバシップのプロビジョニングはできていませんでした。現状、midPointのオブジェクトとADのオブジェクトは以下のような関係性でプロビジョニングされています。
- midPointのユーザーオブジェクト ADの
ou=Users,ou=IDM,dc=ad,dc=example,dc=com
配下にユーザーとしてプロビジョニング - midPointの組織オブジェクト ADの
ou=Groups,ou=IDM,dc=ad,dc=example,dc=com
配下にセキュリティグループとしてプロビジョニング
midPoint内においては、9日目の記事「midPoint にCSVの源泉データをインポートする(ユーザーと組織のアサイン編)」にて、ユーザーと組織をアサインで結び、関連付けていました。しかしながら、その関係性をADに対しては反映できていません。プロビジョニングしたADグループのメンバーは空のままです。
今回は、このmidPoint内のオブジェクトの関係性と連動して、ADに対してグループのメンバーシップをプロビジョニングするように設定を追加します。そこで登場するのが、midPointのアソシエーションです。
17日目までの環境が前提となります。
アソシエーションとは
アソシエーションは、midPointにおいてエンタイトルメント管理を実現するための重要な要素です。アソシエーションは、接続先システムにおけるアカウントとエンタイトルメント(グループやロールなど、権限を表現するもの)の関係性を表現するためのものです。公式ドキュメントでは以下のエンタイトルメントのページ内に記載があります。
どのオブジェクトタイプ同士に関係性があるのかをリソースの設定でアソシエーションとして定義しつつ、どのようにプロビジョニング先にマッピングするかアウトバウンドマッピングで指示することで、midPoint内でユーザーと組織といった関係性をアサインで設定するのと連動して、メンバーシップをプロビジョニングさせることが可能になります。
今回は解説しませんが、アソシエーションのインバウンドマッピングも可能です。例えば源泉システムがADだとした場合、ユーザーのグループへの所属情報を利用して、対応するmidPointの組織をアサインすることができます。
中々言葉で解説しても分かりづらいと思いますので、具体的な設定をこれから行い、動きを確認してみましょう。
リソース設定の修正
まずは、リソース設定のオブジェクトタイプ内にアソシエーションを設定します。
midPoint 4.9からは、アソシエーションはオブジェクトタイプ内ではなく、独立してオブジェクトタイプと同レベルで設定するように機能強化されています。本記事は2024年12月時点で最新LTSバージョンである4.8.5を前提として解説しているため、4.9とは異なる設定方法になっています。
今回はユーザーとグループ間のメンバーシップをプロビジョニングしたいので、ユーザーとグループ間のアソシーションを設定します。アソシエーションでは、サブジェクトとそれが属するオブジェクトの関係を定義します。設定はサブジェクト側のオブジェクトタイプ設定で実施する必要があります。つまり、今回の場合は、サブジェクトとなる「User」オブジェクトタイプの設定で行います。
グループとグループ間のアソシーションを設定すると、ADに対してグループとグループ間のメンバーシップをプロビジョニングすることも可能です。いわゆる、グループのネストに対応できます。これについては、明日の記事で解説したいと思います。
アソシエーションの設定(ユーザーとグループ間)
「AD」リソースの詳細画面より「スキーマ処理」メニューを開き、既に作成済みのオブジェクトタイプである「User」をクリックして設定画面を開きます。
オブジェクトタイプのウィザード画面にて「アソシエーション」をクリックします。
アソシエーションリストの画面が開きます。「アソシエーションの追加」ボタンをクリックします。
行が追加されますので、アソシエーションの関係性の定義を行います(緑色箇所)。
サブジェクトは、今開いているオブジェクトタイプ「User」側になりますので、この行ではユーザーが属するオブジェクトタイプは何とするか指定します。今回はグループのオブジェクトタイプとして「Group」を設定していますので、そちらを示すようにします。この時、ユーザーとグループでADのどの属性を使って関係性を表現するのかも、設定します。
以下の値を設定します。
-
参照:
group
1 -
種類:
エンタイトルメント
-
用途:
default
-
方向:
オブジェクトからサブジェクト
-
アソシエーション属性:
member
-
値属性:
dn
方向、アソシエーション属性、値属性 は、接続先リソース(とコネクター)の仕様に合わせて設定する必要があります。ADの場合、グループメンバーシップの情報は、ユーザー側では管理していません。グループ側のオブジェクトのmember
属性で管理しており、メンバーの出し入れはグループの更新で行います。また、そのmember
属性の値にはユーザーのdn
値が格納されています。よってADの場合は、上記のように設定します。
一方、AD以外のその他のコネクターの場合は、ユーザー側の属性でグループメンバーシップを管理しているケースがあります。その場合は、方向にはサブジェクトからオブジェクト
を設定し、アソシエーション属性にはユーザー側のその属性名(所属グループを示す情報を格納している属性名)、値属性にはそのアソシーション属性に格納されている値が、グループ側のどの属性値なのかを設定することになります。
また、をクリックしてこの行の詳細設定画面を開きます。追加で以下を設定し「実行」ボタンをクリックします。
-
アソシエーション属性のショートカット:
memberOf
-
値属性のショートカット:
dn
-
明示的な参照整合性:
False
接続先リソース(とコネクター)によっては、方向がオブジェクトからサブジェクト
の場合に、サブジェクトが属するグループ情報の一覧をユーザー属性から一気に取得するためのショートカットを用意しているケースがあります。今回使用するAD(とADコネクター)がまさにそのタイプです。
ADのユーザーは、そのユーザーが所属するグループの一覧を示すリードオンリー属性として、memberOf
を提供しています。よって、 アソシエーション属性のショートカットにはmemberOf
を設定します。
また、memberOf
には、グループのdn
値が格納されていますので、値属性のショートカットにはdn
を設定します。
そしてmemberOf
の値は、グループに対してメンバーの出し入れを行うと、自動的に計算され整合性が維持されます。そのような場合は、明示的な参照整合性はFalse
とします。整合性の維持が自動で行われない場合は、True
とするとmidPointが自身で更新を行い、整合性を維持しようとします。
ショートカットが用意されている場合は、活用するとユーザーの所属グループ一覧をフェッチする処理が最適化されるため、処理効率がよいという利点があります。
元のアソシエーションリスト画面に戻るので、「Association設定を保存」ボタンをクリックして保存します。
アソシーションのアウトバウンドマッピングの設定
リソース設定でアソシーションを設定したら、midPointはプロジェクションでそのアソシエーションを認識します。例えば、ユーザーのADのプロジェクションの詳細を開くと、アソシーションという項目で「group」2が存在します。
後はそれをプロビジョニングするためにアウトバウンドマッピングを設定します。このアウトバウンドマッピングは、プロビジョニングを指示するリソースアサイン内で設定します。これまで構築した設定では、プロビジョニングはアーキタイプを経由してリソースアサインを間接アサインして実施しています。つまり、リソースアサインの設定はアーキタイプ内で書かれています。
アーキタイプは現状、「Employee」と「Company Org」の2つのアーキタイプが存在します。ユーザーとグループ間のアソシエーションのプロビジョニングのタイミングを考えると、midPoint上で対応するユーザーに組織がアサインされたタイミングで実施されるとよさそうです。
組織のアサインをトリガーとしてアソシエーションをプロビジョニングすればよいので、今回は「Company Org」アーキタイプに設定を追加します。
Company Orgアーキタイプの設定
「Company Org」アーキタイプの詳細画面を開き、「インデュースメント > リソース」メニューを開きます。現状、このアーキタイプを適用した組織をADグループとしてプロビジョニングするための設定が実施済みです。
今回は、このアーキタイプを適用した組織をユーザーにアサインしたタイミングで、ADユーザーとしてプロビジョニングする際に、アソシエーションのアウトバウンドマッピングを適用してグループメンバーシップのプロビジョニングを行うための設定を追加します。ユーザーから見ると、組織をアサインし、組織はアーキタイプをアサインし、アーキタイプはリソースをインデュースメントで設定、という関係性です。文字で書くと分かりづらいかと思いますので、少し絵で補足します。
現状は以下の状態です。「Company Org」アーキタイプにインデュースメントで設定したADリソース(種類:エンタイトルメント、用途:default)を組織に間接的に適用し、ADグループをプロビジョニングしています。また、「Employee」アーキタイプにインデュースメントで設定したADリソース(種類:アカウント、用途:default)をユーザーに間接的に適用し、ADユーザーをプロビジョニングしています。
ここで、「Company Org」アーキタイプにもう1つADリソース(種類:アカウント、用途:default)へのインデュースメントを追加することで、組織のアサインを経由して末端のユーザーにアソシエーションのアウトバウンドマッピングを適用し、ADにグループメンバーシップをプロビジョニングします(紫色で加筆した部分)。ユーザーと「Company Org」アーキタイプとの間に存在する組織に適用するのではなく、1つ飛ばしてユーザーに対して間接適用させているのがポイントです。それを実現するために、「order」 という設定を利用します。
orderは、アサインまたはインデュースメントの設定時に指定可能なパラメータの一つで、デフォルトでは1
です。これは、アサインの適用先はパス1つ分先である、という意味です。このorderを2
にすると、アサインの適用先はパス2つ分先で処理されます。今、「Company Org」アーキタイプからADリソース(種類:アカウント、用途:default)へのインデュースメントをorder=2で設定しているので、「Company Org」アーキタイプの適用先は、組織をスキップしてその先に位置するユーザーに適用されるわけです。orderを活用することで、アサインで繋げたオブジェクト群に対して、離れたところに位置するオブジェクトを対象としてプロビジョニングなどの処理を適用することができます。
orderを利用した制御については、公式ドキュメントでは以下に記載があります。また、より強力なorderConstraintというパラメータも存在します。
https://docs.evolveum.com/midpoint/reference/support-4.8/roles-policies/metaroles/gensync/
orderConstraintを利用すると、アサインで構成された組織ツリーに対して、「ある特定の組織配下から子、孫など末端組織までも含めた所属ユーザー」や「ある特定の組織〜任意の階層までの組織やユーザー」に対してプロビジョニングを適用するといった、高度な処理を簡潔に表現できます。他のIDM/IGA製品でこのorderやorderConstraintに相当するものは見たことがなく、初めてこの機能を知った時に衝撃を受けました。
使いこなすにはmidPointのアサインとインデュースメントの特性を頭に入れつつ、オブジェクト間の関連を注意深く設計する必要があり、難易度は高いかもしれませんが、チャレンジする価値はあると考えています。
では、「Company Org」アーキタイプに対してADリソース(種類:アカウント、用途:default)のインデュースメントを設定します。インデュースメント追加の画面を開きます。
アプリケーション・リソースの選択では、「AD」を選択して先に進みます。
「種類:アカウント、用途:default」選択して先に進みます。
リソース設定でアソシエーションを設定していると、エンタイトルメントの画面で参照設定可能なShadowオブジェクトのリストが表示されます。今回は特定のグループに限定してアソシエーションを設定したいわけではないので、ここは何も指定せず、先に進みます。
アウトバウンドマッピングの設定が表示されますが、残念ながGUIでアソシエーションのアウトバウンドマッピングは設定できませんので、何も設定せず「設定を保存」ボタンをクリックして一旦保存します。
アーキタイプの詳細画面に戻ります。ここで「RAWデータの編集」ボタンをクリックしてRAW編集画面を表示します。
ADリソース(種類:アカウント、用途:default)へのインデュースメント設定箇所を探し、以下のように追記して保存します。
<inducement id="8">
<construction>
+ <strength>weak</strength>
<resourceRef oid="..." relation="org:default" type="c:ResourceType">
<!-- AD -->
</resourceRef>
<kind>account</kind>
<intent>default</intent>
+ <association>
+ <ref>ri:group</ref>
+ <outbound>
+ <expression>
+ <associationFromLink>
+ <projectionDiscriminator>
+ <kind>entitlement</kind>
+ <intent>default</intent>
+ </projectionDiscriminator>
+ </associationFromLink>
+ </expression>
+ </outbound>
+ </association>
</construction>
+ <focusType>c:UserType</focusType>
+ <order>2</order>
</inducement>
XMLで直接設定した各設定値の補足をしておきます。なお、<association>〜</association>
以外で追加した部分は、GUIでも設定可能ですが便宜上、一緒にXML記述で設定しています。
-
<strength>weak</strength>
:ADリソースに対してユーザープロビジョニングはweakマッピングで適用します。これは、ユーザーがまだADユーザーとしてプロビジョニングされていない場合に、評価しないことを示しています。ADユーザーとしてのプロビジョニングは「Employee」アーキタイプに任せていますので、weakにすることで、「Employee」アーキタイプを適用していないユーザーに組織をアサインしても、プロビジョニングしないようになります。 -
<association>...</association>
:アソシーションのアウトバウンドマッピングです。<associationFromLink>
という専用のタグを使って、ユーザーのアサイン先オブジェクト(今回だと組織)のプロジェクション情報を取得して、所属先となるADグループのShadowオブジェクトを自動的に特定します。<projectionDiscriminator>
で設定したkind(種類)とintent(用途)に一致するプロジェクションが最終的には採用されます。この設定は、<order>2</order>
とセットで使われます3。 -
<focusType>c:UserType</focusType>
:インデュースメントによる間接適用の対象オブジェクトタイプをユーザーに限定します。組織の親子関係はアサインで行われますので、未設定だと子組織や孫組織などに対してもこのインデュースメントが適用されて無駄に処理されたり、意図せぬ動作を引き起こす恐れがありますので、明示的にユーザーに限定しています。 -
<order>2</order>
:先程のorderの説明箇所を参照してください。
プロビジョニングの確認
例によって、まずはユーザーを1件リコンサイルのプレビューを実施して、設定が正しいかどうか確認します。問題なければ保存し、ADに想定通りプロビジョニングされたかどうか確認します。最後にHRシステムからのインポートタスクを実行して、今回設定したアソシエーションのアウトバウンドマッピングを再評価させ、残りのユーザーのグループメンバーシップもプロビジョニングします。
1件リコンサイルによる確認
「1001」ユーザーの詳細画面を開き、リコンサイルのプレビューを確認します。このユーザーは3つの組織に所属していますので、3つのセキュリティグループに所属するようにADにプロビジョニングされます。内容を確認し、想定通りのADグループ情報がプレビューに表示されていたら、保存します。
保存後、該当ユーザーのADのプロジェクション詳細を確認してみます。すると、ADの「memberOf」属性に所属する3つのグループのDNが設定されていることが分かります。
また、画面下にスクロールすると、アソシーションとして3つ設定されていることも分かります。
インポートタスクによる一括プロビジョニング
最後に、「HR import users」タスクを実施してマッピングを再評価させ、残りのユーザーにも適用します。タスク実行完了後、ADにLDAP検索してユーザーにメンバーシップがプロビジョニングされているかmemberOf
属性を参照して確認してみましょう。docker compose exec ad bash -c "ldapsearch -D cn=Administrator,cn=Users,dc=ad,dc=example,dc=com -w p@ssw0rd -b ou=Users,ou=IDM,dc=ad,dc=example,dc=com memberOf"
とコンソールより実行します。
以下のようにCN=1001〜CN=1010のユーザーのmemberOf
が設定されていることが分かります。
$ docker compose exec ad bash -c "ldapsearch -D cn=Administrator,cn=Users,dc=ad,dc=example,dc=com -w p@ssw0rd -b ou=Users,ou=IDM,dc=ad,dc=example,dc=com memberOf"
# extended LDIF
#
# LDAPv3
# base <ou=Users,ou=IDM,dc=ad,dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: memberOf
#
# 1008, Users, IDM, ad.example.com
dn: CN=1008,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=007,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
# 1005, Users, IDM, ad.example.com
dn: CN=1005,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=008,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=009,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
# 1010, Users, IDM, ad.example.com
dn: CN=1010,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=006,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=009,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
# 1002, Users, IDM, ad.example.com
dn: CN=1002,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=006,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=005,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=009,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
# 1001, Users, IDM, ad.example.com
dn: CN=1001,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=006,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=005,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=007,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
# 1009, Users, IDM, ad.example.com
dn: CN=1009,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=005,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=007,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=008,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
# 1004, Users, IDM, ad.example.com
dn: CN=1004,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=009,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=008,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
# 1003, Users, IDM, ad.example.com
dn: CN=1003,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=006,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=007,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
# Users, IDM, ad.example.com
dn: OU=Users,OU=IDM,DC=ad,DC=example,DC=com
# 1006, Users, IDM, ad.example.com
dn: CN=1006,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=005,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
# 1007, Users, IDM, ad.example.com
dn: CN=1007,OU=Users,OU=IDM,DC=ad,DC=example,DC=com
memberOf: CN=006,OU=Groups,OU=IDM,DC=ad,DC=example,DC=com
# search result
search: 2
result: 0 Success
# numResponses: 12
# numEntries: 11
グループ側も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"
とコンソールより実行します。
以下のようにグループのmember
が設定されていることが分かります。
$ 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"
# extended LDIF
#
# LDAPv3
# base <ou=Groups,ou=IDM,dc=ad,dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: member
#
# 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
# 003, Groups, IDM, ad.example.com
dn: CN=003,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
# 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
# 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
# 006, Groups, IDM, ad.example.com
dn: CN=006,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=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
# 002, Groups, IDM, ad.example.com
dn: CN=002,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
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=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
# 004, Groups, IDM, ad.example.com
dn: CN=004,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ではエンタイトルメント管理をアソシエーションというもので汎用的に扱う仕組みになっていることが分かったかと思います。今回はADでしたが、他のリソースでエンタイトルメント管理がサポートされており、コネクターも対応していれば、同じような設定をすることでmidPointを中心としたエンタイトルメント管理が実現可能です。
次回は、グループとグループ間のアソシーションも設定し、ADに対してネストグループをプロビジョニングするようにします。ADを運用されている組織では、ネストグループを活用されているケースも多いと思いますので、このパターンについても是非紹介したいと思います。
-
この値は任意ですが、慣例として
group
と定義することが多いです。ここで定義した参照名を使ってアウトバウンドマッピングを設定します。 ↩ -
アソシーションリストの追加で設定した参照の値です。 ↩
-
もし、
<order>3</order>
で適用したい場合は別途<assignmentPathIndex>1</assignmentPathIndex>
も設定する必要があります。以下の公式ドキュメントに例が記載されています。
https://docs.evolveum.com/midpoint/reference/support-4.8/expressions/expressions/#association-from-link ↩