はじめに
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(ターゲット) | 結果をどのように処理するか、出力先を定義 |
図の引用元:Mapping
マッピングの適用箇所
前回までの記事でもマッピングは登場してきていましたが、改めて、その適用箇所を把握してみましょう。
以下に、同期の全体像を見渡すことができる図があります。初見ではちょっと面食らうかもしれませんが、今回は「mapping」という箇所にだけ注目してみてください。
Synchronization Policies
インバウンド・フェーズ
図の引用元:Synchronization Policies
ユーザー・ポリシー・フェーズ
図の引用元:Synchronization Policies
アウトバウンド・フェーズ
図の引用元: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
<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
<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(またはそのサブタイプ)なので仕組みは同じです。
- 参考)attribute(ResourceAttributeDefinitionType)の inbound と outbound
- 参考)objectTemplate(ObjectTemplateType)の mapping
入力元(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
public class PolyString implements Matchable<PolyString>, Recomputable, Structured, DebugDumpable, ShortDumpable, Serializable, Comparable<Object> {
PolyStringはString派生型ではないため、Stringとの==がtrueとなることはありません。
(Groovyの==の挙動については、後述します。)
PolyString とは?
こちらに説明があります。
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
midPoint内部へアクセスしてみる
Prismは、midPoint関数ライブラリを使用する際に、意識することになります。
midPoint関数ライブラリは、midPoint内部へのアクセスを提供します。 IDM固有およびmidPoint固有のロジックを含む複雑な機能を提供します。
参考)[MidPoint Library](https://wiki.evolveum.com/display/midPoint/Script+Expression+Functions#ScriptExpressionFunctions-MidPointLibrary
https://wiki.evolveum.com/display/midPoint/MidPoint+Script+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(名前空間内の名前)です。値は以下のようなものです。
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を指定してアクセスする必要があるのではないでしょうか?
// Invoking native Prism interface to read fullName, returns "Jack Sparrow"
PrismObject<UserType> userPrism = ....
String fullName = userPrism.findProperty(new QName(NS_C, "fullName").getValue().getValue();
これはライブラリが、setter/getterアクセスできるCompile-time class インタフェースに切り替えて戻してくれている(toObjectableListの箇所)ためです。
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()));
}
インタフェースの切り替えについては、以下に記載があります。
参考)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の実装には存在しています。
public boolean startsWith(String value) {
return this.orig.startsWith(value);
}
どういうことでしょうか? エラーをもう一度よく見てみます。エラーになっているのは確かに、PolyString…。いや、PolyString…Type? 誰ですか、あなた!
実は、ライブラリが返してくれる Compile-time class インタフェースにおいて、PolyString は PolyStringType となっていて、実装が異なります。
PolyStringType の実装を確認してみると、確かに startsWith がありません。
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のデータ構造と、処理で使用しているインタフェースを意識して、確認してみましょう。