keycloak-aplcache
Changes
core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java 3(+2 -1)
federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java 15(+7 -8)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java 40(+40 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java 42(+37 -5)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java 1(+0 -1)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java 93(+88 -5)
federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java 45(+40 -5)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js 129(+127 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html 9(+7 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html 5(+5 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html 5(+5 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html 78(+78 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html 53(+53 -0)
services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java 298(+298 -0)
services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java 140(+20 -120)
Details
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 f03b376..f7f594a 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<ConfigPropertyRepresentation> properties;
+ protected List<ConfigPropertyRepresentation> 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 166c5bf..c020f51 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 6b8f186..33ace78 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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<ProviderConfigProperty> 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 6ef6979..d0d6230 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";
+
+ protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ 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 String getHelpText() {
- return "Some help text - full name mapper - TODO";
+ 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 List<ProviderConfigProperty> getConfigProperties() {
- return null;
+ public String getDisplayType() {
+ return "Full Name";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> 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 47f288d..084b255 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<String> trimmed = new HashSet<String>();
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 cbae850..a5eadd9 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";
+
+ protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ 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<String> modes = new LinkedList<String>();
+ 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 String getHelpText() {
- return "Some help text - role mapper - TODO";
+ 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 List<ProviderConfigProperty> getConfigProperties() {
- return null;
+ public String getDisplayType() {
+ return "Role mappings";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties(RealmModel realm) {
+ List<ProviderConfigProperty> props = new ArrayList<ProviderConfigProperty>(configProperties);
+
+ Map<String, ClientModel> clients = realm.getClientNameMap();
+ List<String> clientIds = new ArrayList<String>(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 564b012..c0b9d79 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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ 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 String getHelpText() {
- return "Some help text TODO";
+ 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 List<ProviderConfigProperty> getConfigProperties() {
- return null;
+ public String getDisplayType() {
+ return "User Attribute";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> 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 56f6008..7a0ae1a 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 2444be4..bc723f3 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 3f72ffe..3a492bb 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 e9f09a2..f192516 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 b2c7da1..f0d8774 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 @@
<li data-ng-show="create">Add User Federation Provider</li>
</ol>
- <h1 data-ng-hide="create"><strong>User Federation Provider</strong> {{instance.displayName|capitalize}}</h1>
- <h1 data-ng-show="create"><strong>Add User Federation Provider</strong></h1>
+ <h1 data-ng-hide="create"><strong>{{instance.providerName|capitalize}} User Federation Provider</strong> {{instance.displayName|capitalize}}</h1>
+ <h1 data-ng-show="create"><strong>Add {{instance.providerName|capitalize}} User Federation Provider</strong></h1>
+
+ <ul class="nav nav-tabs" data-ng-hide="create">
+ <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
+ <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
+ </ul>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset>
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 2294abb..b2f4701 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 @@
<h1 data-ng-hide="create"><strong>Kerberos User Federation Provider</strong> {{instance.displayName|capitalize}}</h1>
<h1 data-ng-show="create"><strong>Add Kerberos User Federation Provider</strong></h1>
+ <ul class="nav nav-tabs" data-ng-hide="create">
+ <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
+ <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
+ </ul>
+
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset>
<legend><span class="text">Required Settings</span></legend>
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 ec916a0..2b86f09 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 @@
<h1 data-ng-hide="create"><strong>LDAP User Federation Provider</strong> {{instance.displayName|capitalize}}</h1>
<h1 data-ng-show="create"><strong>Add LDAP User Federation Provider</strong></h1>
+ <ul class="nav nav-tabs" data-ng-hide="create">
+ <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
+ <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
+ </ul>
+
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset>
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 0000000..0b9c144
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html
@@ -0,0 +1,78 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/user-federation">User Federation</a></li>
+ <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}">{{provider.displayName|capitalize}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}/mappers">User Federation Mappers</a></li>
+ <li class="active" data-ng-show="create">Create User Federation Mapper</li>
+ <li class="active" data-ng-hide="create">{{mapper.name}}</li>
+ </ol>
+
+ <h1 data-ng-hide="create"><strong>User Federation Mapper</strong> {{mapper.name}}</h1>
+ <h1 data-ng-show="create"><strong>Add User Federation Mapper</strong></h1>
+
+ <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
+ <fieldset>
+ <div class="form-group clearfix" data-ng-show="!create">
+ <label class="col-md-2 control-label" for="mapperId">ID </label>
+ <div class="col-md-6">
+ <input class="form-control" id="mapperId" type="text" ng-model="mapper.id" readonly>
+ </div>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="name">Name <span class="required">*</span></label>
+ <div class="col-md-6">
+ <input class="form-control" id="name" type="text" ng-model="mapper.name" data-ng-readonly="!create" required>
+ </div>
+ <kc-tooltip>Name of the mapper.</kc-tooltip>
+ </div>
+ <div class="form-group" data-ng-show="create">
+ <label class="col-md-2 control-label" for="mapperTypeCreate">Mapper Type</label>
+ <div class="col-sm-6">
+ <div>
+ <select class="form-control" id="mapperTypeCreate"
+ ng-model="mapperType"
+ ng-options="mapperType.name for (mapperKey, mapperType) in mapperTypes">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{mapperType.helpText}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-hide="create">
+ <label class="col-md-2 control-label" for="mapperType">Mapper Type</label>
+ <div class="col-md-6">
+ <input class="form-control" id="mapperType" type="text" ng-model="mapperType.name" data-ng-readonly="true">
+ </div>
+ <kc-tooltip>{{mapperType.helpText}}</kc-tooltip>
+ </div>
+ <div data-ng-repeat="option in mapperType.properties" class="form-group">
+ <label class="col-md-2 control-label">{{option.label}}</label>
+
+ <div class="col-sm-4" data-ng-hide="option.type == 'boolean' || option.type == 'List'">
+ <input class="form-control" type="text" data-ng-model="mapper.config[ option.name ]">
+ </div>
+ <div class="col-sm-4" data-ng-show="option.type == 'boolean'">
+ <input ng-model="mapper.config[ option.name ]" value="'true'" name="option.name" id="option.name" onoffswitchmodel />
+ </div>
+ <div class="col-sm-4" data-ng-show="option.type == 'List'">
+ <select ng-model="mapper.config[ option.name ]" ng-options="data for data in option.defaultValue">
+ <option value="" selected> Select one... </option>
+ </select>
+ </div>
+ <kc-tooltip>{{option.helpText}}</kc-tooltip>
+ </div>
+
+ </fieldset>
+ <div class="pull-right form-actions" data-ng-show="create && access.manageRealm">
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ <button kc-save>Save</button>
+ </div>
+
+ <div class="pull-right form-actions" data-ng-show="!create && access.manageRealm">
+ <button kc-reset data-ng-show="changed">Clear changes</button>
+ <button kc-save data-ng-show="changed">Save</button>
+ <button kc-delete data-ng-click="remove()" data-ng-hide="changed">Delete</button>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ 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 0000000..d650100
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html
@@ -0,0 +1,53 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/user-federation">User Federation</a></li>
+ <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}">{{provider.displayName|capitalize}}</a></li>
+ <li>User Federation Mappers</li>
+ </ol>
+
+ <h1><strong>{{provider.providerName === 'ldap' ? 'LDAP' : (provider.providerName|capitalize)}} User Federation Provider</strong> {{provider.displayName|capitalize}}</h1>
+
+ <ul class="nav nav-tabs" data-ng-hide="create">
+ <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}">Settings</a></li>
+ <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}/mappers">Mappers</a></li>
+ </ul>
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="4">
+ <div class="form-inline">
+ <div class="form-group">
+ <div class="input-group">
+ <input type="text" placeholder="Search..." data-ng-model="search.name" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ <div class="input-group-addon">
+ <i class="fa fa-search" type="submit"></i>
+ </div>
+ </div>
+ </div>
+ <div class="pull-right" data-ng-show="hasAnyMapperTypes">
+ <a class="btn btn-primary" href="#/create/user-federation-mappers/{{realm.realm}}/{{provider.providerName}}/{{provider.id}}">Create</a>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="mappers.length == 0">
+ <th>Name</th>
+ <th>Category</th>
+ <th>Type</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="mapper in mappers | filter:search">
+ <td><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}/mappers/{{mapper.id}}">{{mapper.name}}</a></td>
+ <td>{{mapperTypes[mapper.federationMapperType].category}}</td>
+ <td>{{mapperTypes[mapper.federationMapperType].name}}</td>
+ </tr>
+ <tr data-ng-show="mappers.length == 0">
+ <td>No mappers available</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ 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 0000000..ca714a9
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/mappers/MapperConfigValidationException.java
@@ -0,0 +1,15 @@
+package org.keycloak.mappers;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 386ce13..309036c 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface UserFederationMapperFactory extends ProviderFactory<UserFederationMapper>, 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<ProviderConfigProperty> 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 988d9ec..77f825f 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<RequiredCredentialModel> 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 c2a4730..e2899fe 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 0000000..e5deb3d
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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<String, UserFederationMapperTypeRepresentation> getMapperTypes() {
+ this.auth.requireView();
+ KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+ Map<String, UserFederationMapperTypeRepresentation> types = new HashMap<>();
+ List<ProviderFactory> 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<ProviderConfigProperty> 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<UserFederationMapperRepresentation> getMappers() {
+ this.auth.requireView();
+ List<UserFederationMapperRepresentation> 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/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
index ddda4ff..d3dd9ab 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 4d20bc8..276698d 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));