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

View file

@ -164,6 +164,14 @@ public interface AuthenticationManagementResource {
@DELETE @DELETE
void removeRequiredAction(@PathParam("alias") String alias); 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}") @Path("config-description/{providerId}")
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)

View file

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

View file

@ -66,6 +66,9 @@ public class RequiredActionProviderEntity {
@Column(name="DEFAULT_ACTION") @Column(name="DEFAULT_ACTION")
protected boolean defaultAction; protected boolean defaultAction;
@Column(name="PRIORITY")
protected int priority;
@ElementCollection @ElementCollection
@MapKeyColumn(name="NAME") @MapKeyColumn(name="NAME")
@Column(name="VALUE") @Column(name="VALUE")
@ -136,6 +139,14 @@ public class RequiredActionProviderEntity {
this.name = name; this.name = name;
} }
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; 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.CR1.xml"/>
<include file="META-INF/jpa-changelog-authz-4.0.0.Beta3.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-authz-4.2.0.Final.xml"/>
<include file="META-INF/jpa-changelog-4.2.0.xml"/>
</databaseChangeLog> </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_1;
import org.keycloak.migration.migrators.MigrateTo3_4_2; import org.keycloak.migration.migrators.MigrateTo3_4_2;
import org.keycloak.migration.migrators.MigrateTo4_0_0; 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.migration.migrators.Migration;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -72,7 +73,8 @@ public class MigrationModelManager {
new MigrateTo3_4_0(), new MigrateTo3_4_0(),
new MigrateTo3_4_1(), new MigrateTo3_4_1(),
new MigrateTo3_4_2(), new MigrateTo3_4_2(),
new MigrateTo4_0_0() new MigrateTo4_0_0(),
new MigrateTo4_2_0()
}; };
public static void migrate(KeycloakSession session) { 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.setName("Verify Email");
verifyEmail.setProviderId(UserModel.RequiredAction.VERIFY_EMAIL.name()); verifyEmail.setProviderId(UserModel.RequiredAction.VERIFY_EMAIL.name());
verifyEmail.setDefaultAction(false); verifyEmail.setDefaultAction(false);
verifyEmail.setPriority(50);
realm.addRequiredActionProvider(verifyEmail); realm.addRequiredActionProvider(verifyEmail);
} }
@ -45,6 +46,7 @@ public class DefaultRequiredActions {
updateProfile.setName("Update Profile"); updateProfile.setName("Update Profile");
updateProfile.setProviderId(UserModel.RequiredAction.UPDATE_PROFILE.name()); updateProfile.setProviderId(UserModel.RequiredAction.UPDATE_PROFILE.name());
updateProfile.setDefaultAction(false); updateProfile.setDefaultAction(false);
updateProfile.setPriority(40);
realm.addRequiredActionProvider(updateProfile); realm.addRequiredActionProvider(updateProfile);
} }
@ -55,6 +57,7 @@ public class DefaultRequiredActions {
totp.setName("Configure OTP"); totp.setName("Configure OTP");
totp.setProviderId(UserModel.RequiredAction.CONFIGURE_TOTP.name()); totp.setProviderId(UserModel.RequiredAction.CONFIGURE_TOTP.name());
totp.setDefaultAction(false); totp.setDefaultAction(false);
totp.setPriority(10);
realm.addRequiredActionProvider(totp); realm.addRequiredActionProvider(totp);
} }
@ -65,6 +68,7 @@ public class DefaultRequiredActions {
updatePassword.setName("Update Password"); updatePassword.setName("Update Password");
updatePassword.setProviderId(UserModel.RequiredAction.UPDATE_PASSWORD.name()); updatePassword.setProviderId(UserModel.RequiredAction.UPDATE_PASSWORD.name());
updatePassword.setDefaultAction(false); updatePassword.setDefaultAction(false);
updatePassword.setPriority(30);
realm.addRequiredActionProvider(updatePassword); realm.addRequiredActionProvider(updatePassword);
} }
@ -75,6 +79,7 @@ public class DefaultRequiredActions {
termsAndConditions.setName("Terms and Conditions"); termsAndConditions.setName("Terms and Conditions");
termsAndConditions.setProviderId("terms_and_conditions"); termsAndConditions.setProviderId("terms_and_conditions");
termsAndConditions.setDefaultAction(false); termsAndConditions.setDefaultAction(false);
termsAndConditions.setPriority(20);
realm.addRequiredActionProvider(termsAndConditions); realm.addRequiredActionProvider(termsAndConditions);
} }

View file

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

View file

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

View file

@ -994,16 +994,10 @@ public class AuthenticationManager {
protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession, protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession,
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user, HttpRequest request, EventBuilder event, RealmModel realm, UserModel user,
Set<String> requiredActions) { 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()); RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId());
if (factory == null) { 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?"); 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; 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) { 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 // 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.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import static javax.ws.rs.core.Response.Status.NOT_FOUND; import static javax.ws.rs.core.Response.Status.NOT_FOUND;
@ -880,6 +881,7 @@ public class AuthenticationManagementResource {
requiredAction.setName(name); requiredAction.setName(name);
requiredAction.setProviderId(providerId); requiredAction.setProviderId(providerId);
requiredAction.setDefaultAction(false); requiredAction.setDefaultAction(false);
requiredAction.setPriority(getNextRequiredActionPriority());
requiredAction.setEnabled(true); requiredAction.setEnabled(true);
requiredAction = realm.addRequiredActionProvider(requiredAction); requiredAction = realm.addRequiredActionProvider(requiredAction);
@ -887,7 +889,12 @@ public class AuthenticationManagementResource {
adminEvent.operation(OperationType.CREATE).resource(ResourceType.REQUIRED_ACTION).resourcePath(uriInfo).representation(data).success(); 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 * Get required actions
* *
@ -913,6 +920,7 @@ public class AuthenticationManagementResource {
rep.setAlias(model.getAlias()); rep.setAlias(model.getAlias());
rep.setName(model.getName()); rep.setName(model.getName());
rep.setDefaultAction(model.isDefaultAction()); rep.setDefaultAction(model.isDefaultAction());
rep.setPriority(model.getPriority());
rep.setEnabled(model.isEnabled()); rep.setEnabled(model.isEnabled());
rep.setConfig(model.getConfig()); rep.setConfig(model.getConfig());
return rep; return rep;
@ -959,6 +967,7 @@ public class AuthenticationManagementResource {
update.setAlias(rep.getAlias()); update.setAlias(rep.getAlias());
update.setProviderId(model.getProviderId()); update.setProviderId(model.getProviderId());
update.setDefaultAction(rep.isDefaultAction()); update.setDefaultAction(rep.isDefaultAction());
update.setPriority(rep.getPriority());
update.setEnabled(rep.isEnabled()); update.setEnabled(rep.isEnabled());
update.setConfig(rep.getConfig()); update.setConfig(rep.getConfig());
realm.updateRequiredActionProvider(update); realm.updateRequiredActionProvider(update);
@ -984,6 +993,74 @@ public class AuthenticationManagementResource {
adminEvent.operation(OperationType.DELETE).resource(ResourceType.REQUIRED_ACTION).resourcePath(uriInfo).success(); 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 * 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.LinkedList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* *
@ -52,12 +53,15 @@ public class ActionUtil {
public static void addRequiredActionForRealm(RealmRepresentation testRealm, String providerId) { public static void addRequiredActionForRealm(RealmRepresentation testRealm, String providerId) {
List<RequiredActionProviderRepresentation> requiredActions = testRealm.getRequiredActions(); List<RequiredActionProviderRepresentation> requiredActions = testRealm.getRequiredActions();
if (requiredActions == null) requiredActions = new LinkedList(); if (requiredActions == null) requiredActions = new LinkedList();
RequiredActionProviderRepresentation last = requiredActions.get(requiredActions.size() - 1);
RequiredActionProviderRepresentation action = new RequiredActionProviderRepresentation(); RequiredActionProviderRepresentation action = new RequiredActionProviderRepresentation();
action.setAlias(providerId); action.setAlias(providerId);
action.setProviderId(providerId); action.setProviderId(providerId);
action.setEnabled(true); action.setEnabled(true);
action.setDefaultAction(true); action.setDefaultAction(true);
action.setPriority(last.getPriority() + 1);
requiredActions.add(action); requiredActions.add(action);
testRealm.setRequiredActions(requiredActions); 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 @Test
public void testCRUDRequiredAction() { public void testCRUDRequiredAction() {
int lastPriority = authMgmtResource.getRequiredActions().get(authMgmtResource.getRequiredActions().size() - 1).getPriority();
// Just Dummy RequiredAction is not registered in the realm // Just Dummy RequiredAction is not registered in the realm
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions(); List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
Assert.assertEquals(1, result.size()); Assert.assertEquals(1, result.size());
@ -96,6 +98,9 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
compareRequiredAction(rep, newRequiredAction(DummyRequiredActionFactory.PROVIDER_ID, "Dummy Action", compareRequiredAction(rep, newRequiredAction(DummyRequiredActionFactory.PROVIDER_ID, "Dummy Action",
true, false, Collections.<String, String>emptyMap())); 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 // Update not-existent - should fail
try { try {
authMgmtResource.updateRequiredAction("not-existent", rep); 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.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -198,6 +199,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testOfflineScopeAddedToClient(); testOfflineScopeAddedToClient();
} }
protected void testMigrationTo4_2_0() {
testRequiredActionsPriority(this.masterRealm, this.migrationRealm);
}
private void testCliConsoleScopeSize(RealmResource realm) { private void testCliConsoleScopeSize(RealmResource realm) {
ClientRepresentation cli = realm.clients().findByClientId(Constants.ADMIN_CLI_CLIENT_ID).get(0); ClientRepresentation cli = realm.clients().findByClientId(Constants.ADMIN_CLI_CLIENT_ID).get(0);
ClientRepresentation console = realm.clients().findByClientId(Constants.ADMIN_CONSOLE_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() { protected String getMigrationMode() {
return System.getProperty("migration.mode"); return System.getProperty("migration.mode");
} }
@ -481,6 +505,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
protected void testMigrationTo4_x() { protected void testMigrationTo4_x() {
testMigrationTo4_0_0(); testMigrationTo4_0_0();
testMigrationTo4_2_0();
} }

View file

@ -437,6 +437,18 @@ public class AdminEventPaths {
return uri.toString(); 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 // ATTACK DETECTION
public static String attackDetectionClearBruteForceForUserPath(String username) { 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, module.controller('RequiredActionsCtrl', function($scope, realm, unregisteredRequiredActions,
$modal, $route, $modal, $route,
RegisterRequiredAction, RequiredActions, Notifications) { RegisterRequiredAction, RequiredActions, RequiredActionRaisePriority, RequiredActionLowerPriority, Notifications) {
console.log('RequiredActionsCtrl'); console.log('RequiredActionsCtrl');
$scope.realm = realm; $scope.realm = realm;
$scope.unregisteredRequiredActions = unregisteredRequiredActions; $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() { $scope.register = function() {
var controller = function($scope, $modalInstance) { var controller = function($scope, $modalInstance) {
$scope.unregisteredRequiredActions = unregisteredRequiredActions; $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) { module.factory('UnregisteredRequiredActions', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/authentication/unregistered-required-actions', { return $resource(authUrl + '/admin/realms/:realm/authentication/unregistered-required-actions', {
realm : '@realm' realm : '@realm'

View file

@ -18,8 +18,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="requiredAction in requiredActions | orderBy : 'name'" data-ng-show="requiredActions.length > 0"> <tr ng-repeat="requiredAction in requiredActions" data-ng-show="requiredActions.length > 0">
<td>{{requiredAction.name}}</td> <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.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> <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> </tr>