Improve case specific scope use + test realm

This commit is contained in:
AFaust 2021-10-19 11:28:43 +02:00
parent cff32d017b
commit 4a2f4a5f67
17 changed files with 2077 additions and 1024 deletions

@ -157,7 +157,7 @@
<property name="keycloakTicketTokenCache" ref="${moduleId}-ticketTokenCache" />
</bean>
<bean id="idmClient" class="${project.artifactId}.client.IDMClientImpl">
<bean id="identitiesClient" class="${project.artifactId}.client.IdentitiesClientImpl">
<property name="deployment" ref="keycloakDeployment" />
<property name="accessTokenService" ref="accessTokenService.impl" />
<property name="userName" value="${keycloak.synchronization.user}" />
@ -165,9 +165,17 @@
<property name="requiredClientScopes" value="${keycloak.synchronization.requiredClientScopes}" />
</bean>
<bean id="rolesClient" class="${project.artifactId}.client.RolesClientImpl">
<property name="deployment" ref="keycloakDeployment" />
<property name="accessTokenService" ref="accessTokenService.impl" />
<property name="userName" value="${keycloak.roles.user}" />
<property name="password" value="${keycloak.roles.password}" />
<property name="requiredClientScopes" value="${keycloak.roles.requiredClientScopes}" />
</bean>
<bean id="userRegistry" class="${project.artifactId}.sync.KeycloakUserRegistry">
<property name="active" value="${keycloak.synchronization.enabled}" />
<property name="idmClient" ref="idmClient" />
<property name="identitiesClient" ref="identitiesClient" />
<property name="personLoadBatchSize" value="${keycloak.synchronization.personLoadBatchSize}" />
<property name="groupLoadBatchSize" value="${keycloak.synchronization.groupLoadBatchSize}" />
</bean>
@ -178,7 +186,7 @@
<bean id="roleService.impl" class="${project.artifactId}.roles.RoleServiceImpl">
<property name="adapterConfig" ref="keycloakAdapterConfig" />
<property name="idmClient" ref="idmClient" />
<property name="rolesClient" ref="rolesClient" />
<property name="enabled" value="${keycloak.roles.mapRoles}" />
<property name="processRealmRoles" value="${keycloak.roles.mapRealmRoles}" />
<property name="processResourceRoles" value="${keycloak.roles.mapResourceRoles}" />
@ -194,11 +202,11 @@
<bean id="userToken.default" class="${project.artifactId}.authentication.DefaultPersonProcessor" />
<bean id="userFilter.containedInGroup" class="${project.artifactId}.sync.GroupContainmentUserFilter">
<property name="idmClient" ref="idmClient" />
<property name="identitiesClient" ref="identitiesClient" />
</bean>
<bean id="groupFilter.containedInGroup" class="${project.artifactId}.sync.GroupContainmentGroupFilter">
<property name="idmClient" ref="idmClient" />
<property name="identitiesClient" ref="identitiesClient" />
</bean>
<bean id="authorityMapper.simpleAttributes" abstract="true">

@ -49,6 +49,9 @@ keycloak.authentication.userToken.default.property.mapEmail=true
keycloak.authentication.userToken.default.property.mapPhoneNumber=true
keycloak.authentication.userToken.default.property.mapPhoneNumberAsMobile=false
keycloak.roles.user=
keycloak.roles.password=
keycloak.roles.requiredClientScopes=
keycloak.roles.mapRoles=true
keycloak.roles.mapRealmRoles=true
keycloak.roles.mapResourceRoles=true

@ -0,0 +1,340 @@
/*
* Copyright 2019 - 2021 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.client;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MappingIterator;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.function.Consumer;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.util.PropertyCheck;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.util.JsonSerialization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import de.acosix.alfresco.keycloak.repo.token.AccessTokenHolder;
import de.acosix.alfresco.keycloak.repo.token.AccessTokenService;
/**
* Implements the abstract base for a client to the Keycloak admin ReST API specific to IDM structures.
*
* @author Axel Faust
*/
public abstract class AbstractIDMClientImpl implements InitializingBean
{
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractIDMClientImpl.class);
protected KeycloakDeployment deployment;
protected AccessTokenService accessTokenService;
protected String userName;
protected String password;
protected final Collection<String> requiredClientScopes = new HashSet<>();
protected AccessTokenHolder accessToken;
/**
* {@inheritDoc}
*/
@Override
public void afterPropertiesSet()
{
PropertyCheck.mandatory(this, "keycloakDeployment", this.deployment);
PropertyCheck.mandatory(this, "accessTokenService", this.accessTokenService);
}
/**
* @param deployment
* the deployment to set
*/
public void setDeployment(final KeycloakDeployment deployment)
{
this.deployment = deployment;
}
/**
* @param accessTokenService
* the accessTokenService to set
*/
public void setAccessTokenService(final AccessTokenService accessTokenService)
{
this.accessTokenService = accessTokenService;
}
/**
* @param userName
* the userName to set
*/
public void setUserName(final String userName)
{
this.userName = userName;
}
/**
* @param password
* the password to set
*/
public void setPassword(final String password)
{
this.password = password;
}
/**
* @param requiredClientScopes
* the requiredClientScopes to set
*/
public void setRequiredClientScopes(final String requiredClientScopes)
{
this.requiredClientScopes.clear();
if (requiredClientScopes != null && !requiredClientScopes.isEmpty())
{
this.requiredClientScopes.addAll(Arrays.asList(requiredClientScopes.trim().split(" ")));
}
}
/**
* Loads and processes a batch of generic entities from Keycloak.
*
* @param <T>
* the type of the response entities
* @param uri
* the URI to call
* @param entityProcessor
* the processor handling the loaded entities
* @param entityClass
* the type of the expected response entities
* @return the number of processed entities
*/
protected <T> int processEntityBatch(final URI uri, final Consumer<T> entityProcessor, final Class<T> entityClass)
{
final HttpGet get = new HttpGet(uri);
get.addHeader("Accept", MimetypeMap.MIMETYPE_JSON);
get.addHeader("Authorization", "Bearer " + this.getValidAccessTokenForRequest());
try
{
final HttpClient client = this.deployment.getClient();
final HttpResponse response = client.execute(get);
final int status = response.getStatusLine().getStatusCode();
final HttpEntity httpEntity = response.getEntity();
if (status != 200)
{
EntityUtils.consumeQuietly(httpEntity);
throw new IOException("Bad status: " + status);
}
if (httpEntity == null)
{
throw new IOException("Response does not contain a body");
}
final InputStream is = httpEntity.getContent();
try
{
final MappingIterator<T> iterator = JsonSerialization.mapper.readerFor(entityClass).readValues(is);
int entitiesProcessed = 0;
while (iterator.hasNextValue())
{
final T loadedEntity = iterator.nextValue();
entityProcessor.accept(loadedEntity);
entitiesProcessed++;
}
return entitiesProcessed;
}
finally
{
try
{
is.close();
}
catch (final IOException e)
{
LOGGER.trace("Error closing entity stream", e);
}
}
}
catch (final IOException ioex)
{
LOGGER.error("Failed to retrieve entities", ioex);
throw new AlfrescoRuntimeException("Failed to retrieve entities", ioex);
}
}
/**
* Executes a generic HTTP GET operation yielding a JSON response.
*
* @param uri
* the URI to call
* @param responseProcessor
* the processor handling the response JSON
*/
protected void processGenericGet(final URI uri, final Consumer<JsonNode> responseProcessor)
{
final HttpGet get = new HttpGet(uri);
get.addHeader("Accept", MimetypeMap.MIMETYPE_JSON);
get.addHeader("Authorization", "Bearer " + this.getValidAccessTokenForRequest());
try
{
final HttpClient client = this.deployment.getClient();
final HttpResponse response = client.execute(get);
final int status = response.getStatusLine().getStatusCode();
final HttpEntity httpEntity = response.getEntity();
if (status != 200)
{
EntityUtils.consumeQuietly(httpEntity);
throw new IOException("Bad status: " + status);
}
if (httpEntity == null)
{
throw new IOException("Response does not contain a body");
}
final InputStream is = httpEntity.getContent();
try
{
final JsonNode root = JsonSerialization.mapper.readTree(is);
responseProcessor.accept(root);
}
finally
{
try
{
is.close();
}
catch (final IOException e)
{
LOGGER.trace("Error closing entity stream", e);
}
}
}
catch (final IOException ioex)
{
LOGGER.error("Failed to retrieve entities", ioex);
throw new AlfrescoRuntimeException("Failed to retrieve entities", ioex);
}
}
/**
* Executes a generic HTTP GET operation yielding a mapped response entity.
*
* @param <T>
* the type of the response entity
* @param uri
* the URI to call
* @param responseType
* the class object for the type of the response entity
* @return the response entity
*
*/
protected <T> T processGenericGet(final URI uri, final Class<T> responseType)
{
final HttpGet get = new HttpGet(uri);
get.addHeader("Accept", MimetypeMap.MIMETYPE_JSON);
get.addHeader("Authorization", "Bearer " + this.getValidAccessTokenForRequest());
try
{
final HttpClient client = this.deployment.getClient();
final HttpResponse response = client.execute(get);
final int status = response.getStatusLine().getStatusCode();
final HttpEntity httpEntity = response.getEntity();
if (status != 200)
{
EntityUtils.consumeQuietly(httpEntity);
throw new IOException("Bad status: " + status);
}
if (httpEntity == null)
{
throw new IOException("Response does not contain a body");
}
final InputStream is = httpEntity.getContent();
try
{
final T responseEntity = JsonSerialization.mapper.readValue(is, responseType);
return responseEntity;
}
finally
{
try
{
is.close();
}
catch (final IOException e)
{
LOGGER.trace("Error closing entity stream", e);
}
}
}
catch (final IOException ioex)
{
LOGGER.error("Failed to retrieve entities", ioex);
throw new AlfrescoRuntimeException("Failed to retrieve entities", ioex);
}
}
/**
* Retrieves / determines a valid access token for a request to the admin ReST API.
*
* @return the valid access token to use in a request immediately following this operation
*/
protected String getValidAccessTokenForRequest()
{
if (this.accessToken == null)
{
synchronized (this)
{
if (this.accessToken == null)
{
if (this.userName != null && !this.userName.isEmpty())
{
this.accessToken = this.accessTokenService.obtainAccessToken(this.userName, this.password,
this.requiredClientScopes);
}
else
{
this.accessToken = this.accessTokenService.obtainAccessToken(this.requiredClientScopes);
}
}
}
}
return this.accessToken.getAccessToken();
}
}

@ -1,181 +0,0 @@
/*
* Copyright 2019 - 2021 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.client;
import java.util.function.Consumer;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
/**
* Instances of this interface wrap the relevant Keycloak admin ReST API for the synchronisation of users, groups and roles from a Keycloak
* realm.
*
* @author Axel Faust
*/
public interface IDMClient
{
/**
* Retrieves the number of users within the Keycloak IDM database.
*
* @return the count of users in the Keycloak database
*/
int countUsers();
/**
* Retrieves the number of groups within the Keycloak IDM database.
*
* @return the count of groups in the Keycloak database
*/
int countGroups();
/**
* Retrieves the details of one specific group from Keycloak.
*
* @param groupId
* the ID of the group in Keycloak
* @return the group details
*/
GroupRepresentation getGroup(String groupId);
/**
* Loads and processes the registered clients from Keycloak using an externally specified processor.
*
* @param clientProcessor
* the processor handling the loaded clients
* @return the number of processed clients
*/
int processClients(Consumer<ClientRepresentation> clientProcessor);
/**
* Loads and processes a batch of users from Keycloak using an externally specified processor.
*
* @param offset
* the index of the first user to retrieve
* @param userBatchSize
* the number of users to load in one batch
* @param userProcessor
* the processor handling the loaded users
* @return the number of processed users
*/
int processUsers(int offset, int userBatchSize, Consumer<UserRepresentation> userProcessor);
/**
* Loads and processes a batch of groups of a specific user from Keycloak using an externally specified processor.
*
* @param userId
* the ID of user for which to process groups
* @param offset
* the index of the first group to retrieve
* @param groupBatchSize
* the number of groups to load in one batch
* @param groupProcessor
* the processor handling the loaded groups
* @return the number of processed groups
*/
int processUserGroups(String userId, int offset, int groupBatchSize, Consumer<GroupRepresentation> groupProcessor);
/**
* Loads and processes a batch of groups from Keycloak using an externally specified processor.
*
* @param offset
* the index of the first group to retrieve
* @param groupBatchSize
* the number of groups to load in one batch
* @param groupProcessor
* the processor handling the loaded groups
* @return the number of processed groups
*/
int processGroups(int offset, int groupBatchSize, Consumer<GroupRepresentation> groupProcessor);
/**
* Loads and processes a batch of users / members of a group from Keycloak using an externally specified processor.
*
* @param groupId
* the ID of group for which to process members
* @param offset
* the index of the first user to retrieve
* @param userBatchSize
* the number of users to load in one batch
* @param userProcessor
* the processor handling the loaded users
* @return the number of processed users
*/
int processMembers(String groupId, int offset, int userBatchSize, Consumer<UserRepresentation> userProcessor);
/**
* Loads and processes a batch of realm roles from Keycloak using an externally specified processor.
*
* @param offset
* the index of the first role to retrieve
* @param roleBatchSize
* the number of roles to load in one batch
* @param roleProcessor
* the processor handling the loaded roles
* @return the number of processed roles
*/
int processRealmRoles(int offset, int roleBatchSize, Consumer<RoleRepresentation> roleProcessor);
/**
* Loads and processes a batch of realm roles from Keycloak using an externally specified processor.
*
* @param search
* a search term to filter roles
* @param offset
* the index of the first role to retrieve
* @param roleBatchSize
* the number of roles to load in one batch
* @param roleProcessor
* the processor handling the loaded roles
* @return the number of processed roles
*/
int processRealmRoles(String search, int offset, int roleBatchSize, Consumer<RoleRepresentation> roleProcessor);
/**
* Loads and processes a batch of client roles from Keycloak using an externally specified processor.
*
* @param clientId
* the {@link ClientRepresentation#getId() (technical) ID} of a client from which to process defined roles
* @param offset
* the index of the first role to retrieve
* @param roleBatchSize
* the number of roles to load in one batch
* @param roleProcessor
* the processor handling the loaded roles
* @return the number of processed roles
*/
int processClientRoles(String clientId, int offset, int roleBatchSize, Consumer<RoleRepresentation> roleProcessor);
/**
* Loads and processes a batch of client roles from Keycloak using an externally specified processor.
*
* @param clientId
* the {@link ClientRepresentation#getId() (technical) ID} of a client from which to process defined roles
* @param search
* a search term to filter roles
* @param offset
* the index of the first role to retrieve
* @param roleBatchSize
* the number of roles to load in one batch
* @param roleProcessor
* the processor handling the loaded roles
* @return the number of processed roles
*/
int processClientRoles(String clientId, String search, int offset, int roleBatchSize, Consumer<RoleRepresentation> roleProcessor);
}

@ -1,644 +0,0 @@
/*
* Copyright 2019 - 2021 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.client;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MappingIterator;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.util.JsonSerialization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import de.acosix.alfresco.keycloak.repo.token.AccessTokenHolder;
import de.acosix.alfresco.keycloak.repo.token.AccessTokenService;
/**
* Implements the API for a client to the Keycloak admin ReST API specific to IDM structures.
*
* @author Axel Faust
*/
public class IDMClientImpl implements InitializingBean, IDMClient
{
private static final Logger LOGGER = LoggerFactory.getLogger(IDMClientImpl.class);
protected KeycloakDeployment deployment;
protected AccessTokenService accessTokenService;
protected String userName;
protected String password;
protected final Collection<String> requiredClientScopes = new HashSet<>();
protected AccessTokenHolder accessToken;
/**
* {@inheritDoc}
*/
@Override
public void afterPropertiesSet()
{
PropertyCheck.mandatory(this, "keycloakDeployment", this.deployment);
PropertyCheck.mandatory(this, "accessTokenService", this.accessTokenService);
}
/**
* @param deployment
* the deployment to set
*/
public void setDeployment(final KeycloakDeployment deployment)
{
this.deployment = deployment;
}
/**
* @param accessTokenService
* the accessTokenService to set
*/
public void setAccessTokenService(final AccessTokenService accessTokenService)
{
this.accessTokenService = accessTokenService;
}
/**
* @param userName
* the userName to set
*/
public void setUserName(final String userName)
{
this.userName = userName;
}
/**
* @param password
* the password to set
*/
public void setPassword(final String password)
{
this.password = password;
}
/**
* @param requiredClientScopes
* the requiredClientScopes to set
*/
public void setRequiredClientScopes(final String requiredClientScopes)
{
this.requiredClientScopes.clear();
if (requiredClientScopes != null && !requiredClientScopes.isEmpty())
{
this.requiredClientScopes.addAll(Arrays.asList(requiredClientScopes.trim().split(" ")));
}
}
/**
*
* {@inheritDoc}
*/
@Override
public int countUsers()
{
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/users/count")
.build(this.deployment.getRealm());
final AtomicInteger count = new AtomicInteger(0);
this.processGenericGet(uri, root -> {
if (root.isInt())
{
count.set(root.intValue());
}
else
{
throw new AlfrescoRuntimeException("Keycloak admin API did not yield expected data for user count");
}
});
return count.get();
}
/**
*
* {@inheritDoc}
*/
@Override
public int countGroups()
{
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups/count")
.build(this.deployment.getRealm());
final AtomicInteger count = new AtomicInteger(0);
this.processGenericGet(uri, root -> {
if (root.isObject() && root.has("count"))
{
count.set(root.get("count").intValue());
}
else
{
throw new AlfrescoRuntimeException("Keycloak admin API did not yield expected JSON data for group count");
}
});
return count.get();
}
/**
*
* {@inheritDoc}
*/
@Override
public GroupRepresentation getGroup(final String groupId)
{
ParameterCheck.mandatoryString("groupId", groupId);
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups/{groupId}")
.build(this.deployment.getRealm(), groupId);
final GroupRepresentation group = this.processGenericGet(uri, GroupRepresentation.class);
return group;
}
/**
*
* {@inheritDoc}
*/
@Override
public int processClients(final Consumer<ClientRepresentation> clientProcessor)
{
ParameterCheck.mandatory("clientProcessor", clientProcessor);
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/clients")
.build(this.deployment.getRealm());
final int processedClients = this.processEntityBatch(uri, clientProcessor, ClientRepresentation.class);
return processedClients;
}
/**
*
* {@inheritDoc}
*/
@Override
public int processUsers(final int offset, final int userBatchSize, final Consumer<UserRepresentation> userProcessor)
{
ParameterCheck.mandatory("userProcessor", userProcessor);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/users")
.queryParam("first", offset).queryParam("max", userBatchSize).build(this.deployment.getRealm());
final int processedUsers = this.processEntityBatch(uri, userProcessor, UserRepresentation.class);
return processedUsers;
}
/**
*
* {@inheritDoc}
*/
@Override
public int processUserGroups(final String userId, final int offset, final int groupBatchSize,
final Consumer<GroupRepresentation> groupProcessor)
{
ParameterCheck.mandatoryString("userId", userId);
ParameterCheck.mandatory("groupProcessor", groupProcessor);
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/users/{user}/groups")
.queryParam("first", offset).queryParam("max", groupBatchSize).build(this.deployment.getRealm(), userId);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (groupBatchSize <= 0)
{
throw new IllegalArgumentException("groupBatchSize must be a positive integer");
}
final int processedGroups = this.processEntityBatch(uri, groupProcessor, GroupRepresentation.class);
return processedGroups;
}
/**
*
* {@inheritDoc}
*/
@Override
public int processGroups(final int offset, final int groupBatchSize, final Consumer<GroupRepresentation> groupProcessor)
{
ParameterCheck.mandatory("groupProcessor", groupProcessor);
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups")
.queryParam("first", offset).queryParam("max", groupBatchSize).build(this.deployment.getRealm());
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (groupBatchSize <= 0)
{
throw new IllegalArgumentException("groupBatchSize must be a positive integer");
}
final int processedGroups = this.processEntityBatch(uri, groupProcessor, GroupRepresentation.class);
return processedGroups;
}
/**
*
* {@inheritDoc}
*/
@Override
public int processMembers(final String groupId, final int offset, final int userBatchSize,
final Consumer<UserRepresentation> userProcessor)
{
ParameterCheck.mandatoryString("groupId", groupId);
ParameterCheck.mandatory("userProcessor", userProcessor);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
.path("/admin/realms/{realm}/groups/{groupId}/members").queryParam("first", offset).queryParam("max", userBatchSize)
.build(this.deployment.getRealm(), groupId);
final int processedUsers = this.processEntityBatch(uri, userProcessor, UserRepresentation.class);
return processedUsers;
}
/**
*
* {@inheritDoc}
*/
@Override
public int processRealmRoles(final int offset, final int userBatchSize, final Consumer<RoleRepresentation> roleProcessor)
{
ParameterCheck.mandatory("roleProcessor", roleProcessor);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/roles")
.queryParam("first", offset).queryParam("max", userBatchSize).build(this.deployment.getRealm());
final int processedRoles = this.processEntityBatch(uri, roleProcessor, RoleRepresentation.class);
return processedRoles;
}
/**
*
* {@inheritDoc}
*/
@Override
public int processRealmRoles(final String search, final int offset, final int userBatchSize,
final Consumer<RoleRepresentation> roleProcessor)
{
ParameterCheck.mandatory("roleProcessor", roleProcessor);
ParameterCheck.mandatoryString("search", search);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/roles")
.queryParam("first", offset).queryParam("max", userBatchSize).queryParam("search", search)
.build(this.deployment.getRealm());
final int processedRoles = this.processEntityBatch(uri, roleProcessor, RoleRepresentation.class);
return processedRoles;
}
/**
*
* {@inheritDoc}
*/
@Override
public int processClientRoles(final String clientId, final int offset, final int userBatchSize,
final Consumer<RoleRepresentation> roleProcessor)
{
ParameterCheck.mandatoryString("clientId", clientId);
ParameterCheck.mandatory("roleProcessor", roleProcessor);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
.path("/admin/realms/{realm}/clients/{clientId}/roles").queryParam("first", offset).queryParam("max", userBatchSize)
.build(this.deployment.getRealm(), clientId);
final int processedRoles = this.processEntityBatch(uri, roleProcessor, RoleRepresentation.class);
return processedRoles;
}
/**
*
* {@inheritDoc}
*/
@Override
public int processClientRoles(final String clientId, final String search, final int offset, final int userBatchSize,
final Consumer<RoleRepresentation> roleProcessor)
{
ParameterCheck.mandatoryString("clientId", clientId);
ParameterCheck.mandatoryString("search", search);
ParameterCheck.mandatory("roleProcessor", roleProcessor);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
.path("/admin/realms/{realm}/clients/{clientId}/roles").queryParam("first", offset).queryParam("max", userBatchSize)
.queryParam("search", search).build(this.deployment.getRealm(), clientId);
final int processedRoles = this.processEntityBatch(uri, roleProcessor, RoleRepresentation.class);
return processedRoles;
}
/**
* Loads and processes a batch of generic entities from Keycloak.
*
* @param <T>
* the type of the response entities
* @param uri
* the URI to call
* @param entityProcessor
* the processor handling the loaded entities
* @param entityClass
* the type of the expected response entities
* @return the number of processed entities
*/
protected <T> int processEntityBatch(final URI uri, final Consumer<T> entityProcessor, final Class<T> entityClass)
{
final HttpGet get = new HttpGet(uri);
get.addHeader("Accept", MimetypeMap.MIMETYPE_JSON);
get.addHeader("Authorization", "Bearer " + this.getValidAccessTokenForRequest());
try
{
final HttpClient client = this.deployment.getClient();
final HttpResponse response = client.execute(get);
final int status = response.getStatusLine().getStatusCode();
final HttpEntity httpEntity = response.getEntity();
if (status != 200)
{
EntityUtils.consumeQuietly(httpEntity);
throw new IOException("Bad status: " + status);
}
if (httpEntity == null)
{
throw new IOException("Response does not contain a body");
}
final InputStream is = httpEntity.getContent();
try
{
final MappingIterator<T> iterator = JsonSerialization.mapper.readerFor(entityClass).readValues(is);
int entitiesProcessed = 0;
while (iterator.hasNextValue())
{
final T loadedEntity = iterator.nextValue();
entityProcessor.accept(loadedEntity);
entitiesProcessed++;
}
return entitiesProcessed;
}
finally
{
try
{
is.close();
}
catch (final IOException e)
{
LOGGER.trace("Error closing entity stream", e);
}
}
}
catch (final IOException ioex)
{
LOGGER.error("Failed to retrieve entities", ioex);
throw new AlfrescoRuntimeException("Failed to retrieve entities", ioex);
}
}
/**
* Executes a generic HTTP GET operation yielding a JSON response.
*
* @param uri
* the URI to call
* @param responseProcessor
* the processor handling the response JSON
*/
protected void processGenericGet(final URI uri, final Consumer<JsonNode> responseProcessor)
{
final HttpGet get = new HttpGet(uri);
get.addHeader("Accept", MimetypeMap.MIMETYPE_JSON);
get.addHeader("Authorization", "Bearer " + this.getValidAccessTokenForRequest());
try
{
final HttpClient client = this.deployment.getClient();
final HttpResponse response = client.execute(get);
final int status = response.getStatusLine().getStatusCode();
final HttpEntity httpEntity = response.getEntity();
if (status != 200)
{
EntityUtils.consumeQuietly(httpEntity);
throw new IOException("Bad status: " + status);
}
if (httpEntity == null)
{
throw new IOException("Response does not contain a body");
}
final InputStream is = httpEntity.getContent();
try
{
final JsonNode root = JsonSerialization.mapper.readTree(is);
responseProcessor.accept(root);
}
finally
{
try
{
is.close();
}
catch (final IOException e)
{
LOGGER.trace("Error closing entity stream", e);
}
}
}
catch (final IOException ioex)
{
LOGGER.error("Failed to retrieve entities", ioex);
throw new AlfrescoRuntimeException("Failed to retrieve entities", ioex);
}
}
/**
* Executes a generic HTTP GET operation yielding a mapped response entity.
*
* @param <T>
* the type of the response entity
* @param uri
* the URI to call
* @param responseType
* the class object for the type of the response entity
* @return the response entity
*
*/
protected <T> T processGenericGet(final URI uri, final Class<T> responseType)
{
final HttpGet get = new HttpGet(uri);
get.addHeader("Accept", MimetypeMap.MIMETYPE_JSON);
get.addHeader("Authorization", "Bearer " + this.getValidAccessTokenForRequest());
try
{
final HttpClient client = this.deployment.getClient();
final HttpResponse response = client.execute(get);
final int status = response.getStatusLine().getStatusCode();
final HttpEntity httpEntity = response.getEntity();
if (status != 200)
{
EntityUtils.consumeQuietly(httpEntity);
throw new IOException("Bad status: " + status);
}
if (httpEntity == null)
{
throw new IOException("Response does not contain a body");
}
final InputStream is = httpEntity.getContent();
try
{
final T responseEntity = JsonSerialization.mapper.readValue(is, responseType);
return responseEntity;
}
finally
{
try
{
is.close();
}
catch (final IOException e)
{
LOGGER.trace("Error closing entity stream", e);
}
}
}
catch (final IOException ioex)
{
LOGGER.error("Failed to retrieve entities", ioex);
throw new AlfrescoRuntimeException("Failed to retrieve entities", ioex);
}
}
/**
* Retrieves / determines a valid access token for a request to the admin ReST API.
*
* @return the valid access token to use in a request immediately following this operation
*/
protected String getValidAccessTokenForRequest()
{
if (this.accessToken == null)
{
synchronized (this)
{
if (this.accessToken == null)
{
if (this.userName != null && !this.userName.isEmpty())
{
this.accessToken = this.accessTokenService.obtainAccessToken(this.userName, this.password,
this.requiredClientScopes);
}
else
{
this.accessToken = this.accessTokenService.obtainAccessToken(this.requiredClientScopes);
}
}
}
}
return this.accessToken.getAccessToken();
}
}

@ -0,0 +1,110 @@
/*
* Copyright 2019 - 2021 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.client;
import java.util.function.Consumer;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
/**
* Instances of this interface wrap the relevant Keycloak admin ReST API for the synchronisation of users and groups from a Keycloak realm.
*
* @author Axel Faust
*/
public interface IdentitiesClient
{
/**
* Retrieves the number of users within the Keycloak IDM database.
*
* @return the count of users in the Keycloak database
*/
int countUsers();
/**
* Retrieves the number of groups within the Keycloak IDM database.
*
* @return the count of groups in the Keycloak database
*/
int countGroups();
/**
* Retrieves the details of one specific group from Keycloak.
*
* @param groupId
* the ID of the group in Keycloak
* @return the group details
*/
GroupRepresentation getGroup(String groupId);
/**
* Loads and processes a batch of users from Keycloak using an externally specified processor.
*
* @param offset
* the index of the first user to retrieve
* @param userBatchSize
* the number of users to load in one batch
* @param userProcessor
* the processor handling the loaded users
* @return the number of processed users
*/
int processUsers(int offset, int userBatchSize, Consumer<UserRepresentation> userProcessor);
/**
* Loads and processes a batch of groups of a specific user from Keycloak using an externally specified processor.
*
* @param userId
* the ID of user for which to process groups
* @param offset
* the index of the first group to retrieve
* @param groupBatchSize
* the number of groups to load in one batch
* @param groupProcessor
* the processor handling the loaded groups
* @return the number of processed groups
*/
int processUserGroups(String userId, int offset, int groupBatchSize, Consumer<GroupRepresentation> groupProcessor);
/**
* Loads and processes a batch of groups from Keycloak using an externally specified processor.
*
* @param offset
* the index of the first group to retrieve
* @param groupBatchSize
* the number of groups to load in one batch
* @param groupProcessor
* the processor handling the loaded groups
* @return the number of processed groups
*/
int processGroups(int offset, int groupBatchSize, Consumer<GroupRepresentation> groupProcessor);
/**
* Loads and processes a batch of users / members of a group from Keycloak using an externally specified processor.
*
* @param groupId
* the ID of group for which to process members
* @param offset
* the index of the first user to retrieve
* @param userBatchSize
* the number of users to load in one batch
* @param userProcessor
* the processor handling the loaded users
* @return the number of processed users
*/
int processMembers(String groupId, int offset, int userBatchSize, Consumer<UserRepresentation> userProcessor);
}

@ -0,0 +1,201 @@
/*
* Copyright 2019 - 2021 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.client;
import java.net.URI;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.util.ParameterCheck;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
/**
* Implements the API for a client to the Keycloak admin ReST API specific to users and groups.
*
* @author Axel Faust
*/
public class IdentitiesClientImpl extends AbstractIDMClientImpl implements IdentitiesClient
{
/**
*
* {@inheritDoc}
*/
@Override
public int countUsers()
{
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/users/count")
.build(this.deployment.getRealm());
final AtomicInteger count = new AtomicInteger(0);
this.processGenericGet(uri, root -> {
if (root.isInt())
{
count.set(root.intValue());
}
else
{
throw new AlfrescoRuntimeException("Keycloak admin API did not yield expected data for user count");
}
});
return count.get();
}
/**
*
* {@inheritDoc}
*/
@Override
public int countGroups()
{
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups/count")
.build(this.deployment.getRealm());
final AtomicInteger count = new AtomicInteger(0);
this.processGenericGet(uri, root -> {
if (root.isObject() && root.has("count"))
{
count.set(root.get("count").intValue());
}
else
{
throw new AlfrescoRuntimeException("Keycloak admin API did not yield expected JSON data for group count");
}
});
return count.get();
}
/**
*
* {@inheritDoc}
*/
@Override
public GroupRepresentation getGroup(final String groupId)
{
ParameterCheck.mandatoryString("groupId", groupId);
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups/{groupId}")
.build(this.deployment.getRealm(), groupId);
return this.processGenericGet(uri, GroupRepresentation.class);
}
/**
*
* {@inheritDoc}
*/
@Override
public int processUsers(final int offset, final int userBatchSize, final Consumer<UserRepresentation> userProcessor)
{
ParameterCheck.mandatory("userProcessor", userProcessor);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/users")
.queryParam("first", offset).queryParam("max", userBatchSize).build(this.deployment.getRealm());
return this.processEntityBatch(uri, userProcessor, UserRepresentation.class);
}
/**
*
* {@inheritDoc}
*/
@Override
public int processUserGroups(final String userId, final int offset, final int groupBatchSize,
final Consumer<GroupRepresentation> groupProcessor)
{
ParameterCheck.mandatoryString("userId", userId);
ParameterCheck.mandatory("groupProcessor", groupProcessor);
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/users/{user}/groups")
.queryParam("first", offset).queryParam("max", groupBatchSize).build(this.deployment.getRealm(), userId);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (groupBatchSize <= 0)
{
throw new IllegalArgumentException("groupBatchSize must be a positive integer");
}
return this.processEntityBatch(uri, groupProcessor, GroupRepresentation.class);
}
/**
*
* {@inheritDoc}
*/
@Override
public int processGroups(final int offset, final int groupBatchSize, final Consumer<GroupRepresentation> groupProcessor)
{
ParameterCheck.mandatory("groupProcessor", groupProcessor);
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups")
.queryParam("first", offset).queryParam("max", groupBatchSize).build(this.deployment.getRealm());
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (groupBatchSize <= 0)
{
throw new IllegalArgumentException("groupBatchSize must be a positive integer");
}
return this.processEntityBatch(uri, groupProcessor, GroupRepresentation.class);
}
/**
*
* {@inheritDoc}
*/
@Override
public int processMembers(final String groupId, final int offset, final int userBatchSize,
final Consumer<UserRepresentation> userProcessor)
{
ParameterCheck.mandatoryString("groupId", groupId);
ParameterCheck.mandatory("userProcessor", userProcessor);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
.path("/admin/realms/{realm}/groups/{groupId}/members").queryParam("first", offset).queryParam("max", userBatchSize)
.build(this.deployment.getRealm(), groupId);
return this.processEntityBatch(uri, userProcessor, UserRepresentation.class);
}
}

@ -0,0 +1,99 @@
/*
* Copyright 2019 - 2021 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.client;
import java.util.function.Consumer;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
/**
* Instances of this interface wrap the relevant Keycloak admin ReST API for the synchronisation of roles from a Keycloak realm.
*
* @author Axel Faust
*/
public interface RolesClient
{
/**
* Loads and processes the registered clients from Keycloak using an externally specified processor.
*
* @param clientProcessor
* the processor handling the loaded clients
* @return the number of processed clients
*/
int processClients(Consumer<ClientRepresentation> clientProcessor);
/**
* Loads and processes a batch of realm roles from Keycloak using an externally specified processor.
*
* @param offset
* the index of the first role to retrieve
* @param roleBatchSize
* the number of roles to load in one batch
* @param roleProcessor
* the processor handling the loaded roles
* @return the number of processed roles
*/
int processRealmRoles(int offset, int roleBatchSize, Consumer<RoleRepresentation> roleProcessor);
/**
* Loads and processes a batch of realm roles from Keycloak using an externally specified processor.
*
* @param search
* a search term to filter roles
* @param offset
* the index of the first role to retrieve
* @param roleBatchSize
* the number of roles to load in one batch
* @param roleProcessor
* the processor handling the loaded roles
* @return the number of processed roles
*/
int processRealmRoles(String search, int offset, int roleBatchSize, Consumer<RoleRepresentation> roleProcessor);
/**
* Loads and processes a batch of client roles from Keycloak using an externally specified processor.
*
* @param clientId
* the {@link ClientRepresentation#getId() (technical) ID} of a client from which to process defined roles
* @param offset
* the index of the first role to retrieve
* @param roleBatchSize
* the number of roles to load in one batch
* @param roleProcessor
* the processor handling the loaded roles
* @return the number of processed roles
*/
int processClientRoles(String clientId, int offset, int roleBatchSize, Consumer<RoleRepresentation> roleProcessor);
/**
* Loads and processes a batch of client roles from Keycloak using an externally specified processor.
*
* @param clientId
* the {@link ClientRepresentation#getId() (technical) ID} of a client from which to process defined roles
* @param search
* a search term to filter roles
* @param offset
* the index of the first role to retrieve
* @param roleBatchSize
* the number of roles to load in one batch
* @param roleProcessor
* the processor handling the loaded roles
* @return the number of processed roles
*/
int processClientRoles(String clientId, String search, int offset, int roleBatchSize, Consumer<RoleRepresentation> roleProcessor);
}

@ -0,0 +1,154 @@
/*
* Copyright 2019 - 2021 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.client;
import java.net.URI;
import java.util.function.Consumer;
import org.alfresco.util.ParameterCheck;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
/**
* Implements the API for a client to the Keycloak admin ReST API specific to roles.
*
* @author Axel Faust
*/
public class RolesClientImpl extends AbstractIDMClientImpl implements RolesClient
{
/**
*
* {@inheritDoc}
*/
@Override
public int processClients(final Consumer<ClientRepresentation> clientProcessor)
{
ParameterCheck.mandatory("clientProcessor", clientProcessor);
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/clients")
.build(this.deployment.getRealm());
return this.processEntityBatch(uri, clientProcessor, ClientRepresentation.class);
}
/**
*
* {@inheritDoc}
*/
@Override
public int processRealmRoles(final int offset, final int userBatchSize, final Consumer<RoleRepresentation> roleProcessor)
{
ParameterCheck.mandatory("roleProcessor", roleProcessor);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/roles")
.queryParam("first", offset).queryParam("max", userBatchSize).build(this.deployment.getRealm());
return this.processEntityBatch(uri, roleProcessor, RoleRepresentation.class);
}
/**
*
* {@inheritDoc}
*/
@Override
public int processRealmRoles(final String search, final int offset, final int userBatchSize,
final Consumer<RoleRepresentation> roleProcessor)
{
ParameterCheck.mandatory("roleProcessor", roleProcessor);
ParameterCheck.mandatoryString("search", search);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/roles")
.queryParam("first", offset).queryParam("max", userBatchSize).queryParam("search", search)
.build(this.deployment.getRealm());
return this.processEntityBatch(uri, roleProcessor, RoleRepresentation.class);
}
/**
*
* {@inheritDoc}
*/
@Override
public int processClientRoles(final String clientId, final int offset, final int userBatchSize,
final Consumer<RoleRepresentation> roleProcessor)
{
ParameterCheck.mandatoryString("clientId", clientId);
ParameterCheck.mandatory("roleProcessor", roleProcessor);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
.path("/admin/realms/{realm}/clients/{clientId}/roles").queryParam("first", offset).queryParam("max", userBatchSize)
.build(this.deployment.getRealm(), clientId);
return this.processEntityBatch(uri, roleProcessor, RoleRepresentation.class);
}
/**
*
* {@inheritDoc}
*/
@Override
public int processClientRoles(final String clientId, final String search, final int offset, final int userBatchSize,
final Consumer<RoleRepresentation> roleProcessor)
{
ParameterCheck.mandatoryString("clientId", clientId);
ParameterCheck.mandatoryString("search", search);
ParameterCheck.mandatory("roleProcessor", roleProcessor);
if (offset < 0)
{
throw new IllegalArgumentException("offset must be a non-negative integer");
}
if (userBatchSize <= 0)
{
throw new IllegalArgumentException("userBatchSize must be a positive integer");
}
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
.path("/admin/realms/{realm}/clients/{clientId}/roles").queryParam("first", offset).queryParam("max", userBatchSize)
.queryParam("search", search).build(this.deployment.getRealm(), clientId);
return this.processEntityBatch(uri, roleProcessor, RoleRepresentation.class);
}
}

@ -39,8 +39,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import de.acosix.alfresco.keycloak.repo.client.IDMClient;
import de.acosix.alfresco.keycloak.repo.client.RolesClient;
/**
*
* @author Axel Faust
*/
public class RoleServiceImpl implements RoleService, InitializingBean
{
@ -50,7 +54,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
protected AdapterConfig adapterConfig;
protected IDMClient idmClient;
protected RolesClient rolesClient;
protected boolean enabled;
@ -83,7 +87,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
@Override
public void afterPropertiesSet()
{
PropertyCheck.mandatory(this, "idmClient", this.idmClient);
PropertyCheck.mandatory(this, "rolesClient", this.rolesClient);
if (this.enabled && this.processRealmRoles)
{
@ -113,17 +117,17 @@ public class RoleServiceImpl implements RoleService, InitializingBean
}
/**
* @param idmClient
* the idmClient to set
* @param rolesClient
* the rolesClient to set
*/
public void setIdmClient(final IDMClient idmClient)
public void setRolesClient(final RolesClient rolesClient)
{
this.idmClient = idmClient;
this.rolesClient = rolesClient;
}
/**
* @param adapterConfig
* the adapterConfig to set
* the adapterConfig to set
*/
public void setAdapterConfig(final AdapterConfig adapterConfig)
{
@ -132,7 +136,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
/**
* @param enabled
* the enabled to set
* the enabled to set
*/
public void setEnabled(final boolean enabled)
{
@ -141,7 +145,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
/**
* @param processRealmRoles
* the processRealmRoles to set
* the processRealmRoles to set
*/
public void setProcessRealmRoles(final boolean processRealmRoles)
{
@ -150,7 +154,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
/**
* @param processResourceRoles
* the processResourceRoles to set
* the processResourceRoles to set
*/
public void setProcessResourceRoles(final boolean processResourceRoles)
{
@ -159,7 +163,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
/**
* @param realmRoleNameFilter
* the realmRoleNameFilter to set
* the realmRoleNameFilter to set
*/
public void setRealmRoleNameFilter(final RoleNameFilter realmRoleNameFilter)
{
@ -168,7 +172,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
/**
* @param realmRoleNameMapper
* the realmRoleNameMapper to set
* the realmRoleNameMapper to set
*/
public void setRealmRoleNameMapper(final RoleNameMapper realmRoleNameMapper)
{
@ -177,7 +181,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
/**
* @param defaultResourceRoleNameFilter
* the defaultResourceRoleNameFilter to set
* the defaultResourceRoleNameFilter to set
*/
public void setDefaultResourceRoleNameFilter(final RoleNameFilter defaultResourceRoleNameFilter)
{
@ -186,7 +190,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
/**
* @param defaultResourceRoleNameMapper
* the defaultResourceRoleNameMapper to set
* the defaultResourceRoleNameMapper to set
*/
public void setDefaultResourceRoleNameMapper(final RoleNameMapper defaultResourceRoleNameMapper)
{
@ -195,7 +199,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
/**
* @param resourceRoleNameFilter
* the resourceRoleNameFilter to set
* the resourceRoleNameFilter to set
*/
public void setResourceRoleNameFilter(final Map<String, RoleNameFilter> resourceRoleNameFilter)
{
@ -204,7 +208,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
/**
* @param resourceRoleNameMapper
* the resourceRoleNameMapper to set
* the resourceRoleNameMapper to set
*/
public void setResourceRoleNameMapper(final Map<String, RoleNameMapper> resourceRoleNameMapper)
{
@ -213,7 +217,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
/**
* @param hiddenMappedRoles
* the hiddenMappedRoles to set
* the hiddenMappedRoles to set
*/
public void setHiddenMappedRoles(final List<String> hiddenMappedRoles)
{
@ -327,7 +331,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
{
final UnaryOperator<String> realmRoleResolver = rn -> {
final Set<String> matchingRoles = new HashSet<>();
this.idmClient.processRealmRoles(rn, 0, Integer.MAX_VALUE, roleResult -> {
this.rolesClient.processRealmRoles(rn, 0, Integer.MAX_VALUE, roleResult -> {
if (roleResult.getName().equalsIgnoreCase(rn))
{
matchingRoles.add(roleResult.getName());
@ -351,7 +355,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
{
final BinaryOperator<String> clientRoleResolver = (client, rn) -> {
final Set<String> matchingRoles = new HashSet<>();
this.idmClient.processClientRoles(client, rn, 0, Integer.MAX_VALUE, roleResult -> {
this.rolesClient.processClientRoles(client, rn, 0, Integer.MAX_VALUE, roleResult -> {
if (roleResult.getName().equalsIgnoreCase(rn))
{
matchingRoles.add(roleResult.getName());
@ -554,7 +558,7 @@ public class RoleServiceImpl implements RoleService, InitializingBean
try
{
LOGGER.debug("Loading IDs for registered clients from Keycloak");
final int processedClients = this.idmClient.processClients(client -> {
final int processedClients = this.rolesClient.processClients(client -> {
// Keycloak terminology is not 100% consistent
// what the Keycloak adapter calls the resourceName is the client ID in IDM representation
// we use clientId in our API to refer to the technical identifier which can actually be used in the ReST API to access the
@ -613,11 +617,11 @@ public class RoleServiceImpl implements RoleService, InitializingBean
if (clientId != null)
{
this.idmClient.processClientRoles(clientId, 0, Integer.MAX_VALUE, processor);
this.rolesClient.processClientRoles(clientId, 0, Integer.MAX_VALUE, processor);
}
else
{
this.idmClient.processRealmRoles(0, Integer.MAX_VALUE, processor);
this.rolesClient.processRealmRoles(0, Integer.MAX_VALUE, processor);
}
return results;

@ -24,7 +24,7 @@ import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.springframework.beans.factory.InitializingBean;
import de.acosix.alfresco.keycloak.repo.client.IDMClient;
import de.acosix.alfresco.keycloak.repo.client.IdentitiesClient;
/**
* This class provides common configuration and logic relevant for any filter based on authority group containments.
@ -34,7 +34,7 @@ import de.acosix.alfresco.keycloak.repo.client.IDMClient;
public abstract class BaseGroupContainmentFilter implements InitializingBean
{
protected IDMClient idmClient;
protected IdentitiesClient identitiesClient;
protected List<String> groupPaths;
@ -55,22 +55,22 @@ public abstract class BaseGroupContainmentFilter implements InitializingBean
@Override
public void afterPropertiesSet()
{
PropertyCheck.mandatory(this, "idmClient", this.idmClient);
PropertyCheck.mandatory(this, "identitiesClient", this.identitiesClient);
if (this.groupIds != null && !this.groupIds.isEmpty())
{
this.idResolvedGroupPaths = new ArrayList<>();
this.groupIds.stream().map(id -> this.idmClient.getGroup(id).getPath()).forEach(this.idResolvedGroupPaths::add);
this.groupIds.stream().map(id -> this.identitiesClient.getGroup(id).getPath()).forEach(this.idResolvedGroupPaths::add);
}
}
/**
* @param idmClient
* the idmClient to set
* @param identitiesClient
* the identitiesClient to set
*/
public void setIdmClient(final IDMClient idmClient)
public void setIdentitiesClient(final IdentitiesClient identitiesClient)
{
this.idmClient = idmClient;
this.identitiesClient = identitiesClient;
}
/**

@ -56,7 +56,7 @@ public class GroupContainmentUserFilter extends BaseGroupContainmentFilter
int processedGroups = 1;
while (processedGroups > 0)
{
processedGroups = this.idmClient.processUserGroups(user.getId(), offset, this.groupLoadBatchSize, group -> {
processedGroups = this.identitiesClient.processUserGroups(user.getId(), offset, this.groupLoadBatchSize, group -> {
parentGroupIds.add(group.getId());
parentGroupPaths.add(group.getPath());
});

@ -25,8 +25,10 @@ import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntConsumer;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.management.subsystems.ActivateableBean;
@ -44,8 +46,7 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import de.acosix.alfresco.keycloak.repo.client.IDMClient;
import de.acosix.alfresco.keycloak.repo.client.IDMClientImpl;
import de.acosix.alfresco.keycloak.repo.client.IdentitiesClient;
/**
* This class provides a Keycloak-based user registry to support synchronisation with Keycloak managed users and groups.
@ -61,7 +62,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
protected ApplicationContext applicationContext;
protected IDMClient idmClient;
protected IdentitiesClient identitiesClient;
protected Collection<UserFilter> userFilters;
@ -82,16 +83,16 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
public void afterPropertiesSet()
{
PropertyCheck.mandatory(this, "applicationContext", this.applicationContext);
PropertyCheck.mandatory(this, "idmClient", this.idmClient);
PropertyCheck.mandatory(this, "identitiesClient", this.identitiesClient);
this.userFilters = Collections.unmodifiableList(
new ArrayList<>(this.applicationContext.getBeansOfType(UserFilter.class, false, true).values()));
this.groupFilters = Collections.unmodifiableList(
new ArrayList<>(this.applicationContext.getBeansOfType(GroupFilter.class, false, true).values()));
this.userProcessors = Collections.unmodifiableList(
new ArrayList<>(this.applicationContext.getBeansOfType(UserProcessor.class, false, true).values()));
this.groupProcessors = Collections.unmodifiableList(
new ArrayList<>(this.applicationContext.getBeansOfType(GroupProcessor.class, false, true).values()));
this.userFilters = Collections
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(UserFilter.class, false, true).values()));
this.groupFilters = Collections
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(GroupFilter.class, false, true).values()));
this.userProcessors = Collections
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(UserProcessor.class, false, true).values()));
this.groupProcessors = Collections
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(GroupProcessor.class, false, true).values()));
}
/**
@ -105,7 +106,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
/**
* @param active
* the active to set
* the active to set
*/
public void setActive(final boolean active)
{
@ -122,17 +123,17 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
}
/**
* @param idmClient
* the idmClient to set
* @param identitiesClient
* the identitiesClient to set
*/
public void setIdmClient(final IDMClientImpl idmClient)
public void setIdentitiesClient(final IdentitiesClient identitiesClient)
{
this.idmClient = idmClient;
this.identitiesClient = identitiesClient;
}
/**
* @param personLoadBatchSize
* the personLoadBatchSize to set
* the personLoadBatchSize to set
*/
public void setPersonLoadBatchSize(final int personLoadBatchSize)
{
@ -141,7 +142,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
/**
* @param groupLoadBatchSize
* the groupLoadBatchSize to set
* the groupLoadBatchSize to set
*/
public void setGroupLoadBatchSize(final int groupLoadBatchSize)
{
@ -160,7 +161,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
if (this.active)
{
people = new UserCollection<>(this.personLoadBatchSize, this.idmClient.countUsers(), this::mapUser);
people = new UserCollection<>(this.personLoadBatchSize, this.identitiesClient.countUsers(), this::mapUser);
}
return people;
@ -178,7 +179,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
if (this.active)
{
groups = new GroupCollection<>(this.groupLoadBatchSize, this.idmClient.countGroups(), this::mapGroup);
groups = new GroupCollection<>(this.groupLoadBatchSize, this.identitiesClient.countGroups(), this::mapGroup);
}
return groups;
@ -194,7 +195,8 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
if (this.active)
{
personNames = new UserCollection<>(this.personLoadBatchSize, this.idmClient.countUsers(), UserRepresentation::getUsername);
personNames = new UserCollection<>(this.personLoadBatchSize, this.identitiesClient.countUsers(),
UserRepresentation::getUsername);
}
return personNames;
@ -210,7 +212,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
if (this.active)
{
groupNames = new GroupCollection<>(this.groupLoadBatchSize, this.idmClient.countGroups(),
groupNames = new GroupCollection<>(this.groupLoadBatchSize, this.identitiesClient.countGroups(),
group -> AuthorityType.GROUP.getPrefixString() + group.getId());
}
@ -234,7 +236,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
* Maps a single user from the Keycloak representation into an abstract description of a person node.
*
* @param user
* the user to map
* the user to map
* @return the mapped person node description
*/
protected NodeDescription mapUser(final UserRepresentation user)
@ -256,7 +258,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
* Maps a single group from the Keycloak representation into an abstract description of a group node.
*
* @param group
* the group to map
* the group to map
* @return the mapped group node description
*/
protected NodeDescription mapGroup(final GroupRepresentation group)
@ -283,7 +285,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
int processedMembers = 1;
while (processedMembers > 0)
{
processedMembers = this.idmClient.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 (!skipSync)
{
@ -317,12 +319,12 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
* Constructs a new instance of this class.
*
* @param batchSize
* the size of batches to use for incrementally loading data elements in the iterator
* the size of batches to use for incrementally loading data elements in the iterator
* @param totalUpperBound
* the upper bound of the total number of elements to expect in this collection - this is just an estimation (without
* adjusting for any potential filtering) and will be used as the {@link #size() collection's size}.
* the upper bound of the total number of elements to expect in this collection - this is just an estimation (without
* adjusting for any potential filtering) and will be used as the {@link #size() collection's size}.
* @param mapper
* the mapping handler to turn a low-level authority representation into the actual collection value representation
* the mapping handler to turn a low-level authority representation into the actual collection value representation
*/
protected KeycloakAuthorityCollection(final int batchSize, final int totalUpperBound, final Function<AR, T> mapper)
{
@ -353,22 +355,24 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
* Loads the next batch of authority representations.
*
* @param offset
* the index of the first low-level authority to load
* the index of the first low-level authority to load
* @param batchSize
* the maximum number of low-level authorities to load from the backend
* the maximum number of low-level authorities to load from the backend
* @param filteredCountHandler
* a handler aggregating the count of entities filtered during loading
* @param authorityProcessor
* the processor to consume individual authority representations - the number of representations passed to this processor
* may be different than the number of authorities loaded from the backend due to filtering and potential pre-processing
* (e.g. splitting of groups and sub-groups)
* the processor to consume individual authority representations - the number of representations passed to this processor
* may be different than the number of authorities loaded from the backend due to filtering and potential pre-processing
* (e.g. splitting of groups and sub-groups)
* @return the number of low-level authorities loaded in this batch to properly adjust the offset for the next load operation
*/
protected abstract int loadNext(int offset, int batchSize, Consumer<AR> authorityProcessor);
protected abstract int loadNext(int offset, int batchSize, IntConsumer filteredCountHandler, Consumer<AR> authorityProcessor);
/**
* Converts an authority representation into the type of object to be exposed as values of the collection.
*
* @param authorityRepresentation
* the authority representation to convert
* the authority representation to convert
* @return the converted value
*/
protected T convert(final AR authorityRepresentation)
@ -387,6 +391,8 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
private boolean noMoreResults;
protected final AtomicInteger totalFiltered = new AtomicInteger(0);
/**
* {@inheritDoc}
*/
@ -396,6 +402,13 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
this.checkAndFillBuffer();
final boolean hasNext = !this.buffer.isEmpty() && this.index < this.buffer.size();
if (!hasNext && this.totalFiltered.get() > 0)
{
LOGGER.info("End of collection reached - {} from total count of {} not processed due to configured post-fetch filters",
this.totalFiltered, KeycloakAuthorityCollection.this.totalUpperBound);
}
return hasNext;
}
@ -427,6 +440,7 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
this.index = 0;
this.offset += KeycloakAuthorityCollection.this.loadNext(this.offset, KeycloakAuthorityCollection.this.batchSize,
i -> this.totalFiltered.addAndGet(i),
authority -> this.buffer.add(KeycloakAuthorityCollection.this.convert(authority)));
this.noMoreResults = this.buffer.isEmpty();
@ -447,12 +461,12 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
* Constructs a new instance of this class.
*
* @param batchSize
* the size of batches to use for incrementally loading data elements in the iterator
* the size of batches to use for incrementally loading data elements in the iterator
* @param totalUpperBound
* the upper bound of the total number of elements to expect in this collection - this is just an estimation (without
* adjusting for any potential filtering) and will be used as the {@link #size() collection's size}.
* the upper bound of the total number of elements to expect in this collection - this is just an estimation (without
* adjusting for any potential filtering) and will be used as the {@link #size() collection's size}.
* @param mapper
* the mapping handler to turn a low-level authority representation into the actual collection value representation
* the mapping handler to turn a low-level authority representation into the actual collection value representation
*/
public UserCollection(final int batchSize, final int totalUpperBound, final Function<UserRepresentation, T> mapper)
{
@ -463,16 +477,21 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
* {@inheritDoc}
*/
@Override
protected int loadNext(final int offset, final int batchSize, final Consumer<UserRepresentation> authorityProcessor)
protected int loadNext(final int offset, final int batchSize, final IntConsumer filteredHandler,
final Consumer<UserRepresentation> authorityProcessor)
{
// TODO Evaluate other iteration approaches, e.g. crawling from a configured root group
// How to count totals in advance though?
return KeycloakUserRegistry.this.idmClient.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 (!skipSync)
{
authorityProcessor.accept(user);
}
else
{
filteredHandler.accept(1);
}
});
}
@ -490,12 +509,12 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
* Constructs a new instance of this class.
*
* @param batchSize
* the size of batches to use for incrementally loading data elements in the iterator
* the size of batches to use for incrementally loading data elements in the iterator
* @param totalUpperBound
* the upper bound of the total number of elements to expect in this collection - this is just an estimation (without
* adjusting for any potential filtering) and will be used as the {@link #size() collection's size}.
* the upper bound of the total number of elements to expect in this collection - this is just an estimation (without
* adjusting for any potential filtering) and will be used as the {@link #size() collection's size}.
* @param mapper
* the mapping handler to turn a low-level authority representation into the actual collection value representation
* the mapping handler to turn a low-level authority representation into the actual collection value representation
*/
public GroupCollection(final int batchSize, final int totalUpperBound, final Function<GroupRepresentation, T> mapper)
{
@ -506,22 +525,28 @@ public class KeycloakUserRegistry implements UserRegistry, InitializingBean, Act
* {@inheritDoc}
*/
@Override
protected int loadNext(final int offset, final int batchSize, final Consumer<GroupRepresentation> authorityProcessor)
protected int loadNext(final int offset, final int batchSize, final IntConsumer filteredHandler,
final Consumer<GroupRepresentation> authorityProcessor)
{
// TODO Evaluate other iteration approaches, e.g. crawling from a configured root group
// How to count totals in advance though?
return KeycloakUserRegistry.this.idmClient.processGroups(offset, batchSize, group -> {
this.processGroupsRecursively(group, authorityProcessor);
return KeycloakUserRegistry.this.identitiesClient.processGroups(offset, batchSize, group -> {
this.processGroupsRecursively(group, filteredHandler, authorityProcessor);
});
}
protected void processGroupsRecursively(final GroupRepresentation group, final Consumer<GroupRepresentation> authorityProcessor)
protected void processGroupsRecursively(final GroupRepresentation group, final IntConsumer filteredHandler,
final Consumer<GroupRepresentation> authorityProcessor)
{
final boolean skipSync = KeycloakUserRegistry.this.groupFilters.stream().anyMatch(filter -> !filter.shouldIncludeGroup(group));
if (!skipSync)
{
authorityProcessor.accept(group);
}
else
{
filteredHandler.accept(1);
}
// any filtering applied above does not apply here as any sub-group will be individually checked for filtering by recursive
// processing

@ -27,6 +27,8 @@ keycloak.adapter.credentials.secret=6f70a28f-98cd-41ca-8f2f-368a8797d708
# localhost in auth-server-url won't work for direct access in a Docker deployment
keycloak.adapter.directAuthHost=http://keycloak:8080
keycloak.roles.requiredClientScopes=alfresco-role-service
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A

@ -216,6 +216,45 @@
}
]
},
{
"name": "microprofile-jwt",
"description": "Microprofile - JWT built-in scope",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "upn",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "upn",
"jsonType.label": "String"
}
},
{
"name": "groups",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"user.attribute": "foo",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "groups",
"jsonType.label": "String"
}
}
]
},
{
"name": "email",
"description": "OpenID Connect built-in scope: email",
@ -325,6 +364,28 @@
}
]
},
{
"name": "role_list",
"description": "SAML role list",
"protocol": "saml",
"attributes": {
"consent.screen.text": "${samlRoleListScopeConsentText}",
"display.on.consent.screen": "true"
},
"protocolMappers": [
{
"name": "role list",
"protocol": "saml",
"protocolMapper": "saml-role-list-mapper",
"consentRequired": false,
"config": {
"single": "false",
"attribute.nameformat": "Basic",
"attribute.name": "Role"
}
}
]
},
{
"name": "roles",
"description": "OpenID Connect scope for add user roles to the access token",
@ -393,7 +454,7 @@
]
},
{
"name": "realm-management",
"name": "alfresco-authority-sync",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
@ -401,20 +462,26 @@
},
"protocolMappers": [
{
"name": "Realm Management Client Roles",
"name": "Realm Management Audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "false",
"included.client.audience": "realm-management",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "resource_access.realm-management.roles",
"jsonType.label": "String",
"usermodel.clientRoleMapping.clientId": "realm-management"
"access.token.claim": "true"
}
},
}
]
},
{
"name": "alfresco-role-service",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "Realm Management Audience",
"protocol": "openid-connect",
@ -436,21 +503,6 @@
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "Alfresco Client Roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "false",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "resource_access.alfresco.roles",
"jsonType.label": "String",
"usermodel.clientRoleMapping.clientId": "alfresco"
}
},
{
"name": "Alfresco Audience",
"protocol": "openid-connect",
@ -487,12 +539,62 @@
"profile",
"email",
"roles",
"role_list",
"web-origins"
],
"defaultOptionalClientScopes": [
"address",
"microprofile-jwt",
"phone"
],
"scopeMappings": [
{
"clientScope": "offline_access",
"roles": [
"offline_access"
]
},
{
"clientScope": "alfresco",
"roles": [
"user"
]
}
],
"clientScopeMappings": {
"realm-management": [
{
"clientScope": "alfresco-authority-sync",
"roles": [
"view-users",
"query-groups",
"query-users"
]
},
{
"clientScope": "alfresco-role-service",
"roles": [
"view-clients"
]
}
],
"account": [
{
"client": "account-console",
"roles": [
"manage-account"
]
}
],
"alfresco": [
{
"clientScope": "alfresco",
"roles": [
"admin"
]
}
]
},
"clients": [
{
"id": "alfresco",
@ -514,21 +616,483 @@
"serviceAccountsEnabled": true,
"publicClient": false,
"protocol": "openid-connect",
"fullScopeAllowed": false,
"defaultClientScopes": [
"profile",
"email",
"address",
"phone",
"realm-roles",
"roles",
"alfresco"
],
"optionalClientScopes": [
"realm-management"
"alfresco-authority-sync",
"alfresco-role-service"
]
},
{
"clientId": "realm-management",
"name": "Realm Management",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"bearerOnly": true,
"authorizationServicesEnabled": true,
"protocol": "openid-connect",
"authorizationSettings": {
"allowRemoteResourceManagement": false,
"policyEnforcementMode": "ENFORCING",
"resources": [
{
"name": "client.resource.alfresco",
"type": "Client",
"ownerManagedAccess": false,
"attributes": {
},
"uris": [],
"scopes": [
{
"name": "view"
},
{
"name": "map-roles-client-scope"
},
{
"name": "configure"
},
{
"name": "map-roles"
},
{
"name": "manage"
},
{
"name": "token-exchange"
},
{
"name": "map-roles-composite"
}
]
}
],
"policies": [
{
"name": "alfresco-token-exchange",
"type": "client",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"clients": "[\"alfresco-share\"]"
}
},
{
"name": "token-exchange.permission.client.alfresco",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"client.resource.alfresco\"]",
"scopes": "[\"token-exchange\"]",
"applyPolicies": "[\"alfresco-token-exchange\"]"
}
},
{
"name": "map-roles-composite.permission.client.alfresco",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"client.resource.alfresco\"]",
"scopes": "[\"map-roles-composite\"]"
}
},
{
"name": "map-roles-client-scope.permission.client.alfresco",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"client.resource.alfresco\"]",
"scopes": "[\"map-roles-client-scope\"]"
}
},
{
"name": "map-roles.permission.client.alfresco",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"client.resource.alfresco\"]",
"scopes": "[\"map-roles\"]"
}
},
{
"name": "view.permission.client.alfresco",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"client.resource.alfresco\"]",
"scopes": "[\"view\"]"
}
},
{
"name": "configure.permission.client.alfresco",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"client.resource.alfresco\"]",
"scopes": "[\"configure\"]"
}
},
{
"name": "manage.permission.client.alfresco",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"client.resource.alfresco\"]",
"scopes": "[\"manage\"]"
}
}
],
"scopes": [
{
"name": "token-exchange"
},
{
"name": "configure"
},
{
"name": "map-roles-composite"
},
{
"name": "map-roles-client-scope"
},
{
"name": "map-roles"
},
{
"name": "view"
},
{
"name": "manage"
}
]
}
},
{
"clientId": "account",
"name": "${client_account}",
"rootUrl": "${authBaseUrl}",
"baseUrl": "/realms/test/account/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"/realms/test/account/*"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"defaultClientScopes": [
"web-origins",
"roles",
"profile",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
},
{
"clientId": "account-console",
"name": "${client_account-console}",
"rootUrl": "${authBaseUrl}",
"baseUrl": "/realms/test/account/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"/realms/test/account/*"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"pkce.code.challenge.method": "S256"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"name": "audience resolve",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-resolve-mapper",
"consentRequired": false
}
],
"defaultClientScopes": [
"web-origins",
"roles",
"profile",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
}
],
"roles": {
"realm": [
{
"name": "uma_authorization",
"description": "${role_uma_authorization}"
},
{
"name": "default-roles-test",
"description": "${role_default-roles}",
"composite": true,
"composites": {
"realm": [
"offline_access",
"uma_authorization",
"user"
],
"client": {
"account": [
"view-profile",
"manage-account"
]
}
}
},
{
"name": "offline_access",
"description": "${role_offline-access}"
}
],
"client": {
"realm-management": [
{
"name": "view-identity-providers",
"description": "${role_view-identity-providers}",
"clientRole": true
},
{
"name": "manage-users",
"description": "${role_manage-users}",
"clientRole": true
},
{
"name": "query-groups",
"description": "${role_query-groups}",
"clientRole": true
},
{
"name": "query-users",
"description": "${role_query-users}",
"clientRole": true
},
{
"name": "realm-admin",
"description": "${role_realm-admin}",
"composite": true,
"composites": {
"client": {
"realm-management": [
"view-identity-providers",
"manage-users",
"query-groups",
"query-users",
"view-realm",
"impersonation",
"manage-events",
"manage-authorization",
"manage-identity-providers",
"manage-clients",
"manage-realm",
"view-users",
"view-clients",
"view-events",
"query-realms",
"create-client",
"query-clients",
"view-authorization"
]
}
},
"clientRole": true
},
{
"name": "view-realm",
"description": "${role_view-realm}",
"clientRole": true
},
{
"name": "impersonation",
"description": "${role_impersonation}",
"clientRole": true
},
{
"name": "manage-events",
"description": "${role_manage-events}",
"clientRole": true
},
{
"name": "manage-authorization",
"description": "${role_manage-authorization}",
"clientRole": true
},
{
"name": "manage-identity-providers",
"description": "${role_manage-identity-providers}",
"clientRole": true
},
{
"name": "manage-clients",
"description": "${role_manage-clients}",
"clientRole": true
},
{
"name": "manage-realm",
"description": "${role_manage-realm}",
"clientRole": true
},
{
"name": "view-clients",
"description": "${role_view-clients}",
"composite": true,
"composites": {
"client": {
"realm-management": [
"query-clients"
]
}
},
"clientRole": true
},
{
"name": "view-users",
"description": "${role_view-users}",
"composite": true,
"composites": {
"client": {
"realm-management": [
"query-groups",
"query-users"
]
}
},
"clientRole": true
},
{
"name": "view-events",
"description": "${role_view-events}",
"clientRole": true
},
{
"name": "query-realms",
"description": "${role_query-realms}",
"clientRole": true
},
{
"name": "create-client",
"description": "${role_create-client}",
"clientRole": true
},
{
"name": "query-clients",
"description": "${role_query-clients}",
"clientRole": true
},
{
"name": "view-authorization",
"description": "${role_view-authorization}",
"clientRole": true
}
],
"account": [
{
"name": "view-applications",
"description": "${role_view-applications}",
"clientRole": true
},
{
"name": "manage-account-links",
"description": "${role_manage-account-links}",
"clientRole": true
},
{
"name": "delete-account",
"description": "${role_delete-account}",
"clientRole": true
},
{
"name": "view-consent",
"description": "${role_view-consent}",
"clientRole": true
},
{
"name": "manage-consent",
"description": "${role_manage-consent}",
"composite": true,
"composites": {
"client": {
"account": [
"view-consent"
]
}
},
"clientRole": true
},
{
"name": "view-profile",
"description": "${role_view-profile}",
"clientRole": true
},
{
"name": "manage-account",
"description": "${role_manage-account}",
"composite": true,
"composites": {
"client": {
"account": [
"manage-account-links"
]
}
},
"clientRole": true
}
],
"alfresco": [
{
"name": "admin",
@ -596,14 +1160,8 @@
}
],
"realmRoles": [
"user"
"default-roles-test"
],
"clientRoles": {
"account": [
"view-profile",
"manage-account"
]
},
"groups": [
"/Test A/Test AB",
"/Test B/Test BA"
@ -623,14 +1181,8 @@
}
],
"realmRoles": [
"user"
],
"clientRoles": {
"account": [
"view-profile",
"manage-account"
]
}
"default-roles-test"
]
},
{
"id": "ssuper",
@ -646,13 +1198,9 @@
}
],
"realmRoles": [
"user"
"default-roles-test"
],
"clientRoles": {
"account": [
"view-profile",
"manage-account"
],
"alfresco": [
"admin"
]

@ -27,7 +27,9 @@ keycloak.adapter.credentials.secret=6f70a28f-98cd-41ca-8f2f-368a8797d708
# localhost in auth-server-url won't work for direct access in a Docker deployment
keycloak.adapter.directAuthHost=http://keycloak:8080
keycloak.roles.requiredClientScopes=alfresco-role-service
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
keycloak.synchronization.requiredClientScopes=realm-management
keycloak.synchronization.requiredClientScopes=alfresco-authority-sync

@ -216,6 +216,45 @@
}
]
},
{
"name": "microprofile-jwt",
"description": "Microprofile - JWT built-in scope",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "upn",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "upn",
"jsonType.label": "String"
}
},
{
"name": "groups",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"user.attribute": "foo",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "groups",
"jsonType.label": "String"
}
}
]
},
{
"name": "email",
"description": "OpenID Connect built-in scope: email",
@ -325,6 +364,28 @@
}
]
},
{
"name": "role_list",
"description": "SAML role list",
"protocol": "saml",
"attributes": {
"consent.screen.text": "${samlRoleListScopeConsentText}",
"display.on.consent.screen": "true"
},
"protocolMappers": [
{
"name": "role list",
"protocol": "saml",
"protocolMapper": "saml-role-list-mapper",
"consentRequired": false,
"config": {
"single": "false",
"attribute.nameformat": "Basic",
"attribute.name": "Role"
}
}
]
},
{
"name": "roles",
"description": "OpenID Connect scope for add user roles to the access token",
@ -393,7 +454,7 @@
]
},
{
"name": "realm-management",
"name": "alfresco-authority-sync",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
@ -401,20 +462,26 @@
},
"protocolMappers": [
{
"name": "Realm Management Client Roles",
"name": "Realm Management Audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "false",
"included.client.audience": "realm-management",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "resource_access.realm-management.roles",
"jsonType.label": "String",
"usermodel.clientRoleMapping.clientId": "realm-management"
"access.token.claim": "true"
}
},
}
]
},
{
"name": "alfresco-role-service",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "Realm Management Audience",
"protocol": "openid-connect",
@ -436,21 +503,6 @@
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "Alfresco Client Roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "false",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "resource_access.alfresco.roles",
"jsonType.label": "String",
"usermodel.clientRoleMapping.clientId": "alfresco"
}
},
{
"name": "Alfresco Audience",
"protocol": "openid-connect",
@ -472,21 +524,6 @@
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "Alfresco Share Client Roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "false",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "resource_access.alfresco-share.roles",
"jsonType.label": "String",
"usermodel.clientRoleMapping.clientId": "alfresco-share"
}
},
{
"name": "Alfresco Share Audience",
"protocol": "openid-connect",
@ -537,12 +574,62 @@
"profile",
"email",
"roles",
"role_list",
"web-origins"
],
"defaultOptionalClientScopes": [
"address",
"microprofile-jwt",
"phone"
],
"scopeMappings": [
{
"clientScope": "offline_access",
"roles": [
"offline_access"
]
},
{
"clientScope": "alfresco",
"roles": [
"user"
]
}
],
"clientScopeMappings": {
"realm-management": [
{
"clientScope": "alfresco-authority-sync",
"roles": [
"view-users",
"query-groups",
"query-users"
]
},
{
"clientScope": "alfresco-role-service",
"roles": [
"view-clients"
]
}
],
"account": [
{
"client": "account-console",
"roles": [
"manage-account"
]
}
],
"alfresco": [
{
"clientScope": "alfresco",
"roles": [
"admin"
]
}
]
},
"clients": [
{
"id": "alfresco",
@ -564,16 +651,18 @@
"serviceAccountsEnabled": true,
"publicClient": false,
"protocol": "openid-connect",
"fullScopeAllowed": false,
"defaultClientScopes": [
"profile",
"email",
"address",
"phone",
"realm-roles",
"roles",
"alfresco"
],
"optionalClientScopes": [
"realm-management"
"alfresco-authority-sync",
"alfresco-role-service"
]
},
{
@ -594,8 +683,9 @@
"secret": "a5b3e8bc-39cc-4ddd-8c8f-1c34e7a35975",
"publicClient": false,
"protocol": "openid-connect",
"fullScopeAllowed": false,
"defaultClientScopes": [
"realm-roles",
"roles",
"alfresco-share"
]
},
@ -750,10 +840,318 @@
}
]
}
},
{
"clientId": "account",
"name": "${client_account}",
"rootUrl": "${authBaseUrl}",
"baseUrl": "/realms/test/account/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"/realms/test/account/*"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"defaultClientScopes": [
"web-origins",
"roles",
"profile",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
},
{
"clientId": "account-console",
"name": "${client_account-console}",
"rootUrl": "${authBaseUrl}",
"baseUrl": "/realms/test/account/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"/realms/test/account/*"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"pkce.code.challenge.method": "S256"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"name": "audience resolve",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-resolve-mapper",
"consentRequired": false
}
],
"defaultClientScopes": [
"web-origins",
"roles",
"profile",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
}
],
"roles": {
"realm": [
{
"name": "uma_authorization",
"description": "${role_uma_authorization}"
},
{
"name": "default-roles-test",
"description": "${role_default-roles}",
"composite": true,
"composites": {
"realm": [
"offline_access",
"uma_authorization",
"user"
],
"client": {
"account": [
"view-profile",
"manage-account"
]
}
}
},
{
"name": "offline_access",
"description": "${role_offline-access}"
}
],
"client": {
"realm-management": [
{
"name": "view-identity-providers",
"description": "${role_view-identity-providers}",
"clientRole": true
},
{
"name": "manage-users",
"description": "${role_manage-users}",
"clientRole": true
},
{
"name": "query-groups",
"description": "${role_query-groups}",
"clientRole": true
},
{
"name": "query-users",
"description": "${role_query-users}",
"clientRole": true
},
{
"name": "realm-admin",
"description": "${role_realm-admin}",
"composite": true,
"composites": {
"client": {
"realm-management": [
"view-identity-providers",
"manage-users",
"query-groups",
"query-users",
"view-realm",
"impersonation",
"manage-events",
"manage-authorization",
"manage-identity-providers",
"manage-clients",
"manage-realm",
"view-users",
"view-clients",
"view-events",
"query-realms",
"create-client",
"query-clients",
"view-authorization"
]
}
},
"clientRole": true
},
{
"name": "view-realm",
"description": "${role_view-realm}",
"clientRole": true
},
{
"name": "impersonation",
"description": "${role_impersonation}",
"clientRole": true
},
{
"name": "manage-events",
"description": "${role_manage-events}",
"clientRole": true
},
{
"name": "manage-authorization",
"description": "${role_manage-authorization}",
"clientRole": true
},
{
"name": "manage-identity-providers",
"description": "${role_manage-identity-providers}",
"clientRole": true
},
{
"name": "manage-clients",
"description": "${role_manage-clients}",
"clientRole": true
},
{
"name": "manage-realm",
"description": "${role_manage-realm}",
"clientRole": true
},
{
"name": "view-clients",
"description": "${role_view-clients}",
"composite": true,
"composites": {
"client": {
"realm-management": [
"query-clients"
]
}
},
"clientRole": true
},
{
"name": "view-users",
"description": "${role_view-users}",
"composite": true,
"composites": {
"client": {
"realm-management": [
"query-groups",
"query-users"
]
}
},
"clientRole": true
},
{
"name": "view-events",
"description": "${role_view-events}",
"clientRole": true
},
{
"name": "query-realms",
"description": "${role_query-realms}",
"clientRole": true
},
{
"name": "create-client",
"description": "${role_create-client}",
"clientRole": true
},
{
"name": "query-clients",
"description": "${role_query-clients}",
"clientRole": true
},
{
"name": "view-authorization",
"description": "${role_view-authorization}",
"clientRole": true
}
],
"account": [
{
"name": "view-applications",
"description": "${role_view-applications}",
"clientRole": true
},
{
"name": "manage-account-links",
"description": "${role_manage-account-links}",
"clientRole": true
},
{
"name": "delete-account",
"description": "${role_delete-account}",
"clientRole": true
},
{
"name": "view-consent",
"description": "${role_view-consent}",
"clientRole": true
},
{
"name": "manage-consent",
"description": "${role_manage-consent}",
"composite": true,
"composites": {
"client": {
"account": [
"view-consent"
]
}
},
"clientRole": true
},
{
"name": "view-profile",
"description": "${role_view-profile}",
"clientRole": true
},
{
"name": "manage-account",
"description": "${role_manage-account}",
"composite": true,
"composites": {
"client": {
"account": [
"manage-account-links"
]
}
},
"clientRole": true
}
],
"alfresco": [
{
"name": "admin",
@ -821,14 +1219,8 @@
}
],
"realmRoles": [
"user"
"default-roles-test"
],
"clientRoles": {
"account": [
"view-profile",
"manage-account"
]
},
"groups": [
"/Test A/Test AB",
"/Test B/Test BA"
@ -848,14 +1240,8 @@
}
],
"realmRoles": [
"user"
],
"clientRoles": {
"account": [
"view-profile",
"manage-account"
]
}
"default-roles-test"
]
},
{
"id": "ssuper",
@ -871,13 +1257,9 @@
}
],
"realmRoles": [
"user"
"default-roles-test"
],
"clientRoles": {
"account": [
"view-profile",
"manage-account"
],
"alfresco": [
"admin"
]