keycloak-aplcache

KEYCLOAK-6313 Add required action's priority for customizing

7/4/2018 9:37:03 AM

Changes

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderRepresentation.java
index cbec62a..4e1d76e 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RequiredActionProviderRepresentation.java
@@ -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;
     }
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
index a600ef3..bd89d78 100644
--- 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
@@ -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)
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RequiredActionProviderEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RequiredActionProviderEntity.java
index d8b43fb..02d6187 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RequiredActionProviderEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RequiredActionProviderEntity.java
@@ -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;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index f006180..fa29652 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -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);
     }
 
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.2.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.2.0.xml
new file mode 100644
index 0000000..be0202b
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.2.0.xml
@@ -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>
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
index f087429..7bf1d34 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
@@ -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>
diff --git a/server-spi/src/main/java/org/keycloak/models/RequiredActionProviderModel.java b/server-spi/src/main/java/org/keycloak/models/RequiredActionProviderModel.java
index 891186a..e7a2f19 100755
--- a/server-spi/src/main/java/org/keycloak/models/RequiredActionProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RequiredActionProviderModel.java
@@ -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;
     }
diff --git a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java
index e68f694..1ffc8fe 100755
--- a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java
+++ b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java
@@ -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) {
diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo4_2_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo4_2_0.java
new file mode 100644
index 0000000..e14ca50
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo4_2_0.java
@@ -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);
+        }
+    }
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java
index db38b64..19b802c 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultRequiredActions.java
@@ -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);
         }
 
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 9089fb2..6be4b2c 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -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());
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 60ae93f..0db650c 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -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
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 2874597..86561f6 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
@@ -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);
@@ -985,6 +994,74 @@ public class AuthenticationManagementResource {
     }
 
     /**
+     * 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
      */
     @Path("config-description/{providerId}")
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/ActionUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/ActionUtil.java
index a6a03ab..8b4b4ef 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/ActionUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/ActionUtil.java
@@ -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);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java
new file mode 100644
index 0000000..8adc411
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionPriorityTest.java
@@ -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();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java
index fb72564..0fe4a9b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/RequiredActionsTest.java
@@ -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);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ShiftRequiredActionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ShiftRequiredActionTest.java
new file mode 100644
index 0000000..a7af497
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ShiftRequiredActionTest.java
@@ -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());
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java
index b2498d2..1a1bd2d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java
@@ -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();
     }
 
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdminEventPaths.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdminEventPaths.java
index 0e72780..980465d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdminEventPaths.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdminEventPaths.java
@@ -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) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 44a74a5..9bd365e 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -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;
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index c80cc40..2517ae9 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -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'
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html b/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html
index 17dcc05..ab16456 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html
@@ -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>