mirror of
https://github.com/bmlong137/alfresco-keycloak.git
synced 2025-05-12 21:24:43 +00:00
Improved group synchronisation
- add exact path match condition to allow selection of specific groups - add negative match conditions to exclude specific groups/users - support group names derived from attibutes
This commit is contained in:
parent
725f768535
commit
4b1b0cbd08
@ -1162,6 +1162,10 @@
|
|||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"name": "Test A",
|
"name": "Test A",
|
||||||
|
"attributes": {
|
||||||
|
"alfrescoGroupName": ["Group_A"],
|
||||||
|
"alfrescoGroupDisplayName": ["Group with custom mapped attributes"]
|
||||||
|
},
|
||||||
"subGroups": [
|
"subGroups": [
|
||||||
{
|
{
|
||||||
"name": "Test AA"
|
"name": "Test AA"
|
||||||
|
@ -3,6 +3,7 @@ db.url=jdbc:postgresql://alf-pg:5432/alfresco
|
|||||||
db.username=alfresco
|
db.username=alfresco
|
||||||
db.password=alfresco
|
db.password=alfresco
|
||||||
|
|
||||||
|
index.subsystem.name=solr6
|
||||||
solr.host=solr6
|
solr.host=solr6
|
||||||
solr.port=8983
|
solr.port=8983
|
||||||
|
|
||||||
@ -29,5 +30,6 @@ keycloak.roles.requiredClientScopes=alfresco-role-service
|
|||||||
|
|
||||||
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
|
keycloak.synchronization.groupFilter.pathMatch.property.groupPaths=/Test A
|
||||||
|
|
||||||
keycloak.synchronization.requiredClientScopes=alfresco-authority-sync
|
keycloak.synchronization.requiredClientScopes=alfresco-authority-sync
|
2
pom.xml
2
pom.xml
@ -88,7 +88,7 @@
|
|||||||
<!-- parent is including 6.11 erroneously -->
|
<!-- parent is including 6.11 erroneously -->
|
||||||
<surf.version>9.0</surf.version>
|
<surf.version>9.0</surf.version>
|
||||||
|
|
||||||
<acosix.utility.version>1.4.3</acosix.utility.version>
|
<acosix.utility.version>1.4.6-SNAPSHOT</acosix.utility.version>
|
||||||
<ootbee.support-tools.version>1.2.2.0</ootbee.support-tools.version>
|
<ootbee.support-tools.version>1.2.2.0</ootbee.support-tools.version>
|
||||||
|
|
||||||
<keycloak.docker.image>keycloak/keycloak</keycloak.docker.image>
|
<keycloak.docker.image>keycloak/keycloak</keycloak.docker.image>
|
||||||
|
@ -5,4 +5,4 @@ module.version=${noSnapshotVersion}
|
|||||||
|
|
||||||
module.repo.version.min=5
|
module.repo.version.min=5
|
||||||
|
|
||||||
module.depends.acosix-utility=1.2.5-*
|
module.depends.acosix-utility=1.4.6-*
|
@ -202,10 +202,26 @@
|
|||||||
<property name="identitiesClient" ref="identitiesClient" />
|
<property name="identitiesClient" ref="identitiesClient" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean id="userFilter.notContainedInGroup" class="${project.artifactId}.sync.GroupContainmentUserFilter">
|
||||||
|
<property name="identitiesClient" ref="identitiesClient" />
|
||||||
|
<property name="matchDenies" value="true" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
<bean id="groupFilter.pathMatch" class="${project.artifactId}.sync.GroupPathFilter" />
|
||||||
|
|
||||||
|
<bean id="groupFilter.notPathMatch" class="${project.artifactId}.sync.GroupPathFilter">
|
||||||
|
<property name="matchDenies" value="true" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
<bean id="groupFilter.containedInGroup" class="${project.artifactId}.sync.GroupContainmentGroupFilter">
|
<bean id="groupFilter.containedInGroup" class="${project.artifactId}.sync.GroupContainmentGroupFilter">
|
||||||
<property name="identitiesClient" ref="identitiesClient" />
|
<property name="identitiesClient" ref="identitiesClient" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean id="groupFilter.notContainedInGroup" class="${project.artifactId}.sync.GroupContainmentGroupFilter">
|
||||||
|
<property name="identitiesClient" ref="identitiesClient" />
|
||||||
|
<property name="matchDenies" value="true" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
<bean id="authorityMapper.simpleAttributes" abstract="true">
|
<bean id="authorityMapper.simpleAttributes" abstract="true">
|
||||||
<property name="namespaceService" ref="namespaceService" />
|
<property name="namespaceService" ref="namespaceService" />
|
||||||
</bean>
|
</bean>
|
||||||
|
@ -145,11 +145,25 @@ keycloak.synchronization.userFilter.containedInGroup.property.requireAll=false
|
|||||||
keycloak.synchronization.userFilter.containedInGroup.property.allowTransitive=true
|
keycloak.synchronization.userFilter.containedInGroup.property.allowTransitive=true
|
||||||
keycloak.synchronization.userFilter.containedInGroup.property.groupLoadBatchSize=${keycloak.synchronization.groupLoadBatchSize}
|
keycloak.synchronization.userFilter.containedInGroup.property.groupLoadBatchSize=${keycloak.synchronization.groupLoadBatchSize}
|
||||||
|
|
||||||
|
keycloak.synchronization.userFilter.notContainedInGroup.property.groupPaths=
|
||||||
|
keycloak.synchronization.userFilter.notContainedInGroup.property.groupIds=
|
||||||
|
keycloak.synchronization.userFilter.notContainedInGroup.property.requireAll=false
|
||||||
|
keycloak.synchronization.userFilter.notContainedInGroup.property.allowTransitive=true
|
||||||
|
keycloak.synchronization.userFilter.notContainedInGroup.property.groupLoadBatchSize=${keycloak.synchronization.groupLoadBatchSize}
|
||||||
|
|
||||||
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=
|
||||||
keycloak.synchronization.groupFilter.containedInGroup.property.groupIds=
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupIds=
|
||||||
keycloak.synchronization.groupFilter.containedInGroup.property.requireAll=false
|
keycloak.synchronization.groupFilter.containedInGroup.property.requireAll=false
|
||||||
keycloak.synchronization.groupFilter.containedInGroup.property.allowTransitive=true
|
keycloak.synchronization.groupFilter.containedInGroup.property.allowTransitive=true
|
||||||
|
|
||||||
|
keycloak.synchronization.groupFilter.notContainedInGroup.property.groupPaths=
|
||||||
|
keycloak.synchronization.groupFilter.notContainedInGroup.property.groupIds=
|
||||||
|
keycloak.synchronization.groupFilter.notContainedInGroup.property.requireAll=false
|
||||||
|
keycloak.synchronization.groupFilter.notContainedInGroup.property.allowTransitive=true
|
||||||
|
|
||||||
|
keycloak.synchronization.groupFilter.pathMatch.property.groupPaths=
|
||||||
|
keycloak.synchronization.groupFilter.notPathMatch.property.groupPaths=
|
||||||
|
|
||||||
keycloak.synchronization.userMapper.default.property.enabled=true
|
keycloak.synchronization.userMapper.default.property.enabled=true
|
||||||
keycloak.synchronization.userMapper.default.property.mapNull=true
|
keycloak.synchronization.userMapper.default.property.mapNull=true
|
||||||
keycloak.synchronization.userMapper.default.property.mapFirstName=true
|
keycloak.synchronization.userMapper.default.property.mapFirstName=true
|
||||||
@ -158,6 +172,7 @@ keycloak.synchronization.userMapper.default.property.mapEmail=true
|
|||||||
keycloak.synchronization.userMapper.default.property.mapEnabledState=true
|
keycloak.synchronization.userMapper.default.property.mapEnabledState=true
|
||||||
|
|
||||||
keycloak.synchronization.userMapper.simpleAttributes.property.enabled=true
|
keycloak.synchronization.userMapper.simpleAttributes.property.enabled=true
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.mapBlankString=true
|
||||||
keycloak.synchronization.userMapper.simpleAttributes.property.mapNull=true
|
keycloak.synchronization.userMapper.simpleAttributes.property.mapNull=true
|
||||||
keycloak.synchronization.userMapper.simpleAttributes.property.attributes.map.middleName=cm:middleName
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributes.map.middleName=cm:middleName
|
||||||
keycloak.synchronization.userMapper.simpleAttributes.property.attributes.map.organization=cm:organization
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributes.map.organization=cm:organization
|
||||||
@ -176,4 +191,8 @@ keycloak.synchronization.userMapper.simpleAttributes.property.attributes.map.com
|
|||||||
keycloak.synchronization.groupMapper.default.property.enabled=true
|
keycloak.synchronization.groupMapper.default.property.enabled=true
|
||||||
|
|
||||||
keycloak.synchronization.groupMapper.simpleAttributes.property.enabled=true
|
keycloak.synchronization.groupMapper.simpleAttributes.property.enabled=true
|
||||||
keycloak.synchronization.groupMapper.simpleAttributes.property.mapNull=true
|
keycloak.synchronization.groupMapper.simpleAttributes.property.mapBlankString=false
|
||||||
|
keycloak.synchronization.groupMapper.simpleAttributes.property.mapNull=false
|
||||||
|
# these are not available in default Keycloak groups, but can be optionally set to override default mapping
|
||||||
|
keycloak.synchronization.groupMapper.simpleAttributes.property.attributes.map.alfrescoGroupName=cm:authorityName
|
||||||
|
keycloak.synchronization.groupMapper.simpleAttributes.property.attributes.map.alfrescoGroupDisplayName=cm:authorityDisplayName
|
@ -92,6 +92,17 @@ public interface IdentitiesClient
|
|||||||
*/
|
*/
|
||||||
int processGroups(int offset, int groupBatchSize, Consumer<GroupRepresentation> groupProcessor);
|
int processGroups(int offset, int groupBatchSize, Consumer<GroupRepresentation> groupProcessor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and processes a sub-groups from Keycloak using an externally specified processor.
|
||||||
|
*
|
||||||
|
* @param groupId
|
||||||
|
* the ID of the parent group
|
||||||
|
* @param groupProcessor
|
||||||
|
* the processor handling the loaded groups
|
||||||
|
* @return the number of processed groups
|
||||||
|
*/
|
||||||
|
int processSubGroups(String groupId, Consumer<GroupRepresentation> groupProcessor);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads and processes a batch of users / members of a group from Keycloak using an externally specified processor.
|
* Loads and processes a batch of users / members of a group from Keycloak using an externally specified processor.
|
||||||
*
|
*
|
||||||
|
@ -158,7 +158,8 @@ public class IdentitiesClientImpl extends AbstractIDMClientImpl implements Ident
|
|||||||
ParameterCheck.mandatory("groupProcessor", groupProcessor);
|
ParameterCheck.mandatory("groupProcessor", groupProcessor);
|
||||||
|
|
||||||
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups")
|
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups")
|
||||||
.queryParam("first", offset).queryParam("max", groupBatchSize).build(this.deployment.getRealm());
|
.queryParam("first", offset).queryParam("max", groupBatchSize).queryParam("briefRepresentation", false)
|
||||||
|
.build(this.deployment.getRealm());
|
||||||
|
|
||||||
if (offset < 0)
|
if (offset < 0)
|
||||||
{
|
{
|
||||||
@ -172,6 +173,23 @@ public class IdentitiesClientImpl extends AbstractIDMClientImpl implements Ident
|
|||||||
return this.processEntityBatch(uri, groupProcessor, GroupRepresentation.class);
|
return this.processEntityBatch(uri, groupProcessor, GroupRepresentation.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int processSubGroups(final String groupId, final Consumer<GroupRepresentation> groupProcessor)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatoryString("groupId", groupId);
|
||||||
|
ParameterCheck.mandatory("groupProcessor", groupProcessor);
|
||||||
|
|
||||||
|
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
|
||||||
|
.path("/admin/realms/{realm}/groups/{groupId}/children").substitutePathParam("groupId", groupId, false)
|
||||||
|
.queryParam("briefRepresentation", false).build(this.deployment.getRealm());
|
||||||
|
|
||||||
|
return this.processEntityBatch(uri, groupProcessor, GroupRepresentation.class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
|
@ -20,6 +20,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import org.alfresco.repo.security.sync.NodeDescription;
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
import org.alfresco.service.namespace.NamespaceService;
|
import org.alfresco.service.namespace.NamespaceService;
|
||||||
@ -37,6 +38,8 @@ public abstract class BaseAttributeProcessor implements InitializingBean
|
|||||||
|
|
||||||
protected NamespaceService namespaceService;
|
protected NamespaceService namespaceService;
|
||||||
|
|
||||||
|
protected boolean mapBlankString;
|
||||||
|
|
||||||
protected boolean mapNull;
|
protected boolean mapNull;
|
||||||
|
|
||||||
protected Map<String, String> attributes;
|
protected Map<String, String> attributes;
|
||||||
@ -55,14 +58,13 @@ public abstract class BaseAttributeProcessor implements InitializingBean
|
|||||||
if (this.attributes != null && !this.attributes.isEmpty())
|
if (this.attributes != null && !this.attributes.isEmpty())
|
||||||
{
|
{
|
||||||
this.attributePropertyQNameMappings = new HashMap<>();
|
this.attributePropertyQNameMappings = new HashMap<>();
|
||||||
this.attributes
|
this.attributes.forEach((k, v) -> this.attributePropertyQNameMappings.put(k, QName.resolveToQName(this.namespaceService, v)));
|
||||||
.forEach((k, v) -> this.attributePropertyQNameMappings.put(k, QName.resolveToQName(this.namespaceService, v)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param namespaceService
|
* @param namespaceService
|
||||||
* the namespaceService to set
|
* the namespaceService to set
|
||||||
*/
|
*/
|
||||||
public void setNamespaceService(final NamespaceService namespaceService)
|
public void setNamespaceService(final NamespaceService namespaceService)
|
||||||
{
|
{
|
||||||
@ -71,16 +73,25 @@ public abstract class BaseAttributeProcessor implements InitializingBean
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param attributes
|
* @param attributes
|
||||||
* the attributes to set
|
* the attributes to set
|
||||||
*/
|
*/
|
||||||
public void setAttributes(final Map<String, String> attributes)
|
public void setAttributes(final Map<String, String> attributes)
|
||||||
{
|
{
|
||||||
this.attributes = attributes;
|
this.attributes = attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapBlankString
|
||||||
|
* the mapBlankString to set
|
||||||
|
*/
|
||||||
|
public void setMapBlankString(final boolean mapBlankString)
|
||||||
|
{
|
||||||
|
this.mapBlankString = mapBlankString;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mapNull
|
* @param mapNull
|
||||||
* the mapNull to set
|
* the mapNull to set
|
||||||
*/
|
*/
|
||||||
public void setMapNull(final boolean mapNull)
|
public void setMapNull(final boolean mapNull)
|
||||||
{
|
{
|
||||||
@ -95,9 +106,9 @@ public abstract class BaseAttributeProcessor implements InitializingBean
|
|||||||
* property, again leaving that kind of processing to the Alfresco default functionality of integrity checking.
|
* property, again leaving that kind of processing to the Alfresco default functionality of integrity checking.
|
||||||
*
|
*
|
||||||
* @param attributes
|
* @param attributes
|
||||||
* the list of attributes
|
* the list of attributes
|
||||||
* @param nodeDescription
|
* @param nodeDescription
|
||||||
* the node description to enhance
|
* the node description to enhance
|
||||||
*/
|
*/
|
||||||
protected void map(final Map<String, List<String>> attributes, final NodeDescription nodeDescription)
|
protected void map(final Map<String, List<String>> attributes, final NodeDescription nodeDescription)
|
||||||
{
|
{
|
||||||
@ -112,11 +123,11 @@ public abstract class BaseAttributeProcessor implements InitializingBean
|
|||||||
* Maps an individual attribute to the correlating node property of the node description.
|
* Maps an individual attribute to the correlating node property of the node description.
|
||||||
*
|
*
|
||||||
* @param attribute
|
* @param attribute
|
||||||
* the name of the attribute to map
|
* the name of the attribute to map
|
||||||
* @param attributes
|
* @param attributes
|
||||||
* the list of attributes
|
* the list of attributes
|
||||||
* @param nodeDescription
|
* @param nodeDescription
|
||||||
* the node description to enhance
|
* the node description to enhance
|
||||||
*/
|
*/
|
||||||
protected void mapAttribute(final String attribute, final Map<String, List<String>> attributes, final NodeDescription nodeDescription)
|
protected void mapAttribute(final String attribute, final Map<String, List<String>> attributes, final NodeDescription nodeDescription)
|
||||||
{
|
{
|
||||||
@ -128,12 +139,30 @@ public abstract class BaseAttributeProcessor implements InitializingBean
|
|||||||
if (values.size() == 1)
|
if (values.size() == 1)
|
||||||
{
|
{
|
||||||
value = values.get(0);
|
value = values.get(0);
|
||||||
|
if (!this.mapBlankString && ((String) value).isBlank())
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
value = new ArrayList<>(values);
|
if (!this.mapBlankString)
|
||||||
|
{
|
||||||
|
value = new ArrayList<>(values.stream().filter(Predicate.not(String::isBlank)).toList());
|
||||||
|
if (((List<?>) value).isEmpty())
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = new ArrayList<>(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value != null || this.mapNull)
|
||||||
|
{
|
||||||
|
nodeDescription.getProperties().put(propertyQName, value);
|
||||||
}
|
}
|
||||||
nodeDescription.getProperties().put(propertyQName, value);
|
|
||||||
}
|
}
|
||||||
else if (this.mapNull)
|
else if (this.mapNull)
|
||||||
{
|
{
|
||||||
|
@ -42,6 +42,8 @@ public abstract class BaseGroupContainmentFilter implements InitializingBean
|
|||||||
|
|
||||||
protected List<String> idResolvedGroupPaths;
|
protected List<String> idResolvedGroupPaths;
|
||||||
|
|
||||||
|
protected boolean matchDenies;
|
||||||
|
|
||||||
protected boolean requireAll = false;
|
protected boolean requireAll = false;
|
||||||
|
|
||||||
protected boolean allowTransitive = true;
|
protected boolean allowTransitive = true;
|
||||||
@ -75,7 +77,7 @@ public abstract class BaseGroupContainmentFilter implements InitializingBean
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param groupPaths
|
* @param groupPaths
|
||||||
* the groupPaths to set as a comma-separated string of paths
|
* the groupPaths to set as a comma-separated string of paths
|
||||||
*/
|
*/
|
||||||
public void setGroupPaths(final String groupPaths)
|
public void setGroupPaths(final String groupPaths)
|
||||||
{
|
{
|
||||||
@ -84,16 +86,25 @@ public abstract class BaseGroupContainmentFilter implements InitializingBean
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param groupIds
|
* @param groupIds
|
||||||
* the groupIds to set as a comma-separated string of paths
|
* the groupIds to set as a comma-separated string of paths
|
||||||
*/
|
*/
|
||||||
public void setGroupIds(final String groupIds)
|
public void setGroupIds(final String groupIds)
|
||||||
{
|
{
|
||||||
this.groupIds = groupIds != null && !groupIds.isEmpty() ? Arrays.asList(groupIds.split(",")) : null;
|
this.groupIds = groupIds != null && !groupIds.isEmpty() ? Arrays.asList(groupIds.split(",")) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param matchDenies
|
||||||
|
* the matchDenies to set
|
||||||
|
*/
|
||||||
|
public void setMatchDenies(final boolean matchDenies)
|
||||||
|
{
|
||||||
|
this.matchDenies = matchDenies;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param requireAll
|
* @param requireAll
|
||||||
* the requireAll to set
|
* the requireAll to set
|
||||||
*/
|
*/
|
||||||
public void setRequireAll(final boolean requireAll)
|
public void setRequireAll(final boolean requireAll)
|
||||||
{
|
{
|
||||||
@ -102,7 +113,7 @@ public abstract class BaseGroupContainmentFilter implements InitializingBean
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param allowTransitive
|
* @param allowTransitive
|
||||||
* the allowTransitive to set
|
* the allowTransitive to set
|
||||||
*/
|
*/
|
||||||
public void setAllowTransitive(final boolean allowTransitive)
|
public void setAllowTransitive(final boolean allowTransitive)
|
||||||
{
|
{
|
||||||
@ -111,7 +122,7 @@ public abstract class BaseGroupContainmentFilter implements InitializingBean
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param groupLoadBatchSize
|
* @param groupLoadBatchSize
|
||||||
* the groupLoadBatchSize to set
|
* the groupLoadBatchSize to set
|
||||||
*/
|
*/
|
||||||
public void setGroupLoadBatchSize(final int groupLoadBatchSize)
|
public void setGroupLoadBatchSize(final int groupLoadBatchSize)
|
||||||
{
|
{
|
||||||
@ -122,9 +133,9 @@ public abstract class BaseGroupContainmentFilter implements InitializingBean
|
|||||||
* Checks whether parent groups match the configured restrictions.
|
* Checks whether parent groups match the configured restrictions.
|
||||||
*
|
*
|
||||||
* @param parentGroupIds
|
* @param parentGroupIds
|
||||||
* the list of parent group IDs for an authority
|
* the list of parent group IDs for an authority
|
||||||
* @param parentGroupPaths
|
* @param parentGroupPaths
|
||||||
* the list of parent group paths for an authority
|
* the list of parent group paths for an authority
|
||||||
* @return {@code true} if the parent groups match the configured restrictions, {@code false} otherwise
|
* @return {@code true} if the parent groups match the configured restrictions, {@code false} otherwise
|
||||||
*/
|
*/
|
||||||
protected boolean parentGroupsMatch(final List<String> parentGroupIds, final List<String> parentGroupPaths)
|
protected boolean parentGroupsMatch(final List<String> parentGroupIds, final List<String> parentGroupPaths)
|
||||||
@ -177,9 +188,9 @@ public abstract class BaseGroupContainmentFilter implements InitializingBean
|
|||||||
* Checks whether a specific group path matches any entry in a list of paths using either exact match or prefix matching.
|
* Checks whether a specific group path matches any entry in a list of paths using either exact match or prefix matching.
|
||||||
*
|
*
|
||||||
* @param groupPath
|
* @param groupPath
|
||||||
* the path to check
|
* the path to check
|
||||||
* @param groupPaths
|
* @param groupPaths
|
||||||
* the paths to check against
|
* the paths to check against
|
||||||
* @return {@code true} if the path matches one of the paths in exact match or prefix matching mode
|
* @return {@code true} if the path matches one of the paths in exact match or prefix matching mode
|
||||||
*/
|
*/
|
||||||
protected boolean groupPathOrTransitiveContained(final String groupPath, final Collection<String> groupPaths)
|
protected boolean groupPathOrTransitiveContained(final String groupPath, final Collection<String> groupPaths)
|
||||||
|
@ -17,7 +17,7 @@ package de.acosix.alfresco.keycloak.repo.sync;
|
|||||||
|
|
||||||
import org.alfresco.model.ContentModel;
|
import org.alfresco.model.ContentModel;
|
||||||
import org.alfresco.repo.security.sync.NodeDescription;
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
import org.alfresco.service.cmr.security.AuthorityType;
|
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
|
||||||
import org.alfresco.util.PropertyMap;
|
import org.alfresco.util.PropertyMap;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
|
||||||
@ -51,8 +51,19 @@ public class DefaultGroupProcessor implements GroupProcessor
|
|||||||
{
|
{
|
||||||
final PropertyMap properties = groupNode.getProperties();
|
final PropertyMap properties = groupNode.getProperties();
|
||||||
|
|
||||||
properties.put(ContentModel.PROP_AUTHORITY_NAME, AuthorityType.GROUP.getPrefixString() + group.getId());
|
final String existingName = DefaultTypeConverter.INSTANCE.convert(String.class,
|
||||||
properties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, group.getName());
|
properties.get(ContentModel.PROP_AUTHORITY_NAME));
|
||||||
|
final String existingDisplayName = DefaultTypeConverter.INSTANCE.convert(String.class,
|
||||||
|
properties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME));
|
||||||
|
|
||||||
|
if (existingName == null || existingName.isBlank())
|
||||||
|
{
|
||||||
|
properties.put(ContentModel.PROP_AUTHORITY_NAME, group.getId());
|
||||||
|
}
|
||||||
|
if (existingDisplayName == null || existingDisplayName.isBlank())
|
||||||
|
{
|
||||||
|
properties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, group.getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2025 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Values of this enumeration denote the decision/vote of a filter for synchronisation of a user or group.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public enum FilterResult
|
||||||
|
{
|
||||||
|
|
||||||
|
ALLOW,
|
||||||
|
DENY,
|
||||||
|
ABSTAIN;
|
||||||
|
|
||||||
|
}
|
@ -15,9 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package de.acosix.alfresco.keycloak.repo.sync;
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -29,8 +27,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
*
|
*
|
||||||
* @author Axel Faust
|
* @author Axel Faust
|
||||||
*/
|
*/
|
||||||
public class GroupContainmentGroupFilter extends BaseGroupContainmentFilter
|
public class GroupContainmentGroupFilter extends BaseGroupContainmentFilter implements GroupFilter
|
||||||
implements GroupFilter
|
|
||||||
{
|
{
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(GroupContainmentGroupFilter.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(GroupContainmentGroupFilter.class);
|
||||||
@ -40,38 +37,46 @@ public class GroupContainmentGroupFilter extends BaseGroupContainmentFilter
|
|||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldIncludeGroup(final GroupRepresentation group)
|
public FilterResult shouldIncludeGroup(final GroupRepresentation group)
|
||||||
{
|
{
|
||||||
boolean matches;
|
final FilterResult res;
|
||||||
|
|
||||||
if ((this.groupPaths != null && !this.groupPaths.isEmpty()) || (this.groupIds != null && !this.groupIds.isEmpty()))
|
if ((this.groupPaths != null && !this.groupPaths.isEmpty()) || (this.groupIds != null && !this.groupIds.isEmpty()))
|
||||||
{
|
{
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Checking group {} ({}) for containment in groups with paths {} / IDs {}, using allowTransitive={} and requireAll={}",
|
"Checking group {} ({}) for containment in groups with paths {} / IDs {}, using allowTransitive={}, requireAll={}, matchDenies={}",
|
||||||
group.getId(), group.getPath(), this.groupPaths, this.groupIds, this.allowTransitive, this.requireAll);
|
group.getId(), group.getPath(), this.groupPaths, this.groupIds, this.allowTransitive, this.requireAll,
|
||||||
|
this.matchDenies);
|
||||||
|
|
||||||
// no need to retrieve parent group ID as path should be sufficient
|
// no need to retrieve parent group ID as path should be sufficient
|
||||||
// Keycloak groups can only ever have one parent
|
// Keycloak groups can only ever have one parent
|
||||||
|
|
||||||
final List<String> parentGroupIds = Collections.emptyList();
|
|
||||||
final List<String> parentGroupPaths = new ArrayList<>();
|
|
||||||
|
|
||||||
final String groupPath = group.getPath();
|
final String groupPath = group.getPath();
|
||||||
final String parentPath = groupPath.substring(0, groupPath.lastIndexOf('/'));
|
final String parentPath = groupPath.substring(0, groupPath.lastIndexOf('/'));
|
||||||
if (!parentPath.isEmpty())
|
if (!parentPath.isEmpty())
|
||||||
{
|
{
|
||||||
parentGroupPaths.add(parentPath);
|
final boolean parentGroupsMatch = this.parentGroupsMatch(Collections.emptyList(), Collections.singletonList(parentPath));
|
||||||
|
if (this.matchDenies)
|
||||||
|
{
|
||||||
|
res = parentGroupsMatch ? FilterResult.DENY : FilterResult.ABSTAIN;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
res = parentGroupsMatch ? FilterResult.ALLOW : FilterResult.ABSTAIN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// no parents to check
|
||||||
|
res = FilterResult.ABSTAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
matches = this.parentGroupsMatch(parentGroupIds, parentGroupPaths);
|
LOGGER.debug("Group containment result for group {}: {}", group.getId(), res);
|
||||||
|
|
||||||
LOGGER.debug("Group containment result for group {}: {}", group.getId(), matches);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
matches = true;
|
res = FilterResult.ABSTAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,7 @@ import org.springframework.beans.factory.InitializingBean;
|
|||||||
*
|
*
|
||||||
* @author Axel Faust
|
* @author Axel Faust
|
||||||
*/
|
*/
|
||||||
public class GroupContainmentUserFilter extends BaseGroupContainmentFilter
|
public class GroupContainmentUserFilter extends BaseGroupContainmentFilter implements UserFilter, InitializingBean
|
||||||
implements UserFilter, InitializingBean
|
|
||||||
{
|
{
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(GroupContainmentUserFilter.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(GroupContainmentUserFilter.class);
|
||||||
@ -40,14 +39,15 @@ public class GroupContainmentUserFilter extends BaseGroupContainmentFilter
|
|||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldIncludeUser(final UserRepresentation user)
|
public FilterResult shouldIncludeUser(final UserRepresentation user)
|
||||||
{
|
{
|
||||||
boolean matches;
|
final FilterResult res;
|
||||||
|
|
||||||
if ((this.groupPaths != null && !this.groupPaths.isEmpty()) || (this.groupIds != null && !this.groupIds.isEmpty()))
|
if ((this.groupPaths != null && !this.groupPaths.isEmpty()) || (this.groupIds != null && !this.groupIds.isEmpty()))
|
||||||
{
|
{
|
||||||
LOGGER.debug("Checking user {} for containment in groups with paths {} / IDs {}, using allowTransitive={} and requireAll={}",
|
LOGGER.debug(
|
||||||
user.getUsername(), this.groupPaths, this.groupIds, this.allowTransitive, this.requireAll);
|
"Checking user {} for containment in groups with paths {} / IDs {}, using allowTransitive={}, requireAll={}, matchDenies={}",
|
||||||
|
user.getUsername(), this.groupPaths, this.groupIds, this.allowTransitive, this.requireAll, this.matchDenies);
|
||||||
|
|
||||||
final List<String> parentGroupIds = new ArrayList<>();
|
final List<String> parentGroupIds = new ArrayList<>();
|
||||||
final List<String> parentGroupPaths = new ArrayList<>();
|
final List<String> parentGroupPaths = new ArrayList<>();
|
||||||
@ -63,15 +63,31 @@ public class GroupContainmentUserFilter extends BaseGroupContainmentFilter
|
|||||||
offset += processedGroups;
|
offset += processedGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
matches = this.parentGroupsMatch(parentGroupIds, parentGroupPaths);
|
if (parentGroupIds.isEmpty() || parentGroupPaths.isEmpty())
|
||||||
|
{
|
||||||
|
final boolean parentGroupsMatch = this.parentGroupsMatch(parentGroupIds, parentGroupPaths);
|
||||||
|
if (this.matchDenies)
|
||||||
|
{
|
||||||
|
res = parentGroupsMatch ? FilterResult.DENY : FilterResult.ABSTAIN;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
res = parentGroupsMatch ? FilterResult.ALLOW : FilterResult.ABSTAIN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// no parents to check
|
||||||
|
res = FilterResult.ABSTAIN;
|
||||||
|
}
|
||||||
|
|
||||||
LOGGER.debug("Group containment result for user {}: {}", user.getUsername(), matches);
|
LOGGER.debug("Group containment result for user {}: {}", user.getUsername(), res);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
matches = true;
|
res = FilterResult.ABSTAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,8 @@ public interface GroupFilter
|
|||||||
* Determines whether this group should be included in the synchronisation.
|
* Determines whether this group should be included in the synchronisation.
|
||||||
*
|
*
|
||||||
* @param group
|
* @param group
|
||||||
* the group to consider
|
* the group to consider
|
||||||
* @return {@code true} if the group should be synchronised, {@code false} if not
|
* @return the filter result
|
||||||
*/
|
*/
|
||||||
boolean shouldIncludeGroup(GroupRepresentation group);
|
FilterResult shouldIncludeGroup(GroupRepresentation group);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2025 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides filter capabilities for groups to be synchronised based on their parent group and whether they are contained in
|
||||||
|
* specific groups.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class GroupPathFilter implements GroupFilter
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(GroupPathFilter.class);
|
||||||
|
|
||||||
|
protected List<String> groupPaths;
|
||||||
|
|
||||||
|
protected boolean matchDenies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param groupPaths
|
||||||
|
* the groupPaths to set as a comma-separated string of paths
|
||||||
|
*/
|
||||||
|
public void setGroupPaths(final String groupPaths)
|
||||||
|
{
|
||||||
|
this.groupPaths = groupPaths != null && !groupPaths.isEmpty() ? Arrays.asList(groupPaths.split(",")) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param matchDenies
|
||||||
|
* the matchDenies to set
|
||||||
|
*/
|
||||||
|
public void setMatchDenies(final boolean matchDenies)
|
||||||
|
{
|
||||||
|
this.matchDenies = matchDenies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public FilterResult shouldIncludeGroup(final GroupRepresentation group)
|
||||||
|
{
|
||||||
|
final FilterResult res;
|
||||||
|
|
||||||
|
if ((this.groupPaths != null && !this.groupPaths.isEmpty()))
|
||||||
|
{
|
||||||
|
LOGGER.debug("Checking group {} ({}) against paths {}, using matchDenies={}", group.getId(), group.getPath(), this.groupPaths,
|
||||||
|
this.matchDenies);
|
||||||
|
|
||||||
|
final String groupPath = group.getPath();
|
||||||
|
final boolean containsGroup = this.groupPaths.contains(groupPath);
|
||||||
|
if (this.matchDenies)
|
||||||
|
{
|
||||||
|
res = containsGroup ? FilterResult.DENY : FilterResult.ABSTAIN;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
res = containsGroup ? FilterResult.ALLOW : FilterResult.ABSTAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.debug("Group path check result for group {}: {}", group.getId(), res);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
res = FilterResult.ABSTAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
@ -30,10 +30,12 @@ import java.util.function.Consumer;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.IntConsumer;
|
import java.util.function.IntConsumer;
|
||||||
|
|
||||||
|
import org.alfresco.error.AlfrescoRuntimeException;
|
||||||
import org.alfresco.model.ContentModel;
|
import org.alfresco.model.ContentModel;
|
||||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||||
import org.alfresco.repo.security.sync.NodeDescription;
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
import org.alfresco.repo.security.sync.UserRegistry;
|
import org.alfresco.repo.security.sync.UserRegistry;
|
||||||
|
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
|
||||||
import org.alfresco.service.cmr.security.AuthorityType;
|
import org.alfresco.service.cmr.security.AuthorityType;
|
||||||
import org.alfresco.service.namespace.QName;
|
import org.alfresco.service.namespace.QName;
|
||||||
import org.alfresco.util.PropertyCheck;
|
import org.alfresco.util.PropertyCheck;
|
||||||
@ -263,22 +265,28 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
|
|||||||
*/
|
*/
|
||||||
protected NodeDescription mapGroup(final GroupRepresentation group)
|
protected NodeDescription mapGroup(final GroupRepresentation group)
|
||||||
{
|
{
|
||||||
// need to use group ID as unique name as Keycloak group name itself is non-unique
|
|
||||||
|
|
||||||
final NodeDescription groupD = new NodeDescription(group.getId());
|
final NodeDescription groupD = new NodeDescription(group.getId());
|
||||||
final PropertyMap groupProperties = groupD.getProperties();
|
final PropertyMap groupProperties = groupD.getProperties();
|
||||||
|
|
||||||
final String groupName = AuthorityType.GROUP.getPrefixString() + group.getId();
|
LOGGER.debug("Mapping group {} ({})", group.getName(), group.getId());
|
||||||
LOGGER.debug("Mapping group {}", groupName);
|
|
||||||
|
|
||||||
this.groupProcessors.forEach(processor -> processor.mapGroup(group, groupD));
|
this.groupProcessors.forEach(processor -> processor.mapGroup(group, groupD));
|
||||||
|
|
||||||
// always wins against user-defined mappings for cm:authorityName
|
// make sure groupName is mapped + prefixed
|
||||||
|
String groupName = DefaultTypeConverter.INSTANCE.convert(String.class, groupProperties.get(ContentModel.PROP_AUTHORITY_NAME));
|
||||||
|
if (groupName == null || groupName.isBlank())
|
||||||
|
{
|
||||||
|
// should never happen due to DefaultGroupProcessor
|
||||||
|
groupName = group.getId();
|
||||||
|
}
|
||||||
|
if (AuthorityType.getAuthorityType(groupName) != AuthorityType.GROUP)
|
||||||
|
{
|
||||||
|
groupName = AuthorityType.GROUP.getPrefixString() + group.getId();
|
||||||
|
}
|
||||||
groupProperties.put(ContentModel.PROP_AUTHORITY_NAME, groupName);
|
groupProperties.put(ContentModel.PROP_AUTHORITY_NAME, groupName);
|
||||||
|
|
||||||
final Set<String> childAssociations = groupD.getChildAssociations();
|
final Set<String> childAssociations = groupD.getChildAssociations();
|
||||||
group.getSubGroups().stream()
|
group.getSubGroups().stream().filter(subGroup -> isGroupAllowed(this.groupFilters, subGroup))
|
||||||
.filter(subGroup -> !this.groupFilters.stream().anyMatch(filter -> !filter.shouldIncludeGroup(subGroup)))
|
|
||||||
.forEach(subGroup -> childAssociations.add(AuthorityType.GROUP.getPrefixString() + subGroup.getId()));
|
.forEach(subGroup -> childAssociations.add(AuthorityType.GROUP.getPrefixString() + subGroup.getId()));
|
||||||
|
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
@ -286,8 +294,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
|
|||||||
while (processedMembers > 0)
|
while (processedMembers > 0)
|
||||||
{
|
{
|
||||||
processedMembers = this.identitiesClient.processMembers(group.getId(), offset, this.personLoadBatchSize, user -> {
|
processedMembers = this.identitiesClient.processMembers(group.getId(), offset, this.personLoadBatchSize, user -> {
|
||||||
final boolean skipSync = this.userFilters.stream().anyMatch(filter -> !filter.shouldIncludeUser(user));
|
if (KeycloakUserRegistry.isUserAllowed(this.userFilters, user))
|
||||||
if (!skipSync)
|
|
||||||
{
|
{
|
||||||
childAssociations.add(user.getUsername());
|
childAssociations.add(user.getUsername());
|
||||||
}
|
}
|
||||||
@ -483,8 +490,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
|
|||||||
// TODO Evaluate other iteration approaches, e.g. crawling from a configured root group
|
// TODO Evaluate other iteration approaches, e.g. crawling from a configured root group
|
||||||
// How to count totals in advance though?
|
// How to count totals in advance though?
|
||||||
return KeycloakUserRegistry.this.identitiesClient.processUsers(offset, batchSize, user -> {
|
return KeycloakUserRegistry.this.identitiesClient.processUsers(offset, batchSize, user -> {
|
||||||
final boolean skipSync = KeycloakUserRegistry.this.userFilters.stream().anyMatch(filter -> !filter.shouldIncludeUser(user));
|
if (KeycloakUserRegistry.isUserAllowed(KeycloakUserRegistry.this.userFilters, user))
|
||||||
if (!skipSync)
|
|
||||||
{
|
{
|
||||||
authorityProcessor.accept(user);
|
authorityProcessor.accept(user);
|
||||||
}
|
}
|
||||||
@ -530,16 +536,17 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
|
|||||||
{
|
{
|
||||||
// TODO Evaluate other iteration approaches, e.g. crawling from a configured root group
|
// TODO Evaluate other iteration approaches, e.g. crawling from a configured root group
|
||||||
// How to count totals in advance though?
|
// How to count totals in advance though?
|
||||||
return KeycloakUserRegistry.this.identitiesClient.processGroups(offset, batchSize, group -> {
|
final AtomicInteger count = new AtomicInteger();
|
||||||
this.processGroupsRecursively(group, filteredHandler, authorityProcessor);
|
final int loadedDirect = KeycloakUserRegistry.this.identitiesClient.processGroups(offset, batchSize, group -> {
|
||||||
|
this.processGroupsRecursively(group, filteredHandler, authorityProcessor, count);
|
||||||
});
|
});
|
||||||
|
return count.addAndGet(loadedDirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void processGroupsRecursively(final GroupRepresentation group, final IntConsumer filteredHandler,
|
protected void processGroupsRecursively(final GroupRepresentation group, final IntConsumer filteredHandler,
|
||||||
final Consumer<GroupRepresentation> authorityProcessor)
|
final Consumer<GroupRepresentation> authorityProcessor, final AtomicInteger count)
|
||||||
{
|
{
|
||||||
final boolean skipSync = KeycloakUserRegistry.this.groupFilters.stream().anyMatch(filter -> !filter.shouldIncludeGroup(group));
|
if (KeycloakUserRegistry.isGroupAllowed(KeycloakUserRegistry.this.groupFilters, group))
|
||||||
if (!skipSync)
|
|
||||||
{
|
{
|
||||||
authorityProcessor.accept(group);
|
authorityProcessor.accept(group);
|
||||||
}
|
}
|
||||||
@ -548,9 +555,58 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
|
|||||||
filteredHandler.accept(1);
|
filteredHandler.accept(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// any filtering applied above does not apply here as any sub-group will be individually checked for filtering by recursive
|
final List<GroupRepresentation> subGroups = group.getSubGroups();
|
||||||
// processing
|
if (subGroups == null || subGroups.isEmpty())
|
||||||
group.getSubGroups().forEach(subGroup -> this.processGroupsRecursively(subGroup, filteredHandler, authorityProcessor));
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final int loadedChilren = KeycloakUserRegistry.this.identitiesClient.processSubGroups(group.getId(), subGroup -> {
|
||||||
|
this.processGroupsRecursively(subGroup, filteredHandler, authorityProcessor, count);
|
||||||
|
});
|
||||||
|
count.addAndGet(loadedChilren);
|
||||||
|
}
|
||||||
|
catch (final AlfrescoRuntimeException ex)
|
||||||
|
{
|
||||||
|
LOGGER.warn("Failed to load sub groups for {} ({})", group.getName(), group.getId(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
subGroups.stream().forEach(subGroup -> this.processGroupsRecursively(subGroup, filteredHandler, authorityProcessor, count));
|
||||||
|
count.addAndGet(subGroups.size());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isUserAllowed(final Collection<UserFilter> filters, final UserRepresentation user)
|
||||||
|
{
|
||||||
|
final FilterResult res = filters.stream().map(f -> f.shouldIncludeUser(user)).reduce(KeycloakUserRegistry::combine)
|
||||||
|
.orElse(FilterResult.ABSTAIN);
|
||||||
|
return res == FilterResult.ALLOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isGroupAllowed(final Collection<GroupFilter> filters, final GroupRepresentation group)
|
||||||
|
{
|
||||||
|
final FilterResult res = filters.stream().map(f -> f.shouldIncludeGroup(group)).reduce(KeycloakUserRegistry::combine)
|
||||||
|
.orElse(FilterResult.ABSTAIN);
|
||||||
|
return res == FilterResult.ALLOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FilterResult combine(final FilterResult a, final FilterResult b)
|
||||||
|
{
|
||||||
|
FilterResult res;
|
||||||
|
if (a == FilterResult.DENY || b == FilterResult.DENY)
|
||||||
|
{
|
||||||
|
res = FilterResult.DENY;
|
||||||
|
}
|
||||||
|
else if (a == FilterResult.ABSTAIN)
|
||||||
|
{
|
||||||
|
res = b;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
res = a;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,8 @@ public interface UserFilter
|
|||||||
* Determines whether this user should be included in the synchronisation.
|
* Determines whether this user should be included in the synchronisation.
|
||||||
*
|
*
|
||||||
* @param user
|
* @param user
|
||||||
* the user to consider
|
* the user to consider
|
||||||
* @return {@code true} if the user should be synchronised, {@code false} if not
|
* @return the filter result
|
||||||
*/
|
*/
|
||||||
boolean shouldIncludeUser(UserRepresentation user);
|
FilterResult shouldIncludeUser(UserRepresentation user);
|
||||||
}
|
}
|
||||||
|
@ -194,6 +194,27 @@
|
|||||||
<addHeader>false</addHeader>
|
<addHeader>false</addHeader>
|
||||||
</transformer>
|
</transformer>
|
||||||
</transformers>
|
</transformers>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>org.keycloak:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
<filter>
|
||||||
|
<artifact>org.jboss.logging:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
<filter>
|
||||||
|
<artifact>com.fasterxml.jackson.core:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||||
|
<exclude>META-INF/versions/9/module-info.class</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user