Remove recursively when deleting an authentication executor
Closes #24795 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
2578705f78
commit
ea4155bbcd
4 changed files with 89 additions and 28 deletions
|
@ -41,25 +41,25 @@ public class MigrateTo22_0_0 implements Migration {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void migrate(KeycloakSession session) {
|
public void migrate(KeycloakSession session) {
|
||||||
session.realms().getRealmsStream().forEach(this::removeHttpChallengeFlow);
|
session.realms().getRealmsStream().forEach(realm -> removeHttpChallengeFlow(session, realm));
|
||||||
//login, account, email themes are handled by JpaUpdate22_0_0_RemoveRhssoThemes
|
//login, account, email themes are handled by JpaUpdate22_0_0_RemoveRhssoThemes
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
|
public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
|
||||||
removeHttpChallengeFlow(realm);
|
removeHttpChallengeFlow(session, realm);
|
||||||
updateLoginTheme(realm);
|
updateLoginTheme(realm);
|
||||||
updateAccountTheme(realm);
|
updateAccountTheme(realm);
|
||||||
updateEmailTheme(realm);
|
updateEmailTheme(realm);
|
||||||
updateClientAttributes(realm);
|
updateClientAttributes(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeHttpChallengeFlow(RealmModel realm) {
|
private void removeHttpChallengeFlow(KeycloakSession session, RealmModel realm) {
|
||||||
AuthenticationFlowModel httpChallenge = realm.getFlowByAlias(HTTP_CHALLENGE_FLOW);
|
AuthenticationFlowModel httpChallenge = realm.getFlowByAlias(HTTP_CHALLENGE_FLOW);
|
||||||
if (httpChallenge == null) return;
|
if (httpChallenge == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
KeycloakModelUtils.deepDeleteAuthenticationFlow(realm, httpChallenge, () -> {}, () -> {});
|
KeycloakModelUtils.deepDeleteAuthenticationFlow(session, realm, httpChallenge, () -> {}, () -> {});
|
||||||
LOG.debugf("Removed '%s' authentication flow in realm '%s'", HTTP_CHALLENGE_FLOW, realm.getName());
|
LOG.debugf("Removed '%s' authentication flow in realm '%s'", HTTP_CHALLENGE_FLOW, realm.getName());
|
||||||
} catch (ModelException me) {
|
} 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",
|
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",
|
||||||
|
|
|
@ -29,7 +29,9 @@ import org.keycloak.common.util.SecretGenerator;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.deployment.DeployedConfigurationsManager;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
import org.keycloak.models.AuthenticatorConfigModel;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientScopeModel;
|
import org.keycloak.models.ClientScopeModel;
|
||||||
|
@ -44,7 +46,6 @@ import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.KeycloakSessionTask;
|
import org.keycloak.models.KeycloakSessionTask;
|
||||||
import org.keycloak.models.KeycloakSessionTaskWithResult;
|
import org.keycloak.models.KeycloakSessionTaskWithResult;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RealmProvider;
|
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.ScopeContainerModel;
|
import org.keycloak.models.ScopeContainerModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
@ -861,12 +862,13 @@ public final class KeycloakModelUtils {
|
||||||
/**
|
/**
|
||||||
* Recursively remove authentication flow (including all subflows and executions) from the model storage
|
* Recursively remove authentication flow (including all subflows and executions) from the model storage
|
||||||
*
|
*
|
||||||
* @param realm
|
* @param session The keycloak session
|
||||||
|
* @param realm The realm
|
||||||
* @param authFlow flow to delete
|
* @param authFlow flow to delete
|
||||||
* @param flowUnavailableHandler Will be executed when flow or some of it's subflow is null
|
* @param flowUnavailableHandler Will be executed when flow, sub-flow or executor is null
|
||||||
* @param builtinFlowHandler will be executed when flow is built-in flow
|
* @param builtinFlowHandler will be executed when flow is built-in flow
|
||||||
*/
|
*/
|
||||||
public static void deepDeleteAuthenticationFlow(RealmModel realm, AuthenticationFlowModel authFlow, Runnable flowUnavailableHandler, Runnable builtinFlowHandler) {
|
public static void deepDeleteAuthenticationFlow(KeycloakSession session, RealmModel realm, AuthenticationFlowModel authFlow, Runnable flowUnavailableHandler, Runnable builtinFlowHandler) {
|
||||||
if (authFlow == null) {
|
if (authFlow == null) {
|
||||||
flowUnavailableHandler.run();
|
flowUnavailableHandler.run();
|
||||||
return;
|
return;
|
||||||
|
@ -876,14 +878,47 @@ public final class KeycloakModelUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
realm.getAuthenticationExecutionsStream(authFlow.getId())
|
realm.getAuthenticationExecutionsStream(authFlow.getId())
|
||||||
.map(AuthenticationExecutionModel::getFlowId)
|
.forEachOrdered(authExecutor -> deepDeleteAuthenticationExecutor(session, realm, authExecutor, flowUnavailableHandler, builtinFlowHandler));
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.map(realm::getAuthenticationFlowById)
|
|
||||||
.forEachOrdered(subflow -> deepDeleteAuthenticationFlow(realm, subflow, flowUnavailableHandler, builtinFlowHandler));
|
|
||||||
|
|
||||||
realm.removeAuthenticationFlow(authFlow);
|
realm.removeAuthenticationFlow(authFlow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively remove authentication executor (including sub-flows and configs) from the model storage
|
||||||
|
*
|
||||||
|
* @param session The keycloak session
|
||||||
|
* @param realm The realm
|
||||||
|
* @param authExecutor The authentication executor to remove
|
||||||
|
* @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) {
|
||||||
|
if (authExecutor == null) {
|
||||||
|
flowUnavailableHandler.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively remove sub flows
|
||||||
|
if (authExecutor.getFlowId() != null) {
|
||||||
|
AuthenticationFlowModel authFlow = realm.getAuthenticationFlowById(authExecutor.getFlowId());
|
||||||
|
deepDeleteAuthenticationFlow(session, realm, authFlow, flowUnavailableHandler, builtinFlowHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the config if not shared
|
||||||
|
if (authExecutor.getAuthenticatorConfig() != null) {
|
||||||
|
DeployedConfigurationsManager configManager = new DeployedConfigurationsManager(session);
|
||||||
|
if (configManager.getDeployedAuthenticatorConfig(authExecutor.getAuthenticatorConfig()) == null) {
|
||||||
|
AuthenticatorConfigModel config = configManager.getAuthenticatorConfig(realm, authExecutor.getAuthenticatorConfig());
|
||||||
|
if (config != null) {
|
||||||
|
realm.removeAuthenticatorConfig(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the executor at the end
|
||||||
|
realm.removeAuthenticatorExecution(authExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
public static ClientScopeModel getClientScopeByName(RealmModel realm, String clientScopeName) {
|
public static ClientScopeModel getClientScopeByName(RealmModel realm, String clientScopeName) {
|
||||||
return realm.getClientScopesStream()
|
return realm.getClientScopesStream()
|
||||||
.filter(clientScope -> Objects.equals(clientScopeName, clientScope.getName()))
|
.filter(clientScope -> Objects.equals(clientScopeName, clientScope.getName()))
|
||||||
|
|
|
@ -350,10 +350,13 @@ public class AuthenticationManagementResource {
|
||||||
public void deleteFlow(@Parameter(description = "Flow id") @PathParam("id") String id) {
|
public void deleteFlow(@Parameter(description = "Flow id") @PathParam("id") String id) {
|
||||||
auth.realm().requireManageRealm();
|
auth.realm().requireManageRealm();
|
||||||
|
|
||||||
KeycloakModelUtils.deepDeleteAuthenticationFlow(realm, realm.getAuthenticationFlowById(id),
|
AuthenticationFlowModel flow = realm.getAuthenticationFlowById(id);
|
||||||
() -> {
|
if (flow == null) {
|
||||||
throw new NotFoundException("Could not find flow with id");
|
throw new NotFoundException("Flow not found");
|
||||||
},
|
}
|
||||||
|
|
||||||
|
KeycloakModelUtils.deepDeleteAuthenticationFlow(session, realm, flow,
|
||||||
|
() -> {}, // allow deleting even with missing references
|
||||||
() -> {
|
() -> {
|
||||||
throw new BadRequestException("Can't delete built in flow");
|
throw new BadRequestException("Can't delete built in flow");
|
||||||
}
|
}
|
||||||
|
@ -388,8 +391,9 @@ public class AuthenticationManagementResource {
|
||||||
AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias);
|
AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias);
|
||||||
if (flow == null) {
|
if (flow == null) {
|
||||||
logger.debug("flow not found: " + flowAlias);
|
logger.debug("flow not found: " + flowAlias);
|
||||||
return Response.status(NOT_FOUND).build();
|
throw new NotFoundException("Flow not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationFlowModel copy = copyFlow(session, realm, flow, newName);
|
AuthenticationFlowModel copy = copyFlow(session, realm, flow, newName);
|
||||||
|
|
||||||
data.put("id", copy.getId());
|
data.put("id", copy.getId());
|
||||||
|
@ -662,8 +666,7 @@ public class AuthenticationManagementResource {
|
||||||
rep.setRequirement(execution.getRequirement().name());
|
rep.setRequirement(execution.getRequirement().name());
|
||||||
rep.setFlowId(execution.getFlowId());
|
rep.setFlowId(execution.getFlowId());
|
||||||
result.add(rep);
|
result.add(rep);
|
||||||
AuthenticationFlowModel subFlow = realm.getAuthenticationFlowById(execution.getFlowId());
|
recurseExecutions(flowRef, result, level + 1);
|
||||||
recurseExecutions(subFlow, result, level + 1);
|
|
||||||
} else {
|
} else {
|
||||||
String providerId = execution.getAuthenticator();
|
String providerId = execution.getAuthenticator();
|
||||||
ConfigurableAuthenticatorFactory factory = CredentialHelper.getConfigurableAuthenticatorFactory(session, providerId);
|
ConfigurableAuthenticatorFactory factory = CredentialHelper.getConfigurableAuthenticatorFactory(session, providerId);
|
||||||
|
@ -944,19 +947,19 @@ public class AuthenticationManagementResource {
|
||||||
if (model == null) {
|
if (model == null) {
|
||||||
session.getTransactionManager().setRollbackOnly();
|
session.getTransactionManager().setRollbackOnly();
|
||||||
throw new NotFoundException("Illegal execution");
|
throw new NotFoundException("Illegal execution");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationFlowModel parentFlow = getParentFlow(model);
|
AuthenticationFlowModel parentFlow = getParentFlow(model);
|
||||||
if (parentFlow.isBuiltIn()) {
|
if (parentFlow.isBuiltIn()) {
|
||||||
throw new BadRequestException("It is illegal to remove execution from a built in flow");
|
throw new BadRequestException("It is illegal to remove execution from a built in flow");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(model.getFlowId() != null) {
|
KeycloakModelUtils.deepDeleteAuthenticationExecutor(session, realm, model,
|
||||||
AuthenticationFlowModel nonTopLevelFlow = realm.getAuthenticationFlowById(model.getFlowId());
|
() -> {}, // allow deleting even with missing references
|
||||||
realm.removeAuthenticationFlow(nonTopLevelFlow);
|
() -> {
|
||||||
}
|
throw new BadRequestException("It is illegal to remove execution from a built in flow");
|
||||||
|
}
|
||||||
realm.removeAuthenticatorExecution(model);
|
);
|
||||||
|
|
||||||
adminEvent.operation(OperationType.DELETE).resource(ResourceType.AUTH_EXECUTION).resourcePath(session.getContext().getUri()).success();
|
adminEvent.operation(OperationType.DELETE).resource(ResourceType.AUTH_EXECUTION).resourcePath(session.getContext().getUri()).success();
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ package org.keycloak.testsuite.admin.authentication;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticatorFactory;
|
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticatorFactory;
|
||||||
import org.keycloak.common.Profile;
|
|
||||||
import org.keycloak.common.util.StreamUtil;
|
import org.keycloak.common.util.StreamUtil;
|
||||||
import org.keycloak.events.admin.OperationType;
|
import org.keycloak.events.admin.OperationType;
|
||||||
import org.keycloak.events.admin.ResourceType;
|
import org.keycloak.events.admin.ResourceType;
|
||||||
|
@ -30,7 +29,6 @@ import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentatio
|
||||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||||
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
|
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
|
||||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||||
import org.keycloak.testsuite.ProfileAssume;
|
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.util.AdminEventPaths;
|
import org.keycloak.testsuite.util.AdminEventPaths;
|
||||||
import org.keycloak.testsuite.util.ContainerAssume;
|
import org.keycloak.testsuite.util.ContainerAssume;
|
||||||
|
@ -84,6 +82,29 @@ public class FlowTest extends AbstractAuthenticationTest {
|
||||||
// Under the old code, this would throw an error because "grandchild"
|
// Under the old code, this would throw an error because "grandchild"
|
||||||
// was left in the database
|
// was left in the database
|
||||||
addFlowToParent("child", "grandchild");
|
addFlowToParent("child", "grandchild");
|
||||||
|
|
||||||
|
authMgmtResource.deleteFlow(findFlowByAlias("Foo", authMgmtResource.getFlows()).getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveExecutionSubflow() {
|
||||||
|
createFlow(newFlow("Foo", "Foo flow", "generic", true, false));
|
||||||
|
addFlowToParent("Foo", "child");
|
||||||
|
addFlowToParent("child", "grandchild");
|
||||||
|
|
||||||
|
// remove the foo child but using the execution
|
||||||
|
List<AuthenticationExecutionInfoRepresentation> fooExecutions = authMgmtResource.getExecutions("Foo");
|
||||||
|
AuthenticationExecutionInfoRepresentation childExececution = fooExecutions.stream()
|
||||||
|
.filter(r -> "child".equals(r.getDisplayName()) && r.getLevel() == 0).findAny().orElse(null);
|
||||||
|
assertNotNull(childExececution);
|
||||||
|
authMgmtResource.removeExecution(childExececution.getId());
|
||||||
|
assertAdminEvents.clear();
|
||||||
|
|
||||||
|
// check subflows were removed and can be re-created
|
||||||
|
addFlowToParent("Foo", "child");
|
||||||
|
addFlowToParent("child", "grandchild");
|
||||||
|
|
||||||
|
authMgmtResource.deleteFlow(findFlowByAlias("Foo", authMgmtResource.getFlows()).getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addFlowToParent(String parentAlias, String childAlias) {
|
private void addFlowToParent(String parentAlias, String childAlias) {
|
||||||
|
@ -315,6 +336,8 @@ public class FlowTest extends AbstractAuthenticationTest {
|
||||||
|
|
||||||
authMgmtResource.addExecutionFlow("parent", params);
|
authMgmtResource.addExecutionFlow("parent", params);
|
||||||
assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.authAddExecutionFlowPath("parent"), params, ResourceType.AUTH_EXECUTION_FLOW);
|
assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.authAddExecutionFlowPath("parent"), params, ResourceType.AUTH_EXECUTION_FLOW);
|
||||||
|
|
||||||
|
authMgmtResource.deleteFlow(findFlowByAlias("parent", authMgmtResource.getFlows()).getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue