外部IDと動的グループメンバーシップへの移行 migrating-to-external-identity

概要 overview

AEM as a Cloud ServiceでData Synchronizationが有効になっている場合、SAML Authentication Handlerは、ユーザーとグループの作成を管理する際に、動的グループメンバーシップを持つ外部IDに自動的に移行するように設定できます。 プロジェクトでカスタムコードを使用してユーザーまたはグループを作成する場合は、ローカルユーザーやグループではなく、外部ユーザーやグループを作成するようにプロジェクトを更新する必要があります。

外部ユーザーとグループが必要な理由 why-external-required

ローカルユーザーやグループから、動的なグループメンバーシップを持つ外部IDに移行することは、いくつかの重要な理由で不可欠です。

パフォーマンスの最適化:

  • リポジトリ書き込みの削減:従来のローカルグループメンバーシップでは、グループノードの単一の複数値プロパティでリポジトリにメンバーシップ関係を書き込む必要があります。 動的グループメンバーシップでは、ユーザーはすべてのグループプリンシパルを含む単一のrep:externalPrincipalNames プロパティを持つため、グループノードを同期する必要がありません
  • より高速な同期:パブリッシュ層ノード間でユーザーを同期する場合、動的グループメンバーシップを持つ外部ユーザーは、従来のグループメンバーシップを持つローカルユーザーと比較して、データ転送と書き込み操作が大幅に少なくて済みます
  • スケーラビリティ:多数のユーザーとグループを持つシステムは、リポジトリのオーバーヘッドが大幅に削減されることでメリットが得られます。 動的なグループメンバーシップは、大規模なグループでも効率的に拡張できます。

このドキュメントでは、次の技術面でのガイダンスを提供します。

  • 外部ID モデルについて
  • 外部ユーザーとグループを作成するためのカスタムコードの変更
  • 既存のローカルユーザーとグループを外部ID モデルに移行する

外部IDについて understanding-external-identity

外部ユーザー external-users

外部ユーザーは、rep:externalId プロパティによって識別され、ユーザーを外部ID プロバイダーにリンクします。 形式は次のとおりです。

userId;idpName

例:john.doe;saml-idp。

NOTE
idpNameは、認証ハンドラー設定で定義されているOak ID プロバイダー(Idp)の名前を参照します。 SAML統合の場合、これはSAML認証ハンドラーのidpIdentifier属性に設定された値です。

キーのプロパティ:

  • rep:externalId: ユーザーを外部としてマークする必須プロパティ (例:john.doe;saml-idp)
  • rep:externalPrincipalNames:動的メンバーシップの外部グループ プリンシパルを含む複数値プロパティ
  • rep:lastSynced:前回の同期のタイムスタンプ
  • rep:lastDynamicSync:最後の動的グループメンバーシップ同期のタイムスタンプ

外部グループ external-groups

外部グループもrep:externalId プロパティによって識別され、プリンシパル名の形式を使用します。

groupId;idpName

例:content-authors;saml-idp

動的グループメンバーシップ dynamic-group-membership

リポジトリに保存されているユーザーとグループ間の直接関係の代わりに、動的グループメンバーシップは、ユーザーノードのrep:externalPrincipalNames プロパティを使用します。 ユーザーが外部グループのIDに一致する外部プリンシパル名を持っている場合、そのユーザーはそのグループのメンバーになります。 詳しくは、Apache Oakのドキュメント ​を参照してください。

メリット:

  • リポジトリへの書き込みが減少しました(ユーザーがグループに追加または削除されたときに、グループメンバーシップノードが変更されません)
  • パブリッシュ層ノード間の同期を高速化
  • 拡張可能なグループメンバーシップ管理
  • データ同期の要件に対応

サービスユーザー設定 service-user-configuration

外部ユーザーとグループを作成または変更するすべての操作は、rep:externalIdおよびrep:externalPrincipalNames プロパティのデフォルト保護をバイパスするように適切に設定された​ サービスユーザー ​を使用して実行する必要があります。

サービスユーザーが必要な理由 why-is-a-service-user-required

デフォルトでは、Oakのセキュリティにより、通常のセッションでは、次のような保護されたプロパティが変更されなくなります。

  • rep:externalId - ユーザー/グループを外部としてマークします
  • rep:externalPrincipalNames – 動的なグループ メンバーシップ プリンシパルを保存します

これらのプロパティを変更できるのは、適切に設定されたサービスユーザーのみです。

サービスユーザーの設定とマッピング service-user-configuration-mapping

外部IDを管理するためのサービスユーザーの設定には、次の3つの調整された設定が必要です。

  1. repoinit経由でサービスユーザーを作成
  2. ExternalPrincipal保護の設定
  3. サービスユーザーをアプリケーションバンドルにマッピングします。

これらの手順について詳しくは、以下を参照してください。

手順1:Repoinitを使用してサービスユーザーを作成する create-the-serviice-user-via-repoinit

この手順では、repoinit スクリプトを使用して、必要な権限を持つサービスユーザーを作成する方法を詳しく説明します。

設定ファイル: org.apache.sling.jcr.repoinit.RepositoryInitializer~group-provisioner.cfg.json

例示的な場所: ui.config/src/main/content/jcr_root/apps/yourproject/osgiconfig/config.publish/

{
  "scripts": [
    "create service user group-provisioner with path system/yourproject",
    "set ACL for group-provisioner\n  allow jcr:read,jcr:readAccessControl,jcr:modifyAccessControl,rep:userManagement,rep:write on /home/users\n  allow jcr:read,jcr:readAccessControl,jcr:modifyAccessControl,rep:userManagement,rep:write on /home/groups\nend"
  ]
}

権限の概要

  • jcr:read: ユーザーとグループの読み取り
  • jcr:readAccessControl: ACLの読み取り
  • jcr:modifyAccessControl: ACLの変更(プロパティの設定に必要)
  • rep:userManagement: ユーザー/グループの作成と管理
  • rep:write: rep:externalIdとrep:externalPrincipalNamesを含むプロパティを書き込む
NOTE
サービスユーザーは、/home/users/system/yourprojectの下に作成され、他のシステムユーザーと整理されます。

手順2:ExternalPrincipal保護の設定 configure-externalprincipal-protection

次に、外部ID プロパティに適用される保護を回避できるように、サービスユーザーをホワイトリストに登録するための設定例を示します。

設定ファイル名: org.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal.ExternalPrincipalConfiguration.cfg.json

場所の例: ui.config/src/main/content/jcr_root/apps/yourproject/osgiconfig/config.publish/

{
  "protectExternalIdentities": "Warn",
  "systemPrincipalNames": [
    "group-provisioner",
    "saml-migration-service"
  ]
}

設定プロパティ:

  • protectExternalIdentities:外部ID プロパティの保護レベルを制御します:

    • "Strict": ホワイトリスト内のシステム プリンシパルのみが外部プロパティを変更できます。 これは本番環境で推奨されるレベルです。
    • "Warn":警告は記録されますが、変更は可能です。 開発/テストに役立ちます。
    • "None":保護なし。 推奨されません。
  • systemPrincipalNames: rep:externalIdとrep:externalPrincipalNamesを変更できるサービスユーザー名のリスト。 外部IDを管理する必要があるすべてのサービスユーザーを含めます(例:group-provisioner、saml-migration-service)。

IMPORTANT
systemPrincipalNamesのサービス ユーザー名は、repoinit スクリプトで作成されたサービス ユーザーIDと完全に一致する必要があります。

手順3:サービスユーザーマッピング service-user-mapping

コードが使用できるように、サービスユーザーをアプリケーションバンドルにマッピングします。

設定ファイル: org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~group-provisioner.cfg.json

場所: ui.config/src/main/content/jcr_root/apps/yourproject/osgiconfig/config.publish/

{
  "user.mapping": [
    "yourproject.core:group-provisioner=[group-provisioner]"
  ]
}

マッピング形式:

  • yourproject.core: シンボリック バンドル名(pom.xml <Bundle-SymbolicName>に見つかりました)
  • group-provisioner (=以前): コードで使用するサブサービス名
  • [group-provisioner] (=以降): repoinitで作成された実際のサービスユーザーID

コードでのサービスユーザーの使用 using-the-service-user-in-code

移行またはユーザー/グループ作成操作を実行するためにセッションを開く場合は、サービスユーザーを使用する必要があります。

import org.apache.sling.jcr.api.SlingRepository;

@Reference
private SlingRepository repository;

// Login as the service user
Session serviceSession = repository.loginService("group-provisioner", null);

try {
    UserManager userManager = ((JackrabbitSession) serviceSession).getUserManager();
    // Perform operations...
    serviceSession.save();
} finally {
    if (serviceSession != null && serviceSession.isLive()) {
        serviceSession.logout();
    }
}
IMPORTANT
サービスユーザーの設定が適切でない場合、rep:externalIdまたはrep:externalPrincipalNamesを設定しようとすると、権限エラーが発生して失敗します。 移行を試みる前に、サービスユーザーがExternalPrincipal設定で適切に設定されていることを確認してください。

完全な設定例 complete-configuration-example

以下に、3つの設定をすべて一緒に示す完全な作業例を示します。

ファイル構造 file-structure

ui.config/src/main/content/jcr_root/apps/yourproject/osgiconfig/
└── config.publish/
    ├── org.apache.sling.jcr.repoinit.RepositoryInitializer~group-provisioner.cfg.json
    ├── org.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal.ExternalPrincipalConfiguration.cfg.json
    └── org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~group-provisioner.cfg.json

カスタムコードの変更 modifying-custom-code

外部ユーザーの作成 creating-external-users

前(ローカルユーザー):

UserManager userManager = ((JackrabbitSession) session).getUserManager();
User user = userManager.createUser(userId, password);

後(外部ユーザー):

import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;

UserManager userManager = ((JackrabbitSession) session).getUserManager();
ValueFactory valueFactory = session.getValueFactory();

// Create user with principal
Principal userPrincipal = new Principal() {
    @Override
    public String getName() {
        return userId;
    }
};

User user = userManager.createUser(userId, null, userPrincipal, null);

// Set rep:externalId
ExternalIdentityRef externalRef = new ExternalIdentityRef(userId, idpName);
String externalId = externalRef.getString(); // Format: userId;idpName
user.setProperty("rep:externalId", valueFactory.createValue(externalId));

// Set sync timestamps to far future (workaround for OAK-12079)
// Set to 10 years in the future to prevent premature cleanup of external group memberships
// See: https://issues.apache.org/jira/browse/OAK-12079
java.util.Calendar future = java.util.Calendar.getInstance();
future.add(java.util.Calendar.YEAR, 10);
user.setProperty("rep:lastSynced", valueFactory.createValue(future));
user.setProperty("rep:lastDynamicSync", valueFactory.createValue(future));

session.save();

外部グループの作成 creating-external-groups

前(ローカルグループ):

UserManager userManager = ((JackrabbitSession) session).getUserManager();
Group group = userManager.createGroup(groupId);

後(外部グループ):

import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;

UserManager userManager = ((JackrabbitSession) session).getUserManager();
ValueFactory valueFactory = session.getValueFactory();

// Create group with principal
Principal groupPrincipal = new Principal() {
    @Override
    public String getName() {
        return groupId;
    }
};

Group group = userManager.createGroup(groupPrincipal);

// Set rep:externalId
ExternalIdentityRef externalRef = new ExternalIdentityRef(groupId, idpName);
String externalId = externalRef.getString(); // Format: groupId;idpName
group.setProperty("rep:externalId", valueFactory.createValue(externalId));

session.save();

動的グループメンバーシップの割り当て assigning-dynamic-membership

前(直接メンバーシップ):

Group group = (Group) userManager.getAuthorizable(groupId);
User user = (User) userManager.getAuthorizable(userId);
group.addMember(user);

後(動的メンバーシップ):

User user = (User) userManager.getAuthorizable(userId);
ValueFactory valueFactory = session.getValueFactory();

// Get existing external principal names
Value[] existingValues = user.getProperty("rep:externalPrincipalNames");
List<String> principalNames = new ArrayList<>();

if (existingValues != null) {
    for (Value value : existingValues) {
        principalNames.add(value.getString());
    }
}

// Add new principal name (format: groupId;idpName)
String dynamicGroupPrincipal = groupId + ";" + idpName;
if (!principalNames.contains(dynamicGroupPrincipal)) {
    principalNames.add(dynamicGroupPrincipal);

    // Create new Value array
    Value[] newValues = new Value[principalNames.size()];
    for (int i = 0; i < principalNames.size(); i++) {
        newValues[i] = valueFactory.createValue(principalNames.get(i));
    }

    // Set the property
    user.setProperty("rep:externalPrincipalNames", newValues);

    // Update sync timestamps to far future (workaround for OAK-12079)
    // Set to 10 years in the future to prevent premature cleanup of external group memberships
    // See: https://issues.apache.org/jira/browse/OAK-12079
    java.util.Calendar future = java.util.Calendar.getInstance();
    future.add(java.util.Calendar.YEAR, 10);
    user.setProperty("rep:lastDynamicSync", valueFactory.createValue(future));
    user.setProperty("rep:lastSynced", valueFactory.createValue(future));
}

session.save();

移行プロセス migration-process

データ同期サービスを有効にする前にカスタムコードを更新した場合、既存のローカルユーザーとグループを外部IDに移行する必要はありません。

ローカルユーザーとグループが既にリポジトリに保持されており、環境が積極的に使用されている場合は、中断や不整合を避けるために、次のようなマルチステップの移行を実行することをお勧めします。

IMPORTANT
rep:externalIdおよびrep:externalPrincipalNames プロパティの保護をバイパスする権限が付与された、適切に設定されたサービスユーザー(例:group-provisioner)を使用して、すべての移行手順を実行する必要があります。 詳しくは、​ サービスユーザー設定を参照してください。

手順1:外部グループ構造の作成 step-1-create-external-group-structure

移行する必要がある各ローカルグループについて:

  1. プリンシパル名<localGroupId>;<idpName>を持つ対応する外部グループを作成します。 外部グループとローカルグループをリンクするのに役立つ命名規則を使用する
  2. 値<localGroupId>;<idpName>を持つ外部グループのrep:externalId プロパティを設定します
  3. 外部グループを元のローカルグループのメンバーとして追加します。

検証

  • すべてのローカルグループに対応する外部グループがあるかどうかを確認することで、結果を検証できます。 さらに、すべての外部グループは、対応するローカルグループのメンバーです。

サーブレット エンドポイントの例:

@SlingServletPaths("/bin/migration/step1")
public class MigrationStep1Servlet extends SlingAllMethodsServlet {

    @Override
    protected void doPost(SlingHttpServletRequest request,
                          SlingHttpServletResponse response) {
        String groupPath = request.getParameter("groupPath");
        String idpName = request.getParameter("idpName");

        // Check if the caller is authorized to run the servlet
        isAuthorizedCaller(request, response);

        // Get local group
        Authorizable localGroupAuth = userManager.getAuthorizableByPath(groupPath);
        Group localGroup = (Group) localGroupAuth;
        String localGroupId = localGroup.getID();

        // Create external group
        String externalGroupPrincipalName = localGroupId + ";" + idpName;
        // The function createExternalGroup performs the following steps:
        // 1. Creates a new external group with the given principal name (format: "<localGroupId>;<idpName>").
        // 2. Sets the 'rep:externalId' property on the group to mark it as an external group (value: "<localGroupId>;<idpName>").
        // 3. Sets the 'rep:principalName' property for the group if required.
        // 4. Assigns any other required group metadata, such as a title or description, if needed.
        // 5. Persists the new group node in the repository at the appropriate path under /home/groups.
        // 6. Returns the created Group object so it can be used for further operations, such as membership assignment.
        Group externalGroup = createExternalGroup(externalGroupPrincipalName, localGroupId, idpName);

        // Add external group to local group
        localGroup.addMember(externalGroup);

        session.save();
    }
}

使用状況:

curl -X POST "http://localhost:4503/bin/migration/step1?groupPath=/home/groups/c/content-authors&idpName=saml-idp"

手順2:ユーザーの変換と動的メンバーシップの割り当て step-2-convert-users-and-assign-dynamic-membership

ローカルグループのメンバーである各ユーザーについて:

  1. rep:externalIdが設定されていることを確認します(必要に応じて外部ユーザーに変換)。
  2. グループ メンバーシップごとに、対応する外部グループ プリンシパルをrep:externalPrincipalNamesに追加します
  3. 同期タイムスタンプを更新します。
IMPORTANT
このプロセス中に「everyone」などのシステムグループをスキップします。

サーブレット エンドポイントの例:

@SlingServletPaths("/bin/migration/step2")
public class MigrationStep2Servlet extends SlingAllMethodsServlet {

    @Override
    protected void doPost(SlingHttpServletRequest request,
                          SlingHttpServletResponse response) {
        String userId = request.getParameter("userId");
        String idpName = request.getParameter("idpName");

        // Check if the caller is authorized to run the servlet
        isAuthorizedCaller(request, response);

        // Login as the service user
        Session serviceSession = repository.loginService("group-provisioner", null);

        try {
            UserManager userManager = ((JackrabbitSession) serviceSession).getUserManager();
            User user = (User) userManager.getAuthorizable(userId);

            // Ensure user has rep:externalId
            Value[] externalIdValues = user.getProperty("rep:externalId");
            if (externalIdValues == null || externalIdValues.length == 0) {
                ExternalIdentityRef externalRef = new ExternalIdentityRef(userId, idpName);
                user.setProperty("rep:externalId",
                            valueFactory.createValue(externalRef.getString()));
            }

            // Get all group memberships
            Iterator<Group> groupIterator = user.declaredMemberOf();
            List<String> principalNames = new ArrayList<>();

            while (groupIterator.hasNext()) {
                Group group = groupIterator.next();
                String groupId = group.getID();

                // Skip system groups
                if ("everyone".equals(groupId)) {
                    continue;
                }

                // Add dynamic group principal
                String dynamicGroupPrincipal = groupId + ";" + idpName;
                principalNames.add(dynamicGroupPrincipal);
            }

            // Set rep:externalPrincipalNames
            if (!principalNames.isEmpty()) {
                Value[] newValues = new Value[principalNames.size()];
                for (int i = 0; i < principalNames.size(); i++) {
                    newValues[i] = valueFactory.createValue(principalNames.get(i));
                }
                user.setProperty("rep:externalPrincipalNames", newValues);
            }

            // Update timestamps to far future (workaround for OAK-12079)
            // Set to 10 years in the future to prevent premature cleanup of external group memberships
            // See: https://issues.apache.org/jira/browse/OAK-12079
            java.util.Calendar future = java.util.Calendar.getInstance();
            future.add(java.util.Calendar.YEAR, 10);
            user.setProperty("rep:lastDynamicSync", valueFactory.createValue(future));
            user.setProperty("rep:lastSynced", valueFactory.createValue(future));

        // Perform operations...
        serviceSession.save();
    } finally {
        if (serviceSession != null && serviceSession.isLive()) {
            serviceSession.logout();
        }
}    }
}

使用状況:

curl -X POST "http://localhost:4503/bin/migration/step2?userId=john.doe&idpName=saml-idp"

検証

これを検証するには、すべてのユーザーがrep:externalIdおよびrep:externalPrincipalName属性を持ち、すべての外部グループのprincipalNameを持っていることを確認します。 ユーザーは、外部グループのローカルグループ および​のメンバーです。

手順3:直接ユーザーメンバーシップの削除 step-3-remove-direct-user-memberships

ユーザーが動的グループメンバーシップを設定した後:

  1. ローカルグループから直接ユーザーメンバーシップを削除する
  2. グループ間メンバーシップを維持(外部グループメンバーシップを含む)

サーブレット エンドポイントの例:

@SlingServletPaths("/bin/migration/step3")
public class MigrationStep3Servlet extends SlingAllMethodsServlet {

    @Override
    protected void doPost(SlingHttpServletRequest request,
                          SlingHttpServletResponse response) {

        // Check if the caller is authorized to run the servlet
        isAuthorizedCaller(request, response);

        String groupPath = request.getParameter("groupPath");

        Authorizable localGroupAuth = userManager.getAuthorizableByPath(groupPath);
        Group localGroup = (Group) localGroupAuth;

        // Process each member
        Iterator<Authorizable> members = localGroup.getDeclaredMembers();

        while (members.hasNext()) {
            Authorizable member = members.next();

            // Remove only user members, keep group members
            if (!member.isGroup()) {
                localGroup.removeMember(member);
            }
        }

        session.save();
    }
}

使用状況:

curl -X POST "http://localhost:4503/bin/migration/step3?groupPath=/home/groups/c/content-authors"

検証

  • これを検証するには、すべてのローカルグループに対応する外部グループまたは他のグループのみがメンバーとして含まれていることを確認します。

移行ワークフロー migration-workflow

移行前のチェックリスト pre-migration-checklist

  • サービスユーザーの設定:適切な権限を持つサービスユーザー(例:group-provisioner)を作成して設定します
  • ExternalPrincipal Configuration​の検証:サービス ユーザーがrep:externalIdおよびrep:externalPrincipalNamesの保護をバイパスするように設定されていることを確認します
  • サービスユーザー権限をテスト: サービスユーザーが開発中に外部ID プロパティを設定できることを確認します
  • ユーザーまたはグループを作成するすべてのカスタムコードを特定
  • 外部ID モデルを使用するためのカスタムコードのレビューと更新
  • 開発環境で更新されたコードをテストする
  • 移行するすべての既存のローカルユーザーとグループをインベントリ化します
  • 下位環境での移行プロセスのテスト

実行ステップ execution-steps

  1. 更新されたコードをデプロイ:カスタムコードの変更をデプロイして、外部ユーザー/グループを作成します

  2. 外部グループを作成 (ローカル グループごとに):

    code language-bash
    curl -X POST "http://localhost:4503/bin/migration/step1?groupPath=/home/groups/g/my-group&idpName=saml-idp"
    
  3. ユーザーの移行 (各ユーザー):

    code language-bash
    curl -X POST "http://localhost:4503/bin/migration/step2?userId=username&idpName=saml-idp"
    
  4. クリーンアップ (移行された各グループについて):

    code language-bash
    curl -X POST "http://localhost:4503/bin/migration/step3?groupPath=/home/groups/g/my-group"
    
  5. 検証: ユーザーグループ メンバーシップの確認とアクセス権限のテスト

  6. データ同期を有効にする:機能を有効にするには、カスタマーサポートにお問い合わせください

移行後の検証 post-migration-validation

移行を確認します。

  1. ユーザーのプロパティを確認:

    ユーザーノードで、次の存在を確認します。

    • rep:externalId:形式はuserId;idpNameである必要があります
    • rep:externalPrincipalNames:形式groupId;idpNameのグループ プリンシパルの配列
    • rep:lastSynced: タイムスタンプが遠い未来に設定されました(移行日から約10年)
    • rep:lastDynamicSync: タイムスタンプが遠い未来に設定されました(移行日から約10年)

    注意: OAK-12079の回避策として、タイムスタンプは意図的に遠い未来の日付に設定されています。 これは予期された動作です。

  2. グループのプロパティを確認:

    ローカル・グループ・ノードでは、次の存在を確認します。

    • 形式groupId;idpNameの外部グループメンバー
    • ダイレクトユーザーメンバーなし(手順3以降のみ)
  3. ユーザーのログインをテスト: ユーザーがログインでき、正しい権限を持っていることを確認します

  4. アクセス制御をテスト: CUG/ACLで保護されているコンテンツにユーザーがアクセスできることを確認します

トラブルシューティング troubleshooting

よくある問題 common-issues

問題:rep:externalIdまたはrep:externalPrincipalNames​の設定時に権限エラーが発生しました

エラーメッセージ:

  • javax.jcr.AccessDeniedException: Access denied
  • OakAccess0000: Access denied
  • Cannot set property 'rep:externalId'

解決策:外部ID プロパティに対する保護をバイパスする権限が付与された、適切に設定されたサービスユーザーを使用して、セッションを開く必要があります。

解決する手順:

  1. サービスユーザーの存在を確認: サービスユーザー(例:group-provisioner)がrepoinitを介して作成されていることを確認します
  2. サービスユーザーマッピングを確認: サーブレットまたはサービスがrepository.loginService("group-provisioner", null)を使用していることを確認します
  3. ExternalPrincipal設定を確認: org.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal.ExternalPrincipalConfigurationが正しく設定されていることを確認します
  4. サービスユーザーの権限を確認: サービスユーザーは/home/usersと/home/groupsにrep:writeとrep:userManagementの権限を必要としています

完全な設定手順については、​ サービスユーザー設定を参照してください。

問題:OakConstraint0072: Property 'rep:externalPrincipalNames' requires 'rep:externalId' to be present

解決策: ユーザーはrep:externalPrincipalNamesを設定する前にrep:externalIdを設定する必要があります。 まず、ステップ 2でユーザーを外部ユーザーに変換します。

問題:移行後にユーザーがグループメンバーシップを失う

解決策:次のことを確認します。

  • 正しいプリンシパル名の形式(groupId;idpName)で外部グループが作成されました
  • 外部グループがローカルグループのメンバーとして追加されました(手順1)
  • ユーザーがrep:externalPrincipalNamesに正しい外部プリンシパル名を持っています(手順2)
  • 手順3のクリーンアップは、手順1と手順2が完了した後にのみ実行されました

問題:ユーザーログイン後、外部グループメンバーシップが予期せず削除される(OAK-12079)

問題: Oakのバグ OAK-12079が原因で、Oak同期メカニズムがrep:lastSyncedおよびrep:lastDynamicSyncのタイムスタンプに基づいて外部グループメンバーシップを早期にクリーンアップする場合があります。

解決策: rep:lastSyncedとrep:lastDynamicSyncのタイムスタンプを、現在の時刻ではなく、遠い未来(10年後)の日付に設定します。 これにより、同期クリーンアップ プロセスで外部グループ メンバーシップが削除されるのを防ぐことができます。

実装:

// Workaround for OAK-12079
// Set to 10 years in the future to prevent premature cleanup
// See: https://issues.apache.org/jira/browse/OAK-12079
java.util.Calendar future = java.util.Calendar.getInstance();
future.add(java.util.Calendar.YEAR, 10);
user.setProperty("rep:lastSynced", valueFactory.createValue(future));
user.setProperty("rep:lastDynamicSync", valueFactory.createValue(future));

なぜこれが機能するのか: タイムスタンプを遠い未来の日付に設定すると、Oak同期ロジックはこれらのユーザーを「最近同期された」ユーザーとして扱い、外部プリンシパル名とグループメンバーシップを削除するクリーンアッププロセスをトリガーしません。

メモ:これは、OAK-12079が将来のOak リリースで解決されるまでの一時的な回避策です。 このドキュメントのすべてのコード例には、既にこの回避策が含まれています。

問題:システムグループ「everyone」がエラーを引き起こす

解決策: ユーザーの移行中は、常に「全員」システムグループをスキップします(手順2)。 このグループは、AEMによって自動的に管理されます。

ロールバック手順 rollback-procedure

移行で問題が発生した場合:

  1. 移行プロセスを停止
  2. 重要なデータが影響を受けた場合のバックアップからの復元
  3. コードの変更をロールバックして、動的グループメンバーシップを持つ外部ユーザーとグループを作成します
  4. 移行を再試行する前に、問題を確認して修正します。

ベストプラクティス best-practices

  • 徹底的にテストする:開発環境とステージング環境での移行は、本番稼働前に必ずテストします
  • バッチ処理:大規模なユーザーベースの場合、タイムアウトの問題を回避するために、一括移行を処理します
  • パフォーマンスを監視:移行中のリポジトリのパフォーマンスを監視する
  • 監査証跡を維持: トラブルシューティング用にすべての移行操作を記録します
  • サービスユーザー権限:移行サーブレットで、必要な権限を持つ適切なサービスユーザーが使用されていることを確認します。 rep:externalIdおよびrep:externalPrincipalNames プロパティの保護をバイパスするには、ExternalPrincipal設定でサービスユーザーを設定する必要があります
  • Idempotent操作:安全に再実行できるように移行コードを設計します
  • 各ステップで検証:続行する前に、各移行ステップの結果を確認してください

移行サーブレットの保護 securing-migration-servlets

移行サーブレットには、ユーザーとグループを作成および変更するための高度な権限があります。 不正アクセスを防ぐには、これらのエンドポイントへのアクセスを制限することが重要です。

推奨されるアプローチは、Adobe IMS統合を使用してこれらのサーブレットを保護し、許可されたテクニカルアカウントのみがアクセスできるようにすることです。

手順1:AEM Developer Consoleでテクニカルアカウントを作成する create-a-technical-account-in-aem-developer-console

  1. Experience Managerに移動し、Cloud Manager​に移動します

  2. プログラムを選択し、テクニカルアカウントを作成する環境をクリックします

  3. 環境の省略記号メニューの​ Developer Console ​をクリックします

  4. AEM Developer Consoleで、「統合」タブに移動します

  5. 「テクニカルアカウントを新規作成」をクリックします

  6. 統合の名前を指定します(例:「移行サービスアカウント」)。

  7. 「作成」をクリックします。

  8. 作成された統合の次の値に注意してください。

    • クライアント ID
    • クライアントの秘密鍵
    • テクニカルアカウント ID (これは、サーブレットにアクセスするユーザーIDになります – 形式:XXXXXXXXXXXXXXXXXXXXXXXX@techacct.adobe.com)

詳しい手順については、​ サーバーサイド APIのアクセストークンの生成ドキュメント ​を参照してください。

呼び出し元が許可されているかどうかを確認するサンプルコード:

    private boolean isAuthorizedCaller(SlingHttpServletRequest request,
                                       SlingHttpServletResponse response) {

        Session session = request.getResourceResolver().adaptTo(Session.class);
        String callerId = session != null ? session.getUserID() : null;

        if (!ALLOWED_TECHNICAL_ACCOUNT.equals(callerId)) {
            LOG.warn("Unauthorized access attempt by user: '{}' (expected: '{}')", callerId,   ALLOWED_TECHNICAL_ACCOUNT);
            response.setStatus(SlingHttpServletResponse.SC_FORBIDDEN);
            return false;
        }

        return true;
    }

詳細な防御:IP ベースの制限 defense-in-depth-ip-based-restrictions

セキュリティの追加レイヤーとして、CDN ルールを設定して、IP アドレスによる移行エンドポイントへのアクセスを制限できます。 これは、移行が既知のインフラストラクチャから実行される場合に便利です。

NOTE
IPの制限だけでは不十分です。 上記のように、常に認証チェックと組み合わせてください。

セキュリティチェックリスト security-checklist

移行サーブレットを実稼動環境にデプロイする前に、次の手順を実行します。

  • AEM Developer ConsoleでのIMS統合の作成
  • テクニカルアカウント IDを検証するためのサーブレットの設定
  • 開発/ステージング環境での認証フローのテスト
  • CDN レベルでのIP ベースの追加制限を検討する
  • 移行完了後に移行サーブレットを無効にするか削除するかを計画する
  • 移行エンドポイントへのすべてのアクセスを監査してログに記録する
IMPORTANT
移行が完了したら、潜在的なセキュリティリスクを排除するために、移行サーブレットをデプロイメントから無効にするか削除することを検討してください。

その他のリソース additional-resources

recommendation-more-help
experience-manager-cloud-service-help-main-toc