From dadb47060966ec524534219be933f1169dc25e3b Mon Sep 17 00:00:00 2001 From: Marko Strukelj Date: Thu, 28 Jan 2016 15:24:00 +0100 Subject: [PATCH] KEYCLOAK-1967 Add support for authentication flows into admin-rest-client --- ...AuthenticationExecutionRepresentation.java | 77 +++++ ...ticationExecutionExportRepresentation.java | 50 ++++ ...enticationExecutionInfoRepresentation.java | 128 ++++++++ ...AuthenticationExecutionRepresentation.java | 87 ++---- .../idm/AuthenticationFlowRepresentation.java | 15 +- ...AuthenticatorConfigInfoRepresentation.java | 65 ++++ .../AuthenticationManagementResource.java | 194 ++++++++++++ .../admin/client/resource/RealmResource.java | 4 + .../models/utils/ModelToRepresentation.java | 9 +- .../models/utils/RepresentationToModel.java | 30 +- .../AuthenticationManagementResource.java | 279 ++++-------------- .../AbstractAuthenticationTest.java | 151 ++++++++++ .../authentication/ExecutionTest.java | 173 +++++++++++ .../testsuite/authentication/FlowTest.java | 128 ++++++++ .../authentication/InitialFlowsTest.java | 200 +++++++++++++ .../authentication/InitialProvidersTest.java | 101 +++++++ .../authentication/ShiftExecutionTest.java | 81 +++++ 17 files changed, 1463 insertions(+), 309 deletions(-) create mode 100644 core/src/main/java/org/keycloak/representations/idm/AbstractAuthenticationExecutionRepresentation.java create mode 100755 core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionExportRepresentation.java create mode 100755 core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionInfoRepresentation.java mode change 100755 => 100644 core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionRepresentation.java create mode 100644 core/src/main/java/org/keycloak/representations/idm/AuthenticatorConfigInfoRepresentation.java create mode 100644 integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthenticationManagementResource.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/AbstractAuthenticationTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/ExecutionTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/FlowTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/InitialFlowsTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/InitialProvidersTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/ShiftExecutionTest.java diff --git a/core/src/main/java/org/keycloak/representations/idm/AbstractAuthenticationExecutionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AbstractAuthenticationExecutionRepresentation.java new file mode 100644 index 0000000000..8cd4f285b5 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/AbstractAuthenticationExecutionRepresentation.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.idm; + +import java.io.Serializable; + +/** + * @author Marko Strukelj + */ +public class AbstractAuthenticationExecutionRepresentation implements Serializable { + + private String authenticatorConfig; + private String authenticator; + private boolean authenticatorFlow; + private String requirement; + private int priority; + + public String getAuthenticatorConfig() { + return authenticatorConfig; + } + + public void setAuthenticatorConfig(String authenticatorConfig) { + this.authenticatorConfig = authenticatorConfig; + } + + public String getAuthenticator() { + return authenticator; + } + + public void setAuthenticator(String authenticator) { + this.authenticator = authenticator; + } + + public String getRequirement() { + return requirement; + } + + public void setRequirement(String requirement) { + this.requirement = requirement; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + /** + * Is the referenced authenticator a flow? + * + * @return + */ + public boolean isAutheticatorFlow() { + return authenticatorFlow; + } + + public void setAutheticatorFlow(boolean autheticatorFlow) { + this.authenticatorFlow = autheticatorFlow; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionExportRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionExportRepresentation.java new file mode 100755 index 0000000000..74a8358250 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionExportRepresentation.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.idm; + +/** +* @author Bill Burke +* @version $Revision: 1 $ +*/ +public class AuthenticationExecutionExportRepresentation extends AbstractAuthenticationExecutionRepresentation { + + private String flowAlias; + private boolean userSetupAllowed; + + + public boolean isUserSetupAllowed() { + return userSetupAllowed; + } + + public void setUserSetupAllowed(boolean userSetupAllowed) { + this.userSetupAllowed = userSetupAllowed; + } + + /** + * If this execution is a flow, this is the flowId pointing to an AuthenticationFlowModel + * + * @return + */ + public String getFlowAlias() { + return flowAlias; + } + + public void setFlowAlias(String flowId) { + this.flowAlias = flowId; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionInfoRepresentation.java new file mode 100755 index 0000000000..1ff76a6465 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionInfoRepresentation.java @@ -0,0 +1,128 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.idm; + +import java.io.Serializable; +import java.util.List; + +/** +* @author Bill Burke +* @version $Revision: 1 $ +*/ +public class AuthenticationExecutionInfoRepresentation implements Serializable { + + protected String id; + protected String requirement; + protected String displayName; + protected List requirementChoices; + protected Boolean configurable; + protected Boolean authenticationFlow; + protected String providerId; + protected String authenticationConfig; + protected String flowId; + protected int level; + protected int index; + + public String getId() { + return id; + } + + public void setId(String execution) { + this.id = execution; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getRequirement() { + return requirement; + } + + public void setRequirement(String requirement) { + this.requirement = requirement; + } + + public List getRequirementChoices() { + return requirementChoices; + } + + public void setRequirementChoices(List requirementChoices) { + this.requirementChoices = requirementChoices; + } + + public Boolean getConfigurable() { + return configurable; + } + + public void setConfigurable(Boolean configurable) { + this.configurable = configurable; + } + + public String getProviderId() { + return providerId; + } + + public void setProviderId(String providerId) { + this.providerId = providerId; + } + + public String getAuthenticationConfig() { + return authenticationConfig; + } + + public void setAuthenticationConfig(String authenticationConfig) { + this.authenticationConfig = authenticationConfig; + } + + public Boolean getAuthenticationFlow() { + return authenticationFlow; + } + + public void setAuthenticationFlow(Boolean authenticationFlow) { + this.authenticationFlow = authenticationFlow; + } + + public int getLevel() { + return level; + } + + public void setLevel(int level) { + this.level = level; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public String getFlowId() { + return flowId; + } + + public void setFlowId(String flowId) { + this.flowId = flowId; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionRepresentation.java old mode 100755 new mode 100644 index 7b5ee7655f..532166c291 --- a/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/AuthenticationExecutionRepresentation.java @@ -17,87 +17,36 @@ package org.keycloak.representations.idm; -import java.io.Serializable; -import java.util.Comparator; - /** -* @author Bill Burke -* @version $Revision: 1 $ -*/ -public class AuthenticationExecutionRepresentation implements Serializable { + * @author Marko Strukelj + */ +public class AuthenticationExecutionRepresentation extends AbstractAuthenticationExecutionRepresentation { - private String authenticatorConfig; - private String authenticator; - private String flowAlias; - private boolean autheticatorFlow; - private String requirement; - private boolean userSetupAllowed; - private int priority; + private String id; + private String flowId; + private String parentFlow; - public String getAuthenticatorConfig() { - return authenticatorConfig; + public String getId() { + return id; } - public void setAuthenticatorConfig(String authenticatorConfig) { - this.authenticatorConfig = authenticatorConfig; + public void setId(String id) { + this.id = id; } - public String getAuthenticator() { - return authenticator; + public String getFlowId() { + return flowId; } - public void setAuthenticator(String authenticator) { - this.authenticator = authenticator; + public void setFlowId(String flowId) { + this.flowId = flowId; } - public String getRequirement() { - return requirement; + public String getParentFlow() { + return parentFlow; } - public void setRequirement(String requirement) { - this.requirement = requirement; + public void setParentFlow(String parentFlow) { + this.parentFlow = parentFlow; } - - public int getPriority() { - return priority; - } - - public void setPriority(int priority) { - this.priority = priority; - } - - public boolean isUserSetupAllowed() { - return userSetupAllowed; - } - - public void setUserSetupAllowed(boolean userSetupAllowed) { - this.userSetupAllowed = userSetupAllowed; - } - - /** - * If this execution is a flow, this is the flowId pointing to an AuthenticationFlowModel - * - * @return - */ - public String getFlowAlias() { - return flowAlias; - } - - public void setFlowAlias(String flowId) { - this.flowAlias = flowId; - } - - /** - * Is the referenced authenticator a flow? - * - * @return - */ - public boolean isAutheticatorFlow() { - return autheticatorFlow; - } - - public void setAutheticatorFlow(boolean autheticatorFlow) { - this.autheticatorFlow = autheticatorFlow; - } - } diff --git a/core/src/main/java/org/keycloak/representations/idm/AuthenticationFlowRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AuthenticationFlowRepresentation.java index 0e7e4fb8cb..ee58fc4b7b 100755 --- a/core/src/main/java/org/keycloak/representations/idm/AuthenticationFlowRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/AuthenticationFlowRepresentation.java @@ -26,12 +26,21 @@ import java.util.List; */ public class AuthenticationFlowRepresentation implements Serializable { + private String id; private String alias; private String description; private String providerId; private boolean topLevel; private boolean builtIn; - protected List authenticationExecutions; + protected List authenticationExecutions; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } public String getAlias() { return alias; @@ -73,11 +82,11 @@ public class AuthenticationFlowRepresentation implements Serializable { this.builtIn = builtIn; } - public List getAuthenticationExecutions() { + public List getAuthenticationExecutions() { return authenticationExecutions; } - public void setAuthenticationExecutions(List authenticationExecutions) { + public void setAuthenticationExecutions(List authenticationExecutions) { this.authenticationExecutions = authenticationExecutions; } } diff --git a/core/src/main/java/org/keycloak/representations/idm/AuthenticatorConfigInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AuthenticatorConfigInfoRepresentation.java new file mode 100644 index 0000000000..a26998b214 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/AuthenticatorConfigInfoRepresentation.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.idm; + +import java.util.List; + +/** + * @author Marko Strukelj + */ +public class AuthenticatorConfigInfoRepresentation { + + protected String name; + protected String providerId; + protected String helpText; + + protected List properties; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHelpText() { + return helpText; + } + + public String getProviderId() { + return providerId; + } + + public void setProviderId(String providerId) { + this.providerId = providerId; + } + + public void setHelpText(String helpText) { + this.helpText = helpText; + } + + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } +} + diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthenticationManagementResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthenticationManagementResource.java new file mode 100644 index 0000000000..197aeb69d9 --- /dev/null +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthenticationManagementResource.java @@ -0,0 +1,194 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.admin.client.resource; + +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigInfoRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; +import org.keycloak.representations.idm.ConfigPropertyRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Map; + +/** + * @author Marko Strukelj + */ +public interface AuthenticationManagementResource { + + @GET + @Path("/form-providers") + @Produces(MediaType.APPLICATION_JSON) + List> getFormProviders(); + + @Path("/authenticator-providers") + @GET + @Produces(MediaType.APPLICATION_JSON) + List> getAuthenticatorProviders(); + + @Path("/client-authenticator-providers") + @GET + @Produces(MediaType.APPLICATION_JSON) + List> getClientAuthenticatorProviders(); + + @Path("/form-action-providers") + @GET + @Produces(MediaType.APPLICATION_JSON) + List> getFormActionProviders(); + + @Path("/flows") + @GET + @Produces(MediaType.APPLICATION_JSON) + List getFlows(); + + @Path("/flows") + @POST + @Consumes(MediaType.APPLICATION_JSON) + Response createFlow(AuthenticationFlowRepresentation model); + + @Path("/flows/{id}") + @GET + @Produces(MediaType.APPLICATION_JSON) + AuthenticationFlowRepresentation getFlow(@PathParam("id") String id); + + @Path("/flows/{id}") + @DELETE + void deleteFlow(@PathParam("id") String id); + + @Path("/flows/{flowAlias}/copy") + @POST + @Consumes(MediaType.APPLICATION_JSON) + Response copy(@PathParam("flowAlias") String flowAlias, Map data); + + @Path("/flows/{flowAlias}/executions/flow") + @POST + @Consumes(MediaType.APPLICATION_JSON) + void addExecutionFlow(@PathParam("flowAlias") String flowAlias, Map data); + + @Path("/flows/{flowAlias}/executions/execution") + @POST + @Consumes(MediaType.APPLICATION_JSON) + void addExecution(@PathParam("flowAlias") String flowAlias, Map data); + + @Path("/flows/{flowAlias}/executions") + @GET + @Produces(MediaType.APPLICATION_JSON) + Response getExecutions(@PathParam("flowAlias") String flowAlias); + + @Path("/flows/{flowAlias}/executions") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + void updateExecutions(@PathParam("flowAlias") String flowAlias, AuthenticationExecutionInfoRepresentation rep); + + @Path("/executions") + @POST + @Consumes(MediaType.APPLICATION_JSON) + Response addExecution(AuthenticationExecutionRepresentation model); + + @Path("/executions/{executionId}/raise-priority") + @POST + void raisePriority(@PathParam("executionId") String execution); + + @Path("/executions/{executionId}/lower-priority") + @POST + void lowerPriority(@PathParam("executionId") String execution); + + @Path("/executions/{executionId}") + @DELETE + void removeExecution(@PathParam("executionId") String execution); + + @Path("/executions/{executionId}/config") + @POST + @Consumes(MediaType.APPLICATION_JSON) + Response newExecutionConfig(@PathParam("executionId") String execution, AuthenticatorConfigRepresentation config); + + @Path("/executions/{executionId}/config/{id}") + @GET + @Produces(MediaType.APPLICATION_JSON) + AuthenticatorConfigRepresentation getAuthenticatorConfig(@PathParam("executionId") String execution,@PathParam("id") String id); + + @Path("unregistered-required-actions") + @GET + @Produces(MediaType.APPLICATION_JSON) + List> getUnregisteredRequiredActions(); + + @Path("register-required-action") + @POST + @Consumes(MediaType.APPLICATION_JSON) + void registereRequiredAction(Map data); + + @Path("required-actions") + @GET + @Produces(MediaType.APPLICATION_JSON) + List getRequiredActions(); + + @Path("required-actions/{alias}") + @GET + @Produces(MediaType.APPLICATION_JSON) + RequiredActionProviderRepresentation getRequiredAction(@PathParam("alias") String alias); + + @Path("required-actions/{alias}") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + void updateRequiredAction(@PathParam("alias") String alias, RequiredActionProviderRepresentation rep); + + @Path("required-actions/{alias}") + @DELETE + void updateRequiredAction(@PathParam("alias") String alias); + + @Path("config-description/{providerId}") + @GET + @Produces(MediaType.APPLICATION_JSON) + AuthenticatorConfigInfoRepresentation getAuthenticatorConfigDescription(@PathParam("providerId") String providerId); + + @Path("per-client-config-description") + @GET + @Produces(MediaType.APPLICATION_JSON) + Map> getPerClientConfigDescription(); + + @Path("config") + @POST + @Consumes(MediaType.APPLICATION_JSON) + Response createAuthenticatorConfig(AuthenticatorConfigRepresentation config); + + @Path("config/{id}") + @GET + @Produces(MediaType.APPLICATION_JSON) + AuthenticatorConfigRepresentation getAuthenticatorConfig(@PathParam("id") String id); + + @Path("config/{id}") + @DELETE + void removeAuthenticatorConfig(@PathParam("id") String id); + + @Path("config/{id}") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + void updateAuthenticatorConfig(@PathParam("id") String id, AuthenticatorConfigRepresentation config); +} diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index 87f6763c1f..498b3acd9f 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -142,4 +142,8 @@ public interface RealmResource { @Path("clients-initial-access") ClientInitialAccessResource clientInitialAccess(); + @Path("authentication") + @Consumes(MediaType.APPLICATION_JSON) + AuthenticationManagementResource flows(); + } diff --git a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 30f7d5f1ae..8b59053102 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -46,7 +46,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.AuthDetailsRepresentation; -import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; +import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.ClientRepresentation; @@ -680,12 +680,13 @@ public class ModelToRepresentation { public static AuthenticationFlowRepresentation toRepresentation(RealmModel realm, AuthenticationFlowModel model) { AuthenticationFlowRepresentation rep = new AuthenticationFlowRepresentation(); + rep.setId(model.getId()); rep.setBuiltIn(model.isBuiltIn()); rep.setTopLevel(model.isTopLevel()); rep.setProviderId(model.getProviderId()); rep.setAlias(model.getAlias()); rep.setDescription(model.getDescription()); - rep.setAuthenticationExecutions(new LinkedList()); + rep.setAuthenticationExecutions(new LinkedList()); for (AuthenticationExecutionModel execution : realm.getAuthenticationExecutions(model.getId())) { rep.getAuthenticationExecutions().add(toRepresentation(realm, execution)); } @@ -693,8 +694,8 @@ public class ModelToRepresentation { } - public static AuthenticationExecutionRepresentation toRepresentation(RealmModel realm, AuthenticationExecutionModel model) { - AuthenticationExecutionRepresentation rep = new AuthenticationExecutionRepresentation(); + public static AuthenticationExecutionExportRepresentation toRepresentation(RealmModel realm, AuthenticationExecutionModel model) { + AuthenticationExecutionExportRepresentation rep = new AuthenticationExecutionExportRepresentation(); if (model.getAuthenticatorConfig() != null) { AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(model.getAuthenticatorConfig()); rep.setAuthenticatorConfig(config.getAlias()); diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index f144c28fc2..c9d89b462a 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -48,7 +48,9 @@ import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; +import org.keycloak.common.util.UriUtils; import org.keycloak.representations.idm.ApplicationRepresentation; +import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; @@ -65,13 +67,13 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.ScopeMappingRepresentation; import org.keycloak.representations.idm.SocialLinkRepresentation; import org.keycloak.representations.idm.UserConsentRepresentation; import org.keycloak.representations.idm.UserFederationMapperRepresentation; import org.keycloak.representations.idm.UserFederationProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.common.util.UriUtils; import java.io.IOException; import java.util.ArrayList; @@ -83,7 +85,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; -import org.keycloak.representations.idm.RolesRepresentation; public class RepresentationToModel { @@ -442,11 +443,13 @@ public class RepresentationToModel { } for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) { AuthenticationFlowModel model = toModel(flowRep); + // make sure new id is generated for new AuthenticationFlowModel instance + model.setId(null); model = newRealm.addAuthenticationFlow(model); } for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) { AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias()); - for (AuthenticationExecutionRepresentation exeRep : flowRep.getAuthenticationExecutions()) { + for (AuthenticationExecutionExportRepresentation exeRep : flowRep.getAuthenticationExecutions()) { AuthenticationExecutionModel execution = toModel(newRealm, exeRep); execution.setParentFlow(model.getId()); newRealm.addAuthenticatorExecution(execution); @@ -1464,6 +1467,7 @@ public class RepresentationToModel { public static AuthenticationFlowModel toModel(AuthenticationFlowRepresentation rep) { AuthenticationFlowModel model = new AuthenticationFlowModel(); + model.setId(rep.getId()); model.setBuiltIn(rep.isBuiltIn()); model.setTopLevel(rep.isTopLevel()); model.setProviderId(rep.getProviderId()); @@ -1473,7 +1477,7 @@ public class RepresentationToModel { } - public static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationExecutionRepresentation rep) { + public static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationExecutionExportRepresentation rep) { AuthenticationExecutionModel model = new AuthenticationExecutionModel(); if (rep.getAuthenticatorConfig() != null) { AuthenticatorConfigModel config = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig()); @@ -1490,6 +1494,24 @@ public class RepresentationToModel { return model; } + public static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationExecutionRepresentation rep) { + AuthenticationExecutionModel model = new AuthenticationExecutionModel(); + model.setId(rep.getId()); + model.setFlowId(rep.getFlowId()); + + model.setAuthenticator(rep.getAuthenticator()); + model.setPriority(rep.getPriority()); + model.setParentFlow(rep.getParentFlow()); + model.setAuthenticatorFlow(rep.isAutheticatorFlow()); + model.setRequirement(AuthenticationExecutionModel.Requirement.valueOf(rep.getRequirement())); + + if (rep.getAuthenticatorConfig() != null) { + AuthenticatorConfigModel cfg = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig()); + model.setAuthenticatorConfig(cfg.getId()); + } + return model; + } + public static AuthenticatorConfigModel toModel(AuthenticatorConfigRepresentation rep) { AuthenticatorConfigModel model = new AuthenticatorConfigModel(); model.setAlias(rep.getAlias()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java index cf7024fc7d..6c9bba991c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java @@ -34,9 +34,17 @@ import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderFactory; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigInfoRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.ConfigPropertyRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.ServicesLogger; import org.keycloak.utils.CredentialHelper; @@ -83,108 +91,6 @@ public class AuthenticationManagementResource { this.adminEvent = adminEvent; } - public static class AuthenticationExecutionRepresentation { - protected String id; - protected String requirement; - protected String displayName; - protected List requirementChoices; - protected Boolean configurable; - protected Boolean authenticationFlow; - protected String providerId; - protected String authenticationConfig; - protected String flowId; - protected int level; - protected int index; - - public String getId() { - return id; - } - - public void setId(String execution) { - this.id = execution; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - public String getRequirement() { - return requirement; - } - - public void setRequirement(String requirement) { - this.requirement = requirement; - } - - public List getRequirementChoices() { - return requirementChoices; - } - - public void setRequirementChoices(List requirementChoices) { - this.requirementChoices = requirementChoices; - } - - public Boolean getConfigurable() { - return configurable; - } - - public void setConfigurable(Boolean configurable) { - this.configurable = configurable; - } - - public String getProviderId() { - return providerId; - } - - public void setProviderId(String providerId) { - this.providerId = providerId; - } - - public String getAuthenticationConfig() { - return authenticationConfig; - } - - public void setAuthenticationConfig(String authenticationConfig) { - this.authenticationConfig = authenticationConfig; - } - - public Boolean getAuthenticationFlow() { - return authenticationFlow; - } - - public void setAuthenticationFlow(Boolean authenticationFlow) { - this.authenticationFlow = authenticationFlow; - } - - public int getLevel() { - return level; - } - - public void setLevel(int level) { - this.level = level; - } - - public int getIndex() { - return index; - } - - public void setIndex(int index) { - this.index = index; - } - - public String getFlowId() { - return flowId; - } - - public void setFlowId(String flowId) { - this.flowId = flowId; - } - } - /** * Get form providers * @@ -269,12 +175,12 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List getFlows() { + public List getFlows() { this.auth.requireView(); - List flows = new LinkedList<>(); + List flows = new LinkedList<>(); for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) { if (flow.isTopLevel()) { - flows.add(flow); + flows.add(ModelToRepresentation.toRepresentation(realm, flow)); } } return flows; @@ -283,27 +189,26 @@ public class AuthenticationManagementResource { /** * Create a new authentication flow * - * @param model Authentication flow model + * @param flow Authentication flow representation * @return */ @Path("/flows") @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) - public Response createFlow(AuthenticationFlowModel model) { + public Response createFlow(AuthenticationFlowRepresentation flow) { this.auth.requireManage(); - if (model.getAlias() == null || model.getAlias().isEmpty()) { + if (flow.getAlias() == null || flow.getAlias().isEmpty()) { return ErrorResponse.exists("Failed to create flow with empty alias name"); } - if (realm.getFlowByAlias(model.getAlias()) != null) { - return ErrorResponse.exists("Flow " + model.getAlias() + " already exists"); + if (realm.getFlowByAlias(flow.getAlias()) != null) { + return ErrorResponse.exists("Flow " + flow.getAlias() + " already exists"); } - realm.addAuthenticationFlow(model); + realm.addAuthenticationFlow(RepresentationToModel.toModel(flow)); return Response.status(201).build(); - } /** @@ -316,14 +221,14 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public AuthenticationFlowModel getFlow(@PathParam("id") String id) { + public AuthenticationFlowRepresentation getFlow(@PathParam("id") String id) { this.auth.requireView(); AuthenticationFlowModel flow = realm.getAuthenticationFlowById(id); if (flow == null) { throw new NotFoundException("Could not find flow with id"); } - return flow; + return ModelToRepresentation.toRepresentation(realm, flow); } /** @@ -478,8 +383,16 @@ public class AuthenticationManagementResource { if (parentFlow == null) { throw new BadRequestException("Parent flow doesn't exists"); } + if (parentFlow.isBuiltIn()) { + throw new BadRequestException("It is illegal to add execution to a built in flow"); + } String provider = data.get("provider"); + // make sure provider is one of the registered providers + ProviderFactory f = session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, provider); + if (f == null) { + throw new BadRequestException("No authentication provider found for id: " + provider); + } AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); execution.setParentFlow(parentFlow.getId()); execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED); @@ -507,7 +420,7 @@ public class AuthenticationManagementResource { logger.debug("flow not found: " + flowAlias); return Response.status(NOT_FOUND).build(); } - List result = new LinkedList<>(); + List result = new LinkedList<>(); int level = 0; @@ -515,11 +428,11 @@ public class AuthenticationManagementResource { return Response.ok(result).build(); } - public void recurseExecutions(AuthenticationFlowModel flow, List result, int level) { + public void recurseExecutions(AuthenticationFlowModel flow, List result, int level) { int index = 0; List executions = realm.getAuthenticationExecutions(flow.getId()); for (AuthenticationExecutionModel execution : executions) { - AuthenticationExecutionRepresentation rep = new AuthenticationExecutionRepresentation(); + AuthenticationExecutionInfoRepresentation rep = new AuthenticationExecutionInfoRepresentation(); rep.setLevel(level); rep.setIndex(index++); rep.setRequirementChoices(new LinkedList()); @@ -575,7 +488,7 @@ public class AuthenticationManagementResource { @PUT @NoCache @Consumes(MediaType.APPLICATION_JSON) - public void updateExecutions(@PathParam("flowAlias") String flowAlias, AuthenticationExecutionRepresentation rep) { + public void updateExecutions(@PathParam("flowAlias") String flowAlias, AuthenticationExecutionInfoRepresentation rep) { this.auth.requireManage(); AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias); @@ -599,14 +512,15 @@ public class AuthenticationManagementResource { /** * Add new authentication execution * - * @param model JSON model describing authentication execution + * @param execution JSON model describing authentication execution */ @Path("/executions") @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) - public Response addExecution(AuthenticationExecutionModel model) { + public Response addExecution(AuthenticationExecutionRepresentation execution) { this.auth.requireManage(); + AuthenticationExecutionModel model = RepresentationToModel.toModel(realm, execution); AuthenticationFlowModel parentFlow = getParentFlow(model); if (parentFlow.isBuiltIn()) { throw new BadRequestException("It is illegal to add execution to a built in flow"); @@ -745,14 +659,14 @@ public class AuthenticationManagementResource { * Update execution with new configuration * * @param execution Execution id - * @param config JSON with new configuration + * @param json JSON with new configuration * @return */ @Path("/executions/{executionId}/config") @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) - public Response newExecutionConfig(@PathParam("executionId") String execution, AuthenticatorConfigModel config) { + public Response newExecutionConfig(@PathParam("executionId") String execution, AuthenticatorConfigRepresentation json) { this.auth.requireManage(); AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution); @@ -761,6 +675,7 @@ public class AuthenticationManagementResource { throw new NotFoundException("Illegal execution"); } + AuthenticatorConfigModel config = RepresentationToModel.toModel(json); config = realm.addAuthenticatorConfig(config); model.setAuthenticatorConfig(config.getId()); realm.updateAuthenticatorExecution(model); @@ -777,63 +692,14 @@ public class AuthenticationManagementResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public AuthenticatorConfigModel getAuthenticatorConfig(@PathParam("executionId") String execution,@PathParam("id") String id) { + public AuthenticatorConfigRepresentation getAuthenticatorConfig(@PathParam("executionId") String execution,@PathParam("id") String id) { this.auth.requireView(); AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(id); if (config == null) { throw new NotFoundException("Could not find authenticator config"); } - return config; - } - - - public static class RequiredActionProviderRepresentation { - private String alias; - private String name; - private boolean enabled; - private boolean defaultAction; - private Map config = new HashMap(); - - public String getAlias() { - return alias; - } - - public void setAlias(String alias) { - this.alias = alias; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public boolean isDefaultAction() { - return defaultAction; - } - - public void setDefaultAction(boolean defaultAction) { - this.defaultAction = defaultAction; - } - - public Map getConfig() { - return config; - } - - public void setConfig(Map config) { - this.config = config; - } + return ModelToRepresentation.toRepresentation(config); } /** @@ -976,47 +842,6 @@ public class AuthenticationManagementResource { realm.removeRequiredActionProvider(model); } - public class AuthenticatorConfigDescription { - protected String name; - protected String providerId; - protected String helpText; - - protected List properties; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getHelpText() { - return helpText; - } - - public String getProviderId() { - return providerId; - } - - public void setProviderId(String providerId) { - this.providerId = providerId; - } - - public void setHelpText(String helpText) { - this.helpText = helpText; - } - - public List getProperties() { - return properties; - } - - public void setProperties(List properties) { - this.properties = properties; - } - } - - /** * Get authenticator provider's configuration description */ @@ -1024,13 +849,13 @@ public class AuthenticationManagementResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public AuthenticatorConfigDescription getAuthenticatorConfigDescription(@PathParam("providerId") String providerId) { + public AuthenticatorConfigInfoRepresentation getAuthenticatorConfigDescription(@PathParam("providerId") String providerId) { this.auth.requireView(); ConfigurableAuthenticatorFactory factory = CredentialHelper.getConfigurableAuthenticatorFactory(session, providerId); if (factory == null) { throw new NotFoundException("Could not find authenticator provider"); } - AuthenticatorConfigDescription rep = new AuthenticatorConfigDescription(); + AuthenticatorConfigInfoRepresentation rep = new AuthenticatorConfigInfoRepresentation(); rep.setProviderId(providerId); rep.setName(factory.getDisplayType()); rep.setHelpText(factory.getHelpText()); @@ -1084,14 +909,14 @@ public class AuthenticationManagementResource { /** * Create new authenticator configuration - * @param config JSON describing new authenticator configuration + * @param rep JSON describing new authenticator configuration */ @Path("config") @POST @NoCache - public Response createAuthenticatorConfig(AuthenticatorConfigModel config) { + public Response createAuthenticatorConfig(AuthenticatorConfigRepresentation rep) { this.auth.requireManage(); - config = realm.addAuthenticatorConfig(config); + AuthenticatorConfigModel config = realm.addAuthenticatorConfig(RepresentationToModel.toModel(rep)); return Response.created(uriInfo.getAbsolutePathBuilder().path(config.getId()).build()).build(); } @@ -1103,14 +928,14 @@ public class AuthenticationManagementResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public AuthenticatorConfigModel getAuthenticatorConfig(@PathParam("id") String id) { + public AuthenticatorConfigRepresentation getAuthenticatorConfig(@PathParam("id") String id) { this.auth.requireView(); AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(id); if (config == null) { throw new NotFoundException("Could not find authenticator config"); } - return config; + return ModelToRepresentation.toRepresentation(config); } /** @@ -1143,25 +968,21 @@ public class AuthenticationManagementResource { /** * Update authenticator configuration * @param id Configuration id - * @param config JSON describing new state of authenticator configuration + * @param rep JSON describing new state of authenticator configuration */ @Path("config/{id}") @PUT @Consumes(MediaType.APPLICATION_JSON) @NoCache - public void updateAuthenticatorConfig(@PathParam("id") String id, AuthenticatorConfigModel config) { + public void updateAuthenticatorConfig(@PathParam("id") String id, AuthenticatorConfigRepresentation rep) { this.auth.requireManage(); AuthenticatorConfigModel exists = realm.getAuthenticatorConfigById(id); if (exists == null) { throw new NotFoundException("Could not find authenticator config"); } - exists.setAlias(config.getAlias()); - exists.setConfig(config.getConfig()); + exists.setAlias(rep.getAlias()); + exists.setConfig(rep.getConfig()); realm.updateAuthenticatorConfig(exists); } - - - - } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/AbstractAuthenticationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/AbstractAuthenticationTest.java new file mode 100644 index 0000000000..4911793566 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/AbstractAuthenticationTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.authentication; + +import org.junit.Assert; +import org.junit.Before; +import org.keycloak.admin.client.resource.AuthenticationManagementResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; + + +/** + * @author Marko Strukelj + */ +public abstract class AbstractAuthenticationTest extends AbstractKeycloakTest { + + static final String REALM_NAME = "test"; + + static final String REQUIRED = "REQUIRED"; + static final String OPTIONAL = "OPTIONAL"; + static final String DISABLED = "DISABLED"; + static final String ALTERNATIVE = "ALTERNATIVE"; + + RealmResource realmResource; + AuthenticationManagementResource authMgmtResource; + + @Before + public void before() { + realmResource = adminClient.realms().realm(REALM_NAME); + authMgmtResource = realmResource.flows(); + } + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation testRealmRep = new RealmRepresentation(); + testRealmRep.setRealm(REALM_NAME); + testRealmRep.setEnabled(true); + testRealms.add(testRealmRep); + } + + + AuthenticationExecutionInfoRepresentation findExecutionByProvider(String provider, List executions) { + for (AuthenticationExecutionInfoRepresentation exec : executions) { + if (provider.equals(exec.getProviderId())) { + return exec; + } + } + return null; + } + + + AuthenticationFlowRepresentation findFlowByAlias(String alias, List flows) { + for (AuthenticationFlowRepresentation flow : flows) { + if (alias.equals(flow.getAlias())) { + return flow; + } + } + return null; + } + + void compareExecution(AuthenticationExecutionInfoRepresentation expected, AuthenticationExecutionInfoRepresentation actual) { + Assert.assertEquals("Execution requirement - " + actual.getProviderId(), expected.getRequirement(), actual.getRequirement()); + Assert.assertEquals("Execution display name - " + actual.getProviderId(), expected.getDisplayName(), actual.getDisplayName()); + Assert.assertEquals("Execution configurable - " + actual.getProviderId(), expected.getConfigurable(), actual.getConfigurable()); + Assert.assertEquals("Execution provider id - " + actual.getProviderId(), expected.getProviderId(), actual.getProviderId()); + Assert.assertEquals("Execution level - " + actual.getProviderId(), expected.getLevel(), actual.getLevel()); + Assert.assertEquals("Execution index - " + actual.getProviderId(), expected.getIndex(), actual.getIndex()); + Assert.assertEquals("Execution authentication flow - " + actual.getProviderId(), expected.getAuthenticationFlow(), actual.getAuthenticationFlow()); + Assert.assertEquals("Execution requirement choices - " + actual.getProviderId(), expected.getRequirementChoices(), actual.getRequirementChoices()); + } + + void compareFlows(AuthenticationFlowRepresentation expected, AuthenticationFlowRepresentation actual) { + Assert.assertEquals("Flow alias", expected.getAlias(), actual.getAlias()); + Assert.assertEquals("Flow description", expected.getDescription(), actual.getDescription()); + Assert.assertEquals("Flow providerId", expected.getProviderId(), actual.getProviderId()); + Assert.assertEquals("Flow top level", expected.isTopLevel(), actual.isTopLevel()); + Assert.assertEquals("Flow built-in", expected.isBuiltIn(), actual.isBuiltIn()); + } + + + AuthenticationFlowRepresentation newFlow(String alias, String description, + String providerId, boolean topLevel, boolean builtIn) { + AuthenticationFlowRepresentation flow = new AuthenticationFlowRepresentation(); + flow.setAlias(alias); + flow.setDescription(description); + flow.setProviderId(providerId); + flow.setTopLevel(topLevel); + flow.setBuiltIn(builtIn); + return flow; + } + + AuthenticationExecutionInfoRepresentation newExecution(String displayName, String providerId, Boolean configurable, + int level, int index, String requirement, Boolean authFlow, String[] choices) { + + AuthenticationExecutionInfoRepresentation execution = new AuthenticationExecutionInfoRepresentation(); + execution.setRequirement(requirement); + execution.setDisplayName(displayName); + execution.setConfigurable(configurable); + execution.setProviderId(providerId); + execution.setLevel(level); + execution.setIndex(index); + execution.setAuthenticationFlow(authFlow); + if (choices != null) { + execution.setRequirementChoices(Arrays.asList(choices)); + } + return execution; + } + + + AuthenticatorConfigRepresentation newConfig(String alias, String[] keyvalues) { + AuthenticatorConfigRepresentation config = new AuthenticatorConfigRepresentation(); + config.setAlias(alias); + + if (keyvalues == null) { + throw new IllegalArgumentException("keyvalues == null"); + } + if (keyvalues.length % 2 != 0) { + throw new IllegalArgumentException("keyvalues should have even number of elements"); + } + + LinkedHashMap params = new LinkedHashMap<>(); + for (int i = 0; i < keyvalues.length; i += 2) { + params.put(keyvalues[i], keyvalues[i + 1]); + } + config.setConfig(params); + return config; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/ExecutionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/ExecutionTest.java new file mode 100644 index 0000000000..5c76c5801e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/ExecutionTest.java @@ -0,0 +1,173 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.authentication; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.List; + +/** + * @author Marko Strukelj + */ +public class ExecutionTest extends AbstractAuthenticationTest { + + @Test + public void testAddRemoveExecution() { + + // try add execution to built-in flow + HashMap params = new HashMap<>(); + params.put("provider", "idp-review-profile"); + try { + authMgmtResource.addExecution("browser", params); + Assert.fail("add execution to built-in flow should fail"); + } catch (BadRequestException expected) { + } + + // copy built-in flow so we get a new editable flow + params.put("newName", "Copy of browser"); + Response response = authMgmtResource.copy("browser", params); + try { + Assert.assertEquals("Copy flow", 201, response.getStatus()); + } finally { + response.close(); + } + + // add execution using inexistent provider + params.put("provider", "test-execution"); + try { + authMgmtResource.addExecution("Copy of browser", params); + Assert.fail("add execution with inexistent provider should fail"); + } catch(BadRequestException expected) { + } + + // add execution - should succeed + params.put("provider", "idp-review-profile"); + authMgmtResource.addExecution("Copy of browser", params); + + // check execution was added + response = authMgmtResource.getExecutions("Copy of browser"); + AuthenticationExecutionInfoRepresentation exec; + AuthenticationExecutionInfoRepresentation authCookieExec; + try { + List executionReps = response.readEntity(new GenericType>() { + }); + exec = findExecutionByProvider("idp-review-profile", executionReps); + Assert.assertNotNull("idp-review-profile added", exec); + + // we'll need auth-cookie later + authCookieExec = findExecutionByProvider("auth-cookie", executionReps); + } finally { + response.close(); + } + + compareExecution(newExecution("Review Profile", "idp-review-profile", true, 0, 3, DISABLED, null, new String[]{REQUIRED, DISABLED}), exec); + + // remove execution + authMgmtResource.removeExecution(exec.getId()); + + // check execution was removed + response = authMgmtResource.getExecutions("Copy of browser"); + try { + List executionReps = response.readEntity(new GenericType>() { + }); + exec = findExecutionByProvider("idp-review-profile", executionReps); + Assert.assertNull("idp-review-profile removed", exec); + } finally { + response.close(); + } + + // now add the execution again using a different method and representation + + // delete auth-cookie + authMgmtResource.removeExecution(authCookieExec.getId()); + + AuthenticationExecutionRepresentation rep = new AuthenticationExecutionRepresentation(); + rep.setPriority(10); + rep.setAuthenticator("auth-cookie"); + rep.setRequirement(OPTIONAL); + + response = authMgmtResource.addExecution(rep); + try { + Assert.assertEquals("added execution missing parent flow", 400, response.getStatus()); + } finally { + response.close(); + } + + // get Copy of browser flow id, and set it on execution + List flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation flow = findFlowByAlias("Copy of browser", flows); + rep.setParentFlow(flow.getId()); + + // add execution - should succeed + response = authMgmtResource.addExecution(rep); + try { + Assert.assertEquals("added execution", 201, response.getStatus()); + } finally { + response.close(); + } + + // check execution was added + List executions; + response = authMgmtResource.getExecutions("Copy of browser"); + try { + executions = response.readEntity(new GenericType>() { + }); + exec = findExecutionByProvider("auth-cookie", executions); + Assert.assertNotNull("auth-cookie added", exec); + } finally { + response.close(); + } + + // Note: there is no checking in addExecution if requirement is one of requirementChoices + // Thus we can have OPTIONAL which is neither ALTERNATIVE, nor DISABLED + compareExecution(newExecution("Cookie", "auth-cookie", false, 0, 2, OPTIONAL, null, new String[]{ALTERNATIVE, DISABLED}), exec); + } + + @Test + public void testUpdateExecution() { + + // get current auth-cookie execution + Response response = authMgmtResource.getExecutions("browser"); + List executionReps = response.readEntity( + new GenericType>() { + }); + AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider("auth-cookie", executionReps); + + Assert.assertEquals("auth-cookie set to ALTERNATIVE", ALTERNATIVE, exec.getRequirement()); + + // switch from DISABLED to ALTERNATIVE + exec.setRequirement(DISABLED); + authMgmtResource.updateExecutions("browser", exec); + + // make sure the change is visible + response = authMgmtResource.getExecutions("browser"); + executionReps = response.readEntity(new GenericType>() {}); + + // get current auth-cookie execution + AuthenticationExecutionInfoRepresentation exec2 = findExecutionByProvider("auth-cookie", executionReps); + compareExecution(exec, exec2); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/FlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/FlowTest.java new file mode 100644 index 0000000000..f0dc62bc31 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/FlowTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.authentication; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.List; + +/** + * @author Marko Strukelj + */ +public class FlowTest extends AbstractAuthenticationTest { + + @Test + public void testAddRemoveFlow() { + + // test that built-in flow cannot be deleted + List flows = authMgmtResource.getFlows(); + for (AuthenticationFlowRepresentation flow : flows) { + try { + authMgmtResource.deleteFlow(flow.getId()); + Assert.fail("deleteFlow should fail for built in flow"); + } catch (BadRequestException e) { + break; + } + } + + // try create new flow using alias of already existing flow + Response response = authMgmtResource.createFlow(newFlow("browser", "Browser flow", "basic-flow", true, false)); + try { + Assert.assertEquals("createFlow using the alias of existing flow should fail", 409, response.getStatus()); + } finally { + response.close(); + } + + // create new flow that should succeed + AuthenticationFlowRepresentation newFlow = newFlow("browser-2", "Browser flow", "basic-flow", true, false); + response = authMgmtResource.createFlow(newFlow); + try { + Assert.assertEquals("createFlow success", 201, response.getStatus()); + } finally { + response.close(); + } + + // check that new flow is returned + flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation found = findFlowByAlias("browser-2", flows); + + Assert.assertNotNull("created flow visible", found); + compareFlows(newFlow, found); + + // delete non-built-in flow + authMgmtResource.deleteFlow(found.getId()); + + // check the deleted flow is no longer returned + flows = authMgmtResource.getFlows(); + found = findFlowByAlias("browser-2", flows); + Assert.assertNull("flow deleted", found); + } + + + @Test + public void testCopyFlow() { + + HashMap params = new HashMap<>(); + params.put("newName", "clients"); + + // copy using existing alias as new name + Response response = authMgmtResource.copy("browser", params); + try { + Assert.assertEquals("Copy flow using the new alias of existing flow should fail", 409, response.getStatus()); + } finally { + response.close(); + } + + // copy non-existing flow + params.clear(); + response = authMgmtResource.copy("non-existent", params); + try { + Assert.assertEquals("Copy non-existing flow", 404, response.getStatus()); + } finally { + response.close(); + } + + // copy that should succeed + params.put("newName", "Copy of browser"); + response = authMgmtResource.copy("browser", params); + try { + Assert.assertEquals("Copy flow", 201, response.getStatus()); + } finally { + response.close(); + } + + // compare original flow with a copy - fields should be the same except id, alias, and builtIn + List flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation browser = findFlowByAlias("browser", flows); + AuthenticationFlowRepresentation copyOfBrowser = findFlowByAlias("Copy of browser", flows); + + Assert.assertNotNull(browser); + Assert.assertNotNull(copyOfBrowser); + + // adjust expected values before comparing + browser.setAlias("Copy of browser"); + browser.setBuiltIn(false); + compareFlows(browser, copyOfBrowser); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/InitialFlowsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/InitialFlowsTest.java new file mode 100644 index 0000000000..f1f341e112 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/InitialFlowsTest.java @@ -0,0 +1,200 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.authentication; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; + +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Marko Strukelj + */ +public class InitialFlowsTest extends AbstractAuthenticationTest { + + private HashMap configs = new HashMap<>(); + private HashMap expectedConfigs = new HashMap<>(); + + { + expectedConfigs.put("idp-review-profile", newConfig("review profile config", new String[]{"update.profile.on.first.login", "missing"})); + expectedConfigs.put("idp-create-user-if-unique", newConfig("create unique user config", new String[]{"require.password.update.after.registration", "false"})); + } + + @Test + public void testInitialFlows() { + + List result = new LinkedList<>(); + + // get all flows + List flows = authMgmtResource.getFlows(); + for (AuthenticationFlowRepresentation flow : flows) { + // get all executions for flow + Response executions = authMgmtResource.getExecutions(flow.getAlias()); + List executionReps = executions.readEntity(new GenericType>() { + }); + + for (AuthenticationExecutionInfoRepresentation exec : executionReps) { + // separately load referenced configurations + String configId = exec.getAuthenticationConfig(); + if (configId != null && !configs.containsKey(configId)) { + configs.put(configId, authMgmtResource.getAuthenticatorConfig(exec.getId(), configId)); + } + } + result.add(new FlowExecutions(flow, executionReps)); + } + + // make sure received flows and their details are as expected + compare(expectedFlows(), orderAlphabetically(result)); + } + + private void compare(List expected, List actual) { + Assert.assertEquals("Flow count", expected.size(), actual.size()); + Iterator it1 = expected.iterator(); + Iterator it2 = actual.iterator(); + while (it1.hasNext()) { + FlowExecutions fe1 = it1.next(); + FlowExecutions fe2 = it2.next(); + + compareFlows(fe1.flow, fe2.flow); + compareExecutions(fe1.executions, fe2.executions); + } + } + + + private void compareExecutions(List expected, List actual) { + Assert.assertEquals("Executions count", expected.size(), actual.size()); + Iterator it1 = expected.iterator(); + Iterator it2 = actual.iterator(); + while (it1.hasNext()) { + AuthenticationExecutionInfoRepresentation exe1 = it1.next(); + AuthenticationExecutionInfoRepresentation exe2 = it2.next(); + + compareExecutionWithConfig(exe1, exe2); + } + } + + private void compareExecutionWithConfig(AuthenticationExecutionInfoRepresentation expected, AuthenticationExecutionInfoRepresentation actual) { + super.compareExecution(expected, actual); + compareAuthConfig(expected, actual); + } + + private void compareAuthConfig(AuthenticationExecutionInfoRepresentation expected, AuthenticationExecutionInfoRepresentation actual) { + AuthenticatorConfigRepresentation cfg1 = expectedConfigs.get(expected.getProviderId()); + AuthenticatorConfigRepresentation cfg2 = configs.get(actual.getAuthenticationConfig()); + + if (cfg1 == null && cfg2 == null) { + return; + } + Assert.assertEquals("Execution configuration alias", cfg1.getAlias(), cfg2.getAlias()); + Assert.assertEquals("Execution configuration params", cfg1.getConfig(), cfg2.getConfig()); + } + + private List orderAlphabetically(List result) { + List sorted = new ArrayList<>(result); + Collections.sort(sorted); + return sorted; + } + + private LinkedList expectedFlows() { + LinkedList expected = new LinkedList<>(); + + AuthenticationFlowRepresentation flow = newFlow("browser", "browser based authentication", "basic-flow", true, true); + List executions = new LinkedList<>(); + executions.add(newExecution("Cookie", "auth-cookie", false, 0, 0, ALTERNATIVE, null, new String[]{ALTERNATIVE, DISABLED})); + executions.add(newExecution("Kerberos", "auth-spnego", false, 0, 1, DISABLED, null, new String[]{ALTERNATIVE, REQUIRED, DISABLED})); + executions.add(newExecution("forms", null, false, 0, 2, ALTERNATIVE, true, new String[]{ALTERNATIVE, REQUIRED, DISABLED})); + executions.add(newExecution("Username Password Form", "auth-username-password-form", false, 1, 0, REQUIRED, null, new String[]{REQUIRED})); + executions.add(newExecution("OTP Form", "auth-otp-form", false, 1, 1, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED})); + expected.add(new FlowExecutions(flow, executions)); + + flow = newFlow("clients", "Base authentication for clients", "client-flow", true, true); + executions = new LinkedList<>(); + executions.add(newExecution("Client Id and Secret", "client-secret", false, 0, 0, ALTERNATIVE, null, new String[]{ALTERNATIVE, DISABLED})); + executions.add(newExecution("Signed Jwt", "client-jwt", false, 0, 1, ALTERNATIVE, null, new String[]{ALTERNATIVE, DISABLED})); + expected.add(new FlowExecutions(flow, executions)); + + flow = newFlow("direct grant", "OpenID Connect Resource Owner Grant", "basic-flow", true, true); + executions = new LinkedList<>(); + executions.add(newExecution("Username Validation", "direct-grant-validate-username", false, 0, 0, REQUIRED, null, new String[]{REQUIRED})); + executions.add(newExecution("Password", "direct-grant-validate-password", false, 0, 1, REQUIRED, null, new String[]{REQUIRED, DISABLED})); + executions.add(newExecution("OTP", "direct-grant-validate-otp", false, 0, 2, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED})); + expected.add(new FlowExecutions(flow, executions)); + + flow = newFlow("first broker login", "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "basic-flow", true, true); + executions = new LinkedList<>(); + executions.add(newExecution("Review Profile", "idp-review-profile", true, 0, 0, REQUIRED, null, new String[]{REQUIRED, DISABLED})); + executions.add(newExecution("Create User If Unique", "idp-create-user-if-unique", true, 0, 1, ALTERNATIVE, null, new String[]{ALTERNATIVE, REQUIRED, DISABLED})); + executions.add(newExecution("Handle Existing Account", null, false, 0, 2, ALTERNATIVE, true, new String[]{ALTERNATIVE, REQUIRED, DISABLED})); + executions.add(newExecution("Confirm link existing account", "idp-confirm-link", false, 1, 0, REQUIRED, null, new String[]{REQUIRED, DISABLED})); + executions.add(newExecution("Verify existing account by Email", "idp-email-verification", false, 1, 1, ALTERNATIVE, null, new String[]{ALTERNATIVE, REQUIRED, DISABLED})); + executions.add(newExecution("Verify Existing Account by Re-authentication", null, false, 1, 2, ALTERNATIVE, true, new String[]{ALTERNATIVE, REQUIRED, DISABLED})); + executions.add(newExecution("Username Password Form for identity provider reauthentication", "idp-username-password-form", false, 2, 0, REQUIRED, null, new String[]{REQUIRED})); + executions.add(newExecution("OTP Form", "auth-otp-form", false, 2, 1, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED})); + expected.add(new FlowExecutions(flow, executions)); + + flow = newFlow("registration", "registration flow", "basic-flow", true, true); + executions = new LinkedList<>(); + executions.add(newExecution("registration form", "registration-page-form", false, 0, 0, REQUIRED, true, new String[]{REQUIRED, DISABLED})); + executions.add(newExecution("Registration User Creation", "registration-user-creation", false, 1, 0, REQUIRED, null, new String[]{REQUIRED, DISABLED})); + executions.add(newExecution("Profile Validation", "registration-profile-action", false, 1, 1, REQUIRED, null, new String[]{REQUIRED, DISABLED})); + executions.add(newExecution("Password Validation", "registration-password-action", false, 1, 2, REQUIRED, null, new String[]{REQUIRED, DISABLED})); + executions.add(newExecution("Recaptcha", "registration-recaptcha-action", true, 1, 3, DISABLED, null, new String[]{REQUIRED, DISABLED})); + expected.add(new FlowExecutions(flow, executions)); + + flow = newFlow("reset credentials", "Reset credentials for a user if they forgot their password or something", "basic-flow", true, true); + executions = new LinkedList<>(); + executions.add(newExecution("Choose User", "reset-credentials-choose-user", false, 0, 0, REQUIRED, null, new String[]{REQUIRED})); + executions.add(newExecution("Send Reset Email", "reset-credential-email", false, 0, 1, REQUIRED, null, new String[]{REQUIRED})); + executions.add(newExecution("Reset Password", "reset-password", false, 0, 2, REQUIRED, null, new String[]{REQUIRED, OPTIONAL, DISABLED})); + executions.add(newExecution("Reset OTP", "reset-otp", false, 0, 3, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED})); + expected.add(new FlowExecutions(flow, executions)); + + flow = newFlow("saml ecp", "SAML ECP Profile Authentication Flow", "basic-flow", true, true); + executions = new LinkedList<>(); + executions.add(newExecution(null, "http-basic-authenticator", false, 0, 0, REQUIRED, null, new String[]{})); + expected.add(new FlowExecutions(flow, executions)); + + return expected; + } + + static class FlowExecutions implements Comparable { + AuthenticationFlowRepresentation flow; + List executions; + + FlowExecutions(AuthenticationFlowRepresentation flow, List executions) { + this.flow = flow; + this.executions = executions; + } + + @Override + public int compareTo(FlowExecutions o) { + return flow.getAlias().compareTo(o.flow.getAlias()); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/InitialProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/InitialProvidersTest.java new file mode 100644 index 0000000000..d8d7f4247e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/InitialProvidersTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.authentication; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * @author Marko Strukelj + */ +public class InitialProvidersTest extends AbstractAuthenticationTest { + + @Test + public void testAuthenticationProvidersList() { + + List> providers = authMgmtResource.getAuthenticatorProviders(); + providers = sortProviders(providers); + + compareProviders(expectedAuthProviders(), providers); + } + + private void compareProviders(List> expected, List> actual) { + + Assert.assertEquals("Providers count", expected.size(), actual.size()); + + Iterator> it1 = expected.iterator(); + Iterator> it2 = actual.iterator(); + + while (it1.hasNext()) { + Assert.assertEquals("Provider", it1.next(), it2.next()); + } + } + + private List> expectedAuthProviders() { + ArrayList> result = new ArrayList<>(); + result.add(newClientProvider("auth-conditional-otp-form", "Conditional OTP Form", "Validates a OTP on a separate OTP form. Only shown if required based on the configured conditions.")); + result.add(newClientProvider("auth-cookie", "Cookie", "Validates the SSO cookie set by the auth server.")); + result.add(newClientProvider("auth-otp-form", "OTP Form", "Validates a OTP on a separate OTP form.")); + result.add(newClientProvider("auth-spnego", "Kerberos", "Initiates the SPNEGO protocol. Most often used with Kerberos.")); + result.add(newClientProvider("auth-username-password-form", "Username Password Form", "Validates a username and password from login form.")); + result.add(newClientProvider("direct-grant-validate-otp", "OTP", "Validates the one time password supplied as a 'totp' form parameter in direct grant request")); + result.add(newClientProvider("direct-grant-validate-password", "Password", "Validates the password supplied as a 'password' form parameter in direct grant request")); + result.add(newClientProvider("direct-grant-validate-username", "Username Validation", "Validates the username supplied as a 'username' form parameter in direct grant request")); + result.add(newClientProvider("http-basic-authenticator", null, null)); + result.add(newClientProvider("idp-confirm-link", "Confirm link existing account", "Show the form where user confirms if he wants to link identity provider with existing account or rather edit user profile data retrieved from identity provider to avoid conflict")); + result.add(newClientProvider("idp-create-user-if-unique", "Create User If Unique", "Detect if there is existing Keycloak account with same email like identity provider. If no, create new user")); + result.add(newClientProvider("idp-email-verification", "Verify existing account by Email", "Email verification of existing Keycloak user, that wants to link his user account with identity provider")); + result.add(newClientProvider("idp-review-profile", "Review Profile", "User reviews and updates profile data retrieved from Identity Provider in the displayed form")); + result.add(newClientProvider("idp-username-password-form", "Username Password Form for identity provider reauthentication", "Validates a password from login form. Username is already known from identity provider authentication")); + result.add(newClientProvider("reset-credential-email", "Send Reset Email", "Send email to user and wait for response.")); + result.add(newClientProvider("reset-credentials-choose-user", "Choose User", "Choose a user to reset credentials for")); + result.add(newClientProvider("reset-otp", "Reset OTP", "Sets the Configure OTP required action if execution is REQUIRED. Will also set it if execution is OPTIONAL and the OTP is currently configured for it.")); + result.add(newClientProvider("reset-password", "Reset Password", "Sets the Update Password required action if execution is REQUIRED. Will also set it if execution is OPTIONAL and the password is currently configured for it.")); + return result; + } + + private Map newClientProvider(String id, String displayName, String description) { + Map obj = new HashMap<>(); + obj.put("id", id); + obj.put("displayName", displayName); + obj.put("description", description); + return obj; + } + + private List> sortProviders(List> providers) { + ArrayList> sorted = new ArrayList<>(providers); + Collections.sort(sorted, new ProviderComparator()); + return sorted; + } + + private static class ProviderComparator implements Comparator> { + @Override + public int compare(Map o1, Map o2) { + return String.valueOf(o1.get("id")).compareTo(String.valueOf(o2.get("id"))); + } + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/ShiftExecutionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/ShiftExecutionTest.java new file mode 100644 index 0000000000..3124537df9 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authentication/ShiftExecutionTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.authentication; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; + +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.List; + +/** + * @author Marko Strukelj + */ +public class ShiftExecutionTest extends AbstractAuthenticationTest { + + @Test + public void testShiftExecution() { + + // copy built-in flow so we get a new editable flow + HashMap params = new HashMap<>(); + params.put("newName", "Copy of browser"); + Response response = authMgmtResource.copy("browser", params); + try { + Assert.assertEquals("Copy flow", 201, response.getStatus()); + } finally { + response.close(); + } + + // get executions + response = authMgmtResource.getExecutions("Copy of browser"); + List executions = response.readEntity(new GenericType>() { + }); + + AuthenticationExecutionInfoRepresentation last = executions.get(executions.size() - 1); + AuthenticationExecutionInfoRepresentation oneButLast = executions.get(executions.size() - 2); + + // shift last execution up + authMgmtResource.raisePriority(last.getId()); + + response = authMgmtResource.getExecutions("Copy of browser"); + List executions2 = response.readEntity(new GenericType>() { + }); + + AuthenticationExecutionInfoRepresentation last2 = executions2.get(executions.size() - 1); + AuthenticationExecutionInfoRepresentation oneButLast2 = executions2.get(executions.size() - 2); + + Assert.assertEquals("Execution shifted up - N", last.getId(), oneButLast2.getId()); + Assert.assertEquals("Execution shifted up - N-1", oneButLast.getId(), last2.getId()); + + // shift one before last down + authMgmtResource.lowerPriority(oneButLast2.getId()); + + response = authMgmtResource.getExecutions("Copy of browser"); + executions2 = response.readEntity(new GenericType>() { + }); + + last2 = executions2.get(executions.size() - 1); + oneButLast2 = executions2.get(executions.size() - 2); + + Assert.assertEquals("Execution shifted down - N", last.getId(), last2.getId()); + Assert.assertEquals("Execution shifted down - N-1", oneButLast.getId(), oneButLast2.getId()); + } +}