midPointでActive DirectoryのForeign Security Principalをいい感じに扱う

[NRI OpenStandia Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nri-openstandia) の24日目は、OSSのIGAプラットフォームである [midPoint](https://github.com/Evolveum/midpoint) に最近コントリビュートして対応が可能になった、Active DirectoryのForeign Security Principal を扱う方法について紹介します。


# Foreign Security Principal とは

Foreign Security Principal (以下、FSP) とは、 **信頼できる外部のフォレストの Security Principal を表すために、Active Directory (以下、AD) によって作成されるオブジェクト** のことをいいます。FSPにより、外部のSecurity Principalを自分のドメイン内のセキュリティグループ (ただし、ドメインローカル限定) に追加し、リソースに対するパーミッションを付与することができます。

詳しくは、Microsoftの以下のページを参照してください。

https://social.technet.microsoft.com/wiki/contents/articles/51367.active-directory-foreign-security-principals-and-special-identities.aspx


# FSPでADドメイン間のグループメンバーシップを表現する

FSPを使うことで、異なるADドメイン間でもフォレスト信頼関係が結ばれていれば、セキュリティグループに他ドメインのユーザを追加し、アクセス制御ができます。もう少し具体的な例で説明します。

以下のADドメインがあるとします。

* `a.example.com`
* `b.example.com`

そして、セキュリティグループとユーザがそれぞれ以下のように存在するとします。

* (ドメインローカルの) セキュリティグループ： **cn=g001,OU=Groups,OU=Sample,DC=a,DC=example,DC=com**
* ユーザ： **cn=u001,OU=Users,OU=Sample,DC=b,DC=example,DC=com**

ここで、このADドメインでフォレスト間の信頼関係を結び、ユーザ (`cn=u001`) を異なるADドメイン上のセキュリティグループ (`cn=g001`) にメンバーとして追加してアクセス制御したい、とします。

これは、ADの管理ツールである「Active Directoryユーザーとコンピューター」で操作を行うことで簡単に実施できます。前提としてフォレスト間の信頼関係が設定されていて、かつセキュリティグループのタイプがドメインローカルであれば[^1]、該当セキュリティグループのメンバー編集画面にて、外部のADドメインのユーザーをメンバーとして設定できるようになります。

[^1]: ADの仕様で、ドメインローカルタイプのセキュリティグループのみが、外部ドメインのユーザをグループに追加することができます。

ただし、設定しても実際の外部ADドメインのユーザ (`cn=u001`) の情報が直接メンバーとして登録されるわけではありません。ここで登場するのがFSPです。メンバーとして登録した瞬間に、この例だと `a.example.com` のAD側で、**ou=ForeignSecurityPrincipals** コンテナ配下にFSPオブジェクトが1つ自動作成されます。そして、セキュリティグループのメンバーにはこの自動的に作成されたFSPオブジェクトが設定されています。

実は、このFSPの`cn`値は、外部ADドメインのユーザ (`cn=u001`) の `objectSid` の値を指しています。つまり、下図のようにFSPオブジェクトを間に挟んで、間接的に外部ドメインのユーザがセキュリティグループに登録される形になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/1ad81b45-74f9-c73e-9ea0-d410f2c87735.png)

また、注意すべきAD上の制限として、このFSPオブジェクトを直接作成することができません。セキュリティグループのメンバーに追加したタイミングで、AD側で自動生成される仕組みとなります。

## LDAPプロトコルでFSPを扱う場合の注意点

ADの管理ツールである「Active Directoryユーザーとコンピューター」で操作を行う分には意識しなくてよいですが、LDAPプロトコルでこのFSPを扱う場合には他の注意点があります。

セキュリティグループのメンバーとして登録する際のLDIFは、以下のようにする必要があります。通常、`member`にはDNフォーマットで値を設定しますが、`<SID=` + `objectSid` + `>`という特殊なフォーマットになります。`<>`で挟むことが必要なので注意してください。

```
dn: cn=g001,OU=Groups,OU=Sample,DC=a,DC=example,DC=com
changetype: modify
add: member
member: <SID=外部ADドメインのユーザのobjectSidの値>
```

この特殊フォーマットが必要なのは、セキュリティグループのメンバーに登録する際だけです。一旦登録した後に、セキュリティグループからメンバーを解除する場合は、実体として登録されたFSPのDNを`member`に指定して解除することができます (特殊フォーマットで`objectSid`を指定してメンバー解除することも可能)。


# midPointでどう扱うか

ここまで、AD側の仕組みの話をしました。いよいよ次に、midPointからどのようにしてこのようなグループメンバーシップ関係をプロビジョニングすることができるか、見ていきましょう。

## ShadowObjectの構成

midPointでは、プロビジョニング先や源泉のデータをShadowObjectとして表現しています。`a.example.com` ドメインに閉じてみると、`g001`のセキュリティグループとFSPオブジェクトの間の通常のグループメンバーシップとして見ることができます。よって、下図のようにmidPoint内で各種オブジェクトを構成し、AD側にプロビジョニングできればよさそうです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/feee19c0-1f8a-e369-d7f1-03fa46a88161.png)


midPoint内で`u001`のUserTypeに対して、`b.example.com`には通常のADの`user`オブジェクトでプロビジョニングを行い、`g001`のOrgTypeにアサインされたタイミングで、`a.example.com`にはFSPオブジェクトとしてプロビジョニングさせるという考え方です。

## midPointでFSPオブジェクトを扱う上での課題

前述の通りのShadowObjectを構成するように後は設定すればOK、といいたいところなのですが、AD側の仕様のところで説明したとおり、以下のAD側の仕様があります。

1. FSPオブジェクトを直接作成できない。代わりに、セキュリティグループにメンバーとして登録する必要がある。
2. メンバーに登録する際は、LDAPプロトコルでは`<SID=` + `objectSid` + `>`という特殊なフォーマットで指定する必要がある。

midPointによるADプロビジョニングは、標準でmidPoint本体にバンドルされている [Active Directory Connector](https://docs.evolveum.com/connectors/connectors/com.evolveum.polygon.connector.ldap.ad.AdLdapConnector/) というコネクタを利用しますが、内部ではLDAPプロトコルを使用しています。コネクタはリソース (つまり、AD) に対するCRUD操作を実装していますので、FSPオブジェクトの作成時には処理を行わない、メンバー登録時には特殊フォーマットを利用するという特別な処理が必要になります。つまり、このコネクタがFSPを意識した処理を実装している必要があります。

残念ながら、[Active Directory Connector](https://docs.evolveum.com/connectors/connectors/com.evolveum.polygon.connector.ldap.ad.AdLdapConnector/) はこの機能は備えておりませんでした。そこで、以下のプルリクエストを送りFSP対応機能を今回コントリビューションしました。

https://github.com/Evolveum/connector-ldap/pull/24

2021/12/24時点ではmidPoint本体にはまだバンドルされていませんが、JARファイルを[Active Directory Connectorのサイト](https://docs.evolveum.com/connectors/connectors/com.evolveum.polygon.connector.ldap.ad.AdLdapConnector/)より取得して利用することができます。**v3.3.1以降**のものを利用してください。

## リソース定義 (Active Directory Connector設定) / `a.example.com`用

Active Directory Connector v3.3.1 以降を使い、設定します。以下、設定ポイントです。

### FSP対応機能の有効化

デフォルトでは、コントリビューションしたFSP対応機能は無効になっています。有効にするには、以下のように`allowFSPProcessing`オプションを`true`に設定します。

```xml
        <icfc:configurationProperties xmlns:gen70="http://midpoint.evolveum.com/xml/ns/public/connector/icf-1/bundle/com.evolveum.polygon.connector-ldap/com.evolveum.polygon.connector.ldap.ad.AdLdapConnector">
            ...
            <!--
                フォレスト間グループメンバーシップをFSPを利用して行うため有効化
            -->
            <gen70:allowFSPProcessing>true</gen70:allowFSPProcessing>
        </icfc:configurationProperties>
    </connectorConfiguration>
```

また、FSPのスキーマ情報を取得するようにも設定しておきます(`generationConstraints`でフィルタする場合)。

```xml
    <schema>
        <generationConstraints>
            ...
            <!--
                FSPのスキーマを取得
            -->
            <generateObjectClass>ri:foreignSecurityPrincipal</generateObjectClass>
        </generationConstraints>
    </schema>
```

### `schemaHandling`設定

まず、セキュリティグループそのもののプロビジョニング設定です。注意点として、ドメインローカルタイプのセキュリティグループとして作成する必要があります。これは、`groupType`に`-2147483644`を設定すればOKです。それ以外は従来のプロビジョニング設定と同じです。

```xml
        <objectType>
            <kind>entitlement</kind>
            <intent>group</intent>
            <displayName>グループ</displayName>
            <default>false</default>
            <objectClass>ri:group</objectClass>
            ...
            <attribute>
                <c:ref>ri:groupType</c:ref>
                <outbound>
                    <strength>strong</strength>
                    <expression>
                        <script>
                            <code>
                                // ADの仕様上、フォレスト間グループメンバーシップをFSPで行うにはドメインローカルタイプのグループとする

                                // Global Security Group
                                // Global: -2147483646
                                // Domain Local: -2147483644
                                // Universal: -2147483640
                                return -2147483644
                            </code>
                        </script>
                    </expression>
                </outbound>
            </attribute>
        </objectType>
```

次に、FSPオブジェクトのプロビジョニング設定です。ポイントは以下の3点です。

* 作成するRDNには、`b.example.com`のユーザの`objectSid`を取得して`cn=objectSidの値`を設定する。
* 該当のセキュリティグループに対してAssociationの設定を行う。
* `ri:dn`の`matchingRule`には`default`を利用してCase-sensitiveモードで処理する。

例では、`kind=account, intent=fsp`として定義しています。

```xml
        <objectType>
            <kind>account</kind>
            <intent>fsp</intent>
            <displayName>foreignSecurityPrincipal</displayName>
            <default>false</default>
            <objectClass>ri:foreignSecurityPrincipal</objectClass>
            <baseContext>
                <objectClass>ri:organizationalUnit</objectClass>
                <filter>
                    <q:equal>
                        <q:path>attributes/dn</q:path>
                        <expression>
                            <script>
                                <code>
                                    "CN=ForeignSecurityPrincipals,DC=a,DC=example,DC=com"
                                </code>
                            </script>
                        </expression>
                    </q:equal>
                </filter>
            </baseContext>
            <attribute>
                <ref>ri:dn</ref>
                <!--
                    DN用のMatchingRuleはここでは使用しない。
                    DNは本来Case-insensitiveなため内部的に小文字で統一して管理されるが
                    DNを構成するSIDの値が小文字に変換されるとエラーとなる。
                    そこで、Case-sensitiveモードで処理する。
                <matchingRule>distinguishedName</matchingRule>
                -->
                <matchingRule>default</matchingRule>
                <outbound>
                    <strength>weak</strength>
                    <source>
                        <path>$focus/name</path>
                    </source>
                    <expression>
                        <script>
                            <code>
                                import com.evolveum.midpoint.xml.ns._public.common.common_3.*

                                // b.example.comのユーザのobjectSidを取得
                                shadow = midpoint.getLinkedShadow(focus, "611a0df5-3809-46af-a5fe-122a14a8a8ec", ShadowKindType.ACCOUNT, "default")
                                sid = basic.getAttributeValue(shadow, "objectSid")

                                return basic.composeDnWithSuffix("cn", sid, "CN=ForeignSecurityPrincipals,DC=a,DC=example,DC=com")
                            </code>
                        </script>
                    </expression>
                </outbound>
            </attribute>
            <!--
                FSPオブジェクトをセキュリティグループのメンバーとする
            -->
            <association>
                <ref>ri:group</ref>
                <displayName>AD Cross-forest Group Membership</displayName>
                <tolerant>false</tolerant>
                <kind>entitlement</kind>
                <intent>group</intent>
                <direction>objectToSubject</direction>
                <associationAttribute>ri:member</associationAttribute>
                <valueAttribute>ri:dn</valueAttribute>
                <shortcutAssociationAttribute>ri:memberOf</shortcutAssociationAttribute>
                <shortcutValueAttribute>ri:dn</shortcutValueAttribute>
                <explicitReferentialIntegrity>false</explicitReferentialIntegrity>
            </association>
        </objectType>
```

## リソース定義 (Active Directory Connector設定) / `b.example.com`用

例の構成だと、`b.example.com`側ではFSPオブジェクトを扱う必要がありませんので、こちら側のリソース定義は特に注意点はありません。単に、`u001`を`b.example.com`側の`user`オブジェクトとしてプロビジョニングするように通常の設定を行うだけでOKです。


## プロビジョニング用のメタロールの作成

ADプロビジョニング用のメタロールをArchetypeTypeやRoleTypeで作成します。UserType用とOrgType用の2つを用意します。

### UserType用

`b.example.com`に`u001`をプロビジョニングするだけなので、ここは通常のプロビジョニング用メタロールを設定すればOKです。

### OrgType用

`a.example.com`に`g001`をプロビジョニングするための通常の設定に加えて、ここではFSPオブジェクトをプロビジョニングするための設定が必要になります。以下がその部分です。`a.example.com`用のリソース定義で設定した `kind=account, intent=fsp` に対してAssociationを設定することになります。

ここでのポイントは、`condition`タグを利用して、(このメタロールがアサインされた) OrgTypeにUserTypeがアサインされた場合に、該当のUserTypeが`b.example.com`に対するShadowObjectを持っているかどうかを条件とします。`b.example.com`に対するShadowObjectを持つということは、`b.example.com`に`user`オブジェクトがプロビジョニングされているということであり、FSPオブジェクトを使ってグループメンバーシップを結ぶ対象となります。

```xml
    <!--
        b.example.comのユーザのShadowObjectを持つユーザが
        組織に所属した場合に、FSPでグループメンバーシップをプロビジョニングする
    -->
    <inducement>
        <construction>
            <strength>strong</strength>
            <resourceRef oid="8a40cfef-a6ff-4baa-a88f-01c08789025d" relation="org:default" type="c:ResourceType">
            </resourceRef>
            <kind>account</kind>
            <intent>fsp</intent>
            <association>
                <c:ref>ri:group</c:ref>
                <outbound>
                    <authoritative>true</authoritative>
                    <strength>strong</strength>
                    <expression>
                        <associationFromLink>
                            <projectionDiscriminator>
                                <kind>entitlement</kind>
                                <intent>group</intent>
                            </projectionDiscriminator>
                        </associationFromLink>
                    </expression>
                </outbound>
            </association>
        </construction>
        <condition>
            <expression>
                <script>
                    <code>
                        import com.evolveum.midpoint.xml.ns._public.common.common_3.*

                        if (focus == null) {
                            return false
                        }

                        // b.example.comのユーザのShadowObjectが存在するかチェック
                        shadow = midpoint.getLinkedShadow(focus, "611a0df5-3809-46af-a5fe-122a14a8a8ec", ShadowKindType.ACCOUNT, "default")

                        return shadow != null
                    </code>
                </script>
            </expression>
        </condition>
        <order>2</order>
        <focusType>c:UserType</focusType>
    </inducement>
```

# 動作確認

以下のオペレーションを実施することでFSPオブジェクトが想定通りプロビジョニングされることを確認できます。

1. midPointで `g001` のOrgTypeを作成してメタロールを適用 => `a.example.com` に `g001` セキュリティグループがプロビジョニングされる。
2. midPointで `u001` のUserTypeを作成してメタロールを適用 => `b.example.com` に `u001` ユーザがプロビジョニングされる。
3. midPointで `u001` のUserTypeに、`g001` のOrgTypeをアサインする => `a.example.com` にFSPオブジェクトが作成され、`g001`のメンバーにそのFSPオブジェクトが設定される。


# 確認した環境

このFSPオブジェクトを利用したグループメンバーシップのプロビジョニングは、以下の環境で動作確認済みです。AD互換のSamba 4でもフォレスト間信頼が実装されており、FSPオブジェクトをAD同様に扱うことが可能になっています。なお、Samba 4でフォレスト間信頼を設定するには、[こちらのStefan Kania氏のサイト](https://www.kania-online.de/veroeffentlichungen/)で公開されている [Seting up trusts between two Samba-domains](https://www.kania-online.de/wp-content/uploads/2019/06/trusts-tutorial-en.pdf) を参考にするとよいでしょう。

* Windows Server 2019
* AWS Managed Microsoft AD
* Samba 4.13.14

ただし、AWS Managed Microsoft ADの場合はAWSの仕様により、用意されている管理者アカウントでもAD操作権限が制限されており、以下の操作ができません。

* FSPオブジェクトの`memberOf`が参照できない
* FSPオブジェクトを削除できない

そこで、もし`a.example.com`がAWS Managed Microsoft ADの場合は、以下の様にリソース定義を設定することでこの問題を回避することができます。

* `<association>`: `memberOf`参照を利用したショートカットを使わないようにする
* `<existence>`: FSPオブジェクトが削除されないようにする

具体的な設定例は以下のようになります。

```xml
            <association>
                <ref>ri:group</ref>
                <displayName>AD cross-forest group membership</displayName>
                <tolerant>false</tolerant>
                <kind>entitlement</kind>
                <intent>group</intent>
                <direction>objectToSubject</direction>
                <associationAttribute>ri:member</associationAttribute>
                <valueAttribute>ri:dn</valueAttribute>
                <!--
                    AWS Managed Microsoft ADの場合はショートカットは使えない
                <shortcutAssociationAttribute>ri:memberOf</shortcutAssociationAttribute>
                <shortcutValueAttribute>ri:dn</shortcutValueAttribute>
                -->
                <explicitReferentialIntegrity>false</explicitReferentialIntegrity>
            </association>
            <activation>
                <existence>
                    <outbound>
                        <expression>
                            <script>
                                <code>
                                    // AWS Managed Microsoft ADではFSPオブジェクトの削除が
                                    // 禁止されているため、削除を抑止する
                                    true
                                </code>
                            </script>
                        </expression>
                    </outbound>
                </existence>
            </activation>
```

# まとめ

[NRI OpenStandia Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nri-openstandia) の24日目は、midPointでADのFSPをどう扱えばよいかをご紹介しました。FSPを利用したグループメンバーシップを設定する必要のある大規模・複雑なAD構成を持っており、midPointでうまくエンタイトルメント管理をされたい方は是非試してみてください。