Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class EntityManagerProxy {

private Set<EntityManagerProxy> entityManagerProxies;
private EntityManager em;
private final KeycloakSession session;
private final boolean batchEnabled;
private final int batchSize;
private int changeCount = 0;
Expand All @@ -67,19 +68,20 @@ public static EntityManager create(KeycloakSession session, EntityManager em, bo
}
boolean batchEnabled = session.getAttributeOrDefault(Constants.STORAGE_BATCH_ENABLED, false);
int batchSize = session.getAttributeOrDefault(Constants.STORAGE_BATCH_SIZE, 100);
return create(em, entityManagerProxies, batchEnabled, batchSize);
return create(session, em, entityManagerProxies, batchEnabled, batchSize);
}

static EntityManager create(EntityManager em, Set<EntityManagerProxy> entityManagerProxies,
static EntityManager create(KeycloakSession session, EntityManager em, Set<EntityManagerProxy> entityManagerProxies,
boolean batchEnabled, int batchSize) {
EntityManagerProxy converter = new EntityManagerProxy(em, entityManagerProxies, batchEnabled, batchSize);
EntityManagerProxy converter = new EntityManagerProxy(session, em, entityManagerProxies, batchEnabled, batchSize);
if (entityManagerProxies != null) {
entityManagerProxies.add(converter);
}
return (EntityManager) Proxy.newProxyInstance(EntityManager.class.getClassLoader(), new Class[]{EntityManager.class}, converter::invoke);
}

private EntityManagerProxy(EntityManager em, Set<EntityManagerProxy> entityManagerProxies, boolean batchEnabled, int batchSize) {
private EntityManagerProxy(KeycloakSession session, EntityManager em, Set<EntityManagerProxy> entityManagerProxies, boolean batchEnabled, int batchSize) {
this.session = session;
this.batchEnabled = batchEnabled;
this.batchSize = batchSize;
this.em = em;
Expand All @@ -96,10 +98,11 @@ EntityManager getEntityManager() {

private Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
boolean batched = EntityManagers.isBatchMode();
boolean readOnly = session != null && session.isReadOnly();
try {
flushInBatchIfEnabled(method);
Object result = method.invoke(em, args);
if (batched && result instanceof Query query) {
if ((batched || readOnly) && result instanceof Query query) {
// TODO: it would be safer if there were a way to validate
// if this or disabling persist/detach where correct for a given batch
// and types were correct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class EntityManagerProxyTest {
public void testClosure() {
HashSet<EntityManagerProxy> proxies = new HashSet<EntityManagerProxy>();
EntityManager em = (EntityManager)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{EntityManager.class}, (proxy, method, args) -> null);
EntityManager proxy = EntityManagerProxy.create(em, proxies, false, 1);
EntityManager proxy = EntityManagerProxy.create(null, em, proxies, false, 1);
assertEquals(1, proxies.size());
proxy.close();
// once closed, the entity manager should not be tracked
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;

import org.keycloak.component.ComponentModel;
import org.keycloak.provider.InvalidationHandler.InvalidableObjectType;
Expand Down Expand Up @@ -127,6 +128,18 @@ default <T> T getAttributeOrDefault(String attribute, T defaultValue) {

Map<String, Object> getAttributes();

/**
* Returns whether this session is currently in read-only mode.
*/
boolean isReadOnly();

/**
* Executes the given supplier in read-only mode. The read-only flag is set before invoking the
* supplier and restored after the supplier returns. If the supplier returns a Stream, the flag
* remains active until the stream is closed, making it safe for lazily consumed streams.
*/
<T> T runAsReadOnly(Supplier<T> supplier);

/**
* Invalidates intermediate states of the given objects, both immediately and at the end of this session.
* @param type Type of the objects to invalidate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.keycloak.common.util.StackUtil;
import org.keycloak.component.ComponentFactory;
Expand Down Expand Up @@ -86,6 +87,7 @@ public abstract class DefaultKeycloakSession implements KeycloakSession {
private VaultTranscriber vaultTranscriber;
private ClientPolicyManager clientPolicyManager;
private boolean closed = false;
private boolean readOnly = false;

public DefaultKeycloakSession(DefaultKeycloakSessionFactory factory) {
this.factory = factory;
Expand Down Expand Up @@ -151,6 +153,33 @@ public Map<String, Object> getAttributes() {
return Collections.unmodifiableMap(attributes);
}

@Override
public boolean isReadOnly() {
return readOnly;
}

@Override
@SuppressWarnings("unchecked")
public <T> T runAsReadOnly(Supplier<T> supplier) {
boolean previous = readOnly;
readOnly = true;
boolean restore = true;
try {
T result = supplier.get();
if (result instanceof Stream<?> stream) {
T closing = (T) stream.onClose(() -> readOnly = previous);
// the close handler is registered, so it takes over restoring the flag
restore = false;
return closing;
}
return result;
} finally {
if (restore) {
readOnly = previous;
}
}
}

@Override
public KeycloakTransactionManager getTransactionManager() {
return transactionManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ private Stream<UserRepresentation> searchForUser(Map<String, String> attributes,
}
}

return toRepresentation(realm, usersEvaluator, briefRepresentation, session.users().searchForUserStream(realm, attributes, firstResult, maxResults));
return session.runAsReadOnly(() -> toRepresentation(realm, usersEvaluator, briefRepresentation, session.users().searchForUserStream(realm, attributes, firstResult, maxResults)));
}

private Stream<UserRepresentation> toRepresentation(RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Stream<UserModel> userModels) {
Expand Down
Loading