required actions storage/display

This commit is contained in:
Bill Burke 2015-06-16 22:48:39 -04:00
parent 3b78fa2d5d
commit dddc5181e7
24 changed files with 345 additions and 31 deletions

View file

@ -77,6 +77,7 @@
<constraints nullable="false"/>
</column>
<column name="ALIAS" type="VARCHAR(255)"/>
<column name="NAME" type="VARCHAR(255)"/>
<column name="REALM_ID" type="VARCHAR(36)"/>
<column name="ENABLED" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>

View file

@ -1611,13 +1611,28 @@ module.controller('AuthenticationFlowsCtrl', function($scope, realm, Authenticat
});
module.controller('RequiredActionsCtrl', function($scope, realm, RequiredActions, Notifications, Dialog, $location) {
console.log('RequiredActionsCtrl');
$scope.realm = realm;
$scope.requiredActions = [];
var setupRequiredActionsForm = function() {
console.log('setupRequiredActionsForm');
RequiredActions.query({id: realm.realm}, function(data) {
$scope.requiredActions = [];
for (var i = 0; i < data.length; i++) {
$scope.requiredActions.push(data[i]);
}
});
};
$scope.updateRequiredAction = function(action) {
RequiredActions.update({realm: realm.realm, alias: action.alias}, action, function() {
Notifications.success("Auth requirement updated");
setupForm();
setupRequiredActionsForm();
});
}
});
module.controller('DefaultRequiredActionsCtrl', function($scope, realm, RequiredActions, Notifications, Dialog, $location) {
$scope.realm = realm;
setupRequiredActionsForm();
});
@ -1627,3 +1642,4 @@ module.controller('DefaultRequiredActionsCtrl', function($scope, realm, Required

View file

@ -228,8 +228,8 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
RequiredActions.query({id: realm.realm}, function(data) {
$scope.userReqActionList = [];
for (var i = 0; i < data.length; i++) {
console.log("listed required action: " + data[i].text);
item = { id: data[i].id, text: data[i].text };
console.log("listed required action: " + data[i].name);
item = data[i];
$scope.userReqActionList.push(item);
}

View file

@ -63,6 +63,14 @@ module.factory('UserListLoader', function(Loader, User, $route, $q) {
});
});
module.factory('RequiredActionsListLoader', function(Loader, RequiredActions, $route, $q) {
return Loader.query(RequiredActions, function() {
return {
realm : $route.current.params.realm
}
});
});
module.factory('RealmSessionStatsLoader', function(Loader, RealmSessionStats, $route, $q) {
return Loader.get(RealmSessionStats, function() {
return {

View file

@ -187,8 +187,13 @@ module.factory('RealmAdminEvents', function($resource) {
});
module.factory('RequiredActions', function($resource) {
return $resource(authUrl + '/admin/realms/:id/required-actions', {
id : '@realm'
return $resource(authUrl + '/admin/realms/:id/authentication/required-actions/:alias', {
realm : '@realm',
alias : '@alias'
}, {
update : {
method : 'PUT'
}
});
});

View file

@ -27,7 +27,7 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="execution in executions">
<tr ng-repeat="execution in executions" data-ng-show="executions.length > 0">
<td ng-show="execution.subFlow"></td>
<td><h2>{{execution.referenceType}}</h2></td>
<td ng-hide="execution.subFlow"></td>

View file

@ -2,6 +2,25 @@
<h1>Authentication</h1>
<kc-tabs-authentication></kc-tabs-authentication>
<table class="table table-striped table-bordered">
<thead>
<tr data-ng-hide="requiredActions.length == 0">
<th>Required Action</th>
<th>Enabled</th>
<th>Default Action</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="requiredAction in requiredActions" data-ng-show="requiredActions.length > 0">
<td>{{requiredAction.name}}</td>
<td>{{requiredAction.enabled}}</td>
<td>{{requiredAction.defaultAction}}</td>
</tr>
<tr data-ng-show="requiredActions.length == 0">
<td>No required actions configured</td>
</tr>
</tbody>
</table>
</div>

View file

@ -80,7 +80,7 @@
<div class="col-md-6">
<select ui-select2 id="reqActions" ng-model="user.requiredActions" data-placeholder="Select an action..." multiple>
<option ng-repeat="action in userReqActionList" value="{{action.id}}">{{action.text}}</option>
<option ng-repeat="action in userReqActionList" value="{{action.alias}}">{{action.name}}</option>
</select>
</div>
<kc-tooltip>Require an action when the user logs in. 'Verify email' sends an email to the user to verify their email address. 'Update profile' requires user to enter in new personal information. 'Update password' requires user to enter in a new password. 'Configure TOTP' requires setup of a mobile password generator.</kc-tooltip>

View file

@ -10,6 +10,7 @@ import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.DefaultRequiredActions;
import java.util.List;
import java.util.Map;
@ -28,10 +29,6 @@ public class MigrateTo1_3_0 {
public void migrate(KeycloakSession session) {
List<RealmModel> realms = session.realms().getRealms();
for (RealmModel realm : realms) {
if (realm.getAuthenticationFlows().size() == 0) {
DefaultAuthenticationFlows.addFlows(realm);
}
migrateLDAPProviders(session, realm);
}

View file

@ -4,6 +4,7 @@ import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.DefaultRequiredActions;
import java.util.List;
@ -20,6 +21,7 @@ public class MigrateTo1_4_0 {
for (RealmModel realm : realms) {
if (realm.getAuthenticationFlows().size() == 0) {
DefaultAuthenticationFlows.addFlows(realm);
DefaultRequiredActions.addActions(realm);
}
}

View file

@ -204,6 +204,7 @@ public interface RealmModel extends RoleContainerModel {
void updateRequiredActionProvider(RequiredActionProviderModel model);
void removeRequiredActionProvider(RequiredActionProviderModel model);
RequiredActionProviderModel getRequiredActionProviderById(String id);
RequiredActionProviderModel getRequiredActionProviderByAlias(String alias);
List<IdentityProviderModel> getIdentityProviders();
IdentityProviderModel getIdentityProviderByAlias(String alias);

View file

@ -11,6 +11,7 @@ public class RequiredActionProviderModel {
private String id;
private String alias;
private String name;
private String providerId;
private boolean enabled;
private boolean defaultAction;
@ -33,6 +34,20 @@ public class RequiredActionProviderModel {
this.alias = alias;
}
/**
* Used for display purposes. Probably should clean this code up and make alias and name the same, but
* the old code references an Enum and the admin console creates a "friendly" name for each enum.
*
* @return
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isEnabled() {
return enabled;
}

View file

@ -9,6 +9,7 @@ import java.util.Map;
public class RequiredActionProviderEntity {
protected String id;
protected String alias;
protected String name;
protected String providerId;
protected boolean enabled;
protected boolean defaultAction;
@ -30,6 +31,14 @@ public class RequiredActionProviderEntity {
this.alias = alias;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isEnabled() {
return enabled;
}

View file

@ -0,0 +1,69 @@
package org.keycloak.models.utils;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class DefaultRequiredActions {
public static void addActions(RealmModel realm) {
if (realm.getRequiredActionProviderByAlias(UserModel.RequiredAction.VERIFY_EMAIL.name()) == null) {
RequiredActionProviderModel verifyEmail = new RequiredActionProviderModel();
verifyEmail.setEnabled(true);
verifyEmail.setAlias(UserModel.RequiredAction.VERIFY_EMAIL.name());
verifyEmail.setName("Verify Email");
verifyEmail.setProviderId(UserModel.RequiredAction.VERIFY_EMAIL.name());
verifyEmail.setDefaultAction(false);
realm.addRequiredActionProvider(verifyEmail);
}
if (realm.getRequiredActionProviderByAlias(UserModel.RequiredAction.UPDATE_PROFILE.name()) == null) {
RequiredActionProviderModel updateProfile = new RequiredActionProviderModel();
updateProfile.setEnabled(true);
updateProfile.setAlias(UserModel.RequiredAction.UPDATE_PROFILE.name());
updateProfile.setName("Update Profile");
updateProfile.setProviderId(UserModel.RequiredAction.UPDATE_PROFILE.name());
updateProfile.setDefaultAction(false);
realm.addRequiredActionProvider(updateProfile);
}
if (realm.getRequiredActionProviderByAlias(UserModel.RequiredAction.CONFIGURE_TOTP.name()) == null) {
RequiredActionProviderModel totp = new RequiredActionProviderModel();
totp.setEnabled(true);
totp.setAlias(UserModel.RequiredAction.CONFIGURE_TOTP.name());
totp.setName("Configure Totp");
totp.setProviderId(UserModel.RequiredAction.CONFIGURE_TOTP.name());
totp.setDefaultAction(false);
realm.addRequiredActionProvider(totp);
}
if (realm.getRequiredActionProviderByAlias(UserModel.RequiredAction.UPDATE_PASSWORD.name()) == null) {
RequiredActionProviderModel updatePassword = new RequiredActionProviderModel();
updatePassword.setEnabled(true);
updatePassword.setAlias(UserModel.RequiredAction.UPDATE_PASSWORD.name());
updatePassword.setName("Update Password");
updatePassword.setProviderId(UserModel.RequiredAction.UPDATE_PASSWORD.name());
updatePassword.setDefaultAction(false);
realm.addRequiredActionProvider(updatePassword);
}
if (realm.getRequiredActionProviderByAlias("terms_and_conditions") == null) {
RequiredActionProviderModel termsAndConditions = new RequiredActionProviderModel();
termsAndConditions.setEnabled(false);
termsAndConditions.setAlias("terms_and_conditions");
termsAndConditions.setName("Terms and Conditions");
termsAndConditions.setProviderId("terms_and_conditions");
termsAndConditions.setDefaultAction(false);
realm.addRequiredActionProvider(termsAndConditions);
}
}
}

View file

@ -1449,6 +1449,7 @@ public class RealmAdapter implements RealmModel {
RequiredActionProviderEntity auth = new RequiredActionProviderEntity();
auth.setId(KeycloakModelUtils.generateId());
auth.setAlias(model.getAlias());
auth.setName(model.getName());
auth.setProviderId(model.getProviderId());
auth.setConfig(model.getConfig());
auth.setEnabled(model.isEnabled());
@ -1477,6 +1478,7 @@ public class RealmAdapter implements RealmModel {
model.setId(entity.getId());
model.setProviderId(entity.getProviderId());
model.setAlias(entity.getAlias());
model.setName(entity.getName());
model.setEnabled(entity.isEnabled());
model.setDefaultAction(entity.isDefaultAction());
Map<String, String> config = new HashMap<>();
@ -1492,6 +1494,7 @@ public class RealmAdapter implements RealmModel {
entity.setAlias(model.getAlias());
entity.setProviderId(model.getProviderId());
entity.setEnabled(model.isEnabled());
entity.setName(model.getName());
entity.setDefaultAction(model.isDefaultAction());
if (entity.getConfig() == null) {
entity.setConfig(model.getConfig());
@ -1521,6 +1524,15 @@ public class RealmAdapter implements RealmModel {
return entity;
}
@Override
public RequiredActionProviderModel getRequiredActionProviderByAlias(String alias) {
for (RequiredActionProviderModel action : getRequiredActionProviders()) {
if (action.getAlias().equals(alias)) return action;
}
return null;
}
@Override

View file

@ -1161,4 +1161,10 @@ public class RealmAdapter implements RealmModel {
if (updated != null) return updated.getRequiredActionProviderById(id);
return cached.getRequiredActionProviders().get(id);
}
@Override
public RequiredActionProviderModel getRequiredActionProviderByAlias(String alias) {
if (updated != null) return updated.getRequiredActionProviderByAlias(alias);
return cached.getRequiredActionProvidersByAlias().get(alias);
}
}

View file

@ -83,6 +83,7 @@ public class CachedRealm {
private Map<String, AuthenticationFlowModel> authenticationFlows = new HashMap<>();
private Map<String, AuthenticatorModel> authenticators = new HashMap<>();
private Map<String, RequiredActionProviderModel> requiredActionProviders = new HashMap<>();
private Map<String, RequiredActionProviderModel> requiredActionProvidersByAlias = new HashMap<>();
private MultivaluedHashMap<String, AuthenticationExecutionModel> authenticationExecutions = new MultivaluedHashMap<>();
private Map<String, AuthenticationExecutionModel> executionsById = new HashMap<>();
@ -204,6 +205,7 @@ public class CachedRealm {
}
for (RequiredActionProviderModel action : model.getRequiredActionProviders()) {
requiredActionProviders.put(action.getId(), action);
requiredActionProvidersByAlias.put(action.getAlias(), action);
}
}
@ -447,4 +449,8 @@ public class CachedRealm {
public Map<String, RequiredActionProviderModel> getRequiredActionProviders() {
return requiredActionProviders;
}
public Map<String, RequiredActionProviderModel> getRequiredActionProvidersByAlias() {
return requiredActionProvidersByAlias;
}
}

View file

@ -1732,6 +1732,7 @@ public class RealmAdapter implements RealmModel {
RequiredActionProviderEntity auth = new RequiredActionProviderEntity();
auth.setId(KeycloakModelUtils.generateId());
auth.setAlias(model.getAlias());
auth.setName(model.getName());
auth.setRealm(realm);
auth.setProviderId(model.getProviderId());
auth.setConfig(model.getConfig());
@ -1767,6 +1768,7 @@ public class RealmAdapter implements RealmModel {
model.setAlias(entity.getAlias());
model.setEnabled(entity.isEnabled());
model.setDefaultAction(entity.isDefaultAction());
model.setName(entity.getName());
Map<String, String> config = new HashMap<>();
if (entity.getConfig() != null) config.putAll(entity.getConfig());
model.setConfig(config);
@ -1781,6 +1783,7 @@ public class RealmAdapter implements RealmModel {
entity.setProviderId(model.getProviderId());
entity.setEnabled(model.isEnabled());
entity.setDefaultAction(model.isDefaultAction());
entity.setName(model.getName());
if (entity.getConfig() == null) {
entity.setConfig(model.getConfig());
} else {
@ -1799,4 +1802,12 @@ public class RealmAdapter implements RealmModel {
}
return actions;
}
@Override
public RequiredActionProviderModel getRequiredActionProviderByAlias(String alias) {
for (RequiredActionProviderModel action : getRequiredActionProviders()) {
if (action.getAlias().equals(alias)) return action;
}
return null;
}
}

View file

@ -30,6 +30,9 @@ public class RequiredActionProviderEntity {
@Column(name="ALIAS")
protected String alias;
@Column(name="NAME")
protected String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "REALM_ID")
protected RealmEntity realm;
@ -104,4 +107,12 @@ public class RequiredActionProviderEntity {
public void setConfig(Map<String, String> config) {
this.config = config;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View file

@ -1607,6 +1607,15 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
return entity;
}
@Override
public RequiredActionProviderModel getRequiredActionProviderByAlias(String alias) {
for (RequiredActionProviderModel action : getRequiredActionProviders()) {
if (action.getAlias().equals(alias)) return action;
}
return null;
}

View file

@ -17,6 +17,7 @@ import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
@ -483,8 +484,9 @@ public class AuthenticationManager {
};
// see if any required actions need triggering, i.e. an expired password
for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class)) {
RequiredActionProvider provider = ((RequiredActionFactory)factory).create(session);
for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
if (!model.isEnabled()) continue;
RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
provider.evaluateTriggers(context);
}
@ -495,7 +497,8 @@ public class AuthenticationManager {
Set<String> requiredActions = user.getRequiredActions();
for (String action : requiredActions) {
RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, action);
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
Response challenge = actionProvider.invokeRequiredAction(context);
if (challenge != null) {
return challenge;

View file

@ -17,6 +17,7 @@ import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.DefaultRequiredActions;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ClientRepresentation;
@ -88,6 +89,7 @@ public class RealmManager {
setupBrokerService(realm);
setupAdminConsole(realm);
setupAuthenticationFlows(realm);
setupRequiredActions(realm);
return realm;
}
@ -96,6 +98,10 @@ public class RealmManager {
if (realm.getAuthenticationFlows().size() == 0) DefaultAuthenticationFlows.addFlows(realm);
}
protected void setupRequiredActions(RealmModel realm) {
if (realm.getRequiredActionProviders().size() == 0) DefaultRequiredActions.addActions(realm);
}
protected void setupAdminConsole(RealmModel realm) {
ClientModel adminConsole = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
if (adminConsole == null) adminConsole = new ClientManager(this).createClient(realm, Constants.ADMIN_CONSOLE_CLIENT_ID);
@ -261,6 +267,7 @@ public class RealmManager {
RepresentationToModel.importRealm(session, rep, realm);
setupAuthenticationFlows(realm);
setupRequiredActions(realm);
// Refresh periodic sync tasks for configured federationProviders
List<UserFederationProviderModel> federationProviders = realm.getUserFederationProviders();

View file

@ -6,13 +6,18 @@ import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.provider.ProviderFactory;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
@ -20,8 +25,10 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
@ -176,4 +183,118 @@ public class AuthenticationManagementResource {
realm.updateAuthenticatorExecution(model);
}
}
public static class RequiredActionProviderRepresentation {
private String alias;
private String name;
private boolean enabled;
private boolean defaultAction;
private Map<String, String> config = new HashMap<String, String>();
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isDefaultAction() {
return defaultAction;
}
public void setDefaultAction(boolean defaultAction) {
this.defaultAction = defaultAction;
}
public Map<String, String> getConfig() {
return config;
}
public void setConfig(Map<String, String> config) {
this.config = config;
}
}
@Path("required-actions")
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<RequiredActionProviderRepresentation> getRequiredActions() {
List<RequiredActionProviderRepresentation> list = new LinkedList<>();
for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
RequiredActionProviderRepresentation rep = toRepresentation(model);
list.add(rep);
}
return list;
}
public static RequiredActionProviderRepresentation toRepresentation(RequiredActionProviderModel model) {
RequiredActionProviderRepresentation rep = new RequiredActionProviderRepresentation();
rep.setAlias(model.getAlias());
rep.setName(model.getName());
rep.setDefaultAction(model.isDefaultAction());
rep.setEnabled(model.isEnabled());
rep.setConfig(model.getConfig());
return rep;
}
@Path("required-actions/{alias}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public RequiredActionProviderRepresentation getRequiredAction(@PathParam("alias") String alias) {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(alias);
if (model == null) {
throw new NotFoundException("Failed to find required action: " + alias);
}
return toRepresentation(model);
}
@Path("required-actions/{alias}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void updateRequiredAction(@PathParam("alias") String alias, RequiredActionProviderRepresentation rep) {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(alias);
if (model == null) {
throw new NotFoundException("Failed to find required action: " + alias);
}
RequiredActionProviderModel update = new RequiredActionProviderModel();
update.setId(update.getId());
update.setName(rep.getName());
update.setAlias(rep.getAlias());
update.setProviderId(model.getProviderId());
update.setDefaultAction(rep.isDefaultAction());
update.setEnabled(rep.isEnabled());
update.setConfig(rep.getConfig());
realm.updateRequiredActionProvider(update);
}
@Path("required-actions/{alias}")
@DELETE
public void updateRequiredAction(@PathParam("alias") String alias) {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(alias);
if (model == null) {
throw new NotFoundException("Failed to find required action: " + alias);
}
realm.removeRequiredActionProvider(model);
}
}

View file

@ -565,18 +565,4 @@ public class RealmAdminResource {
return new IdentityProvidersResource(realm, session, this.auth, adminEvent);
}
@Path("required-actions")
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Map<String, String>> getRequiredActions() {
List<Map<String, String>> list = new LinkedList<>();
for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class)) {
RequiredActionFactory actionFactory = (RequiredActionFactory)factory;
Map<String, String> data = new HashMap<>();
data.put("id", actionFactory.getId());
data.put("text", actionFactory.getDisplayText());
list.add(data);
}
return list;
}
}