midPoint におけるマルチバリュー項目に対するマッピング処理

# はじめに

[midPoint by OpenStandia Advent Calendar 2024](https://qiita.com/advent-calendar/2024/midpoint-by-openstandia) の7日目は、[5日目](https://qiita.com/wadahiro/items/a239a743af5404a7325f)で説明しきれなかった下記の2つ目について、補足します。1つ目については[4日目の記事](https://qiita.com/wadahiro/items/5bcf320b752e696b1abb)で補足していますので、そちらを参照してください。

- 源泉削除に対する同期処理
- **マルチバリュー項目に対するマッピング処理**

:::note info
5日目で構築したDocker Composeを利用した環境が前提となります。
:::

# マルチバリュー項目に対するマッピング処理

[5日目の記事](https://qiita.com/wadahiro/items/a239a743af5404a7325f#%E5%80%A4%E3%81%AE%E6%9B%B4%E6%96%B0%E3%83%9E%E3%83%AB%E3%83%81%E3%83%90%E3%83%AA%E3%83%A5%E3%83%BC%E9%A0%85%E7%9B%AE)にて、マッピング先の属性がマルチバリュー項目の場合に、midPointでは安全のため他の値を消さない（洗い替えしない）という戦略をデフォルトで取っているとお伝えしました。これについて、midPointの公式ガイドでは、以下で述べられています。

- [Mappings: Replacing and Removing Values](https://docs.evolveum.com/midpoint/reference/support-4.8/expressions/practices/removing-values/)
- [Mapping Range](https://docs.evolveum.com/midpoint/reference/support-4.8/expressions/mappings/range/)
- [Range Of Inbound Mappings](https://docs.evolveum.com/midpoint/reference/support-4.8/expressions/mappings/inbound-mapping/#range-of-inbound-mappings)

特に、マルチバリュー項目に関しては、あるマッピング処理結果で生み出された値の反映だけすればよいか、もしくはその値以外の既存の値を削除、つまりすべて洗い替えるかは難しい問題です。マッピング先の属性を決定するのが、とある源泉データ1つであれば分かりやすいです。信頼できるソース（IGA/IDMの世界ではAuthoritative Sourceとよく呼びます）が1つなので、そこからマッピングされる値ですべて洗い替えても問題ないはずです。

しかし、現実にはそうではないケースがあります。例えば、管理者による画面操作で、その属性に値が追加されるケースも考えられます。他にも、別の源泉システムから同じ属性に値を設定するケースも考えられます。そうなると、単純に洗い替えてしまうと危険なわけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/0aa1f248-a247-e1dc-596d-51c8ce04f817.png)

midPointとしては、自動的に洗い替えるのではなくて、マッピングの設定者にどの値を削除するか細かくコントロールすることを求めています。そこで登場するのが、 **「Mapping Range」** です。これは、マッピングの際にどの範囲の値を削除するか制御するために使用される機能です。リソースの設定において、インバウンドマッピングでこのMapping Rangeの設定を行うと、このリソースからmidPointへのデータのマッピングの際に、どの値を削除する/しないを細かく制御することが可能になります。

なお、Mapping Rangeはリソースのインバウンドマッピングに限らず、以下のような他のマッピング設定箇所でも利用可能です。

- リソース設定のアウトバウンドマッピング
- Object Templateでのマッピング

特にアウトバウンドマッピングのケースでは、値の削除する/しないの処理はmidPoint側ではなく、プロビジョニング先になります。往々にしてリソース側で手動メンテされ値が追加されたりすることはありますので、そういった値を洗い替えて削除してしまうと事故になるわけで、midPointとしては慎重にデフォルトでは消さない、という戦略を取っています。

:::note info
実はmidPoint 4.9ではこのあたりの機能強化が行われ、自動的に判断して値を削除するモードが追加されました。[4.9のMapping Rangeのドキュメント](https://docs.evolveum.com/midpoint/reference/support-4.9/expressions/mappings/range/)をみると、Range Definitionに`matchingProvenance`という定義が追加されていることが分かります。これを利用すると、そのマッピングで設定された値をメタデータとして現在の値とは別に保存しておき、次回のマッピングで設定されなければ自動的に削除する、といった制御を行っているようです。
:::

## Mapping Rangeの設定

では、試しにMapping Rangeを設定し、5日目で問題となっていた主務組織と兼務組織で洗い替えを行うようにしてみます。

残念ながらMapping Rangeの設定は、midPoint 4.8/4.9 ではまだGUIサポートがされていません。よって、この設定はリソース設定のRaw更新で行う必要があります。HRシステムの源泉CSVのリソース編集画面を開き、「RAWデータの編集」ボタンをクリックしてRaw編集画面を開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/b002a835-cb4b-0ef5-fdb2-b55400136dfd.png)


`primaryOrgId`と`secondaruOrgIds`のインバウンドマッピング設定箇所を探し、`<target>`タグ配下に、以下の`<set>〜</set>`を挿入して「保存」ボタンをクリックします。`predefined`は事前定義された値で、`all`を指定するとそのマッピングで設定される値以外のものは削除する、つまり洗い替えるという動作になります。

```diff_xml
            <attribute id="15">
                <ref>ri:primaryOrgId</ref>
                <inbound id="16">
                    <strength>strong</strength>
                    <target>
                        <path>organization</path>
+                       <set>
+                           <predefined>all</predefined>
+                       </set>
                    </target>
                </inbound>
            </attribute>
            <attribute id="17">
                <ref>ri:secondaryOrgIds</ref>
                <inbound id="18">
                    <strength>strong</strength>
                    <target>
                        <path>organizationalUnit</path>
+                       <set>
+                           <predefined>all</predefined>
+                       </set>
                    </target>
                </inbound>
            </attribute>
```

## CSVの更新とインポートタスクの実行

では、[5日目では駄目だったケース](https://qiita.com/wadahiro/items/a239a743af5404a7325f#%E5%80%A4%E3%81%AE%E6%9B%B4%E6%96%B0%E3%83%9E%E3%83%AB%E3%83%81%E3%83%90%E3%83%AA%E3%83%A5%E3%83%BC%E9%A0%85%E7%9B%AE)を試してみます。

`1001`ユーザーが以下の状態のCSVをmidPointにインポートした状態から始めます。

```csvs
1001,太郎,山田,taro.yamada@example.com,005,006;007
```

`1001`の詳細画面をみると以下の状態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/ebae12f7-e6d5-2fa2-9faf-cc45a1bc9222.png)

では、CSVを以下のように編集します。


```csvs
1001,太郎,山田,taro.yamada@example.com,005,006;007
↓
1001,太郎,山田,taro.yamada@example.com,006,007;009
```

これで再度インポートタスクを実行します。すると今度は、CSVに記載の値で完全に入れ替わっています。期待通り洗い替えが行われました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/b99550cc-8185-5775-6c8b-a7dc44492016.png)


## 高度なRange Mapping設定

より高度な設定として、`predefined`を使わずにスクリプトを記述することで、何を削除するかロジック実装することができます。例として、先頭が`1`から始まる値の場合は削除対象外としてみます。

まずは現状の状態で、画面から`100`を`組織単位`に追加して保存してみます。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/e9328621-216a-799d-d54d-d47c5934aa59.png)をクリックすると入力項目が追加されるので、`100`を設定します。これで「保存」ボタンをクリックして保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/710e2084-ccf1-dba7-cf79-aa19d84e3446.png)

再度ユーザー詳細画面を開いても、まだ`100`は存在します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/a03156de-a120-b6c9-4aa3-5b6cf9fdd2e5.png)

この状態で、再度インポートタスクを実行します。すると想定どおり洗い替えが行われ、画面から入力した`100`は削除されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/c2c38074-78db-4cdb-ce82-b7a3a882e8c8.png)

では、再度リソース設定のRaw編集画面を開き、以下のように変更します。`predefined`は削除し、`<condition>〜</condition>`を追加します。このタグを使うと、条件をGroovyスクリプトで設定することができます。このスクリプトの評価結果では、boolean値を返すことが期待されています。その値により削除の挙動が決まります。

* `true`の場合：値を削除する
* `false`の場合：値を削除しない

スクリプト内では、`<path>organizationalUnit</path>`とパス定義したものを使って、その属性の値を1つずつその変数名称で参照することができます。今回、`!organizationalUnit?.startsWith("1")`と記述しているので、

* `0`から始まる値：`true` → 削除、つまり洗い替え対象
* `1`から始まる値：`false` → 削除しない、つまり洗い替え対象外

という挙動になります。

```diff_xml
            <attribute id="17">
                <ref>ri:secondaryOrgIds</ref>
                <inbound id="18">
                    <strength>strong</strength>
                    <target>
                        <path>organizationalUnit</path>
+                       <set>
-                           <predefined>all</predefined>
+                           <condition>
+                               <script>
+                                   <code>
+                                       !organizationalUnit?.startsWith("1")
+                                   </code>
+                               </script>
+                           </condition>
                        </set>
                    </target>
                </inbound>
            </attribute>
```

これで再度画面から`100`を追加して保存した上で、インポートタスクを再実行してみましょう。そうすると、今度は`100`が残り続けています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/ee9bfb60-b379-b4a5-cec8-f8d819d9c9bc.png)

本当に`1`から始まるもの以外は洗い替えられるか、またCSVファイルを編集して試してみます。元の所属組織の状態に戻してみます。

```csvs
1001,太郎,山田,taro.yamada@example.com,006,007;009
↓
1001,太郎,山田,taro.yamada@example.com,005,006;007
```

これで再度インポートタスクを実行します。すると結果は以下のようになります。`100`だけが残り、他の値は洗い替えが行われました。このように、`<condition>`タグを利用すると細かくRange Mappingを設定することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/3b54ad51-bf10-e6d0-7289-e07bbc49a7e8.png)


# まとめ

7日目は、5日目で説明しきれなかった **「マルチバリュー項目に対するマッピング処理」** について、補足解説を行いました。

midPointがなぜデフォルトでマルチバリュー項目の値をマッピング処理で洗い替えないか、解説しました。また、その挙動をカスタマイズする **「Range Mapping」** の設定例を紹介しました。

これでようやく次回は、組織データのインポートについて紹介したいと思います。組織データのインポートでは、6日目の記事で解説したリコンシリエーションタスクと、本記事で解説したRange Mappingの両方を使います。お楽しみに！