Help us understand the problem. What is going on with this article?

midPointの「マッピング」におけるスクリプトの利用とPrismデータ構造

はじめに

midPoint by OpenStandia Advent Calendar 2019 の22日目は、midPointにおける「マッピング」でスクリプトを利用する際、最初に戸惑いやすいポイントについて説明します。
実際にマッピングを利用し始めて、スクリプトにより複雑な値の生成を試みようとしている人に向けて、事前に知っておくと開発の助けになるような予備知識を提供できれば、と思います。

※記事の内容やリンク先は、記事公開時点でのmidPointバージョンを基にしています。

基本情報
バージョン 4.1-SNAPSHOT
Git describe v4.1devel-139-g4aaf38bf26
ビルド時刻 Wed, 27 Nov 2019 03:38:18 +0000

※設定例の「…」は省略を示します。

マッピングとは?

ID管理システムの中心となるのが同期ですが、その頭脳がマッピングです。

マッピングは、midPointを介して多くの場所で使用され、ソースプロパティの値をターゲットプロパティにマッピングするメカニズムです。

参考)Mapping

マッピング定義は、3つの基本的な部分で構成されます。

構成部分 定義内容
source(ソース) データを取得する場所を定義、入力変数のマッピング
expression(式) データをどのように 変換/生成/コピー するかを定義
target(ターゲット) 結果をどのように処理するか、出力先を定義

synchronization-mapping.png

図の引用元:Mapping

マッピングの適用箇所

前回までの記事でもマッピングは登場してきていましたが、改めて、その適用箇所を把握してみましょう。

以下に、同期の全体像を見渡すことができる図があります。初見ではちょっと面食らうかもしれませんが、今回は「mapping」という箇所にだけ注目してみてください。
Synchronization Policies

インバウンド・フェーズ

synchronization-phase-inbound.png
図の引用元:Synchronization Policies

ユーザー・ポリシー・フェーズ

synchronization-phase-user.png
図の引用元:Synchronization Policies

アウトバウンド・フェーズ

synchronization-phase-outbound.png
図の引用元:Synchronization Policies

主として利用するのは、以下の3か所になると思います。

同期フェーズ 設定箇所 設定
インバウンド リソース定義schemaHandling inbound
ユーザー・ポリシー オブジェクト・テンプレート mapping
アウトバウンド リソース定義schemaHandling outbound

assignment の accountConstruction にも outbound mapping が書ける のですが「Do not overuse(使いすぎないこと)」とあり、例外的な場合に使用する設定です。
※ 図にありませんが、assignment には、オブジェクト・テンプレートに加えて適用されるマッピングのセットを指定できる focusMappingsもあります。

マッピングの簡単な例

マッピングの簡単な例として、「リソースAのアカウント ⇒ ユーザー ⇒ リソースBのアカウント」の例を以下に示します。

(画面からは触れない設定項目を編集したり、設定ファイルをバージョン管理したりするなど、実際の開発ではXML等で記述された設定を直接編集することが多くなるでしょう。以下、設定をXMLで記載します。)

リソースAのアカウント(firstname、lastname)からユーザー(givenName、familyName)への inbound
リソースA
<resource …>
    <name>Resource A</name>
    …
    <schemaHandling>
        <objectType>
            <objectClass>ri:AccountObjectClass</objectClass>
            …
            <attribute>
                <ref>ri:firstname</ref>
                <inbound>
                    <strength>strong</strength>
                    <target>
                        <path>givenName</path>
                    </target>
                </inbound>
            </attribute>

            <attribute>
                <ref>ri:lastname</ref>
                <inbound>
                    <strength>strong</strength>
                    <target>
                        <path>familyName</path>
                    </target>
                </inbound>
            </attribute>
            …
ユーザーの(givenName、familyName)から(fullName)を設定する mapping
オブジェクト・テンプレート
<objectTemplate …>
    <name>User Template</name>
    …
    <mapping>
        <strength>strong</strength>
        <source>
            <path>givenName</path>
        </source>
        <source>
            <path>familyName</path>
        </source>
        <expression>
            <script>
                <code>
                    givenName + ' ' + familyName
                </code>
            </script>
        </expression>
        <target>
            <path>fullName</path>
        </target>
    </mapping>
    …
ユーザー(fullName)からリソースBのアカウント(fullName)への outbound
リソースB
<resource …>
    <name>Resource B</name>
    …
    <schemaHandling>
        <objectType>
            <objectClass>ri:AccountObjectClass</objectClass>
            …
            <attribute>
                <ref>ri:fullName</ref>
                <outbound>
                    <strength>strong</strength>
                    <source>
                        <path>fullName</path>
                    </source>
                </outbound>
            </attribute>
            …

inbound / mapping / outbound の違い

アイテム名が異なっていますが、どれも MappingType(またはそのサブタイプ)なので仕組みは同じです。

入力元(source)と出力先(target)の指定に違いがあります。
inbound / outbound は、それが設定されているリソース・オブジェクトの属性が source / target なので、属性の指定は不要です。

マッピング source target
inbound attributeで固定※
mapping
outbound attributeで固定

参考)Source and Target Definitions

(※ 1つの attribute に複数の inbound を設定したり、inbound に attribute 以外の source を追加することもできます。Type定義を見ると分かりますが、inbound および source の Multiplicity は [0,-1] で、複数設定が可能です。)

マッピングにおけるスクリプトの利用と、最初に戸惑いやすいポイントについて

マッピングの設定についての詳細は、以下に説明があります。
Mapping

strength(強さ)

マッピングがどの程度の強さで適用されるかを指定するのが、strength(強さ)です。
参考)Mapping Strength

ここではstrong(強い)として、常に適用するようにしています。

expression(式)

マッピングにおいて、値を生成するのが expression(式)です。
参考)Expression

デフォルトは AsIs で、省略した場合は入力値がそのまま出力にコピーされます。先の設定例「リソースA」「リソースB」の inboundとoutbound における expression は AsIs です。
参考)AsIs

expression にスクリプトを記述することで、複雑な変換も可能となります。
先の設定例「オブジェクト・テンプレート」の mapping における expression が Scriptです。
参考)Script

スクリプトの利用において、最初に戸惑いやすいポイントについて、いくつか説明していきます。

その1)PolyString

mapping の expression のスクリプト(言語のデフォルトはGroovy)を、以下のように変更したとします。

ユーザーの(givenName、familyName)から(fullName)を設定する mapping

オブジェクト・テンプレート
    <mapping>
        <strength>strong</strength>
        <source>
            <path>givenName</path>
        </source>
        <source>
            <path>familyName</path>
        </source>
        <expression>
            <script>
                <code>
                    if ('Alice' == givenName) {
                        retturn 'アリス ' + familyName
                    }
                    givenName + ' ' + familyName
                </code>
            </script>
        </expression>
        <target>
            <path>fullName</path>
        </target>
    </mapping>

givenNameがAliceの場合、if文の中に入ることを期待しているようですが、この条件がtrueになることはありません。原因を調べるため、givenName をログに出力してみましょう。
参考)Logging Library

オブジェクト・テンプレート
                <code>
                    log.info('{}, {}', givenName, givenName.class)
                    …
ログ出力
… Alice, class com.evolveum.midpoint.prism.polystring.PolyString

値は Alice のようですが、class が PolyString となっています。
参考)PolyString.java

PolyString.java
public class PolyString implements Matchable<PolyString>, Recomputable, Structured, DebugDumpable, ShortDumpable, Serializable, Comparable<Object> {

PolyStringはString派生型ではないため、Stringとの==がtrueとなることはありません。
(Groovyの==の挙動については、後述します。)

PolyString とは?

こちらに説明があります。
- PolyString
- PolyString Normalization Configuration

PolyStringは、国際対応のために、文字列を2つの異なる形式で保存しています。
(polyは「複数の~」という接頭辞で、「ポリゴン」(多角形)の「ポリ」と同じですね。)

形式 説明
元の形式(orig) ユーザーが入力したテキスト(国際文字、任意の数の空白などが含まれる場合がある) Coup D'état
正規化された形式(norm) PolyString正規化設定に応じて、元の形式から自動的に導出 coup detat

origとnormはString型です。PolyStringのgetOrig()およびtoString()はorigを返します。
試しにログ出力してみましょう。

                <code>
                    log.info("givenName?.orig       : ${givenName?.orig}")
                    log.info("givenName?.norm       : ${givenName?.norm}")
                    log.info("givenName?.toString() : ${givenName?.toString()}")
                    …
ログ出力
… givenName?.orig       : Alice
… givenName?.norm       : alice
… givenName?.toString() : Alice

対処

対処としては、PolyStringから元の形式(orig)をgetOrig()で取り出すか、基本関数ライブラリを使用してStringに変換します。

基本関数ライブラリは、文字列操作、オブジェクトプロパティの取得などのための非常に基本的な関数を提供します。これらは、シンプルで効率的なスタンドアロン関数です。

基本関数ライブラリのstringify関数は、引数に渡したオブジェクトをStringに変換してくれます。stringifyはStringとPolyStringのどちらにも使えて、Stringの場合はそのまま返却、PolyStringの場合はorigを取り出して返却します。
参考)BasicExpressionFunctions.java

stringifyを使用してコードを書き換えてみましょう。意図した結果になります。

オブジェクト・テンプレート
                <code>
                    if ('Alice' == basic.stringify(givenName)) {
                        return 'アリス ' + familyName
                    }
                    givenName + ' ' + familyName
                </code>

備考1)文字列属性がすべてPolyStringというわけではありません

たとえば emailAddress は通常のString型です。
参考)UserType

また、上記の例が「mapping」であることにも注意してください。source は midPointオブジェクトで、入力値は UserTypeのgivenNameでした。これが「inbound」のfirstnameの場合であれば、sourceはリソースからの文字列で、通常のStringです。

備考2)Groovyの==の挙動について

groovyにおいて a == b は、比較可能であれば a.compareTo(b) == 0 に変換され、そうでなければ a.equals(b) に変換されます

参考)Groovy Language Documentation 3.2.11. Behaviour of ==

上記コードの判定式で、左辺値にStringを持ってきていますが、これは説明の都合上で、意図的です。

執筆時点のPolyStringのJava実装は、origが比較対象のStringと等しいとき、

  • equals が false(不一致)
  • compareTo が 0(一致)

を返します。
PolyStringを左辺値とすると、PolyStringのequals では false ですが == は true となり、混乱してしまうので、説明上の都合でこれを回避しました。

(確認用コード)
<code>
import com.evolveum.midpoint.prism.polystring.PolyString

s  = 'a'
ps = new PolyString('a')

log.info('{}', s.equals(ps) )   // false
log.info('{}', ps.equals(s) )   // false

log.info('{}', s == ps )        // false
log.info('{}', ps == s )        // true  ※

log.info('{}', ps.compareTo(s)) // 0(一致) ※equalsでは不一致と判定している比較が、一致と判定されている

この挙動はそのうち変更されるかもしれませんが、現時点での予備知識として展開しておきます。

その2)midPointのデータ表現、Prism

Prism(プリズム)は、midPointのデータ構造です。

  • Prismは、midPointシステム全体でユニバーサルデータ表現として使用されます
  • すべてのmidPointコンポーネントは、プリズムオブジェクトで機能します
  • Prismは、複数の形式で同時にデータを表示できるデータ表現メカニズムです
  • ネイティブPrismインターフェイス(API)を使用してアクセスできますが、同じデータは他のさまざまなインターフェイス(JAXB、DOMなど)からもアクセスできます

参考)Prism Objects

prism.png
図の引用元:Prism Objects

midPoint内部へアクセスしてみる

Prismは、midPoint関数ライブラリを使用する際に、意識することになります。

midPoint関数ライブラリは、midPoint内部へのアクセスを提供します。 IDM固有およびmidPoint固有のロジックを含む複雑な機能を提供します。

参考)MidPoint Library

オブジェクト・テンプレートに、descriptionへのマッピングを追加してみます。
式には以下のようなスクリプトを設定しました。

ユーザーを検索し、自分と同じ姓の人を見つけたら、そのことをdescriptionに出力

オブジェクト・テンプレート
    <mapping>
        <strength>strong</strength>
        <source>
            <path>familyName</path>
        </source>
        <expression>
            <script>
                <code>
                    import com.evolveum.midpoint.prism.impl.query.builder.QueryBuilder
                    import com.evolveum.midpoint.xml.ns._public.common.common_3.UserType

                    if (familyName) {
                        query = QueryBuilder.queryFor(UserType.class, prismContext)
                            .item(UserType.F_FAMILY_NAME).eq(familyName)
                            .build()

                        user = midpoint.searchObjects(UserType.class, query).find{it.oid != focus?.oid}
                        if (user) {
                            return "${user.name}(${user.givenName})の親戚かも?"
                        }
                    }
                </code>
            </script>
        </expression>
        <target>
            <path>description</path>
        </target>
    </mapping>

midPoint関数ライブラリの midpoint.searchObjects でユーザーを検索しています。
注目する箇所は、検索対象として item に指定している UserType.F_FAMILY_NAME で、これは QName(名前空間内の名前)です。値は以下のようなものです。

UserType.F_FAMILY_NAME相当
import javax.xml.namespace.QName
new QName("http://midpoint.evolveum.com/xml/ns/public/common/common-3", "familyName")

Prismデータ構造は拡張可能であるように設計されていて、これを可能にしている主なメカニズムが、ネームスペースです。Prismはコンテナ構造となっていて、構成要素のアイテムの名前はQName(名前空間内の名前)です。上記コードがPrismデータ構造にアクセスしているのが分かります。

midPoint関数ライブラリの戻り値について

上記コードに、1つ不思議な点があるのが分かるでしょうか?
Prismデータ構造にはQNameでアクセスする、というようなことを説明しておいて、取得結果に対してはuser.givenName(Javaだとuser.getGivenName())のように、setter/getterアクセスしています。
本来はwikiにある例のように、ネイティブPrismインタフェースでQNameを指定してアクセスする必要があるのではないでしょうか?

wikiの例
// Invoking native Prism interface to read fullName, returns "Jack Sparrow"
PrismObject<UserType> userPrism = ....
String fullName = userPrism.findProperty(new QName(NS_C, "fullName").getValue().getValue();

参考)Prism Interfaces

これはライブラリが、setter/getterアクセスできるCompile-time class インタフェースに切り替えて戻してくれている(toObjectableListの箇所)ためです。

MidpointFunctionsImpl.java
    public <T extends ObjectType> List<T> searchObjects(
            Class<T> type, ObjectQuery query) throws SchemaException,
            ObjectNotFoundException, SecurityViolationException,
            CommunicationException, ConfigurationException, ExpressionEvaluationException {
        return MiscSchemaUtil.toObjectableList(
                modelService.searchObjects(type, query, 
                        getDefaultGetOptionCollection(), getCurrentTask(), getCurrentResult()));
    }

参考)MidpointFunctionsImpl.java

インタフェースの切り替えについては、以下に記載があります。
参考)Prism Duality

例えば、ネイティブPrismインタフェース と Compile-time classインタフェース の間であれば、以下のように変換できます。

  Native prism interface
asObjectable() ↓ ↑ asPrismObject()
  Compile-time class interface

PolyString再び

ここで1点、PolyStringについての注意点があります。
上記コードの最後の部分を以下のように修正したとします

検索された同姓ユーザーの givenName が A始まりの場合だけ、値を設定

オブジェクト・テンプレート
                        …
                        if (user) {
                            if ( user.givenName.startsWith('A') ) {
                                return "${user.name}(${user.givenName})の親戚かも?"
                            }
                        }
                    }
                </code>

しかし、これはエラーになります。

ログ出力
… No signature of method: com.evolveum.prism.xml.ns._public.types_3.PolyStringType.startsWith() is applicable for argument types: (String) values: …

startsWith(String) が無い? givenName は PolyString ですので、Stringと違ってstartsWithの実装がないのでしょうか? しかし確認してみると、PolyStringの実装には存在しています。

PolyString.java
    public boolean startsWith(String value) {
        return this.orig.startsWith(value);
    }

参考)PolyString.java

どういうことでしょうか? エラーをもう一度よく見てみます。エラーになっているのは確かに、PolyString…。いや、PolyString…Type? 誰ですか、あなた!

実は、ライブラリが返してくれる Compile-time class インタフェースにおいて、PolyString は PolyStringType となっていて、実装が異なります。
PolyStringType の実装を確認してみると、確かに startsWith がありません。

参考)PolyStringType.java

PolyStringとPolyStringTypeが存在している経緯が、以下に説明されています。

参考)PolyString and PolyStringType

この混乱の原因は、主にJAXBフレームワークの制限が原因です。 JAXBフレームワークを使用して、XSDスキーマを解析し、コンパイル時クラスを生成しています。
生成されたすべてのコンパイル時クラスの起源はXSD定義にあるため、それらのプロパティタイプもすべてXSD定義を持っている必要があります。これにはPolyStringが含まれます。
JAXBの重要な部分を変更およびカスタマイズしましたが、この混乱を回避する方法は見つかりませんでした。

ただ、このことさえ理解していれば、対応はPolyStringと同様です。
origを取り出して処理するか、basic.stringifyでStringにします。(basic.stringifyはPolyStringTypeも処理してくれます。)

オブジェクト・テンプレート
                        …
                        if (user) {
                            if ( basic.stringify(user.givenName).startsWith('A') ) {
                                return "${user.name}(${user.givenName})の親戚かも?"
                            }
                        }
                    }
                </code>

まとめ

midPointにおける「マッピング」、特にスクリプトの利用で最初に戸惑いやすいポイントについて、いくつか説明してみました。
スクリプトを利用していて、意図した動作にならない場合は、midPointのデータ構造と、処理で使用しているインタフェースを意識して、確認してみましょう。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした