Skip to content

Commit 53e8725

Browse files
committed
feat(security,hibernate-orm): support security annotations on repositories
1 parent 7c776a4 commit 53e8725

File tree

36 files changed

+1805
-10
lines changed

36 files changed

+1805
-10
lines changed

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1949,6 +1949,37 @@ 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); <1>
1978+
1979+
}
1980+
----
1981+
<1> Only the methods directly declared on the `MyRepository` interface require authentication. Methods inherited from `CrudRepository`, such as the `insert` method, are not secured by the interface-level annotation.
1982+
19521983
[[configuration-reference]]
19531984
== Configuration Reference for Hibernate ORM
19541985

docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ It is possible to use multiple expressions in the role definition.
790790
In development mode, it allows any authenticated user.
791791
<5> Property expression `all-roles` will be treated as a collection type `List`, therefore, the endpoint will be accessible for roles `Administrator`, `Software`, `Tester` and `User`.
792792

793+
[[security-annotations-inheritance]]
793794
=== Endpoint security annotations and Jakarta REST inheritance
794795

795796
Quarkus supports security annotations placed on the endpoint implementation or its class like in the example below:

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
*/
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package io.quarkus.security.spi;
2+
3+
import java.util.Objects;
4+
import java.util.function.Predicate;
5+
6+
import org.jboss.jandex.ClassInfo;
7+
import org.jboss.jandex.DotName;
8+
9+
import io.quarkus.builder.item.MultiBuildItem;
10+
11+
/**
12+
* Security annotations on interfaces are in most cases not inherited by interface implementors.
13+
* This build item allows to register interfaces whose implementors will inherit security annotations.
14+
* If this build item is used extensively, it can get very complex, and we would create situations,
15+
* where users can hardly tell precedence, like what will happen if different repeatable annotation instance
16+
* (like the {@code PermissionsAllowed} annotation) is placed on both implemented and interface method.
17+
* Or does method-level interface annotation take precedence over implementors class-level annotation.
18+
* Examples could continue, for which reason we aim to support simple cases for now.
19+
* <p>
20+
* The all the implementors of interfaces matched by this build item annotation will inherit
21+
* security annotations from interface methods and the interface class-level security annotations only
22+
* apply directly on the methods declared on the interface. All scenarios that are supposed to work are tested.
23+
* Any scenario that is not working is not yet supported.
24+
*/
25+
public final class SecuredInterfaceAnnotationBuildItem extends MultiBuildItem {
26+
27+
private enum SecuredAnnotationTargetKind {
28+
METHOD,
29+
CLASS
30+
}
31+
32+
private final DotName annotationName;
33+
34+
private final SecuredAnnotationTargetKind targetKind;
35+
36+
private SecuredInterfaceAnnotationBuildItem(DotName annotationName, SecuredAnnotationTargetKind targetKind) {
37+
this.annotationName = Objects.requireNonNull(annotationName);
38+
this.targetKind = targetKind;
39+
}
40+
41+
public Predicate<ClassInfo> getIsInterfaceWithTransformations() {
42+
return switch (targetKind) {
43+
case CLASS -> ci -> ci.isInterface() && ci.hasDeclaredAnnotation(annotationName);
44+
case METHOD -> ci -> ci.isInterface() && ci.hasAnnotation(annotationName);
45+
};
46+
}
47+
48+
public DotName getAnnotationName() {
49+
return annotationName;
50+
}
51+
52+
public static SecuredInterfaceAnnotationBuildItem ofClassAnnotation(String annotationName) {
53+
return new SecuredInterfaceAnnotationBuildItem(DotName.createSimple(annotationName), SecuredAnnotationTargetKind.CLASS);
54+
}
55+
56+
public static SecuredInterfaceAnnotationBuildItem ofMethodAnnotation(Class<?> annotation) {
57+
return new SecuredInterfaceAnnotationBuildItem(DotName.createSimple(annotation), SecuredAnnotationTargetKind.METHOD);
58+
}
59+
}

0 commit comments

Comments
 (0)