Add inverse mapping / check

This commit is contained in:
AFaust
2021-01-08 13:46:16 +01:00
parent ea2a2ee43a
commit 89d8ecc5dc
11 changed files with 436 additions and 10 deletions

View File

@@ -130,7 +130,22 @@ public interface IDMClient
* the processor handling the loaded roles * the processor handling the loaded roles
* @return the number of processed roles * @return the number of processed roles
*/ */
int processRoles(int offset, int roleBatchSize, Consumer<RoleRepresentation> roleProcessor); 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. * Loads and processes a batch of client roles from Keycloak using an externally specified processor.
@@ -145,5 +160,22 @@ public interface IDMClient
* the processor handling the loaded roles * the processor handling the loaded roles
* @return the number of processed roles * @return the number of processed roles
*/ */
int processRoles(String clientId, int offset, int roleBatchSize, Consumer<RoleRepresentation> roleProcessor); 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);
} }

View File

@@ -310,7 +310,7 @@ public class IDMClientImpl implements InitializingBean, IDMClient
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public int processRoles(final int offset, final int userBatchSize, final Consumer<RoleRepresentation> roleProcessor) public int processRealmRoles(final int offset, final int userBatchSize, final Consumer<RoleRepresentation> roleProcessor)
{ {
ParameterCheck.mandatory("roleProcessor", roleProcessor); ParameterCheck.mandatory("roleProcessor", roleProcessor);
@@ -335,7 +335,35 @@ public class IDMClientImpl implements InitializingBean, IDMClient
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public int processRoles(final String clientId, final int offset, final int userBatchSize, 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) final Consumer<RoleRepresentation> roleProcessor)
{ {
ParameterCheck.mandatoryString("clientId", clientId); ParameterCheck.mandatoryString("clientId", clientId);
@@ -358,6 +386,35 @@ public class IDMClientImpl implements InitializingBean, IDMClient
return processedRoles; 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. * Loads and processes a batch of generic entities from Keycloak.
* *

View File

@@ -19,6 +19,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -73,6 +74,7 @@ public class AggregateRoleNameMapper implements InitializingBean, RoleNameMapper
@Override @Override
public Optional<String> mapRoleName(final String roleName) public Optional<String> mapRoleName(final String roleName)
{ {
ParameterCheck.mandatoryString("roleName", roleName);
LOGGER.debug("Mapping role {} using granular mappers {}", roleName, this.granularMappers); LOGGER.debug("Mapping role {} using granular mappers {}", roleName, this.granularMappers);
Optional<String> mappedName = Optional.empty(); Optional<String> mappedName = Optional.empty();
for (final RoleNameMapper mapper : this.granularMappers) for (final RoleNameMapper mapper : this.granularMappers)
@@ -87,4 +89,24 @@ public class AggregateRoleNameMapper implements InitializingBean, RoleNameMapper
return mappedName; return mappedName;
} }
/**
* {@inheritDoc}
*/
@Override
public Optional<String> mapAuthorityName(final String authorityName)
{
ParameterCheck.mandatoryString("authorityName", authorityName);
LOGGER.debug("Mapping authority name {} using granular mappers {}", authorityName, this.granularMappers);
Optional<String> mappedName = Optional.empty();
for (final RoleNameMapper mapper : this.granularMappers)
{
mappedName = mapper.mapAuthorityName(authorityName).map(name -> this.upperCaseRoles ? name.toLowerCase(Locale.ENGLISH) : name);
if (mappedName.isPresent())
{
LOGGER.debug("Mapped authority name {} to {} using granular mapper {}", authorityName, mappedName, mapper);
break;
}
}
return mappedName;
}
} }

View File

@@ -17,6 +17,7 @@ package de.acosix.alfresco.keycloak.repo.roles;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* This no-op implementation class of a role service may be used as a default implemenation in a subsystem proxy to avoid failing if no * This no-op implementation class of a role service may be used as a default implemenation in a subsystem proxy to avoid failing if no
@@ -87,4 +88,33 @@ public class NoOpRoleServiceImpl implements RoleService
return Collections.emptyList(); return Collections.emptyList();
} }
/**
*
* {@inheritDoc}
*/
@Override
public boolean isMappedFromKeycloak(final String authorityName)
{
return false;
}
/**
*
* {@inheritDoc}
*/
@Override
public Optional<String> getRoleName(final String authorityName)
{
return Optional.empty();
}
/**
*
* {@inheritDoc}
*/
@Override
public Optional<String> getClientFromRole(final String authorityName)
{
return Optional.empty();
}
} }

View File

@@ -18,6 +18,7 @@ package de.acosix.alfresco.keycloak.repo.roles;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.regex.Pattern;
import org.alfresco.util.ParameterCheck; import org.alfresco.util.ParameterCheck;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -35,6 +36,8 @@ public class PatternRoleNameMapper implements RoleNameMapper
protected Map<String, String> patternMappings; protected Map<String, String> patternMappings;
protected Map<String, String> patternInverseMappings;
protected boolean upperCaseRoles; protected boolean upperCaseRoles;
/** /**
@@ -46,6 +49,15 @@ public class PatternRoleNameMapper implements RoleNameMapper
this.patternMappings = patternMappings; this.patternMappings = patternMappings;
} }
/**
* @param patternInverseMappings
* the patternInverseMappings to set
*/
public void setPatternInverseMappings(final Map<String, String> patternInverseMappings)
{
this.patternInverseMappings = patternInverseMappings;
}
/** /**
* @param upperCaseRoles * @param upperCaseRoles
* the upperCaseRoles to set * the upperCaseRoles to set
@@ -75,7 +87,6 @@ public class PatternRoleNameMapper implements RoleNameMapper
LOGGER.debug("Mapped role {} to {}", roleName, mappedName); LOGGER.debug("Mapped role {} to {}", roleName, mappedName);
return mappedName; return mappedName;
}).map(name -> this.upperCaseRoles ? name.toUpperCase(Locale.ENGLISH) : name); }).map(name -> this.upperCaseRoles ? name.toUpperCase(Locale.ENGLISH) : name);
;
if (!result.isPresent()) if (!result.isPresent())
{ {
@@ -86,4 +97,32 @@ public class PatternRoleNameMapper implements RoleNameMapper
return result; return result;
} }
/**
* {@inheritDoc}
*/
@Override
public Optional<String> mapAuthorityName(final String authorityName)
{
ParameterCheck.mandatoryString("authorityName", authorityName);
Optional<String> result = Optional.empty();
if (this.patternInverseMappings != null)
{
final Optional<String> matchingPattern = this.patternMappings.keySet().stream().filter(pattern -> Pattern
.compile(pattern, this.upperCaseRoles ? Pattern.CASE_INSENSITIVE : 0).matcher(authorityName).matches()).findFirst();
result = matchingPattern.map(pattern -> {
final String replacement = this.patternMappings.get(pattern);
LOGGER.debug("Authority name {} matches inverse mapping pattern {} - applying replacement pattern {}", authorityName,
pattern, replacement);
final String mappedName = Pattern.compile(pattern, this.upperCaseRoles ? Pattern.CASE_INSENSITIVE : 0)
.matcher(authorityName).replaceAll(replacement);
LOGGER.debug("Mapped authority name {} to {}", authorityName, mappedName);
return mappedName;
}).map(name -> this.upperCaseRoles ? name.toLowerCase(Locale.ENGLISH) : name);
}
return result;
}
} }

View File

@@ -70,10 +70,30 @@ public class PrefixAttachingRoleNameMapper implements RoleNameMapper
final String mappedName = this.prefix + roleName; final String mappedName = this.prefix + roleName;
LOGGER.debug("Mapped role {} to {} using prefix attachment", roleName, mappedName); LOGGER.debug("Mapped role {} to {} using prefix attachment", roleName, mappedName);
result = Optional.of(mappedName).map(name -> this.upperCaseRoles ? name.toUpperCase(Locale.ENGLISH) : name); result = Optional.of(mappedName).map(name -> this.upperCaseRoles ? name.toUpperCase(Locale.ENGLISH) : name);
;
} }
return result; return result;
} }
@Override
public Optional<String> mapAuthorityName(final String authorityName)
{
ParameterCheck.mandatoryString("authorityName", authorityName);
Optional<String> result = Optional.empty();
if (this.prefix != null)
{
final String ciAuthorityName = authorityName.toLowerCase(Locale.ENGLISH);
final String ciPrefix = this.prefix.toLowerCase(Locale.ENGLISH);
if (ciAuthorityName.startsWith(ciPrefix))
{
final String mappedName = authorityName.substring(this.prefix.length());
LOGGER.debug("Mapped authority name {} to {} using prefix removal", authorityName, mappedName);
result = Optional.of(mappedName);
}
}
return result;
}
} }

View File

@@ -37,4 +37,14 @@ public interface RoleNameMapper
* operation * operation
*/ */
Optional<String> mapRoleName(String roleName); Optional<String> mapRoleName(String roleName);
/**
* Maps the name of an Alfresco authority to the name of a Keycloak role. This operation should act like the inverse of the
* {@link #mapRoleName(String) original inbound mapping}.
*
* @param authorityName
* the Alfresco authority name
* @return the name of the Keycloak role
*/
Optional<String> mapAuthorityName(String authorityName);
} }

View File

@@ -16,6 +16,7 @@
package de.acosix.alfresco.keycloak.repo.roles; package de.acosix.alfresco.keycloak.repo.roles;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* Instances of this interface allow for lookup / retrieval of Keycloak roles. * Instances of this interface allow for lookup / retrieval of Keycloak roles.
@@ -88,4 +89,31 @@ public interface RoleService
*/ */
List<Role> findRoles(String resourceName, String shortNameFilter); List<Role> findRoles(String resourceName, String shortNameFilter);
/**
* Checks whether the specified authority name is a role mapped from Keycloak.
*
* @param authorityName
* the Alfresco authority name to check
* @return {@code true} if the authority name matches any expected patterns of roles mapped from Keycloak, {@code false} otherwise
*/
boolean isMappedFromKeycloak(String authorityName);
/**
* Retrieves the name of the original Keycloak role from which the specified authority name was mapped from within Keycloak.
*
* @param authorityName
* the Alfresco authority name to process
* @return the name of the Keycloak role from which the authority name was mapped unless the role was not mapped from Keycloak
*/
Optional<String> getRoleName(String authorityName);
/**
* Retrieves the name of the client the specified authority name was mapped from within Keycloak.
*
* @param authorityName
* the Alfresco authority name to process
* @return the name of the client which defines the role unless the role is either not mapped from Keycloak or mapped from the realm
* scope
*/
Optional<String> getClientFromRole(String authorityName);
} }

View File

@@ -18,11 +18,16 @@ package de.acosix.alfresco.keycloak.repo.roles;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BinaryOperator;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.alfresco.service.cmr.security.AuthorityType; import org.alfresco.service.cmr.security.AuthorityType;
@@ -36,7 +41,7 @@ import org.springframework.beans.factory.InitializingBean;
import de.acosix.alfresco.keycloak.repo.client.IDMClient; import de.acosix.alfresco.keycloak.repo.client.IDMClient;
public class RoleServiceImpl implements InitializingBean, RoleService public class RoleServiceImpl implements RoleService, InitializingBean
{ {
private static final Logger LOGGER = LoggerFactory.getLogger(RoleServiceImpl.class); private static final Logger LOGGER = LoggerFactory.getLogger(RoleServiceImpl.class);
@@ -280,6 +285,134 @@ public class RoleServiceImpl implements InitializingBean, RoleService
return this.doFindRoles(resourceName, shortNameFilter); return this.doFindRoles(resourceName, shortNameFilter);
} }
/**
*
* {@inheritDoc}
*/
@Override
public boolean isMappedFromKeycloak(final String authorityName)
{
ParameterCheck.mandatoryString("authorityName", authorityName);
Optional<String> role = Optional.empty();
if (this.processRealmRoles)
{
role = this.realmRoleNameMapper.mapAuthorityName(authorityName);
}
if (this.processResourceRoles)
{
final Iterator<String> resourceIterator = this.resourceRoleNameMapper.keySet().iterator();
while (!role.isPresent() && resourceIterator.hasNext())
{
final RoleNameMapper roleNameMapper = this.resourceRoleNameMapper.get(resourceIterator.next());
role = roleNameMapper.mapAuthorityName(authorityName);
}
}
return role.isPresent();
}
/**
*
* {@inheritDoc}
*/
@Override
public Optional<String> getRoleName(final String authorityName)
{
ParameterCheck.mandatoryString("authorityName", authorityName);
Optional<String> role = Optional.empty();
if (this.processRealmRoles)
{
final UnaryOperator<String> realmRoleResolver = rn -> {
final Set<String> matchingRoles = new HashSet<>();
this.idmClient.processRealmRoles(rn, 0, Integer.MAX_VALUE, roleResult -> {
if (roleResult.getName().equalsIgnoreCase(rn))
{
matchingRoles.add(roleResult.getName());
}
});
String matchingRole = null;
if (matchingRoles.size() == 1)
{
matchingRole = matchingRoles.iterator().next();
}
else
{
LOGGER.warn("Failed to match apparent Keycloak realm role {} to unique role via admin API", rn);
}
return matchingRole;
};
role = this.realmRoleNameMapper.mapAuthorityName(authorityName).map(realmRoleResolver);
}
if (this.processResourceRoles)
{
final BinaryOperator<String> clientRoleResolver = (client, rn) -> {
final Set<String> matchingRoles = new HashSet<>();
this.idmClient.processClientRoles(client, rn, 0, Integer.MAX_VALUE, roleResult -> {
if (roleResult.getName().equalsIgnoreCase(rn))
{
matchingRoles.add(roleResult.getName());
}
});
String matchingRole = null;
if (matchingRoles.size() == 1)
{
matchingRole = matchingRoles.iterator().next();
}
else
{
LOGGER.warn("Failed to match apparent Keycloak role {} from client {} to unique role via admin API", rn, client);
}
return matchingRole;
};
final Iterator<String> resourceIterator = this.resourceRoleNameMapper.keySet().iterator();
while (!role.isPresent() && resourceIterator.hasNext())
{
final String resource = resourceIterator.next();
final RoleNameMapper roleNameMapper = this.resourceRoleNameMapper.get(resource);
role = roleNameMapper.mapAuthorityName(authorityName).map(rn -> clientRoleResolver.apply(resource, rn));
}
}
return role;
}
/**
*
* {@inheritDoc}
*/
@Override
public Optional<String> getClientFromRole(final String authorityName)
{
ParameterCheck.mandatoryString("authorityName", authorityName);
Optional<String> client = Optional.empty();
Optional<String> role = Optional.empty();
if (this.processRealmRoles)
{
role = this.realmRoleNameMapper.mapAuthorityName(authorityName);
}
if (!role.isPresent() && this.processResourceRoles)
{
final Iterator<String> resourceIterator = this.resourceRoleNameMapper.keySet().iterator();
while (!role.isPresent() && resourceIterator.hasNext())
{
final String resource = resourceIterator.next();
final RoleNameMapper roleNameMapper = this.resourceRoleNameMapper.get(resource);
role = roleNameMapper.mapAuthorityName(authorityName);
if (role.isPresent())
{
client = Optional.of(resource);
}
}
}
return client;
}
protected List<Role> doFindRoles(final String shortNameFilter, final boolean realmOnly) protected List<Role> doFindRoles(final String shortNameFilter, final boolean realmOnly)
{ {
final List<Role> roles; final List<Role> roles;
@@ -480,11 +613,11 @@ public class RoleServiceImpl implements InitializingBean, RoleService
if (clientId != null) if (clientId != null)
{ {
this.idmClient.processRoles(clientId, 0, Integer.MAX_VALUE, processor); this.idmClient.processClientRoles(clientId, 0, Integer.MAX_VALUE, processor);
} }
else else
{ {
this.idmClient.processRoles(0, Integer.MAX_VALUE, processor); this.idmClient.processRealmRoles(0, Integer.MAX_VALUE, processor);
} }
return results; return results;
@@ -508,7 +641,6 @@ public class RoleServiceImpl implements InitializingBean, RoleService
{ {
LOGGER.debug("Excluding role {} as it maps to group authority name {}", role.getName(), r); LOGGER.debug("Excluding role {} as it maps to group authority name {}", role.getName(), r);
} }
;
return allowed; return allowed;
}).map(r -> new Role(r, role.getName(), role.getDescription())); }).map(r -> new Role(r, role.getName(), role.getDescription()));

View File

@@ -145,6 +145,31 @@ public class ScriptRoleService extends BaseScopableProcessorExtension implements
return roleArray; return roleArray;
} }
/**
* Checks whether the specified authority name is a role mapped from Keycloak.
*
* @param authorityName
* the Alfresco authority name to check
* @return {@code true} if the authority name matches any expected patterns of roles mapped from Keycloak, {@code false} otherwise
*/
public boolean isMappedFromKeycloak(final String authorityName)
{
return this.roleService.isMappedFromKeycloak(authorityName);
}
/**
* Retrieves the name of the client the specified authority name was mapped from within Keycloak.
*
* @param authorityName
* the Alfresco authority name to process
* @return the name of the client which defines the role unless the role is either not mapped from Keycloak or mapped from the realm
* scope
*/
public String getClientFromRole(final String authorityName)
{
return this.roleService.getClientFromRole(authorityName).orElse(null);
}
protected Scriptable makeRoleArray(final List<Role> roles) protected Scriptable makeRoleArray(final List<Role> roles)
{ {
final Scriptable sitesArray = Context.getCurrentContext().newArray(this.getScope(), roles.toArray(new Object[0])); final Scriptable sitesArray = Context.getCurrentContext().newArray(this.getScope(), roles.toArray(new Object[0]));

View File

@@ -17,6 +17,7 @@ package de.acosix.alfresco.keycloak.repo.roles;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional; import java.util.Optional;
import org.alfresco.util.ParameterCheck; import org.alfresco.util.ParameterCheck;
@@ -82,4 +83,34 @@ public class StaticRoleNameMapper implements RoleNameMapper
return result; return result;
} }
/**
* {@inheritDoc}
*/
@Override
public Optional<String> mapAuthorityName(final String authorityName)
{
ParameterCheck.mandatoryString("authorityName", authorityName);
Optional<String> result = Optional.empty();
if (this.nameMappings != null)
{
for (final Entry<String, String> entry : this.nameMappings.entrySet())
{
if (entry.getValue().equals(authorityName) || (this.upperCaseRoles && entry.getValue().equalsIgnoreCase(authorityName)))
{
final String mappedName = entry.getKey();
LOGGER.debug("Mapped authority name {} to {} using static mapping", authorityName, mappedName);
result = Optional.of(mappedName);
break;
}
}
if (!result.isPresent())
{
LOGGER.debug("No static mapping applies to authority name {}", authorityName);
}
}
return result;
}
} }