KEYCLOAK-6313 Add required action's priority for customizing the execution order

This commit is contained in:
Hiroyuki Wada 2018-07-04 21:37:03 +09:00 committed by Hynek Mlnařík
parent b43392bac8
commit 7c0ca9aad2
22 changed files with 577 additions and 13 deletions

View file

@ -31,6 +31,7 @@ public class RequiredActionProviderRepresentation {
private String providerId;
private boolean enabled;
private boolean defaultAction;
private int priority;
private Map<String, String> config = new HashMap<String, String>();
@ -80,6 +81,14 @@ public class RequiredActionProviderRepresentation {
this.providerId = providerId;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
public Map<String, String> getConfig() {
return config;
}

View file

@ -164,6 +164,14 @@ public interface AuthenticationManagementResource {
@DELETE
void removeRequiredAction(@PathParam("alias") String alias);
@Path("required-actions/{alias}/raise-priority")
@POST
void raiseRequiredActionPriority(@PathParam("alias") String alias);
@Path("required-actions/{alias}/lower-priority")
@POST
void lowerRequiredActionPriority(@PathParam("alias") String alias);
@Path("config-description/{providerId}")
@GET
@Produces(MediaType.APPLICATION_JSON)

View file

@ -1661,6 +1661,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
auth.setConfig(model.getConfig());
auth.setEnabled(model.isEnabled());
auth.setDefaultAction(model.isDefaultAction());
auth.setPriority(model.getPriority());
realm.getRequiredActionProviders().add(auth);
em.persist(auth);
em.flush();
@ -1691,6 +1692,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
model.setAlias(entity.getAlias());
model.setEnabled(entity.isEnabled());
model.setDefaultAction(entity.isDefaultAction());
model.setPriority(entity.getPriority());
model.setName(entity.getName());
Map<String, String> config = new HashMap<>();
if (entity.getConfig() != null) config.putAll(entity.getConfig());
@ -1706,6 +1708,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
entity.setProviderId(model.getProviderId());
entity.setEnabled(model.isEnabled());
entity.setDefaultAction(model.isDefaultAction());
entity.setPriority(model.getPriority());
entity.setName(model.getName());
if (entity.getConfig() == null) {
entity.setConfig(model.getConfig());
@ -1725,6 +1728,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
for (RequiredActionProviderEntity entity : entities) {
actions.add(entityToModel(entity));
}
Collections.sort(actions, RequiredActionProviderModel.RequiredActionComparator.SINGLETON);
return Collections.unmodifiableList(actions);
}

View file

@ -66,6 +66,9 @@ public class RequiredActionProviderEntity {
@Column(name="DEFAULT_ACTION")
protected boolean defaultAction;
@Column(name="PRIORITY")
protected int priority;
@ElementCollection
@MapKeyColumn(name="NAME")
@Column(name="VALUE")
@ -136,6 +139,14 @@ public class RequiredActionProviderEntity {
this.name = name;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * Copyright 2018 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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="wadahiro@gmail.com" id="4.2.0-KEYCLOAK-6313">
<addColumn tableName="REQUIRED_ACTION_PROVIDER">
<column name="PRIORITY" type="INT"/>
</addColumn>
</changeSet>
</databaseChangeLog>

View file

@ -57,4 +57,5 @@
<include file="META-INF/jpa-changelog-authz-4.0.0.CR1.xml"/>
<include file="META-INF/jpa-changelog-authz-4.0.0.Beta3.xml"/>
<include file="META-INF/jpa-changelog-authz-4.2.0.Final.xml"/>
<include file="META-INF/jpa-changelog-4.2.0.xml"/>
</databaseChangeLog>

View file

@ -39,6 +39,7 @@ import org.keycloak.migration.migrators.MigrateTo3_4_0;
import org.keycloak.migration.migrators.MigrateTo3_4_1;
import org.keycloak.migration.migrators.MigrateTo3_4_2;
import org.keycloak.migration.migrators.MigrateTo4_0_0;
import org.keycloak.migration.migrators.MigrateTo4_2_0;
import org.keycloak.migration.migrators.Migration;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@ -72,7 +73,8 @@ public class MigrationModelManager {
new MigrateTo3_4_0(),
new MigrateTo3_4_1(),
new MigrateTo3_4_2(),
new MigrateTo4_0_0()
new MigrateTo4_0_0(),
new MigrateTo4_2_0()
};
public static void migrate(KeycloakSession session) {

View file

@ -0,0 +1,73 @@
/*
* Copyright 2018 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.migration.migrators;
import static java.util.Comparator.comparing;
import java.util.List;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.representations.idm.RealmRepresentation;
/**
* @author <a href="mailto:wadahiro@gmail.com">Hiroyuki Wada</a>
*/
public class MigrateTo4_2_0 implements Migration {
public static final ModelVersion VERSION = new ModelVersion("4.2.0");
private static final Logger LOG = Logger.getLogger(MigrateTo4_2_0.class);
@Override
public ModelVersion getVersion() {
return VERSION;
}
@Override
public void migrate(KeycloakSession session) {
session.realms().getRealms().stream().forEach(r -> {
migrateRealm(session, r, false);
});
}
@Override
public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
migrateRealm(session, realm, true);
}
protected void migrateRealm(KeycloakSession session, RealmModel realm, boolean json) {
// Set default priority of required actions in alphabetical order
List<RequiredActionProviderModel> actions = realm.getRequiredActionProviders().stream()
.sorted(comparing(RequiredActionProviderModel::getName)).collect(Collectors.toList());
int priority = 10;
for (RequiredActionProviderModel model : actions) {
LOG.debugf("Setting priority '%d' for required action '%s' in realm '%s'", priority, model.getAlias(),
realm.getName());
model.setPriority(priority);
priority += 10;
// Save
realm.updateRequiredActionProvider(model);
}
}
}

View file

@ -34,6 +34,7 @@ public class DefaultRequiredActions {
verifyEmail.setName("Verify Email");
verifyEmail.setProviderId(UserModel.RequiredAction.VERIFY_EMAIL.name());
verifyEmail.setDefaultAction(false);
verifyEmail.setPriority(50);
realm.addRequiredActionProvider(verifyEmail);
}
@ -45,6 +46,7 @@ public class DefaultRequiredActions {
updateProfile.setName("Update Profile");
updateProfile.setProviderId(UserModel.RequiredAction.UPDATE_PROFILE.name());
updateProfile.setDefaultAction(false);
updateProfile.setPriority(40);
realm.addRequiredActionProvider(updateProfile);
}
@ -55,6 +57,7 @@ public class DefaultRequiredActions {
totp.setName("Configure OTP");
totp.setProviderId(UserModel.RequiredAction.CONFIGURE_TOTP.name());
totp.setDefaultAction(false);
totp.setPriority(10);
realm.addRequiredActionProvider(totp);
}
@ -65,6 +68,7 @@ public class DefaultRequiredActions {
updatePassword.setName("Update Password");
updatePassword.setProviderId(UserModel.RequiredAction.UPDATE_PASSWORD.name());
updatePassword.setDefaultAction(false);
updatePassword.setPriority(30);
realm.addRequiredActionProvider(updatePassword);
}
@ -75,6 +79,7 @@ public class DefaultRequiredActions {
termsAndConditions.setName("Terms and Conditions");
termsAndConditions.setProviderId("terms_and_conditions");
termsAndConditions.setDefaultAction(false);
termsAndConditions.setPriority(20);
realm.addRequiredActionProvider(termsAndConditions);
}

View file

@ -1862,6 +1862,7 @@ public class RepresentationToModel {
public static RequiredActionProviderModel toModel(RequiredActionProviderRepresentation rep) {
RequiredActionProviderModel model = new RequiredActionProviderModel();
model.setConfig(rep.getConfig());
model.setPriority(rep.getPriority());
model.setDefaultAction(rep.isDefaultAction());
model.setEnabled(rep.isEnabled());
model.setProviderId(rep.getProviderId());

View file

@ -18,6 +18,7 @@
package org.keycloak.models;
import java.io.Serializable;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
@ -27,12 +28,22 @@ import java.util.Map;
*/
public class RequiredActionProviderModel implements Serializable {
public static class RequiredActionComparator implements Comparator<RequiredActionProviderModel> {
public static final RequiredActionComparator SINGLETON = new RequiredActionComparator();
@Override
public int compare(RequiredActionProviderModel o1, RequiredActionProviderModel o2) {
return o1.priority - o2.priority;
}
}
private String id;
private String alias;
private String name;
private String providerId;
private boolean enabled;
private boolean defaultAction;
private int priority;
private Map<String, String> config = new HashMap<String, String>();
@ -90,6 +101,14 @@ public class RequiredActionProviderModel implements Serializable {
this.providerId = providerId;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
public Map<String, String> getConfig() {
return config;
}

View file

@ -994,16 +994,10 @@ public class AuthenticationManager {
protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession,
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user,
Set<String> requiredActions) {
for (String action : requiredActions) {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
if (model == null) {
logger.warnv("Could not find configuration for Required Action {0}, did you forget to register it?", action);
continue;
}
if (!model.isEnabled()) {
continue;
}
List<RequiredActionProviderModel> sortedRequiredActions = sortRequiredActionsByPriority(realm, requiredActions);
for (RequiredActionProviderModel model : sortedRequiredActions) {
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId());
if (factory == null) {
throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
@ -1044,6 +1038,23 @@ public class AuthenticationManager {
return null;
}
private static List<RequiredActionProviderModel> sortRequiredActionsByPriority(RealmModel realm, Set<String> requiredActions) {
List<RequiredActionProviderModel> actions = new ArrayList<>();
for (String action : requiredActions) {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
if (model == null) {
logger.warnv("Could not find configuration for Required Action {0}, did you forget to register it?", action);
continue;
}
if (!model.isEnabled()) {
continue;
}
actions.add(model);
}
Collections.sort(actions, RequiredActionProviderModel.RequiredActionComparator.SINGLETON);
return actions;
}
public static void evaluateRequiredActionTriggers(final KeycloakSession session, final AuthenticationSessionModel authSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) {
// see if any required actions need triggering, i.e. an expired password

View file

@ -72,6 +72,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
@ -880,6 +881,7 @@ public class AuthenticationManagementResource {
requiredAction.setName(name);
requiredAction.setProviderId(providerId);
requiredAction.setDefaultAction(false);
requiredAction.setPriority(getNextRequiredActionPriority());
requiredAction.setEnabled(true);
requiredAction = realm.addRequiredActionProvider(requiredAction);
@ -887,7 +889,12 @@ public class AuthenticationManagementResource {
adminEvent.operation(OperationType.CREATE).resource(ResourceType.REQUIRED_ACTION).resourcePath(uriInfo).representation(data).success();
}
private int getNextRequiredActionPriority() {
List<RequiredActionProviderModel> actions = realm.getRequiredActionProviders();
return actions.isEmpty() ? 0 : actions.get(actions.size() - 1).getPriority() + 1;
}
/**
* Get required actions
*
@ -913,6 +920,7 @@ public class AuthenticationManagementResource {
rep.setAlias(model.getAlias());
rep.setName(model.getName());
rep.setDefaultAction(model.isDefaultAction());
rep.setPriority(model.getPriority());
rep.setEnabled(model.isEnabled());
rep.setConfig(model.getConfig());
return rep;
@ -959,6 +967,7 @@ public class AuthenticationManagementResource {
update.setAlias(rep.getAlias());
update.setProviderId(model.getProviderId());
update.setDefaultAction(rep.isDefaultAction());
update.setPriority(rep.getPriority());
update.setEnabled(rep.isEnabled());
update.setConfig(rep.getConfig());
realm.updateRequiredActionProvider(update);
@ -984,6 +993,74 @@ public class AuthenticationManagementResource {
adminEvent.operation(OperationType.DELETE).resource(ResourceType.REQUIRED_ACTION).resourcePath(uriInfo).success();
}
/**
* Raise required action's priority
*
* @param alias Alias of required action
*/
@Path("required-actions/{alias}/raise-priority")
@POST
@NoCache
public void raiseRequiredActionPriority(@PathParam("alias") String alias) {
auth.realm().requireManageRealm();
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(alias);
if (model == null) {
throw new NotFoundException("Failed to find required action.");
}
List<RequiredActionProviderModel> actions = realm.getRequiredActionProviders();
RequiredActionProviderModel previous = null;
for (RequiredActionProviderModel action : actions) {
if (action.getId().equals(model.getId())) {
break;
}
previous = action;
}
if (previous == null) return;
int tmp = previous.getPriority();
previous.setPriority(model.getPriority());
realm.updateRequiredActionProvider(previous);
model.setPriority(tmp);
realm.updateRequiredActionProvider(model);
adminEvent.operation(OperationType.UPDATE).resource(ResourceType.REQUIRED_ACTION).resourcePath(uriInfo).success();
}
/**
* Lower required action's priority
*
* @param alias Alias of required action
*/
@Path("/required-actions/{alias}/lower-priority")
@POST
@NoCache
public void lowerRequiredActionPriority(@PathParam("alias") String alias) {
auth.realm().requireManageRealm();
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(alias);
if (model == null) {
throw new NotFoundException("Failed to find required action.");
}
List<RequiredActionProviderModel> actions = realm.getRequiredActionProviders();
int i = 0;
for (i = 0; i < actions.size(); i++) {
if (actions.get(i).getId().equals(model.getId())) {
break;
}
}
if (i + 1 >= actions.size()) return;
RequiredActionProviderModel next = actions.get(i + 1);
int tmp = model.getPriority();
model.setPriority(next.getPriority());
realm.updateRequiredActionProvider(model);
next.setPriority(tmp);
realm.updateRequiredActionProvider(next);
adminEvent.operation(OperationType.UPDATE).resource(ResourceType.REQUIRED_ACTION).resourcePath(uriInfo).success();
}
/**
* Get authenticator provider's configuration description
*/

View file

@ -25,6 +25,7 @@ import org.keycloak.testsuite.util.UserBuilder;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
*
@ -52,12 +53,15 @@ public class ActionUtil {
public static void addRequiredActionForRealm(RealmRepresentation testRealm, String providerId) {
List<RequiredActionProviderRepresentation> requiredActions = testRealm.getRequiredActions();
if (requiredActions == null) requiredActions = new LinkedList();
RequiredActionProviderRepresentation last = requiredActions.get(requiredActions.size() - 1);
RequiredActionProviderRepresentation action = new RequiredActionProviderRepresentation();
action.setAlias(providerId);
action.setProviderId(providerId);
action.setEnabled(true);
action.setDefaultAction(true);
action.setPriority(last.getPriority() + 1);
requiredActions.add(action);
testRealm.setRequiredActions(requiredActions);

View file

@ -0,0 +1,154 @@
/*
* Copyright 2018 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.actions;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.authentication.requiredactions.TermsAndConditions;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage;
import org.keycloak.testsuite.pages.TermsAndConditionsPage;
import org.keycloak.testsuite.util.UserBuilder;
/**
* @author <a href="mailto:wadahiro@gmail.com">Hiroyuki Wada</a>
*/
public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Rule
public AssertEvents events = new AssertEvents(this);
@Page
protected AppPage appPage;
@Page
protected LoginPage loginPage;
@Page
protected LoginPasswordUpdatePage changePasswordPage;
@Page
protected LoginUpdateProfileEditUsernameAllowedPage updateProfilePage;
@Page
protected TermsAndConditionsPage termsPage;
@Before
public void setupRequiredActions() {
setRequiredActionEnabled("test", TermsAndConditions.PROVIDER_ID, true, false);
// Because of changing the password in test case, we need to re-create the user.
ApiUtil.removeUserByUsername(testRealm(), "test-user@localhost");
UserRepresentation user = UserBuilder.create().enabled(true).username("test-user@localhost")
.email("test-user@localhost").build();
String testUserId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
setRequiredActionEnabled("test", testUserId, RequiredAction.UPDATE_PASSWORD.name(), true);
setRequiredActionEnabled("test", testUserId, RequiredAction.UPDATE_PROFILE.name(), true);
setRequiredActionEnabled("test", testUserId, TermsAndConditions.PROVIDER_ID, true);
}
@Test
public void executeRequiredActionsWithDefaultPriority() throws Exception {
// Default priority is alphabetical order:
// TermsAndConditions -> UpdatePassword -> UpdateProfile
// Login
loginPage.open();
loginPage.login("test-user@localhost", "password");
// First, accept terms
termsPage.assertCurrent();
termsPage.acceptTerms();
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI)
.detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent();
// Second, change password
changePasswordPage.assertCurrent();
changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
// Finally, update profile
updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost");
events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost")
.detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent();
// Logined
appPage.assertCurrent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
@Test
public void executeRequiredActionsWithCustomPriority() throws Exception {
// Default priority is alphabetical order:
// TermsAndConditions -> UpdatePassword -> UpdateProfile
// After Changing the priority, the order will be:
// UpdatePassword -> UpdateProfile -> TermsAndConditions
testRealm().flows().raiseRequiredActionPriority(UserModel.RequiredAction.UPDATE_PASSWORD.name());
testRealm().flows().lowerRequiredActionPriority("terms_and_conditions");
// Login
loginPage.open();
loginPage.login("test-user@localhost", "password");
// First, change password
changePasswordPage.assertCurrent();
changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
// Second, update profile
updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost");
events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost")
.detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent();
// Finally, accept terms
termsPage.assertCurrent();
termsPage.acceptTerms();
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI)
.detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent();
// Logined
appPage.assertCurrent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
}

View file

@ -72,6 +72,8 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
@Test
public void testCRUDRequiredAction() {
int lastPriority = authMgmtResource.getRequiredActions().get(authMgmtResource.getRequiredActions().size() - 1).getPriority();
// Just Dummy RequiredAction is not registered in the realm
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
Assert.assertEquals(1, result.size());
@ -96,6 +98,9 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
compareRequiredAction(rep, newRequiredAction(DummyRequiredActionFactory.PROVIDER_ID, "Dummy Action",
true, false, Collections.<String, String>emptyMap()));
// Confirm the registered priority - should be N + 1
Assert.assertEquals(lastPriority + 1, rep.getPriority());
// Update not-existent - should fail
try {
authMgmtResource.updateRequiredAction("not-existent", rep);

View file

@ -0,0 +1,85 @@
/*
* Copyright 2018 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.admin.authentication;
import java.util.List;
import javax.ws.rs.NotFoundException;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.testsuite.util.AdminEventPaths;
/**
* @author <a href="mailto:wadahiro@gmail.com">Hiroyuki Wada</a>
*/
public class ShiftRequiredActionTest extends AbstractAuthenticationTest {
@Test
public void testShiftRequiredAction() {
// get action
List<RequiredActionProviderRepresentation> actions = authMgmtResource.getRequiredActions();
RequiredActionProviderRepresentation last = actions.get(actions.size() - 1);
RequiredActionProviderRepresentation oneButLast = actions.get(actions.size() - 2);
// Not possible to raisePriority of not-existent required action
try {
authMgmtResource.raisePriority("not-existent");
Assert.fail("Not expected to raise priority of not existent required action");
} catch (NotFoundException nfe) {
// Expected
}
// shift last required action up
authMgmtResource.raiseRequiredActionPriority(last.getAlias());
assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authRaiseRequiredActionPath(last.getAlias()), ResourceType.REQUIRED_ACTION);
List<RequiredActionProviderRepresentation> actions2 = authMgmtResource.getRequiredActions();
RequiredActionProviderRepresentation last2 = actions2.get(actions.size() - 1);
RequiredActionProviderRepresentation oneButLast2 = actions2.get(actions.size() - 2);
Assert.assertEquals("Required action shifted up - N", last.getAlias(), oneButLast2.getAlias());
Assert.assertEquals("Required action up - N-1", oneButLast.getAlias(), last2.getAlias());
// Not possible to lowerPriority of not-existent required action
try {
authMgmtResource.lowerRequiredActionPriority("not-existent");
Assert.fail("Not expected to raise priority of not existent required action");
} catch (NotFoundException nfe) {
// Expected
}
// shift one before last down
authMgmtResource.lowerRequiredActionPriority(oneButLast2.getAlias());
assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authLowerRequiredActionPath(oneButLast2.getAlias()), ResourceType.REQUIRED_ACTION);
actions2 = authMgmtResource.getRequiredActions();
last2 = actions2.get(actions.size() - 1);
oneButLast2 = actions2.get(actions.size() - 2);
Assert.assertEquals("Required action shifted down - N", last.getAlias(), last2.getAlias());
Assert.assertEquals("Required action shifted down - N-1", oneButLast.getAlias(), oneButLast2.getAlias());
}
}

View file

@ -58,6 +58,7 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@ -198,6 +199,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testOfflineScopeAddedToClient();
}
protected void testMigrationTo4_2_0() {
testRequiredActionsPriority(this.masterRealm, this.migrationRealm);
}
private void testCliConsoleScopeSize(RealmResource realm) {
ClientRepresentation cli = realm.clients().findByClientId(Constants.ADMIN_CLI_CLIENT_ID).get(0);
ClientRepresentation console = realm.clients().findByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID).get(0);
@ -462,6 +467,25 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
}
private void testRequiredActionsPriority(RealmResource... realms) {
log.info("testing required action's priority");
for (RealmResource realm : realms) {
List<RequiredActionProviderRepresentation> actions = realm.flows().getRequiredActions();
// Checking if the actions are in alphabetical order
List<String> nameList = actions.stream().map(x -> x.getName()).collect(Collectors.toList());
List<String> sortedByName = nameList.stream().sorted().collect(Collectors.toList());
assertArrayEquals(nameList.toArray(), sortedByName.toArray());
// Checking the priority
int priority = 10;
for (RequiredActionProviderRepresentation action : actions) {
assertEquals(priority, action.getPriority());
priority += 10;
}
}
}
protected String getMigrationMode() {
return System.getProperty("migration.mode");
}
@ -481,6 +505,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
protected void testMigrationTo4_x() {
testMigrationTo4_0_0();
testMigrationTo4_2_0();
}

View file

@ -437,6 +437,18 @@ public class AdminEventPaths {
return uri.toString();
}
public static String authRaiseRequiredActionPath(String requiredActionAlias) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "raiseRequiredActionPriority")
.build(requiredActionAlias);
return uri.toString();
}
public static String authLowerRequiredActionPath(String requiredActionAlias) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "lowerRequiredActionPriority")
.build(requiredActionAlias);
return uri.toString();
}
// ATTACK DETECTION
public static String attackDetectionClearBruteForceForUserPath(String username) {

View file

@ -2284,7 +2284,7 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
module.controller('RequiredActionsCtrl', function($scope, realm, unregisteredRequiredActions,
$modal, $route,
RegisterRequiredAction, RequiredActions, Notifications) {
RegisterRequiredAction, RequiredActions, RequiredActionRaisePriority, RequiredActionLowerPriority, Notifications) {
console.log('RequiredActionsCtrl');
$scope.realm = realm;
$scope.unregisteredRequiredActions = unregisteredRequiredActions;
@ -2306,6 +2306,20 @@ module.controller('RequiredActionsCtrl', function($scope, realm, unregisteredReq
});
}
$scope.raisePriority = function(action) {
RequiredActionRaisePriority.save({realm: realm.realm, alias: action.alias}, function() {
Notifications.success("Required action's priority raised");
setupRequiredActionsForm();
})
}
$scope.lowerPriority = function(action) {
RequiredActionLowerPriority.save({realm: realm.realm, alias: action.alias}, function() {
Notifications.success("Required action's priority lowered");
setupRequiredActionsForm();
})
}
$scope.register = function() {
var controller = function($scope, $modalInstance) {
$scope.unregisteredRequiredActions = unregisteredRequiredActions;

View file

@ -323,6 +323,20 @@ module.factory('RequiredActions', function($resource) {
});
});
module.factory('RequiredActionRaisePriority', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/authentication/required-actions/:alias/raise-priority', {
realm : '@realm',
alias : '@alias'
});
});
module.factory('RequiredActionLowerPriority', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/authentication/required-actions/:alias/lower-priority', {
realm : '@realm',
alias : '@alias'
});
});
module.factory('UnregisteredRequiredActions', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/authentication/unregistered-required-actions', {
realm : '@realm'

View file

@ -18,8 +18,12 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="requiredAction in requiredActions | orderBy : 'name'" data-ng-show="requiredActions.length > 0">
<td>{{requiredAction.name}}</td>
<tr ng-repeat="requiredAction in requiredActions" data-ng-show="requiredActions.length > 0">
<td class="kc-sorter">
<button data-ng-hide="flow.builtIn" data-ng-disabled="$first" class="btn btn-default btn-sm" data-ng-click="raisePriority(requiredAction)"><i class="fa fa-angle-up"></i></button>
<button data-ng-hide="flow.builtIn" data-ng-disabled="$last" class="btn btn-default btn-sm" data-ng-click="lowerPriority(requiredAction)"><i class="fa fa-angle-down"></i></button>
<span>{{requiredAction.name}}</span></span>
</td>
<td><input type="checkbox" ng-model="requiredAction.enabled" ng-change="updateRequiredAction(requiredAction)" id="{{requiredAction.alias}}.enabled"></td>
<td><input type="checkbox" ng-model="requiredAction.defaultAction" ng-change="updateRequiredAction(requiredAction)" ng-disabled="!requiredAction.enabled" ng-checked="requiredAction.enabled && requiredAction.defaultAction" id="{{requiredAction.alias}}.defaultAction"></td>
</tr>