KEYCLOAK-6313 Add required action's priority for customizing the execution order
This commit is contained in:
parent
b43392bac8
commit
7c0ca9aad2
22 changed files with 577 additions and 13 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue