mirror of
https://github.com/bmlong137/alfresco-keycloak.git
synced 2025-05-26 21:44:41 +00:00
Improve case specific scope use + test realm
This commit is contained in:
parent
cff32d017b
commit
4a2f4a5f67
repository/src
main
globalConfig/subsystems/Authentication/keycloak
java/de/acosix/alfresco/keycloak/repo
test/docker
share/src/test/docker
@ -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
|
||||
|
340
repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/AbstractIDMClientImpl.java
Normal file
340
repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/AbstractIDMClientImpl.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
110
repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/IdentitiesClient.java
Normal file
110
repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/IdentitiesClient.java
Normal file
@ -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);
|
||||
|
||||
}
|
201
repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/IdentitiesClientImpl.java
Normal file
201
repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/IdentitiesClientImpl.java
Normal file
@ -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);
|
||||
}
|
154
repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/RolesClientImpl.java
Normal file
154
repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/RolesClientImpl.java
Normal file
@ -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"
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user