Skip to content

Commit c8221aa

Browse files
committed
Prevent access to EntityManager when looking up PersistenceProvider.
Signed-off-by: Ariel Morelli Andres <[email protected]> Closes: #3425 Original pull request: #3885
1 parent 71ef321 commit c8221aa

File tree

7 files changed

+254
-10
lines changed

7 files changed

+254
-10
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*;
2020

2121
import jakarta.persistence.EntityManager;
22+
import jakarta.persistence.EntityManagerFactory;
2223
import jakarta.persistence.Query;
2324
import jakarta.persistence.metamodel.IdentifiableType;
2425
import jakarta.persistence.metamodel.Metamodel;
@@ -36,6 +37,7 @@
3637
import org.hibernate.ScrollMode;
3738
import org.hibernate.ScrollableResults;
3839
import org.hibernate.proxy.HibernateProxy;
40+
3941
import org.springframework.data.util.CloseableIterator;
4042
import org.springframework.lang.Nullable;
4143
import org.springframework.transaction.support.TransactionSynchronizationManager;
@@ -52,6 +54,7 @@
5254
* @author Jens Schauder
5355
* @author Greg Turnquist
5456
* @author Yuriy Tsarkov
57+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
5558
*/
5659
public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment {
5760

@@ -64,14 +67,14 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
6467
* @see <a href="https://github.com/spring-projects/spring-data-jpa/issues/846">DATAJPA-444</a>
6568
*/
6669
HIBERNATE(//
70+
Collections.singletonList(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), //
6771
Collections.singletonList(HIBERNATE_ENTITY_MANAGER_INTERFACE), //
6872
Collections.singletonList(HIBERNATE_JPA_METAMODEL_TYPE)) {
6973

7074
@Override
7175
public String extractQueryString(Query query) {
7276
return HibernateUtils.getHibernateQuery(query);
7377
}
74-
7578
/**
7679
* Return custom placeholder ({@code *}) as Hibernate does create invalid queries for count queries for objects with
7780
* compound keys.
@@ -114,7 +117,8 @@ public String getCommentHintKey() {
114117
/**
115118
* EclipseLink persistence provider.
116119
*/
117-
ECLIPSELINK(Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE),
120+
ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1, ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2),
121+
Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE),
118122
Collections.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) {
119123

120124
@Override
@@ -147,12 +151,14 @@ public String getCommentHintKey() {
147151
public String getCommentHintValue(String comment) {
148152
return "/* " + comment + " */";
149153
}
154+
150155
},
151156

152157
/**
153158
* Unknown special provider. Use standard JPA.
154159
*/
155-
GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) {
160+
GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE),
161+
Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) {
156162

157163
@Nullable
158164
@Override
@@ -199,6 +205,7 @@ public String getCommentHintKey() {
199205
private static final Collection<PersistenceProvider> ALL = List.of(HIBERNATE, ECLIPSELINK, GENERIC_JPA);
200206

201207
private static final ConcurrentReferenceHashMap<Class<?>, PersistenceProvider> CACHE = new ConcurrentReferenceHashMap<>();
208+
private final Iterable<String> entityManagerFactoryClassNames;
202209
private final Iterable<String> entityManagerClassNames;
203210
private final Iterable<String> metamodelClassNames;
204211

@@ -207,24 +214,38 @@ public String getCommentHintKey() {
207214
/**
208215
* Creates a new {@link PersistenceProvider}.
209216
*
217+
* @param entityManagerFactoryClassNames the names of the provider specific
218+
* {@link jakarta.persistence.EntityManagerFactory} implementations. Must not be {@literal null} or empty.
210219
* @param entityManagerClassNames the names of the provider specific {@link EntityManager} implementations. Must not
211220
* be {@literal null} or empty.
212221
* @param metamodelClassNames must not be {@literal null}.
213222
*/
214-
PersistenceProvider(Iterable<String> entityManagerClassNames, Iterable<String> metamodelClassNames) {
223+
PersistenceProvider(Iterable<String> entityManagerFactoryClassNames, Iterable<String> entityManagerClassNames,
224+
Iterable<String> metamodelClassNames) {
215225

226+
this.entityManagerFactoryClassNames = entityManagerFactoryClassNames;
216227
this.entityManagerClassNames = entityManagerClassNames;
217228
this.metamodelClassNames = metamodelClassNames;
218229

219230
boolean present = false;
220-
for (String entityManagerClassName : entityManagerClassNames) {
231+
for (String emfClassName : entityManagerFactoryClassNames) {
221232

222-
if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) {
233+
if (ClassUtils.isPresent(emfClassName, PersistenceProvider.class.getClassLoader())) {
223234
present = true;
224235
break;
225236
}
226237
}
227238

239+
if (!present) {
240+
for (String entityManagerClassName : entityManagerClassNames) {
241+
242+
if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) {
243+
present = true;
244+
break;
245+
}
246+
}
247+
}
248+
228249
this.present = present;
229250
}
230251

@@ -269,6 +290,36 @@ public static PersistenceProvider fromEntityManager(EntityManager em) {
269290
return cacheAndReturn(entityManagerType, GENERIC_JPA);
270291
}
271292

293+
/**
294+
* Determines the {@link PersistenceProvider} from the given {@link EntityManagerFactory}. If no special one can be
295+
* determined {@link #GENERIC_JPA} will be returned.
296+
*
297+
* @param emf must not be {@literal null}.
298+
* @return will never be {@literal null}.
299+
*/
300+
public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) {
301+
302+
Assert.notNull(emf, "EntityManagerFactory must not be null");
303+
304+
Class<?> entityManagerType = emf.getPersistenceUnitUtil().getClass();
305+
PersistenceProvider cachedProvider = CACHE.get(entityManagerType);
306+
307+
if (cachedProvider != null) {
308+
return cachedProvider;
309+
}
310+
311+
for (PersistenceProvider provider : ALL) {
312+
for (String emfClassName : provider.entityManagerFactoryClassNames) {
313+
if (isOfType(emf.getPersistenceUnitUtil(), emfClassName,
314+
emf.getPersistenceUnitUtil().getClass().getClassLoader())) {
315+
return cacheAndReturn(entityManagerType, provider);
316+
}
317+
}
318+
}
319+
320+
return cacheAndReturn(entityManagerType, GENERIC_JPA);
321+
}
322+
272323
/**
273324
* Determines the {@link PersistenceProvider} from the given {@link Metamodel}. If no special one can be determined
274325
* {@link #GENERIC_JPA} will be returned.
@@ -354,13 +405,20 @@ public boolean isPresent() {
354405
*/
355406
interface Constants {
356407

408+
String GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE = "jakarta.persistence.EntityManagerFactory";
357409
String GENERIC_JPA_ENTITY_MANAGER_INTERFACE = "jakarta.persistence.EntityManager";
410+
411+
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryDelegate";
412+
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl";
358413
String ECLIPSELINK_ENTITY_MANAGER_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManager";
414+
359415
// needed as Spring only exposes that interface via the EM proxy
416+
String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.jpa.internal.PersistenceUnitUtilImpl";
360417
String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.engine.spi.SessionImplementor";
361418

362419
String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel";
363420
String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl";
421+
364422
}
365423

366424
public CloseableIterator<Object> executeQueryWithResultStream(Query jpaQuery) {
@@ -465,5 +523,7 @@ public void close() {
465523
scrollableCursor.close();
466524
}
467525
}
526+
468527
}
528+
469529
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CrudMethodMetadataUnitTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717

1818
import static org.mockito.Mockito.*;
1919

20-
import java.util.Collections;
21-
import java.util.Map;
22-
2320
import jakarta.persistence.EntityManager;
2421
import jakarta.persistence.EntityManagerFactory;
2522
import jakarta.persistence.LockModeType;
@@ -28,6 +25,9 @@
2825
import jakarta.persistence.criteria.CriteriaQuery;
2926
import jakarta.persistence.metamodel.Metamodel;
3027

28+
import java.util.Collections;
29+
import java.util.Map;
30+
3131
import org.junit.jupiter.api.BeforeEach;
3232
import org.junit.jupiter.api.Test;
3333
import org.junit.jupiter.api.extension.ExtendWith;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2011-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository;
17+
18+
import java.util.Optional;
19+
20+
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
21+
import org.jspecify.annotations.Nullable;
22+
23+
/**
24+
* {@code CurrentTenantIdentifierResolver} instance for testing
25+
*
26+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
27+
*/
28+
public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver<String> {
29+
private static final ThreadLocal<@Nullable String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>();
30+
31+
public static void setTenantIdentifier(String tenantIdentifier) {
32+
CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier);
33+
}
34+
35+
public static void removeTenantIdentifier() {
36+
CURRENT_TENANT_IDENTIFIER.remove();
37+
}
38+
39+
@Override
40+
public String resolveCurrentTenantIdentifier() {
41+
return Optional.ofNullable(CURRENT_TENANT_IDENTIFIER.get())
42+
.orElseThrow(() -> new IllegalArgumentException("Could not resolve current tenant identifier"));
43+
}
44+
45+
@Override
46+
public boolean validateExistingCurrentSessions() {
47+
return true;
48+
}
49+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2011-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
import static org.assertj.core.api.Assumptions.*;
20+
21+
import java.util.List;
22+
23+
import org.junit.jupiter.api.AfterEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.context.annotation.ComponentScan;
28+
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.context.annotation.FilterType;
30+
import org.springframework.context.annotation.ImportResource;
31+
import org.springframework.data.jpa.domain.sample.Role;
32+
import org.springframework.data.jpa.provider.PersistenceProvider;
33+
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
34+
import org.springframework.data.jpa.repository.sample.RoleRepository;
35+
import org.springframework.test.context.ContextConfiguration;
36+
import org.springframework.test.context.junit.jupiter.SpringExtension;
37+
import org.springframework.transaction.annotation.Transactional;
38+
39+
import jakarta.persistence.EntityManager;
40+
41+
/**
42+
* Tests for repositories that use multi-tenancy. This tests verifies that repositories can be created an injected
43+
* despite not having a tenant available at creation time
44+
*
45+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
46+
*/
47+
@ExtendWith(SpringExtension.class)
48+
@ContextConfiguration()
49+
class HibernateMultitenancyTests {
50+
51+
@Autowired RoleRepository roleRepository;
52+
@Autowired EntityManager em;
53+
54+
@AfterEach
55+
void tearDown() {
56+
HibernateCurrentTenantIdentifierResolver.removeTenantIdentifier();
57+
}
58+
59+
@Test
60+
void testPersistenceProviderFromFactoryWithoutTenant() {
61+
PersistenceProvider provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory());
62+
assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE);
63+
}
64+
65+
@Test
66+
void testRepositoryWithTenant() {
67+
HibernateCurrentTenantIdentifierResolver.setTenantIdentifier("tenant-id");
68+
assertThatNoException().isThrownBy(() -> roleRepository.findAll());
69+
}
70+
71+
@Test
72+
void testRepositoryWithoutTenantFails() {
73+
assertThatThrownBy(() -> roleRepository.findAll()).isInstanceOf(RuntimeException.class);
74+
}
75+
76+
@Transactional
77+
List<Role> insertAndQuery() {
78+
roleRepository.save(new Role("DRUMMER"));
79+
roleRepository.flush();
80+
return roleRepository.findAll();
81+
}
82+
83+
@ImportResource({ "classpath:multitenancy-test.xml" })
84+
@Configuration
85+
@EnableJpaRepositories(basePackageClasses = HibernateRepositoryTests.class, considerNestedRepositories = true,
86+
includeFilters = @ComponentScan.Filter(classes = { RoleRepository.class }, type = FilterType.ASSIGNABLE_TYPE))
87+
static class TestConfig {}
88+
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.mockito.Mockito.*;
1919

2020
import jakarta.persistence.EntityManager;
21+
import jakarta.persistence.EntityManagerFactory;
2122
import jakarta.persistence.metamodel.Metamodel;
2223

2324
import java.lang.reflect.Method;
@@ -53,6 +54,7 @@
5354
*
5455
* @author Christoph Strobl
5556
* @author Mark Paluch
57+
* @author Ariel Morelli Andres
5658
*/
5759
class AbstractStringBasedJpaQueryUnitTests {
5860

@@ -135,10 +137,12 @@ static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQu
135137
public EntityManager get() {
136138

137139
EntityManager em = Mockito.mock(EntityManager.class);
140+
EntityManagerFactory emf = Mockito.mock(EntityManagerFactory.class);
138141

139142
Metamodel meta = mock(Metamodel.class);
140143
when(em.getMetamodel()).thenReturn(meta);
141144
when(em.getDelegate()).thenReturn(new Object()); // some generic jpa
145+
when(em.getEntityManagerFactory()).thenReturn(emf);
142146

143147
return em;
144148
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
import org.mockito.junit.jupiter.MockitoExtension;
4545
import org.mockito.junit.jupiter.MockitoSettings;
4646
import org.mockito.quality.Strictness;
47-
4847
import org.springframework.data.domain.PageRequest;
4948
import org.springframework.data.jpa.domain.sample.User;
5049
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
@@ -60,6 +59,7 @@
6059
* @author Jens Schauder
6160
* @author Greg Turnquist
6261
* @author Yanming Zhou
62+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
6363
*/
6464
@ExtendWith(MockitoExtension.class)
6565
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -84,6 +84,9 @@ class SimpleJpaRepositoryUnitTests {
8484
void setUp() {
8585

8686
when(em.getDelegate()).thenReturn(em);
87+
when(em.getEntityManagerFactory()).thenReturn(entityManagerFactory);
88+
89+
when(entityManagerFactory.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil);
8790

8891
when(information.getJavaType()).thenReturn(User.class);
8992
when(em.getCriteriaBuilder()).thenReturn(builder);

0 commit comments

Comments
 (0)