FFFF fix: MigrateTo26_6_0 should only add organization step to built-in br… by maeventer · Pull Request #47991 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-26_6_1.adoc
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
== Manual migration if you migrated to 26.6.0

The migration to 26.6.0 might have introduced the `Organization` sub-flow to your custom browser flow in case they were bound to the `browser flow` binding type.

If you upgrade from 26.5.x or earlier directly to 26.6.1, this does not apply to you.

If you have migrated to 26.6.0, review your custom browser flow in each realm and remove the `Organization` sub-flow if this is not desired.

Once you upgraded to 26.6.1, you should see in the built-in browser flow the `Organization` sub-flow.

// ------------------------ Breaking changes ------------------------ //
== Breaking changes

Breaking changes are identified as those that might require changes for existing users to their configurations or applications.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private void removeHttpChallengeFlow(KeycloakSession session, RealmModel realm)
if (httpChallenge == null) return;

try {
KeycloakModelUtils.deepDeleteAuthenticationFlow(session, realm, httpChallenge, () -> {}, () -> {});
KeycloakModelUtils.deepDeleteAuthenticationFlow(session, realm, httpChallenge, () -> {}, () -> {}, httpChallenge.isBuiltIn());
LOG.debugf("Removed '%s' authentication flow in realm '%s'", HTTP_CHALLENGE_FLOW, realm.getName());
} catch (ModelException me) {
LOG.errorf("Authentication flow '%s' is in use in realm '%s' and cannot be removed. Please update your deployment to avoid using this flow before migration to latest Keycloak",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import org.keycloak.models.utils.DefaultAuthenticationFlows;


public class MigrateTo26_6_0 extends RealmMigration {
public class MigrateTo26_6_1 extends RealmMigration {

public static final ModelVersion VERSION = new ModelVersion("26.6.0");
public static final ModelVersion VERSION = new ModelVersion("26.6.1");

@Override
public ModelVersion getVersion() {
Expand All @@ -18,6 +18,6 @@ public ModelVersion getVersion() {

@Override
public void migrateRealm(KeycloakSession session, RealmModel realm) {
DefaultAuthenticationFlows.addOrganizationBrowserFlowStep(realm, realm.getBrowserFlow());
DefaultAuthenticationFlows.addOrganizationBrowserFlowStep(realm, realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
import org.keycloak.migration.migrators.MigrateTo26_3_0;
import org.keycloak.migration.migrators.MigrateTo26_4_0;
import org.keycloak.migration.migrators.MigrateTo26_4_3;
import org.keycloak.migration.migrators.MigrateTo26_6_0;
import org.keycloak.migration.migrators.MigrateTo26_6_1;
import org.keycloak.migration.migrators.MigrateTo2_0_0;
import org.keycloak.migration.migrators.MigrateTo2_1_0;
import org.keycloak.migration.migrators.MigrateTo2_2_0;
Expand Down Expand Up @@ -133,7 +133,7 @@ public class DefaultMigrationManager implements MigrationManager {
new MigrateTo26_3_0(),
new MigrateTo26_4_0(),
new MigrateTo26_4_3(),
new MigrateTo26_6_0()
new MigrateTo26_6_1()
};

private final KeycloakSession session;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1147,17 +1147,17 @@ public static boolean isFlowUsed(KeycloakSession session, RealmModel realm, Auth
* @param flowUnavailableHandler Will be executed when flow, sub-flow or executor is null
* @param builtinFlowHandler will be executed when flow is built-in flow
*/
public static void deepDeleteAuthenticationFlow(KeycloakSession session, RealmModel realm, AuthenticationFlowModel authFlow, Runnable flowUnavailableHandler, Runnable builtinFlowHandler) {
public static void deepDeleteAuthenticationFlow(KeycloakSession session, RealmModel realm, AuthenticationFlowModel authFlow, Runnable flowUnavailableHandler, Runnable builtinFlowHandler, boolean isParentBuiltInFlow) {
if (authFlow == null) {
flowUnavailableHandler.run();
return;
}
if (authFlow.isBuiltIn()) {
if (isParentBuiltInFlow) {
builtinFlowHandler.run();
}

realm.getAuthenticationExecutionsStream(authFlow.getId())
.forEachOrdered(authExecutor -> deepDeleteAuthenticationExecutor(session, realm, authExecutor, flowUnavailableHandler, builtinFlowHandler));
.forEachOrdered(authExecutor -> deepDeleteAuthenticationExecutor(session, realm, authExecutor, flowUnavailableHandler, builtinFlowHandler, isParentBuiltInFlow));

realm.removeAuthenticationFlow(authFlow);
}
Expand All @@ -1171,7 +1171,7 @@ public static void deepDeleteAuthenticationFlow(KeycloakSession session, RealmMo
* @param flowUnavailableHandler Handler that will be executed when flow, sub-flow or executor is null
* @param builtinFlowHandler Handler that will be executed when flow is built-in flow
*/
public static void deepDeleteAuthenticationExecutor(KeycloakSession session, RealmModel realm, AuthenticationExecutionModel authExecutor, Runnable flowUnavailableHandler, Runnable builtinFlowHandler) {
public static void deepDeleteAuthenticationExecutor(KeycloakSession session, RealmModel realm, AuthenticationExecutionModel authExecutor, Runnable flowUnavailableHandler, Runnable builtinFlowHandler, boolean isParentBuiltInFlow) {
if (authExecutor == null) {
flowUnavailableHandler.run();
return;
Expand All @@ -1180,7 +1180,7 @@ public static void deepDeleteAuthenticationExecutor(KeycloakSession session, Rea
// recursively remove sub flows
if (authExecutor.getFlowId() != null) {
AuthenticationFlowModel authFlow = realm.getAuthenticationFlowById(authExecutor.getFlowId());
deepDeleteAuthenticationFlow(session, realm, authFlow, flowUnavailableHandler, builtinFlowHandler);
deepDeleteAuthenticationFlow(session, realm, authFlow, flowUnavailableHandler, builtinFlowHandler, isParentBuiltInFlow);
}

// remove the config if not shared
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,8 @@ public void deleteFlow(@Parameter(description = "Flow id") @PathParam("id") Stri
() -> {}, // allow deleting even with missing references
() -> {
throw new BadRequestException("Can't delete built in flow");
}
},
flow.isBuiltIn()
);

// Use just one event for top-level flow. Using separate events won't work properly for flows of depth 2 or bigger
Expand Down Expand Up @@ -1034,7 +1035,8 @@ public void removeExecution(@Parameter(description = "Execution id") @PathParam(
() -> {}, // allow deleting even with missing references
() -> {
throw new BadRequestException("It is illegal to remove execution from a built in flow");
}
},
parentFlow.isBuiltIn()
);

adminEvent.operation(OperationType.DELETE).resource(ResourceType.AUTH_EXECUTION).resourcePath(session.getContext().getUri()).success();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,45 @@ public void testRemoveExecutionSubflow() {
addFlowToParent("child", "grandchild");
}

@Test
public void testRemoveBuiltinSubflowFromCustomBrowserFlow() {
// Create a custom top-level browser flow that is NOT marked as builtin
createFlow(newFlow("CustomBrowser", "Custom Browser flow", "basic-flow", true, false));

// Add a sub-flow to the custom browser flow (it is not builtin by default)
addFlowToParent("CustomBrowser", "builtin-child");

// Locate the child sub-flow execution in the parent flow
List<AuthenticationExecutionInfoRepresentation> executions = authMgmtResource.getExecutions("CustomBrowser");
AuthenticationExecutionInfoRepresentation childExecution = executions.stream()
.filter(r -> "builtin-child".equals(r.getDisplayName()) && r.getLevel() == 0)
.findAny().orElse(null);
Assertions.assertNotNull(childExecution, "Expected to find the child sub-flow execution");

// Mark the sub-flow itself as builtin via the update API
String subFlowId = childExecution.getFlowId();
Assertions.assertNotNull(subFlowId, "Expected the child execution to reference a sub-flow");
AuthenticationFlowRepresentation subFlow = authMgmtResource.getFlow(subFlowId);
subFlow.setBuiltIn(true);
authMgmtResource.updateFlow(subFlowId, subFlow);
Assertions.assertTrue(authMgmtResource.getFlow(subFlowId).isBuiltIn(),
"Sub-flow should now be marked as builtin");

// Skip the admin events produced by addFlowToParent (CREATE AUTH_EXECUTION_FLOW)
// and updateFlow (UPDATE AUTH_FLOW) since they are not the focus of this test
adminEvents.skip(2);

// Removing the builtin-marked sub-flow from a non-builtin parent flow must succeed
authMgmtResource.removeExecution(childExecution.getId());
AdminEventAssertion.assertEvent(adminEvents.poll(), OperationType.DELETE,
AdminEventPaths.authExecutionPath(childExecution.getId()), ResourceType.AUTH_EXECUTION);

// Verify the sub-flow execution is no longer present in the parent flow
executions = authMgmtResource.getExecutions("CustomBrowser");
Assertions.assertTrue(executions.isEmpty(),
"Expected no executions remaining in the custom browser flow after removal");
}

private void addFlowToParent(String parentAlias, String childAlias) {
Map<String, Object> data = new HashMap<>();
data.put("alias", childAlias);
Expand Down
Loading
0