Sort order of updates for user properties (#32853)
This should reduce deadlocks on the user property table if the users are updated concurrently. Closes #32852 Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
parent
8ef7007e3c
commit
2a95d0abfa
7 changed files with 18 additions and 16 deletions
|
@ -18,15 +18,10 @@
|
||||||
package org.keycloak.storage.ldap.mappers;
|
package org.keycloak.storage.ldap.mappers;
|
||||||
|
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.storage.ldap.LDAPConfig;
|
import org.keycloak.storage.ldap.LDAPConfig;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Possibly add "priority" instead of hardcoding behaviour
|
* TODO: Possibly add "priority" instead of hardcoding behaviour
|
||||||
|
@ -67,7 +62,7 @@ public class LDAPMappersComparator {
|
||||||
if (isO2AttrMapper) {
|
if (isO2AttrMapper) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return compareWithStableOrdering(o1, o2);
|
||||||
}
|
}
|
||||||
} else if (!isO2AttrMapper) {
|
} else if (!isO2AttrMapper) {
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -82,7 +77,7 @@ public class LDAPMappersComparator {
|
||||||
if (isO2UsernameMapper) {
|
if (isO2UsernameMapper) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return compareWithStableOrdering(o1, o2);
|
||||||
}
|
}
|
||||||
} else if (!isO2UsernameMapper) {
|
} else if (!isO2UsernameMapper) {
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -98,13 +93,21 @@ public class LDAPMappersComparator {
|
||||||
if (isO2LdapAttr) {
|
if (isO2LdapAttr) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return compareWithStableOrdering(o1, o2);
|
||||||
}
|
}
|
||||||
} else if (!isO2LdapAttr) {
|
} else if (!isO2LdapAttr) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return compareWithStableOrdering(o1, o2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a stable ordering, so the mappers are always executed in the same order.
|
||||||
|
* This can avoid database deadlocks as the mappers will modify attributes always in the same order.
|
||||||
|
*/
|
||||||
|
private static int compareWithStableOrdering(ComponentModel o1, ComponentModel o2) {
|
||||||
|
return o1.getId().compareTo(o2.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -459,7 +459,7 @@ public class JpaIdentityProviderStorageProvider implements IdentityProviderStora
|
||||||
builder.equal(mapper.get("realmId"), getRealm().getId()),
|
builder.equal(mapper.get("realmId"), getRealm().getId()),
|
||||||
builder.equal(mapper.get("identityProviderAlias"), identityProviderAlias));
|
builder.equal(mapper.get("identityProviderAlias"), identityProviderAlias));
|
||||||
|
|
||||||
TypedQuery<IdentityProviderMapperEntity> typedQuery = em.createQuery(query.select(mapper).where(predicate));
|
TypedQuery<IdentityProviderMapperEntity> typedQuery = em.createQuery(query.select(mapper).where(predicate).orderBy(builder.asc(mapper.get("id"))));
|
||||||
|
|
||||||
return closing(typedQuery.getResultStream().map(this::toModel));
|
return closing(typedQuery.getResultStream().map(this::toModel));
|
||||||
}
|
}
|
||||||
|
|
|
@ -905,7 +905,7 @@ public class DefaultExportImportManager implements ExportImportManager {
|
||||||
user.setLastName(userRep.getLastName());
|
user.setLastName(userRep.getLastName());
|
||||||
user.setFederationLink(userRep.getFederationLink());
|
user.setFederationLink(userRep.getFederationLink());
|
||||||
if (userRep.getAttributes() != null) {
|
if (userRep.getAttributes() != null) {
|
||||||
for (Map.Entry<String, List<String>> entry : userRep.getAttributes().entrySet()) {
|
for (Map.Entry<String, List<String>> entry : userRep.getAttributes().entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) {
|
||||||
List<String> value = entry.getValue();
|
List<String> value = entry.getValue();
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
user.setAttribute(entry.getKey(), new ArrayList<>(value));
|
user.setAttribute(entry.getKey(), new ArrayList<>(value));
|
||||||
|
|
|
@ -115,7 +115,7 @@ public final class DefaultUserProfile implements UserProfile {
|
||||||
try {
|
try {
|
||||||
Map<String, List<String>> writable = new HashMap<>(attributes.getWritable());
|
Map<String, List<String>> writable = new HashMap<>(attributes.getWritable());
|
||||||
|
|
||||||
for (Map.Entry<String, List<String>> attribute : writable.entrySet()) {
|
for (Map.Entry<String, List<String>> attribute : writable.entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) {
|
||||||
String name = attribute.getKey();
|
String name = attribute.getKey();
|
||||||
List<String> currentValue = user.getAttributeStream(name)
|
List<String> currentValue = user.getAttributeStream(name)
|
||||||
.filter(Objects::nonNull).collect(Collectors.toList());
|
.filter(Objects::nonNull).collect(Collectors.toList());
|
||||||
|
|
|
@ -35,7 +35,6 @@ import org.keycloak.services.messages.Messages;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -87,7 +86,7 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
|
||||||
if (federatedUser != null) {
|
if (federatedUser != null) {
|
||||||
federatedUser.setEnabled(true);
|
federatedUser.setEnabled(true);
|
||||||
|
|
||||||
for (Map.Entry<String, List<String>> attr : serializedCtx.getAttributes().entrySet()) {
|
for (Map.Entry<String, List<String>> attr : serializedCtx.getAttributes().entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) {
|
||||||
if (!UserModel.USERNAME.equalsIgnoreCase(attr.getKey())) {
|
if (!UserModel.USERNAME.equalsIgnoreCase(attr.getKey())) {
|
||||||
federatedUser.setAttribute(attr.getKey(), attr.getValue());
|
federatedUser.setAttribute(attr.getKey(), attr.getValue());
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.broker.provider.ExchangeExternalToken;
|
import org.keycloak.broker.provider.ExchangeExternalToken;
|
||||||
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
|
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
|
||||||
import org.keycloak.broker.provider.IdentityProvider;
|
import org.keycloak.broker.provider.IdentityProvider;
|
||||||
import org.keycloak.broker.provider.IdentityProviderFactory;
|
|
||||||
import org.keycloak.broker.provider.IdentityProviderMapper;
|
import org.keycloak.broker.provider.IdentityProviderMapper;
|
||||||
import org.keycloak.broker.provider.IdentityProviderMapperSyncModeDelegate;
|
import org.keycloak.broker.provider.IdentityProviderMapperSyncModeDelegate;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
|
@ -677,7 +676,7 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure user attributes are updated based on attributes set to the context
|
// make sure user attributes are updated based on attributes set to the context
|
||||||
for (Map.Entry<String, List<String>> attr : context.getAttributes().entrySet()) {
|
for (Map.Entry<String, List<String>> attr : context.getAttributes().entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) {
|
||||||
if (!UserModel.USERNAME.equalsIgnoreCase(attr.getKey())) {
|
if (!UserModel.USERNAME.equalsIgnoreCase(attr.getKey())) {
|
||||||
user.setAttribute(attr.getKey(), attr.getValue());
|
user.setAttribute(attr.getKey(), attr.getValue());
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,6 +294,7 @@ public class UserResource {
|
||||||
.getProviderFactoriesStream(RequiredActionProvider.class)
|
.getProviderFactoriesStream(RequiredActionProvider.class)
|
||||||
.map(ProviderFactory::getId)
|
.map(ProviderFactory::getId)
|
||||||
.distinct()
|
.distinct()
|
||||||
|
.sorted()
|
||||||
.forEach(action -> {
|
.forEach(action -> {
|
||||||
if (reqActions.contains(action)) {
|
if (reqActions.contains(action)) {
|
||||||
user.addRequiredAction(action);
|
user.addRequiredAction(action);
|
||||||
|
|
Loading…
Reference in a new issue