From dfe232cf809c97ed39b9b3923a4b683a706a5978 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 25 May 2015 20:46:55 +0200 Subject: [PATCH] KEYCLOAK-886 User Federation Mappers - admin console --- ...serFederationMapperTypeRepresentation.java | 3 +- .../ldap/LDAPFederationProviderFactory.java | 15 +- .../AbstractLDAPFederationMapperFactory.java | 40 +++ .../FullNameLDAPFederationMapperFactory.java | 46 ++- .../mappers/RoleLDAPFederationMapper.java | 1 - .../RoleLDAPFederationMapperFactory.java | 97 +++++- ...rAttributeLDAPFederationMapperFactory.java | 49 ++- .../theme/base/admin/resources/js/app.js | 52 +++ .../admin/resources/js/controllers/users.js | 129 +++++++- .../theme/base/admin/resources/js/loaders.js | 28 ++ .../theme/base/admin/resources/js/services.js | 28 +- .../resources/partials/federated-generic.html | 9 +- .../partials/federated-kerberos.html | 5 + .../resources/partials/federated-ldap.html | 5 + .../partials/federated-mapper-detail.html | 78 +++++ .../resources/partials/federated-mappers.html | 53 ++++ .../MapperConfigValidationException.java | 15 + .../mappers/UserFederationMapperFactory.java | 26 ++ .../models/utils/KeycloakModelUtils.java | 29 ++ .../resources/admin/RealmAdminResource.java | 4 +- .../admin/UserFederationProviderResource.java | 298 ++++++++++++++++++ ...a => UserFederationProvidersResource.java} | 140 ++------ .../federation/FederationTestUtils.java | 6 +- .../keycloak/testsuite/model/ImportTest.java | 2 +- 24 files changed, 996 insertions(+), 162 deletions(-) create mode 100644 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html create mode 100644 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html create mode 100644 model/api/src/main/java/org/keycloak/mappers/MapperConfigValidationException.java create mode 100644 services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java rename services/src/main/java/org/keycloak/services/resources/admin/{UserFederationResource.java => UserFederationProvidersResource.java} (50%) diff --git a/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java index f03b3765c0..f7f594af0a 100644 --- a/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java @@ -1,5 +1,6 @@ package org.keycloak.representations.idm; +import java.util.LinkedList; import java.util.List; /** @@ -11,7 +12,7 @@ public class UserFederationMapperTypeRepresentation { protected String category; protected String helpText; - protected List properties; + protected List properties = new LinkedList<>(); public String getId() { return id; diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java index 166c5bf27b..c020f5139b 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java @@ -24,7 +24,6 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationEventAwareProviderFactory; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; -import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationSyncResult; import org.keycloak.models.UserModel; @@ -89,7 +88,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute(); UserFederationMapperModel mapperModel; - mapperModel = KeycloakModelUtils.createUserFederationMapperModel("usernameMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID, + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("username", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID, UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, usernameLdapAttribute, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); @@ -97,25 +96,25 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi // For AD deployments with sAMAccountName is probably more common to map "cn" to full name of user if (activeDirectory && usernameLdapAttribute.equalsIgnoreCase(LDAPConstants.SAM_ACCOUNT_NAME)) { - mapperModel = KeycloakModelUtils.createUserFederationMapperModel("fullNameMapper", newProviderModel.getId(), FullNameLDAPFederationMapperFactory.ID, + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("full name", newProviderModel.getId(), FullNameLDAPFederationMapperFactory.PROVIDER_ID, FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); realm.addUserFederationMapper(mapperModel); } else { - mapperModel = KeycloakModelUtils.createUserFederationMapperModel("firstNameMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID, + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("first name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID, UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); realm.addUserFederationMapper(mapperModel); } - mapperModel = KeycloakModelUtils.createUserFederationMapperModel("lastNameMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID, + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("last name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID, UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.LAST_NAME, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.SN, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); realm.addUserFederationMapper(mapperModel); - mapperModel = KeycloakModelUtils.createUserFederationMapperModel("emailMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID, + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("email", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID, UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.EMAIL, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL, UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); @@ -125,14 +124,14 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi String modifyTimestampLdapAttrName = activeDirectory ? "whenChanged" : LDAPConstants.MODIFY_TIMESTAMP; // map createTimeStamp as read-only - mapperModel = KeycloakModelUtils.createUserFederationMapperModel("creationDateMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID, + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("creation date", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID, UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.CREATE_TIMESTAMP, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName, UserAttributeLDAPFederationMapper.READ_ONLY, "true"); realm.addUserFederationMapper(mapperModel); // map modifyTimeStamp as read-only - mapperModel = KeycloakModelUtils.createUserFederationMapperModel("modifyDateMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID, + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("modify date", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID, UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.MODIFY_TIMESTAMP, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName, UserAttributeLDAPFederationMapper.READ_ONLY, "true"); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java index 6b8f186d4c..33ace78075 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java @@ -1,25 +1,65 @@ package org.keycloak.federation.ldap.mappers; +import java.util.List; +import java.util.Map; + import org.keycloak.Config; +import org.keycloak.federation.ldap.LDAPFederationProviderFactory; +import org.keycloak.mappers.MapperConfigValidationException; import org.keycloak.mappers.UserFederationMapperFactory; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.provider.ProviderConfigProperty; /** * @author Marek Posolda */ public abstract class AbstractLDAPFederationMapperFactory implements UserFederationMapperFactory { + // Used to map attributes from LDAP to UserModel attributes + public static final String ATTRIBUTE_MAPPER_CATEGORY = "Attribute Mapper"; + + // Used to map roles from LDAP to UserModel users + public static final String ROLE_MAPPER_CATEGORY = "Role Mapper"; + @Override public void init(Config.Scope config) { } + @Override + public String getFederationProviderType() { + return LDAPFederationProviderFactory.PROVIDER_NAME; + } + @Override public void postInit(KeycloakSessionFactory factory) { } + @Override + public List getConfigProperties() { + throw new IllegalStateException("Method not supported for this implementation"); + } + @Override public void close() { } + public static ProviderConfigProperty createConfigProperty(String name, String label, String helpText, String type, Object defaultValue) { + ProviderConfigProperty configProperty = new ProviderConfigProperty(); + configProperty.setName(name); + configProperty.setLabel(label); + configProperty.setHelpText(helpText); + configProperty.setType(type); + configProperty.setDefaultValue(defaultValue); + return configProperty; + } + + protected void checkMandatoryConfigAttribute(String name, String displayName, UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + String attrConfigValue = mapperModel.getConfig().get(name); + if (attrConfigValue == null || attrConfigValue.trim().isEmpty()) { + throw new MapperConfigValidationException("Missing configuration for '" + displayName + "'"); + } + } + } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java index 6ef69791dc..d0d623005f 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java @@ -1,9 +1,14 @@ package org.keycloak.federation.ldap.mappers; +import java.util.ArrayList; import java.util.List; +import org.keycloak.mappers.MapperConfigValidationException; import org.keycloak.mappers.UserFederationMapper; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; import org.keycloak.provider.ProviderConfigProperty; /** @@ -11,21 +16,48 @@ import org.keycloak.provider.ProviderConfigProperty; */ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { - public static final String ID = "full-name-ldap-mapper"; + public static final String PROVIDER_ID = "full-name-ldap-mapper"; - @Override - public String getHelpText() { - return "Some help text - full name mapper - TODO"; + protected static final List configProperties = new ArrayList(); + + static { + ProviderConfigProperty userModelAttribute = createConfigProperty(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute", + "Name of LDAP attribute, which contains fullName of user. In most cases it will be 'cn' ", ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN); + configProperties.add(userModelAttribute); + + ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only", + "For Read-only is data imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false"); + configProperties.add(readOnly); } @Override - public List getConfigProperties() { - return null; + public String getHelpText() { + return "Used to map full-name of user from single attribute in LDAP (usually 'cn' attribute) to firstName and lastName attributes of UserModel in Keycloak DB"; + } + + @Override + public String getDisplayCategory() { + return ATTRIBUTE_MAPPER_CATEGORY; + } + + @Override + public String getDisplayType() { + return "Full Name"; + } + + @Override + public List getConfigProperties(RealmModel realm) { + return configProperties; } @Override public String getId() { - return ID; + return PROVIDER_ID; + } + + @Override + public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + checkMandatoryConfigAttribute(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute", mapperModel); } @Override diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java index 47f288dba1..084b255e4a 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java @@ -178,7 +178,6 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { } String[] objClasses = objectClasses.split(","); - // TODO: util method for trim and convert array to collection? Set trimmed = new HashSet(); for (String objectClass : objClasses) { objectClass = objectClass.trim(); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java index cbae85069f..a5eadd99a7 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java @@ -1,9 +1,18 @@ package org.keycloak.federation.ldap.mappers; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; import java.util.List; +import java.util.Map; +import org.keycloak.mappers.MapperConfigValidationException; import org.keycloak.mappers.UserFederationMapper; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; import org.keycloak.provider.ProviderConfigProperty; /** @@ -11,21 +20,95 @@ import org.keycloak.provider.ProviderConfigProperty; */ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { - public static final String ID = "role-ldap-mapper"; + public static final String PROVIDER_ID = "role-ldap-mapper"; - @Override - public String getHelpText() { - return "Some help text - role mapper - TODO"; + protected static final List configProperties = new ArrayList(); + + static { + ProviderConfigProperty rolesDn = createConfigProperty(RoleLDAPFederationMapper.ROLES_DN, "LDAP Roles DN", + "LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ", ProviderConfigProperty.STRING_TYPE, null); + configProperties.add(rolesDn); + + ProviderConfigProperty roleNameLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.ROLE_NAME_LDAP_ATTRIBUTE, "Role Name LDAP Attribute", + "Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=role1,ou=finance,dc=example,dc=org' ", + ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN); + configProperties.add(roleNameLDAPAttribute); + + ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute", + "Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ", + ProviderConfigProperty.STRING_TYPE, LDAPConstants.MEMBER); + configProperties.add(membershipLDAPAttribute); + + ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleLDAPFederationMapper.ROLE_OBJECT_CLASSES, "Role Object Classes", + "Object classes of the role object divided by comma (if more values needed). In typical LDAP deployment it could be 'groupOfNames' or 'groupOfEntries' ", + ProviderConfigProperty.STRING_TYPE, LDAPConstants.GROUP_OF_NAMES); + configProperties.add(roleObjectClasses); + + List modes = new LinkedList(); + for (RoleLDAPFederationMapper.Mode mode : RoleLDAPFederationMapper.Mode.values()) { + modes.add(mode.toString()); + } + ProviderConfigProperty mode = createConfigProperty(RoleLDAPFederationMapper.MODE, "Mode", + "LDAP_ONLY means that all role mappings are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where role mappings are " + + "retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where role mappings are retrieved from LDAP just at the time when user is imported from LDAP and then " + + "they are saved to local keycloak DB.", + ProviderConfigProperty.LIST_TYPE, modes); + configProperties.add(mode); + + ProviderConfigProperty useRealmRolesMappings = createConfigProperty(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "Use Realm Roles Mapping", + "If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings", ProviderConfigProperty.BOOLEAN_TYPE, "true"); + configProperties.add(useRealmRolesMappings); + + // NOTE: ClientID will be computed dynamically from available clients } @Override - public List getConfigProperties() { - return null; + public String getHelpText() { + return "Used to map role mappings of roles from some LDAP DN to Keycloak role mappings of either realm roles or client roles of particular client"; + } + + @Override + public String getDisplayCategory() { + return ROLE_MAPPER_CATEGORY; + } + + @Override + public String getDisplayType() { + return "Role mappings"; + } + + @Override + public List getConfigProperties(RealmModel realm) { + List props = new ArrayList(configProperties); + + Map clients = realm.getClientNameMap(); + List clientIds = new ArrayList(clients.keySet()); + + ProviderConfigProperty clientIdProperty = createConfigProperty(RoleLDAPFederationMapper.CLIENT_ID, "Client ID", + "Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false", + ProviderConfigProperty.LIST_TYPE, clientIds); + props.add(clientIdProperty); + + return props; } @Override public String getId() { - return ID ; + return PROVIDER_ID; + } + + @Override + public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + checkMandatoryConfigAttribute(RoleLDAPFederationMapper.ROLES_DN, "LDAP Roles DN", mapperModel); + + String realmMappings = mapperModel.getConfig().get(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING); + boolean useRealmMappings = Boolean.parseBoolean(realmMappings); + if (!useRealmMappings) { + String clientId = mapperModel.getConfig().get(RoleLDAPFederationMapper.CLIENT_ID); + if (clientId == null || clientId.trim().isEmpty()) { + throw new MapperConfigValidationException("Client ID needs to be provided in config when Realm Roles Mapping is not used"); + } + } } @Override diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java index 564b012491..c0b9d79233 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java @@ -1,9 +1,13 @@ package org.keycloak.federation.ldap.mappers; +import java.util.ArrayList; import java.util.List; +import org.keycloak.mappers.MapperConfigValidationException; import org.keycloak.mappers.UserFederationMapper; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; import org.keycloak.provider.ProviderConfigProperty; /** @@ -11,21 +15,52 @@ import org.keycloak.provider.ProviderConfigProperty; */ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { - public static final String ID = "user-attribute-ldap-mapper"; + public static final String PROVIDER_ID = "user-attribute-ldap-mapper"; + protected static final List configProperties = new ArrayList(); - @Override - public String getHelpText() { - return "Some help text TODO"; + static { + ProviderConfigProperty userModelAttribute = createConfigProperty(UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", + "Name of mapped UserModel property or UserModel attribute in Keycloak DB. For example 'firstName', 'lastName, 'email', 'street' etc.", ProviderConfigProperty.STRING_TYPE, null); + configProperties.add(userModelAttribute); + + ProviderConfigProperty ldapAttribute = createConfigProperty(UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, "LDAP Attribute", + "Name of mapped attribute on LDAP object. For example 'cn', 'sn, 'mail', 'street' etc.", ProviderConfigProperty.STRING_TYPE, null); + configProperties.add(ldapAttribute); + + ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only", + "Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false"); + configProperties.add(readOnly); } @Override - public List getConfigProperties() { - return null; + public String getHelpText() { + return "Used to map single attribute from LDAP user to attribute of UserModel in Keycloak DB"; + } + + @Override + public String getDisplayCategory() { + return ATTRIBUTE_MAPPER_CATEGORY; + } + + @Override + public String getDisplayType() { + return "User Attribute"; + } + + @Override + public List getConfigProperties(RealmModel realm) { + return configProperties; } @Override public String getId() { - return ID; + return PROVIDER_ID; + } + + @Override + public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException { + checkMandatoryConfigAttribute(UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", mapperModel); + checkMandatoryConfigAttribute(UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, "LDAP Attribute", mapperModel); } @Override diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 56f600825b..7a0ae1a67e 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -952,6 +952,58 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'GenericUserFederationCtrl' }) + .when('/realms/:realm/user-federation/providers/:provider/:instance/mappers', { + templateUrl : function(params){ return resourceUrl + '/partials/federated-mappers.html'; }, + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + provider : function(UserFederationInstanceLoader) { + return UserFederationInstanceLoader(); + }, + mapperTypes : function(UserFederationMapperTypesLoader) { + return UserFederationMapperTypesLoader(); + }, + mappers : function(UserFederationMappersLoader) { + return UserFederationMappersLoader(); + } + }, + controller : 'UserFederationMapperListCtrl' + }) + .when('/realms/:realm/user-federation/providers/:provider/:instance/mappers/:mapperId', { + templateUrl : function(params){ return resourceUrl + '/partials/federated-mapper-detail.html'; }, + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + provider : function(UserFederationInstanceLoader) { + return UserFederationInstanceLoader(); + }, + mapperTypes : function(UserFederationMapperTypesLoader) { + return UserFederationMapperTypesLoader(); + }, + mapper : function(UserFederationMapperLoader) { + return UserFederationMapperLoader(); + } + }, + controller : 'UserFederationMapperCtrl' + }) + .when('/create/user-federation-mappers/:realm/:provider/:instance', { + templateUrl : function(params){ return resourceUrl + '/partials/federated-mapper-detail.html'; }, + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + provider : function(UserFederationInstanceLoader) { + return UserFederationInstanceLoader(); + }, + mapperTypes : function(UserFederationMapperTypesLoader) { + return UserFederationMapperTypesLoader(); + }, + }, + controller : 'UserFederationMapperCreateCtrl' + }) + .when('/realms/:realm/defense/headers', { templateUrl : resourceUrl + '/partials/defense-headers.html', resolve : { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index 2444be45b3..bc723f31e5 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -511,8 +511,8 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif } function triggerSync(action) { - UserFederationSync.get({ action: action, realm: $scope.realm.realm, provider: $scope.instance.id }, function() { - Notifications.success("Sync of users finished successfully"); + UserFederationSync.save({ action: action, realm: $scope.realm.realm, provider: $scope.instance.id }, {}, function(syncResult) { + Notifications.success("Sync of users finished successfully. " + syncResult.status); }, function() { Notifications.error("Error during sync of users"); }); @@ -734,3 +734,128 @@ module.controller('LDAPCtrl', function($scope, $location, $route, Notifications, }); + +module.controller('UserFederationMapperListCtrl', function($scope, $location, Notifications, $route, Dialog, realm, provider, mapperTypes, mappers) { + console.log('UserFederationMapperListCtrl'); + + $scope.realm = realm; + $scope.provider = provider; + + $scope.mapperTypes = mapperTypes; + $scope.mappers = mappers; + + $scope.hasAnyMapperTypes = false; + for (var property in mapperTypes) { + if (!(property.startsWith('$'))) { + $scope.hasAnyMapperTypes = true; + break; + } + } + +}); + +module.controller('UserFederationMapperCtrl', function($scope, realm, provider, mapperTypes, mapper, UserFederationMapper, Notifications, Dialog, $location) { + console.log('UserFederationMapperCtrl'); + $scope.realm = realm; + $scope.provider = provider; + $scope.create = false; + $scope.mapper = angular.copy(mapper); + $scope.changed = false; + $scope.mapperType = mapperTypes[mapper.federationMapperType]; + + $scope.$watch('mapper', function() { + if (!angular.equals($scope.mapper, mapper)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + UserFederationMapper.update({ + realm : realm.realm, + provider: provider.id, + mapperId : mapper.id + }, $scope.mapper, function() { + $scope.changed = false; + mapper = angular.copy($scope.mapper); + $location.url("/realms/" + realm.realm + '/user-federation/providers/' + provider.providerName + '/' + provider.id + '/mappers/' + mapper.id); + Notifications.success("Your changes have been saved."); + }, function(error) { + if (error.status == 400) { + Notifications.error('Error in configuration of mapper: ' + error.data.error_description); + } else { + Notification.error('Unexpected error when creating mapper'); + } + }); + }; + + $scope.reset = function() { + $scope.mapper = angular.copy(mapper); + $scope.changed = false; + }; + + $scope.cancel = function() { + window.history.back(); + }; + + $scope.remove = function() { + Dialog.confirmDelete($scope.mapper.name, 'mapper', function() { + UserFederationMapper.remove({ realm: realm.realm, provider: provider.id, mapperId : $scope.mapper.id }, function() { + Notifications.success("The mapper has been deleted."); + $location.url("/realms/" + realm.realm + '/user-federation/providers/' + provider.providerName + '/' + provider.id + '/mappers'); + }); + }); + }; + +}); + +module.controller('UserFederationMapperCreateCtrl', function($scope, realm, provider, mapperTypes, UserFederationMapper, Notifications, Dialog, $location) { + console.log('UserFederationMapperCreateCtrl'); + $scope.realm = realm; + $scope.provider = provider; + $scope.create = true; + $scope.mapper = { federationProviderDisplayName: provider.displayName, config: {}}; + $scope.mapperTypes = mapperTypes; + $scope.mapperType = null; + + $scope.$watch('mapperType', function() { + if ($scope.mapperType != null) { + $scope.mapper.config = {}; + for ( var i = 0; i < $scope.mapperType.properties.length; i++) { + var property = $scope.mapperType.properties[i]; + if (property.type === 'String' || property.type === 'boolean') { + $scope.mapper.config[ property.name ] = property.defaultValue; + } + } + } + }, true); + + $scope.save = function() { + if ($scope.mapperType == null) { + Notifications.error("You need to select mapper type!"); + return; + } + + $scope.mapper.federationMapperType = $scope.mapperType.id; + UserFederationMapper.save({ + realm : realm.realm, provider: provider.id + }, $scope.mapper, function(data, headers) { + var l = headers().location; + var id = l.substring(l.lastIndexOf("/") + 1); + $location.url('/realms/' + realm.realm +'/user-federation/providers/' + provider.providerName + '/' + provider.id + '/mappers/' + id); + Notifications.success("Mapper has been created."); + }, function(error) { + if (error.status == 400) { + Notifications.error('Error in configuration of mapper: ' + error.data.error_description); + } else { + Notification.error('Unexpected error when creating mapper'); + } + }); + }; + + $scope.cancel = function() { + window.history.back(); + }; + + +}); + diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js index 3f72ffe049..3a492bbf53 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -116,6 +116,34 @@ module.factory('UserFederationFactoryLoader', function(Loader, UserFederationPro }); }); +module.factory('UserFederationMapperTypesLoader', function(Loader, UserFederationMapperTypes, $route, $q) { + return Loader.get(UserFederationMapperTypes, function () { + return { + realm: $route.current.params.realm, + provider: $route.current.params.instance + } + }); +}); + +module.factory('UserFederationMappersLoader', function(Loader, UserFederationMappers, $route, $q) { + return Loader.query(UserFederationMappers, function () { + return { + realm: $route.current.params.realm, + provider: $route.current.params.instance + } + }); +}); + +module.factory('UserFederationMapperLoader', function(Loader, UserFederationMapper, $route, $q) { + return Loader.get(UserFederationMapper, function () { + return { + realm: $route.current.params.realm, + provider: $route.current.params.instance, + mapperId: $route.current.params.mapperId + } + }); +}); + module.factory('UserSessionStatsLoader', function(Loader, UserSessionStats, $route, $q) { return Loader.get(UserSessionStats, function() { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index e9f09a28a7..f192516355 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -238,7 +238,33 @@ module.factory('UserFederationProviders', function($resource) { }); module.factory('UserFederationSync', function($resource) { - return $resource(authUrl + '/admin/realms/:realm/user-federation/sync/:provider'); + return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/sync'); +}); + +module.factory('UserFederationMapperTypes', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/mapper-types', { + realm : '@realm', + provider : '@provider' + }); +}); + +module.factory('UserFederationMappers', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/mappers', { + realm : '@realm', + provider : '@provider' + }); +}); + +module.factory('UserFederationMapper', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/mappers/:mapperId', { + realm : '@realm', + provider : '@provider', + mapperId: '@mapperId' + }, { + update: { + method : 'PUT' + } + }); }); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html index b2c7da1e99..f0d8774f43 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html @@ -5,8 +5,13 @@
  • Add User Federation Provider
  • -

    User Federation Provider {{instance.displayName|capitalize}}

    -

    Add User Federation Provider

    +

    {{instance.providerName|capitalize}} User Federation Provider {{instance.displayName|capitalize}}

    +

    Add {{instance.providerName|capitalize}} User Federation Provider

    + +
    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html index 2294abba82..b2f4701793 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html @@ -8,6 +8,11 @@

    Kerberos User Federation Provider {{instance.displayName|capitalize}}

    Add Kerberos User Federation Provider

    + +
    Required Settings diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html index ec916a0e1c..2b86f0941b 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html @@ -8,6 +8,11 @@

    LDAP User Federation Provider {{instance.displayName|capitalize}}

    Add LDAP User Federation Provider

    + +
    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html new file mode 100644 index 0000000000..0b9c144380 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html @@ -0,0 +1,78 @@ +
    + + +

    User Federation Mapper {{mapper.name}}

    +

    Add User Federation Mapper

    + + +
    +
    + +
    + +
    +
    +
    + +
    + +
    + Name of the mapper. +
    +
    + +
    +
    + +
    +
    + {{mapperType.helpText}} +
    +
    + +
    + +
    + {{mapperType.helpText}} +
    +
    + + +
    + +
    +
    + +
    +
    + +
    + {{option.helpText}} +
    + +
    +
    + + +
    + +
    + + + +
    + +
    + + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html new file mode 100644 index 0000000000..d650100b44 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html @@ -0,0 +1,53 @@ +
    + + +

    {{provider.providerName === 'ldap' ? 'LDAP' : (provider.providerName|capitalize)}} User Federation Provider {{provider.displayName|capitalize}}

    + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + Create +
    +
    +
    NameCategoryType
    {{mapper.name}}{{mapperTypes[mapper.federationMapperType].category}}{{mapperTypes[mapper.federationMapperType].name}}
    No mappers available
    +
    + + \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/mappers/MapperConfigValidationException.java b/model/api/src/main/java/org/keycloak/mappers/MapperConfigValidationException.java new file mode 100644 index 0000000000..ca714a9b0b --- /dev/null +++ b/model/api/src/main/java/org/keycloak/mappers/MapperConfigValidationException.java @@ -0,0 +1,15 @@ +package org.keycloak.mappers; + +/** + * @author Marek Posolda + */ +public class MapperConfigValidationException extends Exception { + + public MapperConfigValidationException(String message) { + super(message); + } + + public MapperConfigValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java index 386ce1307c..309036cb89 100644 --- a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java +++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java @@ -1,10 +1,36 @@ package org.keycloak.mappers; +import java.util.List; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; import org.keycloak.provider.ConfiguredProvider; +import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderFactory; /** * @author Marek Posolda */ public interface UserFederationMapperFactory extends ProviderFactory, ConfiguredProvider { + + /** + * Refers to providerName (type) of the federation provider, which this mapper can be used for. For example "ldap" or "kerberos" + * + * @return providerName + */ + String getFederationProviderType(); + + String getDisplayCategory(); + String getDisplayType(); + + /** + * Called when instance of mapperModel is created for this factory through admin endpoint + * + * @param mapperModel + * @throws MapperConfigValidationException if configuration provided in mapperModel is not valid + */ + void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException; + + // TODO: Remove this and add realm to the method on ConfiguredProvider? + List getConfigProperties(RealmModel realm); } diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index 988d9ecfcc..77f825f00b 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -1,6 +1,7 @@ package org.keycloak.models.utils; import org.bouncycastle.openssl.PEMWriter; +import org.keycloak.constants.KerberosConstants; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -8,6 +9,7 @@ import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; +import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationMapperModel; @@ -349,4 +351,31 @@ public final class KeycloakModelUtils { return mapperModel; } + + /** + * Automatically add "kerberos" to required realm credentials if it's supported by saved provider + * + * @param realm + * @param model + * @return true if kerberos credentials were added + */ + public static boolean checkKerberosCredential(RealmModel realm, UserFederationProviderModel model) { + String allowKerberosCfg = model.getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION); + if (Boolean.valueOf(allowKerberosCfg)) { + boolean found = false; + List currentCreds = realm.getRequiredCredentials(); + for (RequiredCredentialModel cred : currentCreds) { + if (cred.getType().equals(UserCredentialModel.KERBEROS)) { + found = true; + } + } + + if (!found) { + realm.addRequiredCredential(UserCredentialModel.KERBEROS); + return true; + } + } + + return false; + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index c2a4730bee..e2899fe0ea 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -235,8 +235,8 @@ public class RealmAdminResource { } @Path("user-federation") - public UserFederationResource userFederation() { - UserFederationResource fed = new UserFederationResource(realm, auth, adminEvent); + public UserFederationProvidersResource userFederation() { + UserFederationProvidersResource fed = new UserFederationProvidersResource(realm, auth, adminEvent); ResteasyProviderFactory.getInstance().injectProperties(fed); //resourceContext.initResource(fed); return fed; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java new file mode 100644 index 0000000000..e5deb3d382 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java @@ -0,0 +1,298 @@ +package org.keycloak.services.resources.admin; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.events.admin.OperationType; +import org.keycloak.mappers.MapperConfigValidationException; +import org.keycloak.mappers.UserFederationMapper; +import org.keycloak.mappers.UserFederationMapperFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.models.UserFederationSyncResult; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.representations.idm.ConfigPropertyRepresentation; +import org.keycloak.representations.idm.UserFederationMapperRepresentation; +import org.keycloak.representations.idm.UserFederationMapperTypeRepresentation; +import org.keycloak.representations.idm.UserFederationProviderRepresentation; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.managers.UsersSyncManager; +import org.keycloak.timer.TimerProvider; + +/** + * @author Marek Posolda + */ +public class UserFederationProviderResource { + + protected static final Logger logger = Logger.getLogger(UserFederationProviderResource.class); + + private final KeycloakSession session; + private final RealmModel realm; + private final RealmAuth auth; + private final UserFederationProviderModel federationProviderModel; + private final AdminEventBuilder adminEvent; + + @Context + private UriInfo uriInfo; + + public UserFederationProviderResource(KeycloakSession session, RealmModel realm, RealmAuth auth, UserFederationProviderModel federationProviderModel, AdminEventBuilder adminEvent) { + this.session = session; + this.realm = realm; + this.auth = auth; + this.federationProviderModel = federationProviderModel; + this.adminEvent = adminEvent; + } + + /** + * Update a provider + * + * @param rep + */ + @PUT + @NoCache + @Consumes(MediaType.APPLICATION_JSON) + public void updateProviderInstance(UserFederationProviderRepresentation rep) { + auth.requireManage(); + String displayName = rep.getDisplayName(); + if (displayName != null && displayName.trim().equals("")) { + displayName = null; + } + UserFederationProviderModel model = new UserFederationProviderModel(rep.getId(), rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName, + rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync()); + realm.updateUserFederationProvider(model); + new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId()); + boolean kerberosCredsAdded = KeycloakModelUtils.checkKerberosCredential(realm, model); + if (kerberosCredsAdded) { + logger.info("Added 'kerberos' to required realm credentials"); + } + + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success(); + + } + + /** + * get a provider + * + */ + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public UserFederationProviderRepresentation getProviderInstance() { + auth.requireView(); + return ModelToRepresentation.toRepresentation(this.federationProviderModel); + } + + /** + * Delete a provider + * + */ + @DELETE + @NoCache + public void deleteProviderInstance() { + auth.requireManage(); + + realm.removeUserFederationProvider(this.federationProviderModel); + new UsersSyncManager().removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), this.federationProviderModel); + + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); + + } + + /** + * trigger sync of users + * + * @return + */ + @POST + @Path("sync") + @NoCache + public UserFederationSyncResult syncUsers(@QueryParam("action") String action) { + logger.debug("Syncing users"); + auth.requireManage(); + + UsersSyncManager syncManager = new UsersSyncManager(); + UserFederationSyncResult syncResult = null; + if ("triggerFullSync".equals(action)) { + syncResult = syncManager.syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel); + } else if ("triggerChangedUsersSync".equals(action)) { + syncResult = syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel); + } + + adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success(); + return syncResult; + } + + /** + * List of available User Federation mapper types + * + * @return + */ + @GET + @Path("mapper-types") + @NoCache + public Map getMapperTypes() { + this.auth.requireView(); + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + Map types = new HashMap<>(); + List factories = sessionFactory.getProviderFactories(UserFederationMapper.class); + + for (ProviderFactory factory : factories) { + UserFederationMapperFactory mapperFactory = (UserFederationMapperFactory)factory; + if (mapperFactory.getFederationProviderType().equals(this.federationProviderModel.getProviderName())) { + + UserFederationMapperTypeRepresentation rep = new UserFederationMapperTypeRepresentation(); + rep.setId(mapperFactory.getId()); + rep.setCategory(mapperFactory.getDisplayCategory()); + rep.setName(mapperFactory.getDisplayType()); + rep.setHelpText(mapperFactory.getHelpText()); + List configProperties = mapperFactory.getConfigProperties(realm); + for (ProviderConfigProperty prop : configProperties) { + ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation(); + propRep.setName(prop.getName()); + propRep.setLabel(prop.getLabel()); + propRep.setType(prop.getType()); + propRep.setDefaultValue(prop.getDefaultValue()); + propRep.setHelpText(prop.getHelpText()); + rep.getProperties().add(propRep); + } + types.put(rep.getId(), rep); + } + } + return types; + } + + /** + * Get mappers configured for this provider + * + * @return + */ + @GET + @Path("mappers") + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public List getMappers() { + this.auth.requireView(); + List mappers = new LinkedList<>(); + for (UserFederationMapperModel model : realm.getUserFederationMappersByFederationProvider(this.federationProviderModel.getId())) { + mappers.add(ModelToRepresentation.toRepresentation(realm, model)); + } + return mappers; + } + + /** + * Create mapper + * + * @param mapper + * @return + */ + @POST + @Path("mappers") + @Consumes(MediaType.APPLICATION_JSON) + public Response addMapper(UserFederationMapperRepresentation mapper) { + auth.requireManage(); + UserFederationMapperModel model = RepresentationToModel.toModel(realm, mapper); + + validateModel(model); + + model = realm.addUserFederationMapper(model); + + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId()) + .representation(mapper).success(); + + return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); + + } + + /** + * Get mapper + * + * @param id mapperId + * @return + */ + @GET + @NoCache + @Path("mappers/{id}") + @Produces(MediaType.APPLICATION_JSON) + public UserFederationMapperRepresentation getMapperById(@PathParam("id") String id) { + auth.requireView(); + UserFederationMapperModel model = realm.getUserFederationMapperById(id); + if (model == null) throw new NotFoundException("Model not found"); + return ModelToRepresentation.toRepresentation(realm, model); + } + + /** + * Update mapper + * + * @param id + * @param rep + */ + @PUT + @NoCache + @Path("mappers/{id}") + @Consumes(MediaType.APPLICATION_JSON) + public void update(@PathParam("id") String id, UserFederationMapperRepresentation rep) { + auth.requireManage(); + UserFederationMapperModel model = realm.getUserFederationMapperById(id); + if (model == null) throw new NotFoundException("Model not found"); + model = RepresentationToModel.toModel(realm, rep); + + validateModel(model); + + realm.updateUserFederationMapper(model); + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success(); + + } + + /** + * Delete mapper with given ID + * + * @param id + */ + @DELETE + @NoCache + @Path("mappers/{id}") + public void delete(@PathParam("id") String id) { + auth.requireManage(); + UserFederationMapperModel model = realm.getUserFederationMapperById(id); + if (model == null) throw new NotFoundException("Model not found"); + realm.removeUserFederationMapper(model); + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); + + } + + private void validateModel(UserFederationMapperModel model) { + try { + UserFederationMapperFactory mapperFactory = (UserFederationMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationMapper.class, model.getFederationMapperType()); + mapperFactory.validateConfig(model); + } catch (MapperConfigValidationException ex) { + throw new ErrorResponseException("Validation error", ex.getMessage(), Response.Status.BAD_REQUEST); + } + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java similarity index 50% rename from services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java rename to services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java index 842fc5408d..a3bd867aad 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java @@ -3,16 +3,14 @@ package org.keycloak.services.resources.admin; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; -import org.keycloak.constants.KerberosConstants; +import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.events.admin.OperationType; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.models.RequiredCredentialModel; -import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderModel; -import org.keycloak.models.UserFederationSyncResult; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation; @@ -21,14 +19,11 @@ import org.keycloak.services.managers.UsersSyncManager; import org.keycloak.timer.TimerProvider; import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; -import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -43,8 +38,8 @@ import java.util.List; * @author Bill Burke * @version $Revision: 1 $ */ -public class UserFederationResource { - protected static final Logger logger = Logger.getLogger(UserFederationResource.class); +public class UserFederationProvidersResource { + protected static final Logger logger = Logger.getLogger(UserFederationProvidersResource.class); protected RealmModel realm; @@ -58,7 +53,7 @@ public class UserFederationResource { @Context protected KeycloakSession session; - public UserFederationResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) { + public UserFederationProvidersResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) { this.auth = auth; this.realm = realm; this.adminEvent = adminEvent; @@ -88,7 +83,7 @@ public class UserFederationResource { } /** - * Get List of available provider factories + * Get factory with given ID * * @return */ @@ -130,77 +125,17 @@ public class UserFederationResource { UserFederationProviderModel model = realm.addUserFederationProvider(rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName, rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync()); new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId()); - checkKerberosCredential(model); + boolean kerberosCredsAdded = KeycloakModelUtils.checkKerberosCredential(realm, model); + if (kerberosCredsAdded) { + logger.info("Added 'kerberos' to required realm credentials"); + } + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(rep).success(); return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); } - /** - * Update a provider - * - * @param id - * @param rep - */ - @PUT - @Path("instances/{id}") - @Consumes(MediaType.APPLICATION_JSON) - public void updateProviderInstance(@PathParam("id") String id, UserFederationProviderRepresentation rep) { - auth.requireManage(); - String displayName = rep.getDisplayName(); - if (displayName != null && displayName.trim().equals("")) { - displayName = null; - } - UserFederationProviderModel model = new UserFederationProviderModel(id, rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName, - rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync()); - realm.updateUserFederationProvider(model); - new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId()); - checkKerberosCredential(model); - - adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success(); - - } - - /** - * get a provider - * - * @param id - */ - @GET - @NoCache - @Path("instances/{id}") - @Produces(MediaType.APPLICATION_JSON) - public UserFederationProviderRepresentation getProviderInstance(@PathParam("id") String id) { - auth.requireView(); - for (UserFederationProviderModel model : realm.getUserFederationProviders()) { - if (model.getId().equals(id)) { - return ModelToRepresentation.toRepresentation(model); - } - } - throw new NotFoundException("could not find provider"); - } - - /** - * Delete a provider - * - * @param id - */ - @DELETE - @Path("instances/{id}") - public void deleteProviderInstance(@PathParam("id") String id) { - auth.requireManage(); - - UserFederationProviderRepresentation rep = getProviderInstance(id); - UserFederationProviderModel model = new UserFederationProviderModel(id, null, null, -1, null, -1, -1, 0); - realm.removeUserFederationProvider(model); - new UsersSyncManager().removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), model); - - adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); - - } - - /** * list configured providers * @@ -220,53 +155,18 @@ public class UserFederationResource { return reps; } - /** - * trigger sync of users - * - * @return - */ - @POST - @Path("sync/{id}") - @NoCache - public UserFederationSyncResult syncUsers(@PathParam("id") String providerId, @QueryParam("action") String action) { - logger.debug("Syncing users"); - auth.requireManage(); + @Path("instances/{id}") + public UserFederationProviderResource getUserFederationInstance(@PathParam("id") String id) { + this.auth.requireView(); - for (UserFederationProviderModel model : realm.getUserFederationProviders()) { - if (model.getId().equals(providerId)) { - UsersSyncManager syncManager = new UsersSyncManager(); - UserFederationSyncResult syncResult = null; - if ("triggerFullSync".equals(action)) { - syncResult = syncManager.syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), model); - } else if ("triggerChangedUsersSync".equals(action)) { - syncResult = syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), model); - } - - adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success(); - return syncResult; - } + UserFederationProviderModel model = KeycloakModelUtils.findUserFederationProviderById(id, realm); + if (model == null) { + throw new NotFoundException("Could not find federation provider with id: " + id); } - throw new NotFoundException("could not find provider"); - } - - // Automatically add "kerberos" to required realm credentials if it's supported by saved provider - private void checkKerberosCredential(UserFederationProviderModel model) { - String allowKerberosCfg = model.getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION); - if (Boolean.valueOf(allowKerberosCfg)) { - boolean found = false; - List currentCreds = realm.getRequiredCredentials(); - for (RequiredCredentialModel cred : currentCreds) { - if (cred.getType().equals(UserCredentialModel.KERBEROS)) { - found = true; - } - } - - if (!found) { - realm.addRequiredCredential(UserCredentialModel.KERBEROS); - logger.info("Added 'kerberos' to required realm credentials"); - } - } + UserFederationProviderResource instanceResource = new UserFederationProviderResource(session, realm, this.auth, model, adminEvent); + ResteasyProviderFactory.getInstance().injectProperties(instanceResource); + return instanceResource; } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java index ddda4ff856..d3dd9ab373 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java @@ -91,7 +91,7 @@ class FederationTestUtils { } public static void addZipCodeLDAPMapper(RealmModel realm, UserFederationProviderModel providerModel) { - UserFederationMapperModel mapperModel = KeycloakModelUtils.createUserFederationMapperModel("zipCodeMapper", providerModel.getId(), UserAttributeLDAPFederationMapperFactory.ID, + UserFederationMapperModel mapperModel = KeycloakModelUtils.createUserFederationMapperModel("zipCodeMapper", providerModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID, UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, "postal_code", UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.POSTAL_CODE, UserAttributeLDAPFederationMapper.READ_ONLY, "false"); @@ -104,7 +104,7 @@ class FederationTestUtils { mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString()); realm.updateUserFederationMapper(mapperModel); } else { - mapperModel = KeycloakModelUtils.createUserFederationMapperModel("realmRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.ID, + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("realmRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.PROVIDER_ID, RoleLDAPFederationMapper.ROLES_DN, "ou=RealmRoles,dc=keycloak,dc=org", RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "true", RoleLDAPFederationMapper.MODE, mode.toString()); @@ -116,7 +116,7 @@ class FederationTestUtils { mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString()); realm.updateUserFederationMapper(mapperModel); } else { - mapperModel = KeycloakModelUtils.createUserFederationMapperModel("financeRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.ID, + mapperModel = KeycloakModelUtils.createUserFederationMapperModel("financeRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.PROVIDER_ID, RoleLDAPFederationMapper.ROLES_DN, "ou=FinanceRoles,dc=keycloak,dc=org", RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "false", RoleLDAPFederationMapper.CLIENT_ID, "finance", diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java index 4d20bc8eec..276698de93 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java @@ -233,7 +233,7 @@ public class ImportTest extends AbstractModelTest { Assert.assertTrue(fedMappers1.size() == 1); UserFederationMapperModel fullNameMapper = fedMappers1.iterator().next(); Assert.assertEquals("FullNameMapper", fullNameMapper.getName()); - Assert.assertEquals(FullNameLDAPFederationMapperFactory.ID, fullNameMapper.getFederationMapperType()); + Assert.assertEquals(FullNameLDAPFederationMapperFactory.PROVIDER_ID, fullNameMapper.getFederationMapperType()); Assert.assertEquals(ldap1.getId(), fullNameMapper.getFederationProviderId()); Assert.assertEquals("cn", fullNameMapper.getConfig().get(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE));