unlink/remoteimported
This commit is contained in:
parent
f157e2e637
commit
cf5e2a1d20
19 changed files with 562 additions and 202 deletions
|
@ -135,7 +135,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
|||
return editMode;
|
||||
}
|
||||
|
||||
public ComponentModel getModel() {
|
||||
public UserStorageProviderModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
|
|
|
@ -95,6 +95,10 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
|
|||
.property().name(LDAPConstants.EDIT_MODE)
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(UserStorageProviderModel.IMPORT_ENABLED)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("true")
|
||||
.add()
|
||||
.property().name(LDAPConstants.SYNC_REGISTRATIONS)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false")
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.keycloak.models.KeycloakSessionFactory;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -82,6 +83,10 @@ public interface LDAPStorageMapperFactory<T extends LDAPStorageMapper> extends S
|
|||
|
||||
}
|
||||
|
||||
default void onParentUpdate(RealmModel realm, UserStorageProviderModel oldParent, UserStorageProviderModel newParent, ComponentModel mapperModel) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when UserStorageProviderModel is created. This allows you to do initialization of any additional configuration
|
||||
* you need to add. For example, you may be introspecting a database or ldap schema to automatically create mappings.
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.storage.ldap.LDAPConfig;
|
||||
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
||||
|
||||
|
@ -42,38 +43,43 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
|
|||
configProperties = props;
|
||||
}
|
||||
|
||||
private static List<ProviderConfigProperty> getConfigProps(ComponentModel parent) {
|
||||
private static List<ProviderConfigProperty> getConfigProps(ComponentModel p) {
|
||||
String readOnly = "false";
|
||||
if (parent != null) {
|
||||
UserStorageProviderModel parent = new UserStorageProviderModel();
|
||||
if (p != null) {
|
||||
parent = new UserStorageProviderModel(p);
|
||||
LDAPConfig ldapConfig = new LDAPConfig(parent.getConfig());
|
||||
readOnly = ldapConfig.getEditMode() == UserStorageProvider.EditMode.WRITABLE ? "false" : "true";
|
||||
}
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property().name(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE)
|
||||
.label("User Model Attribute")
|
||||
.helpText("Name of mapped UserModel property or UserModel attribute in Keycloak DB. For example 'firstName', 'lastName, 'email', 'street' etc.")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE).label("LDAP Attribute").helpText("Name of mapped attribute on LDAP object. For example 'cn', 'sn, 'mail', 'street' etc.")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(UserAttributeLDAPStorageMapper.READ_ONLY).label("Read Only")
|
||||
.helpText("Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue(readOnly)
|
||||
.add()
|
||||
.property().name(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP).label("Always Read Value From LDAP")
|
||||
.helpText("If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE).defaultValue("false").add()
|
||||
.property().name(UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP).label("Is Mandatory In LDAP")
|
||||
.helpText("If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false").add()
|
||||
.property().name(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE).label("Is Binary Attribute")
|
||||
.helpText("Should be true for binary LDAP attributes")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false").add()
|
||||
.build();
|
||||
ProviderConfigurationBuilder config = ProviderConfigurationBuilder.create()
|
||||
.property().name(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE)
|
||||
.label("User Model Attribute")
|
||||
.helpText("Name of the UserModel property or attribute you want to map the LDAP attribute into. For example 'firstName', 'lastName, 'email', 'street' etc.")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE).label("LDAP Attribute").helpText("Name of mapped attribute on LDAP object. For example 'cn', 'sn, 'mail', 'street' etc.")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(UserAttributeLDAPStorageMapper.READ_ONLY).label("Read Only")
|
||||
.helpText("Read-only attribute is imported from LDAP to UserModel, but it's not saved back to LDAP when user is updated in Keycloak.")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue(readOnly)
|
||||
.add();
|
||||
if (parent.isImportEnabled()) {
|
||||
config.
|
||||
property().name(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP).label("Always Read Value From LDAP")
|
||||
.helpText("If on, then during reading of the LDAP attribute value will always used instead of the value from Keycloak DB")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE).defaultValue("false").add();
|
||||
}
|
||||
config.property().name(UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP).label("Is Mandatory In LDAP")
|
||||
.helpText("If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false").add()
|
||||
.property().name(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE).label("Is Binary Attribute")
|
||||
.helpText("Should be true for binary LDAP attributes")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false").add();
|
||||
return config.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.storage.ldap.LDAPConfig;
|
||||
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
||||
import org.keycloak.storage.ldap.LDAPUtils;
|
||||
|
@ -53,6 +54,7 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
|
|||
protected static final Map<String, UserRolesRetrieveStrategy> userGroupsStrategies = new LinkedHashMap<>();
|
||||
protected static final List<String> MEMBERSHIP_TYPES = new LinkedList<>();
|
||||
protected static final List<String> MODES = new LinkedList<>();
|
||||
protected static final List<String> NO_IMPORT_MODES = new LinkedList<>();
|
||||
protected static final List<String> ROLE_RETRIEVERS;
|
||||
|
||||
// TODO: Merge with RoleLDAPFederationMapperFactory as there are lot of similar properties
|
||||
|
@ -66,6 +68,8 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
|
|||
for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) {
|
||||
MODES.add(mode.toString());
|
||||
}
|
||||
NO_IMPORT_MODES.add(LDAPGroupMapperMode.LDAP_ONLY.toString());
|
||||
NO_IMPORT_MODES.add(LDAPGroupMapperMode.READ_ONLY.toString());
|
||||
ROLE_RETRIEVERS = new LinkedList<>(userGroupsStrategies.keySet());
|
||||
|
||||
List<ProviderConfigProperty> config = getProps(null);
|
||||
|
@ -76,98 +80,112 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
|
|||
String roleObjectClasses = LDAPConstants.GROUP_OF_NAMES;
|
||||
String mode = LDAPGroupMapperMode.LDAP_ONLY.toString();
|
||||
String membershipUserAttribute = LDAPConstants.UID;
|
||||
boolean importEnabled = true;
|
||||
if (parent != null) {
|
||||
LDAPConfig config = new LDAPConfig(parent.getConfig());
|
||||
roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||
mode = config.getEditMode() == UserStorageProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
|
||||
membershipUserAttribute = config.getUsernameLdapAttribute();
|
||||
importEnabled = new UserStorageProviderModel(parent).isImportEnabled();
|
||||
}
|
||||
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property().name(GroupMapperConfig.GROUPS_DN)
|
||||
.label("LDAP Groups DN")
|
||||
.helpText("LDAP DN where are groups of this tree saved. For example 'ou=groups,dc=example,dc=org' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.GROUP_NAME_LDAP_ATTRIBUTE)
|
||||
.label("Group Name LDAP Attribute")
|
||||
.helpText("Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=Group1,ou=groups,dc=example,dc=org' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(LDAPConstants.CN)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.GROUP_OBJECT_CLASSES)
|
||||
.label("Group Object Classes")
|
||||
.helpText("Object class (or classes) of the group object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(roleObjectClasses)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE)
|
||||
.label("Preserve Group Inheritance")
|
||||
.helpText("Flag whether group inheritance from LDAP should be propagated to Keycloak. If false, then all LDAP groups will be mapped as flat top-level groups in Keycloak. Otherwise group inheritance is " +
|
||||
"preserved into Keycloak, but the group sync might fail if LDAP structure contains recursions or multiple parent groups per child groups")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("true")
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
|
||||
.label("Membership LDAP Attribute")
|
||||
.helpText("Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ." +
|
||||
"However when 'Membership Attribute Type' is 'UID' then 'Membership LDAP Attribute' could be typically 'memberUid' .")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(LDAPConstants.MEMBER)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE)
|
||||
.label("Membership Attribute Type")
|
||||
.helpText("DN means that LDAP group has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
|
||||
"UID means that LDAP group has it's members declared in form of pure user uids. For example 'memberUid: john' .")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(MEMBERSHIP_TYPES)
|
||||
.defaultValue(MembershipType.DN.toString())
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.MEMBERSHIP_USER_LDAP_ATTRIBUTE)
|
||||
.label("Membership User LDAP Attribute")
|
||||
.helpText("Used just if Membership Attribute Type is UID. It is name of LDAP attribute on user, which is used for membership mappings. Usually it will be 'uid' . For example if value of " +
|
||||
"'Membership User LDAP Attribute' is 'uid' and " +
|
||||
" LDAP group has 'memberUid: john', then it is expected that particular LDAP user will have attribute 'uid: john' .")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(membershipUserAttribute)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.GROUPS_LDAP_FILTER)
|
||||
.label("LDAP Filter")
|
||||
.helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP groups. Leave this empty if no additional filtering is needed and you want to retrieve all groups from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.MODE)
|
||||
.label("Mode")
|
||||
.helpText("LDAP_ONLY means that all group mappings of users are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where group mappings are " +
|
||||
ProviderConfigurationBuilder config = ProviderConfigurationBuilder.create()
|
||||
.property().name(GroupMapperConfig.GROUPS_DN)
|
||||
.label("LDAP Groups DN")
|
||||
.helpText("LDAP DN where are groups of this tree saved. For example 'ou=groups,dc=example,dc=org' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.GROUP_NAME_LDAP_ATTRIBUTE)
|
||||
.label("Group Name LDAP Attribute")
|
||||
.helpText("Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=Group1,ou=groups,dc=example,dc=org' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(LDAPConstants.CN)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.GROUP_OBJECT_CLASSES)
|
||||
.label("Group Object Classes")
|
||||
.helpText("Object class (or classes) of the group object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(roleObjectClasses)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE)
|
||||
.label("Preserve Group Inheritance")
|
||||
.helpText("Flag whether group inheritance from LDAP should be propagated to Keycloak. If false, then all LDAP groups will be mapped as flat top-level groups in Keycloak. Otherwise group inheritance is " +
|
||||
"preserved into Keycloak, but the group sync might fail if LDAP structure contains recursions or multiple parent groups per child groups")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("true")
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
|
||||
.label("Membership LDAP Attribute")
|
||||
.helpText("Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ." +
|
||||
"However when 'Membership Attribute Type' is 'UID' then 'Membership LDAP Attribute' could be typically 'memberUid' .")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(LDAPConstants.MEMBER)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE)
|
||||
.label("Membership Attribute Type")
|
||||
.helpText("DN means that LDAP group has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
|
||||
"UID means that LDAP group has it's members declared in form of pure user uids. For example 'memberUid: john' .")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(MEMBERSHIP_TYPES)
|
||||
.defaultValue(MembershipType.DN.toString())
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.MEMBERSHIP_USER_LDAP_ATTRIBUTE)
|
||||
.label("Membership User LDAP Attribute")
|
||||
.helpText("Used just if Membership Attribute Type is UID. It is name of LDAP attribute on user, which is used for membership mappings. Usually it will be 'uid' . For example if value of " +
|
||||
"'Membership User LDAP Attribute' is 'uid' and " +
|
||||
" LDAP group has 'memberUid: john', then it is expected that particular LDAP user will have attribute 'uid: john' .")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(membershipUserAttribute)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.GROUPS_LDAP_FILTER)
|
||||
.label("LDAP Filter")
|
||||
.helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP groups. Leave this empty if no additional filtering is needed and you want to retrieve all groups from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add();
|
||||
if (importEnabled) {
|
||||
config.property().name(GroupMapperConfig.MODE)
|
||||
.label("Mode")
|
||||
.helpText("LDAP_ONLY means that all group mappings of users are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where group mappings are " +
|
||||
"retrieved from both LDAP and DB and merged together. New group joins are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where group mappings are " +
|
||||
"retrieved from LDAP just at the time when user is imported from LDAP and then " +
|
||||
"they are saved to local keycloak DB.")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(MODES)
|
||||
.defaultValue(mode)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.USER_ROLES_RETRIEVE_STRATEGY)
|
||||
.label("User Groups Retrieve Strategy")
|
||||
.helpText("Specify how to retrieve groups of user. LOAD_GROUPS_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all groups where 'member' is our user. " +
|
||||
"GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE means that groups of user will be retrieved from 'memberOf' attribute of our user. " +
|
||||
"LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that groups of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension.")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(ROLE_RETRIEVERS)
|
||||
.defaultValue(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.MAPPED_GROUP_ATTRIBUTES)
|
||||
.label("Mapped Group Attributes")
|
||||
.helpText("List of names of attributes divided by comma. This points to the list of attributes on LDAP group, which will be mapped as attributes of Group in Keycloak. " +
|
||||
"Leave this empty if no additional group attributes are required to be mapped in Keycloak. ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC)
|
||||
.label("Drop non-existing groups during sync")
|
||||
.helpText("If this flag is true, then during sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false")
|
||||
.add()
|
||||
.build();
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(MODES)
|
||||
.defaultValue(mode)
|
||||
.add();
|
||||
} else {
|
||||
config.property().name(GroupMapperConfig.MODE)
|
||||
.label("Mode")
|
||||
.helpText("LDAP_ONLY means that specified group mappings are writable to LDAP. "
|
||||
+ "READ_ONLY means that group mappings are not writable to LDAP.")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(NO_IMPORT_MODES)
|
||||
.defaultValue(mode)
|
||||
.add();
|
||||
|
||||
}
|
||||
config.property().name(GroupMapperConfig.USER_ROLES_RETRIEVE_STRATEGY)
|
||||
.label("User Groups Retrieve Strategy")
|
||||
.helpText("Specify how to retrieve groups of user. LOAD_GROUPS_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all groups where 'member' is our user. " +
|
||||
"GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE means that groups of user will be retrieved from 'memberOf' attribute of our user. " +
|
||||
"LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that groups of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension.")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(ROLE_RETRIEVERS)
|
||||
.defaultValue(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.MAPPED_GROUP_ATTRIBUTES)
|
||||
.label("Mapped Group Attributes")
|
||||
.helpText("List of names of attributes divided by comma. This points to the list of attributes on LDAP group, which will be mapped as attributes of Group in Keycloak. " +
|
||||
"Leave this empty if no additional group attributes are required to be mapped in Keycloak. ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC)
|
||||
.label("Drop non-existing groups during sync")
|
||||
.helpText("If this flag is true, then during sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false")
|
||||
.add();
|
||||
return config.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -197,6 +215,37 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onParentUpdate(RealmModel realm, UserStorageProviderModel oldParent, UserStorageProviderModel newParent, ComponentModel mapperModel) {
|
||||
if (!newParent.isImportEnabled()) {
|
||||
if (new RoleMapperConfig(mapperModel).getMode() == LDAPGroupMapperMode.IMPORT) {
|
||||
mapperModel.getConfig().putSingle(GroupMapperConfig.MODE, LDAPGroupMapperMode.READ_ONLY.toString());
|
||||
realm.updateComponent(mapperModel);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
|
||||
ComponentModel parentModel = realm.getComponent(model.getParentId());
|
||||
UserStorageProviderModel parent = new UserStorageProviderModel(parentModel);
|
||||
onParentUpdate(realm, parent, parent, model);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) {
|
||||
ComponentModel parentModel = realm.getComponent(newModel.getParentId());
|
||||
UserStorageProviderModel parent = new UserStorageProviderModel(parentModel);
|
||||
onParentUpdate(realm, parent, parent, newModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
|
||||
return getProps(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
|
||||
checkMandatoryConfigAttribute(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN", config);
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.storage.ldap.LDAPConfig;
|
||||
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
||||
import org.keycloak.storage.ldap.LDAPUtils;
|
||||
|
@ -51,6 +52,7 @@ public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFacto
|
|||
protected static final Map<String, UserRolesRetrieveStrategy> userRolesStrategies = new LinkedHashMap<>();
|
||||
protected static final List<String> MEMBERSHIP_TYPES = new LinkedList<>();
|
||||
protected static final List<String> MODES = new LinkedList<>();
|
||||
protected static final List<String> NO_IMPORT_MODES = new LinkedList<>();
|
||||
protected static final List<String> roleRetrievers;
|
||||
|
||||
static {
|
||||
|
@ -65,6 +67,8 @@ public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFacto
|
|||
for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) {
|
||||
MODES.add(mode.toString());
|
||||
}
|
||||
NO_IMPORT_MODES.add(LDAPGroupMapperMode.LDAP_ONLY.toString());
|
||||
NO_IMPORT_MODES.add(LDAPGroupMapperMode.READ_ONLY.toString());
|
||||
roleRetrievers = new LinkedList<>(userRolesStrategies.keySet());
|
||||
|
||||
List<ProviderConfigProperty> config = getProps(null);
|
||||
|
@ -75,91 +79,132 @@ public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFacto
|
|||
String roleObjectClasses = LDAPConstants.GROUP_OF_NAMES;
|
||||
String mode = LDAPGroupMapperMode.LDAP_ONLY.toString();
|
||||
String membershipUserAttribute = LDAPConstants.UID;
|
||||
boolean importEnabled = true;
|
||||
if (parent != null) {
|
||||
LDAPConfig config = new LDAPConfig(parent.getConfig());
|
||||
roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||
mode = config.getEditMode() == UserStorageProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
|
||||
membershipUserAttribute = config.getUsernameLdapAttribute();
|
||||
importEnabled = new UserStorageProviderModel(parent).isImportEnabled();
|
||||
|
||||
}
|
||||
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property().name(RoleMapperConfig.ROLES_DN)
|
||||
.label("LDAP Roles DN")
|
||||
.helpText("LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.ROLE_NAME_LDAP_ATTRIBUTE)
|
||||
.label("Role Name LDAP Attribute")
|
||||
.helpText("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' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(LDAPConstants.CN)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.ROLE_OBJECT_CLASSES)
|
||||
.label("Role Object Classes")
|
||||
.helpText("Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(roleObjectClasses)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
|
||||
.label("Membership LDAP Attribute")
|
||||
.helpText("Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ." +
|
||||
"However when 'Membership Attribute Type' is 'UID' then 'Membership LDAP Attribute' could be typically 'memberUid' .")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(LDAPConstants.MEMBER)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE)
|
||||
.label("Membership Attribute Type")
|
||||
.helpText("DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
|
||||
"UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(MEMBERSHIP_TYPES)
|
||||
.defaultValue(MembershipType.DN.toString())
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.MEMBERSHIP_USER_LDAP_ATTRIBUTE)
|
||||
.label("Membership User LDAP Attribute")
|
||||
.helpText("Used just if Membership Attribute Type is UID. It is name of LDAP attribute on user, which is used for membership mappings. Usually it will be 'uid' . For example if value of " +
|
||||
"'Membership User LDAP Attribute' is 'uid' and " +
|
||||
" LDAP group has 'memberUid: john', then it is expected that particular LDAP user will have attribute 'uid: john' .")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(membershipUserAttribute)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.ROLES_LDAP_FILTER)
|
||||
.label("LDAP Filter")
|
||||
.helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP roles. Leave this empty if no additional filtering is needed and you want to retrieve all roles from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.MODE)
|
||||
.label("Mode")
|
||||
.helpText("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 " +
|
||||
ProviderConfigurationBuilder config = ProviderConfigurationBuilder.create()
|
||||
.property().name(RoleMapperConfig.ROLES_DN)
|
||||
.label("LDAP Roles DN")
|
||||
.helpText("LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.ROLE_NAME_LDAP_ATTRIBUTE)
|
||||
.label("Role Name LDAP Attribute")
|
||||
.helpText("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' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(LDAPConstants.CN)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.ROLE_OBJECT_CLASSES)
|
||||
.label("Role Object Classes")
|
||||
.helpText("Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(roleObjectClasses)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
|
||||
.label("Membership LDAP Attribute")
|
||||
.helpText("Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ." +
|
||||
"However when 'Membership Attribute Type' is 'UID' then 'Membership LDAP Attribute' could be typically 'memberUid' .")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(LDAPConstants.MEMBER)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE)
|
||||
.label("Membership Attribute Type")
|
||||
.helpText("DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
|
||||
"UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(MEMBERSHIP_TYPES)
|
||||
.defaultValue(MembershipType.DN.toString())
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.MEMBERSHIP_USER_LDAP_ATTRIBUTE)
|
||||
.label("Membership User LDAP Attribute")
|
||||
.helpText("Used just if Membership Attribute Type is UID. It is name of LDAP attribute on user, which is used for membership mappings. Usually it will be 'uid' . For example if value of " +
|
||||
"'Membership User LDAP Attribute' is 'uid' and " +
|
||||
" LDAP group has 'memberUid: john', then it is expected that particular LDAP user will have attribute 'uid: john' .")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(membershipUserAttribute)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.ROLES_LDAP_FILTER)
|
||||
.label("LDAP Filter")
|
||||
.helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP roles. Leave this empty if no additional filtering is needed and you want to retrieve all roles from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add();
|
||||
|
||||
if (importEnabled) {
|
||||
config.property().name(RoleMapperConfig.MODE)
|
||||
.label("Mode")
|
||||
.helpText("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.")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(MODES)
|
||||
.defaultValue(mode)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY)
|
||||
.label("User Roles Retrieve Strategy")
|
||||
.helpText("Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " +
|
||||
"GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " +
|
||||
"LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension.")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(roleRetrievers)
|
||||
.defaultValue(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.USE_REALM_ROLES_MAPPING)
|
||||
.label("Use Realm Roles Mapping")
|
||||
.helpText("If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("true")
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.CLIENT_ID)
|
||||
.label("Client ID")
|
||||
.helpText("Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false")
|
||||
.type(ProviderConfigProperty.CLIENT_LIST_TYPE)
|
||||
.add()
|
||||
.build();
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(MODES)
|
||||
.defaultValue(mode)
|
||||
.add();
|
||||
} else {
|
||||
config.property().name(RoleMapperConfig.MODE)
|
||||
.label("Mode")
|
||||
.helpText("LDAP_ONLY means that specified role mappings are writable to LDAP. READ_ONLY means LDAP is readonly.")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(NO_IMPORT_MODES)
|
||||
.defaultValue(mode)
|
||||
.add();
|
||||
|
||||
}
|
||||
|
||||
config.property().name(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY)
|
||||
.label("User Roles Retrieve Strategy")
|
||||
.helpText("Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " +
|
||||
"GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " +
|
||||
"LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension.")
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(roleRetrievers)
|
||||
.defaultValue(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE)
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.USE_REALM_ROLES_MAPPING)
|
||||
.label("Use Realm Roles Mapping")
|
||||
.helpText("If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("true")
|
||||
.add()
|
||||
.property().name(RoleMapperConfig.CLIENT_ID)
|
||||
.label("Client ID")
|
||||
.helpText("Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false")
|
||||
.type(ProviderConfigProperty.CLIENT_LIST_TYPE)
|
||||
.add();
|
||||
return config.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onParentUpdate(RealmModel realm, UserStorageProviderModel oldParent, UserStorageProviderModel newParent, ComponentModel mapperModel) {
|
||||
if (!newParent.isImportEnabled()) {
|
||||
if (new RoleMapperConfig(mapperModel).getMode() == LDAPGroupMapperMode.IMPORT) {
|
||||
mapperModel.getConfig().putSingle(RoleMapperConfig.MODE, LDAPGroupMapperMode.READ_ONLY.toString());
|
||||
realm.updateComponent(mapperModel);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
|
||||
ComponentModel parentModel = realm.getComponent(model.getParentId());
|
||||
UserStorageProviderModel parent = new UserStorageProviderModel(parentModel);
|
||||
onParentUpdate(realm, parent, parent, model);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) {
|
||||
ComponentModel parentModel = realm.getComponent(newModel.getParentId());
|
||||
UserStorageProviderModel parent = new UserStorageProviderModel(parentModel);
|
||||
onParentUpdate(realm, parent, parent, newModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
|
@ -171,6 +216,11 @@ public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFacto
|
|||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
|
||||
return getProps(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
|
|
|
@ -36,13 +36,37 @@ public interface UserStorageProviderResource {
|
|||
* Action can be "triggerFullSync" or "triggerChangedUsersSync"
|
||||
*
|
||||
*
|
||||
* @param componentId
|
||||
* @param action
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Path("{componentId}/sync")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
SynchronizationResultRepresentation syncUsers(@QueryParam("action") String action);
|
||||
SynchronizationResultRepresentation syncUsers(@PathParam("componentId") String componentId, @QueryParam("action") String action);
|
||||
|
||||
/**
|
||||
* Remove imported users
|
||||
*
|
||||
*
|
||||
* @param componentId
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Path("{componentId}/remove-imported-users")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
void removeImportedUsers(@PathParam("componentId") String componentId);
|
||||
|
||||
/**
|
||||
* Unlink imported users from a storage provider
|
||||
*
|
||||
* @param componentId
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Path("{componentId}/unlink-users")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
void unlink(@PathParam("componentId") String componentId);
|
||||
|
||||
/**
|
||||
* REST invocation for initiating sync for an ldap mapper. This method may be moved in the future. Right now
|
||||
|
|
|
@ -868,6 +868,21 @@ public class UserCacheSession implements UserCache {
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeImportedUsers(RealmModel realm, String storageProviderId) {
|
||||
getDelegate().removeImportedUsers(realm, storageProviderId);
|
||||
clear();
|
||||
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlinkUsers(RealmModel realm, String storageProviderId) {
|
||||
getDelegate().unlinkUsers(realm, storageProviderId);
|
||||
clear();
|
||||
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
|
||||
|
||||
}
|
||||
|
||||
private void addRealmInvalidation(String realmId) {
|
||||
realmInvalidations.add(realmId);
|
||||
invalidationEvents.add(UserCacheRealmInvalidationEvent.create(realmId));
|
||||
|
|
|
@ -380,38 +380,47 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
}
|
||||
|
||||
public void removeUserDataByLink(RealmModel realm, String linkId) {
|
||||
@Override
|
||||
public void removeImportedUsers(RealmModel realm, String storageProviderId) {
|
||||
int num = em.createNamedQuery("deleteUserRoleMappingsByRealmAndLink")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", linkId)
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
num = em.createNamedQuery("deleteUserRequiredActionsByRealmAndLink")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", linkId)
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
num = em.createNamedQuery("deleteFederatedIdentityByRealmAndLink")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", linkId)
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
num = em.createNamedQuery("deleteCredentialAttributeByRealmAndLink")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", linkId)
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
num = em.createNamedQuery("deleteCredentialsByRealmAndLink")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", linkId)
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
num = em.createNamedQuery("deleteUserAttributesByRealmAndLink")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", linkId)
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
num = em.createNamedQuery("deleteUserGroupMembershipsByRealmAndLink")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", linkId)
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
num = em.createNamedQuery("deleteUsersByRealmAndLink")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", linkId)
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlinkUsers(RealmModel realm, String storageProviderId) {
|
||||
em.createNamedQuery("unlinkUsers")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
|
@ -714,7 +723,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
@Override
|
||||
public void preRemove(RealmModel realm, ComponentModel component) {
|
||||
if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
|
||||
removeUserDataByLink(realm, component.getId());
|
||||
removeImportedUsers(realm, component.getId());
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,8 @@ import java.util.Collection;
|
|||
@NamedQuery(name="getRealmUserByServiceAccount", query="select u from UserEntity u where u.serviceAccountClientLink = :clientInternalId and u.realmId = :realmId"),
|
||||
@NamedQuery(name="getRealmUserCount", query="select count(u) from UserEntity u where u.realmId = :realmId"),
|
||||
@NamedQuery(name="deleteUsersByRealm", query="delete from UserEntity u where u.realmId = :realmId"),
|
||||
@NamedQuery(name="deleteUsersByRealmAndLink", query="delete from UserEntity u where u.realmId = :realmId and u.federationLink=:link")
|
||||
@NamedQuery(name="deleteUsersByRealmAndLink", query="delete from UserEntity u where u.realmId = :realmId and u.federationLink=:link"),
|
||||
@NamedQuery(name="unlinkUsers", query="update UserEntity u set u.federationLink = null where u.realmId = :realmId and u.federationLink=:link")
|
||||
})
|
||||
@Entity
|
||||
@Table(name="USER_ENTITY", uniqueConstraints = {
|
||||
|
|
|
@ -60,7 +60,6 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import org.keycloak.models.mongo.keycloak.entities.UserEntity;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -621,8 +620,14 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
|
|||
@Override
|
||||
public void preRemove(RealmModel realm, ComponentModel component) {
|
||||
if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
|
||||
String providerId = component.getId();
|
||||
removeImportedUsers(realm, providerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeImportedUsers(RealmModel realm, String providerId) {
|
||||
DBObject query = new QueryBuilder()
|
||||
.and("federationLink").is(component.getId())
|
||||
.and("federationLink").is(providerId)
|
||||
.get();
|
||||
|
||||
List<MongoUserEntity> mongoUsers = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext);
|
||||
|
@ -635,6 +640,22 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlinkUsers(RealmModel realm, String storageProviderId) {
|
||||
DBObject query = new QueryBuilder()
|
||||
.and("federationLink").is(storageProviderId)
|
||||
.get();
|
||||
|
||||
List<MongoUserEntity> mongoUsers = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext);
|
||||
|
||||
for (MongoUserEntity userEntity : mongoUsers) {
|
||||
// Doing this way to ensure UserRemovedEvent triggered with proper callbacks.
|
||||
UserAdapter user = new UserAdapter(session, realm, userEntity, invocationContext);
|
||||
user.setFederationLink(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) {
|
||||
MongoUserEntity mongoUser = getMongoUserEntity(user);
|
||||
|
|
|
@ -69,6 +69,22 @@ public interface UserProvider extends Provider,
|
|||
UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions);
|
||||
void preRemove(RealmModel realm);
|
||||
|
||||
/**
|
||||
* Removes any imported users from a specific User Storage Provider.
|
||||
*
|
||||
* @param realm
|
||||
* @param storageProviderId
|
||||
*/
|
||||
void removeImportedUsers(RealmModel realm, String storageProviderId);
|
||||
|
||||
/**
|
||||
* Set federation link to null to imported users of a specific User Storage Provider
|
||||
*
|
||||
* @param realm
|
||||
* @param storageProviderId
|
||||
*/
|
||||
void unlinkUsers(RealmModel realm, String storageProviderId);
|
||||
|
||||
void preRemove(RealmModel realm, RoleModel role);
|
||||
void preRemove(RealmModel realm, GroupModel group);
|
||||
|
||||
|
|
|
@ -24,13 +24,11 @@ import org.keycloak.component.ComponentModel;
|
|||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.managers.UserStorageSyncManager;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
||||
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
|
||||
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
|
||||
import org.keycloak.storage.user.SynchronizationResult;
|
||||
|
||||
|
@ -94,7 +92,7 @@ public class UserStorageProviderResource {
|
|||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public SynchronizationResult syncUsers(@PathParam("id") String id,
|
||||
@QueryParam("action") String action) {
|
||||
@QueryParam("action") String action) {
|
||||
auth.requireManage();
|
||||
|
||||
ComponentModel model = realm.getComponent(id);
|
||||
|
@ -129,6 +127,53 @@ public class UserStorageProviderResource {
|
|||
return syncResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove imported users
|
||||
*
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Path("{id}/remove-imported-users")
|
||||
@NoCache
|
||||
public void removeImportedUsers(@PathParam("id") String id) {
|
||||
auth.requireManage();
|
||||
|
||||
ComponentModel model = realm.getComponent(id);
|
||||
if (model == null) {
|
||||
throw new NotFoundException("Could not find component");
|
||||
}
|
||||
if (!model.getProviderType().equals(UserStorageProvider.class.getName())) {
|
||||
throw new NotFoundException("found, but not a UserStorageProvider");
|
||||
}
|
||||
|
||||
session.users().removeImportedUsers(realm, id);
|
||||
}
|
||||
/**
|
||||
* Unlink imported users from a storage provider
|
||||
*
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Path("{id}/unlink-users")
|
||||
@NoCache
|
||||
public void unlinkUsers(@PathParam("id") String id) {
|
||||
auth.requireManage();
|
||||
|
||||
ComponentModel model = realm.getComponent(id);
|
||||
if (model == null) {
|
||||
throw new NotFoundException("Could not find component");
|
||||
}
|
||||
if (!model.getProviderType().equals(UserStorageProvider.class.getName())) {
|
||||
throw new NotFoundException("found, but not a UserStorageProvider");
|
||||
}
|
||||
|
||||
session.users().unlinkUsers(realm, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger sync of mapper data related to ldap mapper (roles, groups, ...)
|
||||
*
|
||||
|
|
|
@ -614,6 +614,16 @@ public class UserStorageManager implements UserProvider, OnUserCache, OnCreateCo
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeImportedUsers(RealmModel realm, String storageProviderId) {
|
||||
localStorage().removeImportedUsers(realm, storageProviderId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlinkUsers(RealmModel realm, String storageProviderId) {
|
||||
localStorage().unlinkUsers(realm, storageProviderId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) {
|
||||
if (StorageId.isLocalStorage(user)) {
|
||||
|
@ -652,4 +662,6 @@ public class UserStorageManager implements UserProvider, OnUserCache, OnCreateCo
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -99,6 +99,26 @@ public class AdapterTest {
|
|||
@Rule
|
||||
public AdapterTestStrategy testStrategy = new AdapterTestStrategy("http://localhost:8081/auth", "http://localhost:8081", keycloakRule);
|
||||
|
||||
//@Test
|
||||
public void testUi() throws Exception {
|
||||
Thread.sleep(1000000000);
|
||||
}
|
||||
|
||||
public static class MySuper {
|
||||
|
||||
}
|
||||
|
||||
public static class Base extends MySuper {
|
||||
public Class superClass() {
|
||||
return super.getClass();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBase() {
|
||||
System.out.println(new Base().superClass().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginSSOAndLogout() throws Exception {
|
||||
testStrategy.testLoginSSOMax();
|
||||
|
|
|
@ -19,6 +19,9 @@ package org.keycloak.testsuite.federation.storage.ldap;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Rule;
|
||||
|
@ -27,9 +30,12 @@ import org.junit.rules.RuleChain;
|
|||
import org.junit.rules.TestRule;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.storage.ldap.LDAPConfig;
|
||||
|
@ -67,7 +73,10 @@ import org.openqa.selenium.WebDriver;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MASTER;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.keycloak.models.AdminRoles.ADMIN;
|
||||
import static org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -174,6 +183,67 @@ public class LDAPProvidersIntegrationTest {
|
|||
|
||||
}
|
||||
|
||||
|
||||
private Keycloak adminClient;
|
||||
|
||||
@Before
|
||||
public void onBefore() {
|
||||
adminClient = Keycloak.getInstance(AUTH_SERVER_ROOT, MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveImportedUsers() {
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
RealmModel appRealm = manager.getRealm("test");
|
||||
UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm);
|
||||
Assert.assertEquals(ldapModel.getId(), user.getFederationLink());
|
||||
} finally {
|
||||
keycloakRule.stopSession(session, true);
|
||||
}
|
||||
|
||||
adminClient.realm("test").userStorage().removeImportedUsers(ldapModel.getId());
|
||||
|
||||
session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
RealmModel appRealm = manager.getRealm("test");
|
||||
UserModel user = session.userLocalStorage().getUserByUsername("johnkeycloak", appRealm);
|
||||
Assert.assertNull(user);
|
||||
} finally {
|
||||
keycloakRule.stopSession(session, true);
|
||||
}
|
||||
}
|
||||
|
||||
// test name prefixed with zz to make sure it runs last as we are unlinking imported users
|
||||
@Test
|
||||
public void zzTestUnlinkUsers() {
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
RealmModel appRealm = manager.getRealm("test");
|
||||
UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm);
|
||||
Assert.assertEquals(ldapModel.getId(), user.getFederationLink());
|
||||
} finally {
|
||||
keycloakRule.stopSession(session, true);
|
||||
}
|
||||
|
||||
adminClient.realm("test").userStorage().unlink(ldapModel.getId());
|
||||
|
||||
session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
RealmModel appRealm = manager.getRealm("test");
|
||||
UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm);
|
||||
Assert.assertNotNull(user);
|
||||
Assert.assertNull(user.getFederationLink());
|
||||
|
||||
} finally {
|
||||
keycloakRule.stopSession(session, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void caseInSensitiveImport() {
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
|
|
|
@ -706,6 +706,8 @@ update-profile-first-login=Update Profile First Login
|
|||
update-profile-first-login.tooltip=Update profile on first login
|
||||
sync-registrations=Sync Registrations
|
||||
ldap.sync-registrations.tooltip=Should newly created users be created within LDAP store? Priority effects which provider is chose to sync the new user.
|
||||
import-enabled=Import Users
|
||||
ldap.import-enabled.tooltip=If true, LDAP users will be imported into Keycloak DB and synced via the configured sync policies.
|
||||
vendor=Vendor
|
||||
ldap.vendor.tooltip=LDAP vendor (provider)
|
||||
username-ldap-attribute=Username LDAP attribute
|
||||
|
|
|
@ -1046,6 +1046,7 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
|
|||
instance.config['evictionMinute'] = [''];
|
||||
instance.config['maxLifespan'] = [''];
|
||||
instance.config['batchSizeForSync'] = [DEFAULT_BATCH_SIZE];
|
||||
//instance.config['importEnabled'] = ['true'];
|
||||
|
||||
if (providerFactory.properties) {
|
||||
|
||||
|
@ -1098,6 +1099,9 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
|
|||
if (!instance.config['priority']) {
|
||||
instance.config['priority'] = ['0'];
|
||||
}
|
||||
if (!instance.config['importEnabled']) {
|
||||
instance.config['importEnabled'] = ['true'];
|
||||
}
|
||||
|
||||
if (providerFactory.properties) {
|
||||
|
||||
|
|
|
@ -33,6 +33,13 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'priority.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block">
|
||||
<label class="col-md-2 control-label" for="importEnabled">{{:: 'import-enabled' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="instance.config['importEnabled'][0]" name="importEnabled" id="importEnabled" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'ldap.import-enabled.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="editMode">{{:: 'edit-mode' | translate}}</label>
|
||||
|
|
Loading…
Reference in a new issue