Merge branch 'feature/login-plugin' into feature/newuser-concurrency

This commit is contained in:
2025-01-09 14:10:07 -05:00
8 changed files with 913 additions and 43 deletions

View File

@@ -10,7 +10,7 @@ cache.${moduleId}.ssoToSessionCache.maxIdleSeconds=0
cache.${moduleId}.ssoToSessionCache.cluster.type=fully-distributed
cache.${moduleId}.ssoToSessionCache.backup-count=1
cache.${moduleId}.ssoToSessionCache.eviction-policy=LRU
cache.${moduleId}.ssoToSessionCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
cache.${moduleId}.ssoToSessionCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.${moduleId}.ssoToSessionCache.readBackupData=false
# explicitly not clearable - should be cleared via Keycloak back-channel action
cache.${moduleId}.ssoToSessionCache.clearable=false
@@ -23,7 +23,7 @@ cache.${moduleId}.sessionToSsoCache.maxIdleSeconds=0
cache.${moduleId}.sessionToSsoCache.cluster.type=fully-distributed
cache.${moduleId}.sessionToSsoCache.backup-count=1
cache.${moduleId}.sessionToSsoCache.eviction-policy=LRU
cache.${moduleId}.sessionToSsoCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
cache.${moduleId}.sessionToSsoCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.${moduleId}.sessionToSsoCache.readBackupData=false
# explicitly not clearable - should be cleared via Keycloak back-channel action
cache.${moduleId}.sessionToSsoCache.clearable=false
@@ -36,7 +36,7 @@ cache.${moduleId}.principalToSessionCache.maxIdleSeconds=0
cache.${moduleId}.principalToSessionCache.cluster.type=fully-distributed
cache.${moduleId}.principalToSessionCache.backup-count=1
cache.${moduleId}.principalToSessionCache.eviction-policy=LRU
cache.${moduleId}.principalToSessionCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
cache.${moduleId}.principalToSessionCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.${moduleId}.principalToSessionCache.readBackupData=false
# explicitly not clearable - should be cleared via Keycloak back-channel action
cache.${moduleId}.principalToSessionCache.clearable=false
@@ -49,7 +49,7 @@ cache.${moduleId}.sessionToPrincipalCache.maxIdleSeconds=0
cache.${moduleId}.sessionToPrincipalCache.cluster.type=fully-distributed
cache.${moduleId}.sessionToPrincipalCache.backup-count=1
cache.${moduleId}.sessionToPrincipalCache.eviction-policy=LRU
cache.${moduleId}.sessionToPrincipalCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
cache.${moduleId}.sessionToPrincipalCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.${moduleId}.sessionToPrincipalCache.readBackupData=false
# explicitly not clearable - should be cleared via Keycloak back-channel action
cache.${moduleId}.sessionToPrincipalCache.clearable=false
@@ -62,7 +62,7 @@ cache.${moduleId}.ticketTokenCache.maxIdleSeconds=0
cache.${moduleId}.ticketTokenCache.cluster.type=fully-distributed
cache.${moduleId}.ticketTokenCache.backup-count=1
cache.${moduleId}.ticketTokenCache.eviction-policy=LRU
cache.${moduleId}.ticketTokenCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
cache.${moduleId}.ticketTokenCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.${moduleId}.ticketTokenCache.readBackupData=false
# dangerous to be cleared, as roles / claims can no longer be mapped
# would always be better to just invalidate the tickets themselves

View File

@@ -171,9 +171,14 @@ public class KeycloakTokenGroupSyncProcessor implements TokenProcessor, Initiali
if (this.syncGroupMembershipOnLogin)
{
AuthenticationUtil.runAsSystem(() -> this.transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
boolean changed = this.syncGroupMemberships(groups);
if (changed)
LOGGER.debug("Group membership changed: {}", accessToken.getSubject());
boolean changed = this.syncGroupMemberships(accessToken.getPreferredUsername(), groups);
if (changed) {
String ticket = this.authenticationService.getCurrentTicket();
if (ticket != null) {
LOGGER.debug("Invalidating Alfresco ticket as group membership changed: {}", ticket);
this.authenticationService.invalidateTicket(ticket);
}
}
return null;
}, false, requiresNew));
}
@@ -238,15 +243,14 @@ public class KeycloakTokenGroupSyncProcessor implements TokenProcessor, Initiali
* the Alfresco group authorities as determined from the Keycloak access token for the current user
* @return true if group membership changed
*/
protected boolean syncGroupMemberships(final Collection<String> groups)
protected boolean syncGroupMemberships(String username, final Collection<String> groups)
{
final String userName = AuthenticationUtil.getFullyAuthenticatedUser();
final String maskedUsername = AlfrescoCompatibilityUtil.maskUsername(userName);
final String maskedUsername = AlfrescoCompatibilityUtil.maskUsername(username);
boolean changed = false;
LOGGER.debug("Synchronising group membership for user {} and token extracted groups {}", maskedUsername, groups);
final Set<String> existingUnprocessedGroups = this.authorityService.getContainingAuthorities(AuthorityType.GROUP, userName, true);
final Set<String> existingUnprocessedGroups = this.authorityService.getContainingAuthorities(AuthorityType.GROUP, username, true);
LOGGER.debug("User {} is currently in the groups {}", maskedUsername, existingUnprocessedGroups);
@@ -256,7 +260,7 @@ public class KeycloakTokenGroupSyncProcessor implements TokenProcessor, Initiali
if (!existingUnprocessedGroups.remove(group) && this.authorityService.authorityExists(group))
{
LOGGER.debug("Adding user {} to group {}", maskedUsername, group);
this.authorityService.addAuthority(group, userName);
this.authorityService.addAuthority(group, username);
changed = true;
}
}
@@ -264,7 +268,7 @@ public class KeycloakTokenGroupSyncProcessor implements TokenProcessor, Initiali
for (final String group : existingUnprocessedGroups)
{
LOGGER.debug("Removing user {} from group {}", maskedUsername, group);
this.authorityService.removeAuthority(group, userName);
this.authorityService.removeAuthority(group, username);
changed = true;
}

View File

@@ -151,6 +151,8 @@ public class KeycloakTokenPersonProcessor implements TokenProcessor, Initializin
this.updatePerson(accessToken, idToken);
return null;
}, false, requiresNew);
AuthenticationUtil.setFullyAuthenticatedUser(accessToken.getPreferredUsername());
}
}
@@ -164,16 +166,16 @@ public class KeycloakTokenPersonProcessor implements TokenProcessor, Initializin
*/
protected void updatePerson(final AccessToken accessToken, final IDToken idToken)
{
final String userName = AuthenticationUtil.getFullyAuthenticatedUser();
final String username = accessToken.getPreferredUsername();
LOGGER.debug("Mapping person property updates for user {}", AlfrescoCompatibilityUtil.maskUsername(userName));
LOGGER.debug("Mapping person property updates for user {}", AlfrescoCompatibilityUtil.maskUsername(username));
final NodeRef person = this.personService.getPerson(userName);
final NodeRef person = this.personService.getPerson(username);
final Map<QName, Serializable> updates = new HashMap<>();
this.userProcessors.forEach(processor -> processor.mapUser(accessToken, idToken != null ? idToken : accessToken, updates));
LOGGER.debug("Determined property updates for person node of user {}", AlfrescoCompatibilityUtil.maskUsername(userName));
LOGGER.debug("Determined property updates for person node of user {}", AlfrescoCompatibilityUtil.maskUsername(username));
final Set<QName> propertiesToRemove = updates.keySet().stream().filter(k -> updates.get(k) == null).collect(Collectors.toSet());
updates.keySet().removeAll(propertiesToRemove);

View File

@@ -4,21 +4,20 @@ import com.fasterxml.jackson.core.JsonParseException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.alfresco.util.ParameterCheck;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier;
@@ -38,6 +37,7 @@ import org.keycloak.util.JsonSerialization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.acosix.alfresco.keycloak.repo.util.NameValueMapAdapter;
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
/**
@@ -284,20 +284,20 @@ public class AccessTokenClient
final HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
.path(ServiceUrlConstants.TOKEN_PATH).build(this.deployment.getRealm()));
final List<NameValuePair> formParams = new ArrayList<>();
final List<NameValuePair> formParams = new LinkedList<>();
postParamProvider.accept(formParams);
Map<String, String> formMap = new HashMap<>();
for (NameValuePair pair : formParams)
formMap.put(pair.getName(), pair.getValue());
final List<Header> headers = new LinkedList<>();
ClientCredentialsProviderUtils.setClientCredentials(
this.deployment.getAdapterConfig(),
this.deployment.getClientAuthenticator(),
Collections.emptyMap(),
formMap);
new NameValueMapAdapter<>(headers, BasicHeader.class),
new NameValueMapAdapter<>(formParams, BasicNameValuePair.class));
for (Header header : headers)
post.addHeader(header);
final UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, "UTF-8");
post.setEntity(form);

View File

@@ -0,0 +1,151 @@
package de.acosix.alfresco.keycloak.repo.util;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.apache.http.NameValuePair;
public class NameValueMapAdapter<T extends NameValuePair> implements Map<String, String> {
private final List<? extends NameValuePair> pairs;
private final Class<T> type;
public NameValueMapAdapter(List<? extends NameValuePair> pairs, Class<T> type) {
this.pairs = pairs;
this.type = type;
}
@Override
public void clear() {
this.pairs.clear();
}
@Override
public boolean containsKey(Object key) {
for (NameValuePair pair : this.pairs)
if (pair.getName().equals(key))
return true;
return false;
}
@Override
public boolean containsValue(Object value) {
for (NameValuePair pair : this.pairs)
if (pair.getValue().equals(value))
return true;
return false;
}
@Override
public Set<Entry<String, String>> entrySet() {
Set<Entry<String, String>> set = new HashSet<Entry<String, String>>();
for (NameValuePair pair : this.pairs) {
set.add(new Entry<String, String>() {
@Override
public String getKey() {
return pair.getName();
}
@Override
public String getValue() {
return pair.getValue();
}
@Override
public String setValue(String value) {
throw new UnsupportedOperationException();
}
});
}
return set;
}
@Override
public String get(Object key) {
for (NameValuePair pair : this.pairs)
if (pair.getName().equals(key))
return pair.getValue();
return null;
}
@Override
public boolean isEmpty() {
return this.pairs.isEmpty();
}
@Override
public Set<String> keySet() {
Set<String> set = new HashSet<>();
for (NameValuePair pair : this.pairs)
set.add(pair.getName());
return set;
}
@Override
public String put(String key, String value) {
ListIterator<NameValuePair> i = (ListIterator<NameValuePair>) this.pairs.listIterator();
while (i.hasNext()) {
NameValuePair pair = i.next();
if (pair.getName().equals(key)) {
i.remove();
i.add(this.newNameValuePair(key, value));
return pair.getValue();
}
}
i.add(this.newNameValuePair(key, value));
return null;
}
@Override
public void putAll(Map<? extends String, ? extends String> m) {
for (Entry<? extends String, ? extends String> e : m.entrySet())
this.put(e.getKey(), e.getValue());
}
@Override
public String remove(Object key) {
ListIterator<NameValuePair> i = (ListIterator<NameValuePair>) this.pairs.listIterator();
while (i.hasNext()) {
NameValuePair pair = i.next();
if (pair.getName().equals(key)) {
i.remove();
return pair.getValue();
}
}
return null;
}
@Override
public int size() {
return this.pairs.size();
}
@Override
public Collection<String> values() {
List<String> list = new ArrayList<>(this.pairs.size());
for (NameValuePair pair : this.pairs)
list.add(pair.getValue());
return list;
}
private T newNameValuePair(String key, String value) {
try {
Constructor<T> constructor = this.type.getConstructor(String.class, String.class);
return constructor.newInstance(key, value);
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new AlfrescoRuntimeException(e.getMessage(), e);
}
}
}