KEYCLOAK-886 User Federation Mappers - admin console

This commit is contained in:
mposolda 2015-05-25 20:46:55 +02:00
parent 269f9fe860
commit dfe232cf80
24 changed files with 996 additions and 162 deletions

View file

@ -1,5 +1,6 @@
package org.keycloak.representations.idm; package org.keycloak.representations.idm;
import java.util.LinkedList;
import java.util.List; import java.util.List;
/** /**
@ -11,7 +12,7 @@ public class UserFederationMapperTypeRepresentation {
protected String category; protected String category;
protected String helpText; protected String helpText;
protected List<ConfigPropertyRepresentation> properties; protected List<ConfigPropertyRepresentation> properties = new LinkedList<>();
public String getId() { public String getId() {
return id; return id;

View file

@ -24,7 +24,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationEventAwareProviderFactory; import org.keycloak.models.UserFederationEventAwareProviderFactory;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserFederationSyncResult; import org.keycloak.models.UserFederationSyncResult;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -89,7 +88,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute(); String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute();
UserFederationMapperModel mapperModel; 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.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, usernameLdapAttribute, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, usernameLdapAttribute,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); 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 // 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)) { 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, FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
realm.addUserFederationMapper(mapperModel); realm.addUserFederationMapper(mapperModel);
} else { } 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.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
realm.addUserFederationMapper(mapperModel); 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.USER_MODEL_ATTRIBUTE, UserModel.LAST_NAME,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.SN, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.SN,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
realm.addUserFederationMapper(mapperModel); 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.USER_MODEL_ATTRIBUTE, UserModel.EMAIL,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL,
UserAttributeLDAPFederationMapper.READ_ONLY, readOnly); UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
@ -125,14 +124,14 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
String modifyTimestampLdapAttrName = activeDirectory ? "whenChanged" : LDAPConstants.MODIFY_TIMESTAMP; String modifyTimestampLdapAttrName = activeDirectory ? "whenChanged" : LDAPConstants.MODIFY_TIMESTAMP;
// map createTimeStamp as read-only // 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.USER_MODEL_ATTRIBUTE, LDAPConstants.CREATE_TIMESTAMP,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName,
UserAttributeLDAPFederationMapper.READ_ONLY, "true"); UserAttributeLDAPFederationMapper.READ_ONLY, "true");
realm.addUserFederationMapper(mapperModel); realm.addUserFederationMapper(mapperModel);
// map modifyTimeStamp as read-only // 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.USER_MODEL_ATTRIBUTE, LDAPConstants.MODIFY_TIMESTAMP,
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName,
UserAttributeLDAPFederationMapper.READ_ONLY, "true"); UserAttributeLDAPFederationMapper.READ_ONLY, "true");

View file

@ -1,25 +1,65 @@
package org.keycloak.federation.ldap.mappers; package org.keycloak.federation.ldap.mappers;
import java.util.List;
import java.util.Map;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
import org.keycloak.mappers.MapperConfigValidationException;
import org.keycloak.mappers.UserFederationMapperFactory; import org.keycloak.mappers.UserFederationMapperFactory;
import org.keycloak.models.KeycloakSessionFactory; 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> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public abstract class AbstractLDAPFederationMapperFactory implements UserFederationMapperFactory { 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 @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
} }
@Override
public String getFederationProviderType() {
return LDAPFederationProviderFactory.PROVIDER_NAME;
}
@Override @Override
public void postInit(KeycloakSessionFactory factory) { public void postInit(KeycloakSessionFactory factory) {
} }
@Override
public List<ProviderConfigProperty> getConfigProperties() {
throw new IllegalStateException("Method not supported for this implementation");
}
@Override @Override
public void close() { 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 + "'");
}
}
} }

View file

@ -1,9 +1,14 @@
package org.keycloak.federation.ldap.mappers; package org.keycloak.federation.ldap.mappers;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.keycloak.mappers.MapperConfigValidationException;
import org.keycloak.mappers.UserFederationMapper; import org.keycloak.mappers.UserFederationMapper;
import org.keycloak.models.KeycloakSession; 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; import org.keycloak.provider.ProviderConfigProperty;
/** /**
@ -11,21 +16,48 @@ import org.keycloak.provider.ProviderConfigProperty;
*/ */
public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { 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 protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
public String getHelpText() {
return "Some help text - full name mapper - TODO"; 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 @Override
public List<ProviderConfigProperty> getConfigProperties() { public String getHelpText() {
return null; 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<ProviderConfigProperty> getConfigProperties(RealmModel realm) {
return configProperties;
} }
@Override @Override
public String getId() { 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 @Override

View file

@ -178,7 +178,6 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
} }
String[] objClasses = objectClasses.split(","); String[] objClasses = objectClasses.split(",");
// TODO: util method for trim and convert array to collection?
Set<String> trimmed = new HashSet<String>(); Set<String> trimmed = new HashSet<String>();
for (String objectClass : objClasses) { for (String objectClass : objClasses) {
objectClass = objectClass.trim(); objectClass = objectClass.trim();

View file

@ -1,9 +1,18 @@
package org.keycloak.federation.ldap.mappers; 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.List;
import java.util.Map;
import org.keycloak.mappers.MapperConfigValidationException;
import org.keycloak.mappers.UserFederationMapper; import org.keycloak.mappers.UserFederationMapper;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; 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; import org.keycloak.provider.ProviderConfigProperty;
/** /**
@ -11,21 +20,95 @@ import org.keycloak.provider.ProviderConfigProperty;
*/ */
public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
public static final String ID = "role-ldap-mapper"; public static final String PROVIDER_ID = "role-ldap-mapper";
@Override protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
public String getHelpText() {
return "Some help text - role mapper - TODO"; 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 @Override
public List<ProviderConfigProperty> getConfigProperties() { public String getHelpText() {
return null; 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<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 @Override
public String getId() { 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 @Override

View file

@ -1,9 +1,13 @@
package org.keycloak.federation.ldap.mappers; package org.keycloak.federation.ldap.mappers;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.keycloak.mappers.MapperConfigValidationException;
import org.keycloak.mappers.UserFederationMapper; import org.keycloak.mappers.UserFederationMapper;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
/** /**
@ -11,21 +15,52 @@ import org.keycloak.provider.ProviderConfigProperty;
*/ */
public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory { 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>();
@Override static {
public String getHelpText() { ProviderConfigProperty userModelAttribute = createConfigProperty(UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute",
return "Some help text TODO"; "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 @Override
public List<ProviderConfigProperty> getConfigProperties() { public String getHelpText() {
return null; 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<ProviderConfigProperty> getConfigProperties(RealmModel realm) {
return configProperties;
} }
@Override @Override
public String getId() { 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 @Override

View file

@ -952,6 +952,58 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'GenericUserFederationCtrl' 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', { .when('/realms/:realm/defense/headers', {
templateUrl : resourceUrl + '/partials/defense-headers.html', templateUrl : resourceUrl + '/partials/defense-headers.html',
resolve : { resolve : {

View file

@ -511,8 +511,8 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif
} }
function triggerSync(action) { function triggerSync(action) {
UserFederationSync.get({ action: action, realm: $scope.realm.realm, provider: $scope.instance.id }, function() { UserFederationSync.save({ action: action, realm: $scope.realm.realm, provider: $scope.instance.id }, {}, function(syncResult) {
Notifications.success("Sync of users finished successfully"); Notifications.success("Sync of users finished successfully. " + syncResult.status);
}, function() { }, function() {
Notifications.error("Error during sync of users"); 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();
};
});

View file

@ -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) { module.factory('UserSessionStatsLoader', function(Loader, UserSessionStats, $route, $q) {
return Loader.get(UserSessionStats, function() { return Loader.get(UserSessionStats, function() {

View file

@ -238,7 +238,33 @@ module.factory('UserFederationProviders', function($resource) {
}); });
module.factory('UserFederationSync', 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'
}
});
}); });

View file

@ -5,8 +5,13 @@
<li data-ng-show="create">Add User Federation Provider</li> <li data-ng-show="create">Add User Federation Provider</li>
</ol> </ol>
<h1 data-ng-hide="create"><strong>User Federation Provider</strong> {{instance.displayName|capitalize}}</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 User Federation Provider</strong></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"> <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset> <fieldset>

View file

@ -8,6 +8,11 @@
<h1 data-ng-hide="create"><strong>Kerberos User Federation Provider</strong> {{instance.displayName|capitalize}}</h1> <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> <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"> <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset> <fieldset>
<legend><span class="text">Required Settings</span></legend> <legend><span class="text">Required Settings</span></legend>

View file

@ -8,6 +8,11 @@
<h1 data-ng-hide="create"><strong>LDAP User Federation Provider</strong> {{instance.displayName|capitalize}}</h1> <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> <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"> <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset> <fieldset>

View file

@ -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>

View file

@ -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>

View file

@ -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);
}
}

View file

@ -1,10 +1,36 @@
package org.keycloak.mappers; 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.ConfiguredProvider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public interface UserFederationMapperFactory extends ProviderFactory<UserFederationMapper>, ConfiguredProvider { 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);
} }

View file

@ -1,6 +1,7 @@
package org.keycloak.models.utils; package org.keycloak.models.utils;
import org.bouncycastle.openssl.PEMWriter; import org.bouncycastle.openssl.PEMWriter;
import org.keycloak.constants.KerberosConstants;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
@ -8,6 +9,7 @@ import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
@ -349,4 +351,31 @@ public final class KeycloakModelUtils {
return mapperModel; 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;
}
} }

View file

@ -235,8 +235,8 @@ public class RealmAdminResource {
} }
@Path("user-federation") @Path("user-federation")
public UserFederationResource userFederation() { public UserFederationProvidersResource userFederation() {
UserFederationResource fed = new UserFederationResource(realm, auth, adminEvent); UserFederationProvidersResource fed = new UserFederationProvidersResource(realm, auth, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(fed); ResteasyProviderFactory.getInstance().injectProperties(fed);
//resourceContext.initResource(fed); //resourceContext.initResource(fed);
return fed; return fed;

View file

@ -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);
}
}
}

View file

@ -3,16 +3,14 @@ package org.keycloak.services.resources.admin;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.NotFoundException; 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.events.admin.OperationType;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel; 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.ModelToRepresentation;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation; import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation;
@ -21,14 +19,11 @@ import org.keycloak.services.managers.UsersSyncManager;
import org.keycloak.timer.TimerProvider; import org.keycloak.timer.TimerProvider;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -43,8 +38,8 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class UserFederationResource { public class UserFederationProvidersResource {
protected static final Logger logger = Logger.getLogger(UserFederationResource.class); protected static final Logger logger = Logger.getLogger(UserFederationProvidersResource.class);
protected RealmModel realm; protected RealmModel realm;
@ -58,7 +53,7 @@ public class UserFederationResource {
@Context @Context
protected KeycloakSession session; protected KeycloakSession session;
public UserFederationResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) { public UserFederationProvidersResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
this.auth = auth; this.auth = auth;
this.realm = realm; this.realm = realm;
this.adminEvent = adminEvent; this.adminEvent = adminEvent;
@ -88,7 +83,7 @@ public class UserFederationResource {
} }
/** /**
* Get List of available provider factories * Get factory with given ID
* *
* @return * @return
*/ */
@ -130,77 +125,17 @@ public class UserFederationResource {
UserFederationProviderModel model = realm.addUserFederationProvider(rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName, UserFederationProviderModel model = realm.addUserFederationProvider(rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName,
rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync()); rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync());
new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId()); 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(); adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(rep).success();
return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); 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 * list configured providers
* *
@ -220,53 +155,18 @@ public class UserFederationResource {
return reps; return reps;
} }
/** @Path("instances/{id}")
* trigger sync of users public UserFederationProviderResource getUserFederationInstance(@PathParam("id") String id) {
* this.auth.requireView();
* @return
*/
@POST
@Path("sync/{id}")
@NoCache
public UserFederationSyncResult syncUsers(@PathParam("id") String providerId, @QueryParam("action") String action) {
logger.debug("Syncing users");
auth.requireManage();
for (UserFederationProviderModel model : realm.getUserFederationProviders()) { UserFederationProviderModel model = KeycloakModelUtils.findUserFederationProviderById(id, realm);
if (model.getId().equals(providerId)) { if (model == null) {
UsersSyncManager syncManager = new UsersSyncManager(); throw new NotFoundException("Could not find federation provider with id: " + id);
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;
}
} }
throw new NotFoundException("could not find provider"); UserFederationProviderResource instanceResource = new UserFederationProviderResource(session, realm, this.auth, model, adminEvent);
} ResteasyProviderFactory.getInstance().injectProperties(instanceResource);
return instanceResource;
// 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<RequiredCredentialModel> 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");
}
}
} }
} }

View file

@ -91,7 +91,7 @@ class FederationTestUtils {
} }
public static void addZipCodeLDAPMapper(RealmModel realm, UserFederationProviderModel providerModel) { 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.USER_MODEL_ATTRIBUTE, "postal_code",
UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.POSTAL_CODE, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.POSTAL_CODE,
UserAttributeLDAPFederationMapper.READ_ONLY, "false"); UserAttributeLDAPFederationMapper.READ_ONLY, "false");
@ -104,7 +104,7 @@ class FederationTestUtils {
mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString()); mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString());
realm.updateUserFederationMapper(mapperModel); realm.updateUserFederationMapper(mapperModel);
} else { } 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.ROLES_DN, "ou=RealmRoles,dc=keycloak,dc=org",
RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "true", RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "true",
RoleLDAPFederationMapper.MODE, mode.toString()); RoleLDAPFederationMapper.MODE, mode.toString());
@ -116,7 +116,7 @@ class FederationTestUtils {
mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString()); mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString());
realm.updateUserFederationMapper(mapperModel); realm.updateUserFederationMapper(mapperModel);
} else { } 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.ROLES_DN, "ou=FinanceRoles,dc=keycloak,dc=org",
RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "false", RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "false",
RoleLDAPFederationMapper.CLIENT_ID, "finance", RoleLDAPFederationMapper.CLIENT_ID, "finance",

View file

@ -233,7 +233,7 @@ public class ImportTest extends AbstractModelTest {
Assert.assertTrue(fedMappers1.size() == 1); Assert.assertTrue(fedMappers1.size() == 1);
UserFederationMapperModel fullNameMapper = fedMappers1.iterator().next(); UserFederationMapperModel fullNameMapper = fedMappers1.iterator().next();
Assert.assertEquals("FullNameMapper", fullNameMapper.getName()); 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(ldap1.getId(), fullNameMapper.getFederationProviderId());
Assert.assertEquals("cn", fullNameMapper.getConfig().get(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE)); Assert.assertEquals("cn", fullNameMapper.getConfig().get(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE));