Check alias is unique for authenticator config when it is created

Closes #31727

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-10-17 17:23:14 +02:00 committed by Marek Posolda
parent dcf1d83199
commit 2004467749
4 changed files with 111 additions and 38 deletions

View file

@ -461,6 +461,10 @@ public class AuthenticationManagementResource {
if (config.getAlias() != null) { if (config.getAlias() != null) {
config.setAlias(newName + " " + config.getAlias()); config.setAlias(newName + " " + config.getAlias());
if (configManager.getAuthenticatorConfigByAlias(realm, config.getAlias()) != null) {
logger.warnf("Authentication execution configuration [%s] already exists", config.getAlias());
throw new IllegalStateException("Authentication execution configuration " + config.getAlias() + " already exists.");
}
} }
AuthenticatorConfigModel newConfig = realm.addAuthenticatorConfig(config); AuthenticatorConfigModel newConfig = realm.addAuthenticatorConfig(config);
@ -1025,24 +1029,41 @@ public class AuthenticationManagementResource {
public Response newExecutionConfig(@Parameter(description = "Execution id") @PathParam("executionId") String execution, @Parameter(description = "JSON with new configuration") AuthenticatorConfigRepresentation json) { public Response newExecutionConfig(@Parameter(description = "Execution id") @PathParam("executionId") String execution, @Parameter(description = "JSON with new configuration") AuthenticatorConfigRepresentation json) {
auth.realm().requireManageRealm(); auth.realm().requireManageRealm();
if (json.getAlias() == null || json.getAlias().isEmpty()) {
throw ErrorResponse.exists("Failed to create authentication execution configuration with empty alias name");
}
ReservedCharValidator.validate(json.getAlias()); ReservedCharValidator.validate(json.getAlias());
AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution); AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution);
if (model == null) { if (model == null) {
session.getTransactionManager().setRollbackOnly(); session.getTransactionManager().setRollbackOnly();
throw new NotFoundException("Illegal execution"); throw new NotFoundException("Illegal execution");
}
// retrieve the previous configuration if assigned
AuthenticatorConfigModel prevConfig = null;
if (model.getAuthenticatorConfig() != null) {
prevConfig = realm.getAuthenticatorConfigById(model.getAuthenticatorConfig());
} }
AuthenticatorConfigModel otherConfig = realm.getAuthenticatorConfigByAlias(json.getAlias());
if (otherConfig != null && (prevConfig == null || !prevConfig.getId().equals(otherConfig.getId()))) {
throw ErrorResponse.exists("Authentication execution configuration " + json.getAlias() + " already exists");
}
// remove the previous config
if (prevConfig != null) {
realm.removeAuthenticatorConfig(prevConfig);
}
AuthenticatorConfigModel config = RepresentationToModel.toModel(json); AuthenticatorConfigModel config = RepresentationToModel.toModel(json);
if (config.getAlias() == null) {
throw ErrorResponse.error("Alias missing", Response.Status.BAD_REQUEST);
}
config = realm.addAuthenticatorConfig(config); config = realm.addAuthenticatorConfig(config);
model.setAuthenticatorConfig(config.getId()); model.setAuthenticatorConfig(config.getId());
realm.updateAuthenticatorExecution(model); realm.updateAuthenticatorExecution(model);
json.setId(config.getId()); json.setId(config.getId());
adminEvent.operation(OperationType.CREATE).resource(ResourceType.AUTH_EXECUTION).resourcePath(session.getContext().getUri()).representation(json).success(); adminEvent.operation(OperationType.CREATE).resource(ResourceType.AUTHENTICATOR_CONFIG).resourcePath(session.getContext().getUri()).representation(json).success();
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(config.getId()).build()).build(); return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(config.getId()).build()).build();
} }
@ -1529,6 +1550,14 @@ public class AuthenticationManagementResource {
public Response createAuthenticatorConfig(@Parameter(description = "JSON describing new authenticator configuration") AuthenticatorConfigRepresentation rep) { public Response createAuthenticatorConfig(@Parameter(description = "JSON describing new authenticator configuration") AuthenticatorConfigRepresentation rep) {
auth.realm().requireManageRealm(); auth.realm().requireManageRealm();
if (rep.getAlias() == null || rep.getAlias().isEmpty()) {
throw ErrorResponse.exists("Failed to create authentication execution configuration with empty alias name");
}
if (realm.getAuthenticatorConfigByAlias(rep.getAlias()) != null) {
throw ErrorResponse.exists("Authentication execution configuration " + rep.getAlias() + " already exists");
}
ReservedCharValidator.validate(rep.getAlias()); ReservedCharValidator.validate(rep.getAlias());
AuthenticatorConfigModel config = realm.addAuthenticatorConfig(RepresentationToModel.toModel(rep)); AuthenticatorConfigModel config = realm.addAuthenticatorConfig(RepresentationToModel.toModel(rep));

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.admin.authentication;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory; import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory;
import org.keycloak.authentication.authenticators.broker.IdpDetectExistingBrokerUserAuthenticatorFactory;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType; import org.keycloak.events.admin.ResourceType;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
@ -30,34 +31,42 @@ import org.keycloak.testsuite.Assert;
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 jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import jakarta.ws.rs.BadRequestException;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class AuthenticatorConfigTest extends AbstractAuthenticationTest { public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
private String flowId;
private String executionId; private String executionId;
private String executionId2;
@Before @Before
public void beforeConfigTest() { public void beforeConfigTest() {
AuthenticationFlowRepresentation flowRep = newFlow("firstBrokerLogin2", "firstBrokerLogin2", "basic-flow", true, false); AuthenticationFlowRepresentation flowRep = newFlow("firstBrokerLogin2", "firstBrokerLogin2", "basic-flow", true, false);
createFlow(flowRep); flowId = createFlow(flowRep);
HashMap<String, Object> params = new HashMap<>(); HashMap<String, Object> params = new HashMap<>();
params.put("provider", IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID); params.put("provider", IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID);
authMgmtResource.addExecution("firstBrokerLogin2", params); authMgmtResource.addExecution("firstBrokerLogin2", params);
assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.authAddExecutionPath("firstBrokerLogin2"), params, ResourceType.AUTH_EXECUTION); assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.authAddExecutionPath("firstBrokerLogin2"), params, ResourceType.AUTH_EXECUTION);
params.put("provider", IdpDetectExistingBrokerUserAuthenticatorFactory.PROVIDER_ID);
authMgmtResource.addExecution("firstBrokerLogin2", params);
assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.authAddExecutionPath("firstBrokerLogin2"), params, ResourceType.AUTH_EXECUTION);
List<AuthenticationExecutionInfoRepresentation> executionReps = authMgmtResource.getExecutions("firstBrokerLogin2"); List<AuthenticationExecutionInfoRepresentation> executionReps = authMgmtResource.getExecutions("firstBrokerLogin2");
AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider(IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID, executionReps); AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider(IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID, executionReps);
Assert.assertNotNull(exec); Assert.assertNotNull(exec);
executionId = exec.getId(); executionId = exec.getId();
exec = findExecutionByProvider(IdpDetectExistingBrokerUserAuthenticatorFactory.PROVIDER_ID, executionReps);
Assert.assertNotNull(exec);
executionId2 = exec.getId();
} }
@Test @Test
@ -72,9 +81,9 @@ public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
AuthenticatorConfigRepresentation cfg = newConfig("foo", IdpCreateUserIfUniqueAuthenticatorFactory.REQUIRE_PASSWORD_UPDATE_AFTER_REGISTRATION, "true"); AuthenticatorConfigRepresentation cfg = newConfig("foo", IdpCreateUserIfUniqueAuthenticatorFactory.REQUIRE_PASSWORD_UPDATE_AFTER_REGISTRATION, "true");
// Attempt to create config for non-existent execution // Attempt to create config for non-existent execution
Response response = authMgmtResource.newExecutionConfig("exec-id-doesnt-exists", cfg); try (Response response = authMgmtResource.newExecutionConfig("exec-id-doesnt-exists", cfg)) {
Assert.assertEquals(404, response.getStatus()); Assert.assertEquals(404, response.getStatus());
response.close(); }
// Create config success // Create config success
String cfgId = createConfig(executionId, cfg); String cfgId = createConfig(executionId, cfg);
@ -102,18 +111,14 @@ public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
public void testUpdateConfig() { public void testUpdateConfig() {
AuthenticatorConfigRepresentation cfg = newConfig("foo", IdpCreateUserIfUniqueAuthenticatorFactory.REQUIRE_PASSWORD_UPDATE_AFTER_REGISTRATION, "true"); AuthenticatorConfigRepresentation cfg = newConfig("foo", IdpCreateUserIfUniqueAuthenticatorFactory.REQUIRE_PASSWORD_UPDATE_AFTER_REGISTRATION, "true");
String cfgId = createConfig(executionId, cfg); String cfgId = createConfig(executionId, cfg);
AuthenticatorConfigRepresentation cfgRep = authMgmtResource.getAuthenticatorConfig(cfgId); final AuthenticatorConfigRepresentation cfgRepNonExistent = authMgmtResource.getAuthenticatorConfig(cfgId);
// Try to update not existent config // Try to update not existent config
try { NotFoundException nfe = Assert.assertThrows(NotFoundException.class, () -> authMgmtResource.updateAuthenticatorConfig("not-existent", cfgRepNonExistent));
authMgmtResource.updateAuthenticatorConfig("not-existent", cfgRep); Assert.assertEquals(404, nfe.getResponse().getStatus());
Assert.fail("Config didn't found");
} catch (NotFoundException nfe) {
// Expected
}
// Assert nothing changed // Assert nothing changed
cfgRep = authMgmtResource.getAuthenticatorConfig(cfgId); AuthenticatorConfigRepresentation cfgRep = authMgmtResource.getAuthenticatorConfig(cfgId);
assertConfig(cfgRep, cfgId, "foo", IdpCreateUserIfUniqueAuthenticatorFactory.REQUIRE_PASSWORD_UPDATE_AFTER_REGISTRATION, "true"); assertConfig(cfgRep, cfgId, "foo", IdpCreateUserIfUniqueAuthenticatorFactory.REQUIRE_PASSWORD_UPDATE_AFTER_REGISTRATION, "true");
// Update success // Update success
@ -141,26 +146,17 @@ public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID, authMgmtResource.getExecutions("firstBrokerLogin2")); IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID, authMgmtResource.getExecutions("firstBrokerLogin2"));
Assert.assertEquals(cfgRep.getId(), execution.getAuthenticationConfig()); Assert.assertEquals(cfgRep.getId(), execution.getAuthenticationConfig());
// Test remove not-existent // Test remove not-existent
try { NotFoundException nfe = Assert.assertThrows(NotFoundException.class, () -> authMgmtResource.removeAuthenticatorConfig("not-existent"));
authMgmtResource.removeAuthenticatorConfig("not-existent"); Assert.assertEquals(404, nfe.getResponse().getStatus());
Assert.fail("Config didn't found");
} catch (NotFoundException nfe) {
// Expected
}
// Test remove our config // Test remove our config
authMgmtResource.removeAuthenticatorConfig(cfgId); authMgmtResource.removeAuthenticatorConfig(cfgId);
assertAdminEvents.assertEvent(testRealmId, OperationType.DELETE, AdminEventPaths.authExecutionConfigPath(cfgId), ResourceType.AUTHENTICATOR_CONFIG); assertAdminEvents.assertEvent(testRealmId, OperationType.DELETE, AdminEventPaths.authExecutionConfigPath(cfgId), ResourceType.AUTHENTICATOR_CONFIG);
// Assert config not found // Assert config not found
try { nfe = Assert.assertThrows(NotFoundException.class, () -> authMgmtResource.getAuthenticatorConfig(cfgRep.getId()));
authMgmtResource.getAuthenticatorConfig(cfgRep.getId()); Assert.assertEquals(404, nfe.getResponse().getStatus());
Assert.fail("Not expected to find config");
} catch (NotFoundException nfe) {
// Expected
}
// Assert execution doesn't have our config // Assert execution doesn't have our config
execution = findExecutionByProvider( execution = findExecutionByProvider(
@ -178,13 +174,61 @@ public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
Assert.assertTrue(description.getProperties().isEmpty()); Assert.assertTrue(description.getProperties().isEmpty());
} }
@Test
public void testDuplicateAuthenticatorConfigAlias() {
// create a config for step1
AuthenticatorConfigRepresentation config1 = new AuthenticatorConfigRepresentation();
config1.setAlias("test-config-1");
config1.setConfig(Map.of("key", "value"));
String config1Id = createConfig(executionId, config1);
// create the same config name for step2, should fail
try (Response response = authMgmtResource.newExecutionConfig(executionId2, config1)) {
Assert.assertEquals(409, response.getStatus());
}
// create a config for step2
AuthenticatorConfigRepresentation config2 = new AuthenticatorConfigRepresentation();
config2.setAlias("test-config-2");
config2.setConfig(Map.of("key", "value"));
String config2Id = createConfig(executionId, config2);
// create a new config for step1, config1 should be removed
AuthenticatorConfigRepresentation config3 = new AuthenticatorConfigRepresentation();
config3.setAlias("test-config-1-modified");
config3.setConfig(Map.of("key", "value"));
String tmpConfig3Id = createConfig(executionId, config3);
NotFoundException nfe = Assert.assertThrows(NotFoundException.class, () -> authMgmtResource.getAuthenticatorConfig(config1Id));
Assert.assertEquals(404, nfe.getResponse().getStatus());
// create a new config with thew same name but that overwrites the previous one
String config3Id = createConfig(executionId, config3);
nfe = Assert.assertThrows(NotFoundException.class, () -> authMgmtResource.getAuthenticatorConfig(tmpConfig3Id));
Assert.assertEquals(404, nfe.getResponse().getStatus());
// delete execution step1, config3 should be removed
authMgmtResource.removeExecution(executionId);
assertAdminEvents.assertEvent(testRealmId, OperationType.DELETE, AdminEventPaths.authExecutionPath(executionId), ResourceType.AUTH_EXECUTION);
nfe = Assert.assertThrows(NotFoundException.class, () -> authMgmtResource.getAuthenticatorConfig(config3Id));
Assert.assertEquals(404, nfe.getResponse().getStatus());
// remove flow, config and exec for step2 should be removed
authMgmtResource.deleteFlow(flowId);
assertAdminEvents.assertEvent(testRealmId, OperationType.DELETE, AdminEventPaths.authFlowPath(flowId), ResourceType.AUTH_FLOW);
nfe = Assert.assertThrows(NotFoundException.class, () -> authMgmtResource.getExecution(executionId));
Assert.assertEquals(404, nfe.getResponse().getStatus());
nfe = Assert.assertThrows(NotFoundException.class, () -> authMgmtResource.getAuthenticatorConfig(config2Id));
Assert.assertEquals(404, nfe.getResponse().getStatus());
}
private String createConfig(String executionId, AuthenticatorConfigRepresentation cfg) { private String createConfig(String executionId, AuthenticatorConfigRepresentation cfg) {
Response resp = authMgmtResource.newExecutionConfig(executionId, cfg); try (Response resp = authMgmtResource.newExecutionConfig(executionId, cfg)) {
Assert.assertEquals(201, resp.getStatus()); Assert.assertEquals(201, resp.getStatus());
String cfgId = ApiUtil.getCreatedId(resp); String cfgId = ApiUtil.getCreatedId(resp);
Assert.assertNotNull(cfgId); Assert.assertNotNull(cfgId);
assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.authAddExecutionConfigPath(executionId), cfg, ResourceType.AUTH_EXECUTION); assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.authAddExecutionConfigPath(executionId), cfg, ResourceType.AUTHENTICATOR_CONFIG);
return cfgId; return cfgId;
}
} }
private AuthenticatorConfigRepresentation newConfig(String alias, String cfgKey, String cfgValue) { private AuthenticatorConfigRepresentation newConfig(String alias, String cfgKey, String cfgValue) {

View file

@ -736,7 +736,7 @@ public class FlowTest extends AbstractAuthenticationTest {
try (Response response = authMgmtResource.newExecutionConfig(executionWithConfig.getId(), executionConfig)) { try (Response response = authMgmtResource.newExecutionConfig(executionWithConfig.getId(), executionConfig)) {
getCleanup().addAuthenticationConfigId(ApiUtil.getCreatedId(response)); getCleanup().addAuthenticationConfigId(ApiUtil.getCreatedId(response));
assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.authAddExecutionConfigPath(executionWithConfig.getId()), executionConfig, ResourceType.AUTH_EXECUTION); assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.authAddExecutionConfigPath(executionWithConfig.getId()), executionConfig, ResourceType.AUTHENTICATOR_CONFIG);
} }
String newFlowName = "Duplicated of " + DefaultAuthenticationFlows.BROWSER_FLOW; String newFlowName = "Duplicated of " + DefaultAuthenticationFlows.BROWSER_FLOW;

View file

@ -395,7 +395,7 @@ public class AuthenticationMethodReferenceTest extends AbstractOIDCScopeTest{
if (execution.getAuthenticationConfig() == null){ if (execution.getAuthenticationConfig() == null){
// create config if it doesn't exist // create config if it doesn't exist
AuthenticatorConfigRepresentation config = new AuthenticatorConfigRepresentation(); AuthenticatorConfigRepresentation config = new AuthenticatorConfigRepresentation();
config.setAlias("test"); config.setAlias(KeycloakModelUtils.generateId());
config.setConfig(new HashMap<>(){{ config.setConfig(new HashMap<>(){{
put(AMR_VALUE_KEY, amrValue); put(AMR_VALUE_KEY, amrValue);
put(AMR_MAX_AGE_KEY, maxAge.toString()); put(AMR_MAX_AGE_KEY, maxAge.toString());