Skip to content

Commit 1329209

Browse files
committed
feat(security,hibernate-orm): support security annotations on repositories
1 parent e25d7c6 commit 1329209

File tree

36 files changed

+1959
-10
lines changed

36 files changed

+1959
-10
lines changed

docs/src/main/asciidoc/hibernate-orm.adoc

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1949,6 +1949,76 @@ Please refer to the corresponding https://hibernate.org/repositories/[Hibernate
19491949
and https://jakarta.ee/specifications/data/1.0/jakarta-data-1.0[Jakarta Data]
19501950
guides to learn what else they have to offer.
19511951

1952+
==== Secure Jakarta Data repositories
1953+
1954+
Quarkus Security can secure Jakarta Data Repositories with security annotations.
1955+
1956+
.Example Jakarta Data repository with method-level security annotation
1957+
[source,java]
1958+
----
1959+
@Repository
1960+
public interface MyRepository extends CrudRepository<MyEntity, Integer> {
1961+
1962+
@RolesAllowed("admin")
1963+
@Delete
1964+
void delete(String name);
1965+
1966+
}
1967+
----
1968+
1969+
.Example Jakarta Data repository with class-level security annotation
1970+
[source,java]
1971+
----
1972+
@Authenticated
1973+
@Repository
1974+
public interface MyRepository extends CrudRepository<MyEntity, Integer> {
1975+
1976+
@Delete
1977+
void delete(String name);
1978+
1979+
}
1980+
----
1981+
1982+
[WARNING]
1983+
====
1984+
Only the methods directly declared on the `MyRepository` interface require authentication.
1985+
Methods inherited from `CrudRepository`, such as the `insert` method, are not secured by the interface-level annotation.
1986+
====
1987+
1988+
[IMPORTANT]
1989+
====
1990+
Generic interface methods using type variables or wildcards cannot currently be secured reliably with standard security annotations.
1991+
For example, consider the generic method `findAll` with the type variable `T` shown below:
1992+
1993+
[source,java]
1994+
----
1995+
public interface MyParentRepository<T extends MyEntity> {
1996+
1997+
@Find
1998+
Stream<T> findAll(Order<T> order);
1999+
2000+
}
2001+
2002+
@Repository
2003+
public interface MyRepository extends MyParentRepository<MyEntity> {
2004+
}
2005+
----
2006+
2007+
You can either apply a security annotation to the calling method in your service layer or replace the type variable `T` with a concrete type, as demonstrated below:
2008+
2009+
[source,java]
2010+
----
2011+
@Repository
2012+
public interface MyRepository {
2013+
2014+
@PermissionsAllowed("find-all")
2015+
@Find
2016+
Stream<MyEntity> findAll(Order<MyEntity> order);
2017+
2018+
}
2019+
----
2020+
====
2021+
19522022
[[configuration-reference]]
19532023
== Configuration Reference for Hibernate ORM
19542024

extensions/hibernate-orm/deployment/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@
6565
<groupId>io.quarkus</groupId>
6666
<artifactId>quarkus-reactive-datasource-spi</artifactId>
6767
</dependency>
68+
<dependency>
69+
<groupId>io.quarkus</groupId>
70+
<artifactId>quarkus-security-spi</artifactId>
71+
</dependency>
6872

6973
<dependency>
7074
<groupId>io.quarkus</groupId>

extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import static io.quarkus.hibernate.orm.deployment.util.HibernateProcessorUtil.setDialectAndStorageEngine;
1111
import static io.quarkus.hibernate.orm.deployment.util.HibernateProcessorUtil.xmlMapperKind;
1212
import static io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME;
13+
import static io.quarkus.security.spi.SecuredInterfaceAnnotationBuildItem.ofClassAnnotation;
14+
import static io.quarkus.security.spi.SecuredInterfaceAnnotationBuildItem.ofMethodAnnotation;
1315

1416
import java.io.IOException;
1517
import java.net.URL;
@@ -44,6 +46,9 @@
4446
import jakarta.persistence.PersistenceUnitTransactionType;
4547
import jakarta.xml.bind.JAXBElement;
4648

49+
import org.hibernate.annotations.processing.Find;
50+
import org.hibernate.annotations.processing.HQL;
51+
import org.hibernate.annotations.processing.SQL;
4752
import org.hibernate.boot.archive.scan.spi.ClassDescriptor;
4853
import org.hibernate.boot.archive.scan.spi.PackageDescriptor;
4954
import org.hibernate.boot.beanvalidation.BeanValidationIntegrator;
@@ -150,6 +155,7 @@
150155
import io.quarkus.reactive.datasource.spi.ReactiveDataSourceBuildItem;
151156
import io.quarkus.runtime.LaunchMode;
152157
import io.quarkus.runtime.configuration.ConfigurationException;
158+
import io.quarkus.security.spi.SecuredInterfaceAnnotationBuildItem;
153159
import net.bytebuddy.description.type.TypeDescription;
154160
import net.bytebuddy.dynamic.ClassFileLocator;
155161
import net.bytebuddy.dynamic.DynamicType;
@@ -171,10 +177,17 @@ public final class HibernateOrmProcessor {
171177

172178
public static final String HIBERNATE_ORM_CONFIG_PREFIX = "quarkus.hibernate-orm.";
173179

180+
/**
181+
* Collection of Hibernate annotations for which, if detected on interface, Hibernate processor generates repository.
182+
*/
183+
public static final Set<Class<?>> HIBERNATE_REPOSITORY_ANNOTATIONS = Set.of(Find.class, HQL.class, SQL.class);
184+
174185
private static final Logger LOG = Logger.getLogger(HibernateOrmProcessor.class);
175186

176187
private static final String INTEGRATOR_SERVICE_FILE = "META-INF/services/org.hibernate.integrator.spi.Integrator";
177188

189+
private static final String JAKARTA_DATA_REPOSITORY_ANNOTATION = "jakarta.data.repository.Repository";
190+
178191
@BuildStep
179192
NativeImageFeatureBuildItem registerServicesForReflection(BuildProducer<ServiceProviderBuildItem> services) {
180193
for (DotName serviceProvider : ClassNames.SERVICE_PROVIDERS) {
@@ -856,6 +869,16 @@ public void registerInjectServiceMethodsForReflection(CombinedIndexBuildItem ind
856869
}
857870
}
858871

872+
@BuildStep
873+
void registerJakartaDataRepositorySecurityAnnotations(Capabilities capabilities,
874+
BuildProducer<SecuredInterfaceAnnotationBuildItem> securedInterfaceAnnotationProducer) {
875+
if (capabilities.isPresent(Capability.SECURITY)) {
876+
securedInterfaceAnnotationProducer.produce(ofClassAnnotation(JAKARTA_DATA_REPOSITORY_ANNOTATION));
877+
HIBERNATE_REPOSITORY_ANNOTATIONS
878+
.forEach(annotation -> securedInterfaceAnnotationProducer.produce(ofMethodAnnotation(annotation)));
879+
}
880+
}
881+
859882
private void handleHibernateORMWithNoPersistenceXml(
860883
HibernateOrmConfig hibernateOrmConfig,
861884
CombinedIndexBuildItem index,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package io.quarkus.hibernate.orm.deployment;
2+
3+
import static java.util.stream.Collectors.toSet;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
6+
import java.io.File;
7+
import java.io.IOException;
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.Target;
11+
import java.net.URL;
12+
import java.util.Arrays;
13+
import java.util.HashSet;
14+
import java.util.List;
15+
import java.util.Set;
16+
17+
import org.assertj.core.api.Assertions;
18+
import org.hibernate.Hibernate;
19+
import org.hibernate.annotations.processing.Find;
20+
import org.jboss.jandex.AnnotationInstance;
21+
import org.jboss.jandex.ClassInfo;
22+
import org.jboss.jandex.DotName;
23+
import org.jboss.jandex.Index;
24+
import org.junit.jupiter.api.BeforeAll;
25+
import org.junit.jupiter.api.Test;
26+
27+
import io.quarkus.deployment.index.IndexingUtil;
28+
29+
/**
30+
* Tests that the list of the 'org.hibernate.annotations.processing' package annotations for which Hibernate Processor
31+
* creates Hibernate repository is up-to-date.
32+
*/
33+
public class HibernateRepositoryAnnotationsTest {
34+
35+
private static final DotName RETENTION = DotName.createSimple(Retention.class.getName());
36+
private static final DotName TARGET = DotName.createSimple(Target.class.getName());
37+
private static final String ANNOTATIONS_PACKAGE = Find.class.getPackage().getName();
38+
39+
private static Index hibernateIndex;
40+
41+
@BeforeAll
42+
public static void index() throws IOException {
43+
hibernateIndex = IndexingUtil.indexJar(determineHibernateJarLocation());
44+
}
45+
46+
@Test
47+
void testAllRepositoryDefiningAnnotationsListed() {
48+
var allProcessingAnnotations = findAllProcessingAnnotations();
49+
var knowRepositoryDefiningAnnotations = HibernateOrmProcessor.HIBERNATE_REPOSITORY_ANNOTATIONS.stream()
50+
.map(Class::getName).collect(toSet());
51+
assertThat(allProcessingAnnotations)
52+
.isNotEmpty()
53+
.contains(knowRepositoryDefiningAnnotations.toArray(new String[0]));
54+
if (knowRepositoryDefiningAnnotations.size() != allProcessingAnnotations.size()) {
55+
for (String annotation : allProcessingAnnotations) {
56+
if (!knowRepositoryDefiningAnnotations.contains(annotation)) {
57+
// if this ever happen, we can introduce a whitelist
58+
Assertions.fail("Annotation " + annotation + " has not been vetted yet."
59+
+ "Please verify this annotation cannot be used as a repository defining annotation.");
60+
}
61+
}
62+
}
63+
}
64+
65+
private static Set<String> findAllProcessingAnnotations() {
66+
Set<String> annotations = new HashSet<>();
67+
for (AnnotationInstance retentionAnnotation : hibernateIndex.getAnnotations(RETENTION)) {
68+
ClassInfo annotation = retentionAnnotation.target().asClass();
69+
if (annotation.name().packagePrefix().equals(ANNOTATIONS_PACKAGE) && allowsMethodTargetType(annotation)) {
70+
annotations.add(annotation.name().toString());
71+
}
72+
}
73+
return annotations;
74+
}
75+
76+
private static boolean allowsMethodTargetType(ClassInfo annotation) {
77+
AnnotationInstance targetAnnotation = annotation.declaredAnnotation(TARGET);
78+
if (targetAnnotation == null) {
79+
// Can target anything
80+
return true;
81+
}
82+
83+
List<String> allowedTargetTypes = Arrays.asList(targetAnnotation.value().asEnumArray());
84+
return allowedTargetTypes.contains(ElementType.METHOD.name());
85+
}
86+
87+
private static File determineHibernateJarLocation() {
88+
URL url = Hibernate.class.getProtectionDomain().getCodeSource().getLocation();
89+
if (!url.getProtocol().equals("file")) {
90+
throw new IllegalStateException("Hibernate JAR is not a local file? " + url);
91+
}
92+
return new File(url.getPath());
93+
}
94+
}

extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
import io.quarkus.security.spi.PermissionsAllowedMetaAnnotationBuildItem;
145145
import io.quarkus.security.spi.RegisterClassSecurityCheckBuildItem;
146146
import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem;
147+
import io.quarkus.security.spi.SecuredInterfaceAnnotationBuildItem;
147148
import io.quarkus.security.spi.SecurityTransformer;
148149
import io.quarkus.security.spi.SecurityTransformer.AuthorizationType;
149150
import io.quarkus.security.spi.SecurityTransformerBuildItem;
@@ -169,14 +170,24 @@ public class SecurityProcessor {
169170

170171
@BuildStep
171172
SecurityTransformerBuildItem createSecurityTransformerBuildItem(
173+
List<SecuredInterfaceAnnotationBuildItem> securedInterfacePredicates,
172174
List<AdditionalSecurityAnnotationBuildItem> additionalSecurityAnnotationBuildItems) {
173175
// collect security annotations
174176
Map<AuthorizationType, Set<DotName>> authorizationTypeToSecurityAnnotations = new EnumMap<>(AuthorizationType.class);
175177
authorizationTypeToSecurityAnnotations.put(SECURITY_CHECK, new HashSet<>(SECURITY_CHECK_ANNOTATIONS));
176178
additionalSecurityAnnotationBuildItems.forEach(i -> authorizationTypeToSecurityAnnotations
177179
.computeIfAbsent(i.getAuthorizationType(), k -> new HashSet<>()).add(i.getSecurityAnnotationName()));
178180

179-
return new SecurityTransformerBuildItem(authorizationTypeToSecurityAnnotations);
181+
Predicate<ClassInfo> isInterfaceWithTransformations = securedInterfacePredicates.stream()
182+
.map(SecuredInterfaceAnnotationBuildItem::getIsInterfaceWithTransformations)
183+
.reduce(Predicate::or)
184+
.orElse(null);
185+
Set<DotName> securedAnnotations = securedInterfacePredicates.stream()
186+
.map(SecuredInterfaceAnnotationBuildItem::getAnnotationName)
187+
.collect(Collectors.toSet());
188+
189+
return new SecurityTransformerBuildItem(authorizationTypeToSecurityAnnotations, isInterfaceWithTransformations,
190+
securedAnnotations);
180191
}
181192

182193
@BuildStep
@@ -188,6 +199,19 @@ List<AdditionalIndexedClassesBuildItem> registerAdditionalIndexedClassesBuildIte
188199
.of(new AdditionalIndexedClassesBuildItem(securityTransformerBuildItem.getAllSecurityAnnotationNames()));
189200
}
190201

202+
@BuildStep
203+
void secureInterfaceImplementations(SecurityTransformerBuildItem securityTransformerBuildItem,
204+
CombinedIndexBuildItem combinedIndexBuildItem,
205+
BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformerProducer) {
206+
SecurityTransformer securityTransformer = createSecurityTransformer(
207+
combinedIndexBuildItem.getIndex(), securityTransformerBuildItem);
208+
var annotationTransformations = securityTransformer.getInterfaceTransformations();
209+
if (annotationTransformations != null) {
210+
annotationTransformations
211+
.forEach(i -> annotationsTransformerProducer.produce(new AnnotationsTransformerBuildItem(i)));
212+
}
213+
}
214+
191215
/**
192216
* Create JCAProviderBuildItems for any configured provider names
193217
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package io.quarkus.security.test.cdi;
2+
3+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import java.lang.annotation.ElementType;
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.RetentionPolicy;
9+
import java.lang.annotation.Target;
10+
11+
import jakarta.annotation.security.DenyAll;
12+
13+
import org.jboss.shrinkwrap.api.ShrinkWrap;
14+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
15+
import org.junit.jupiter.api.Assertions;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.extension.RegisterExtension;
18+
19+
import io.quarkus.security.spi.SecuredInterfaceAnnotationBuildItem;
20+
import io.quarkus.test.QuarkusUnitTest;
21+
22+
/**
23+
* Type variables are currently not supported for secured interfaces.
24+
*/
25+
public class SecurityAnnotationWithTypeVariableValidationFailureTest {
26+
27+
@RegisterExtension
28+
static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap
29+
.create(JavaArchive.class)
30+
.addClasses(SecuredInterface.class, ParametrizedType.class, Repository.class, SecuredInterfaceImpl.class))
31+
.assertException(throwable -> {
32+
assertInstanceOf(RuntimeException.class, throwable);
33+
String exceptionMessage = throwable.getMessage();
34+
assertTrue(exceptionMessage.contains("Unable to determine if the"),
35+
() -> "Unexpected exception message: " + exceptionMessage);
36+
assertTrue(exceptionMessage.contains("SecuredInterfaceImpl#securedMethod"),
37+
() -> "Unexpected exception message: " + exceptionMessage);
38+
assertTrue(exceptionMessage.contains("method should inherit security annotation"),
39+
() -> "Unexpected exception message: " + exceptionMessage);
40+
assertTrue(exceptionMessage.contains("SecuredInterface#securedMethod"),
41+
() -> "Unexpected exception message: " + exceptionMessage);
42+
})
43+
.addBuildChainCustomizer(buildChainBuilder -> buildChainBuilder
44+
.addBuildStep(context -> context
45+
.produce(SecuredInterfaceAnnotationBuildItem.ofClassAnnotation(Repository.class.getName())))
46+
.produces(SecuredInterfaceAnnotationBuildItem.class).build());
47+
48+
@Test
49+
public void runTest() {
50+
Assertions.fail("This test should not run");
51+
}
52+
53+
@Repository
54+
public interface SecuredInterface<T> {
55+
56+
@DenyAll
57+
Object securedMethod(ParametrizedType<T> securedAnnotation);
58+
59+
}
60+
61+
public static class SecuredInterfaceImpl implements SecuredInterface<String> {
62+
63+
@Override
64+
public Object securedMethod(ParametrizedType<String> securedAnnotation) {
65+
return null;
66+
}
67+
}
68+
69+
public static class ParametrizedType<T> {
70+
71+
}
72+
73+
@Retention(RetentionPolicy.RUNTIME)
74+
@Target(ElementType.TYPE)
75+
public @interface Repository {
76+
77+
}
78+
}

0 commit comments

Comments
 (0)