Skip to content

Commit 54de65e

Browse files
committed
feat(security-jpa): configure multiple PUs programmatically
1 parent 4f38bfb commit 54de65e

File tree

26 files changed

+1818
-55
lines changed

26 files changed

+1818
-55
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ The same authorization can be required with the `@PermissionsAllowed(value = { "
600600
* xref:security-authentication-mechanisms.adoc#mtls-programmatic-set-up[Set up the mutual TLS client authentication programmatically]
601601
* xref:security-cors.adoc#cors-filter-programmatic-set-up[Configuring the CORS filter programmatically]
602602
* xref:security-csrf-prevention.adoc#csrf-prevention-programmatic-set-up[Configuring the CSRF prevention programmatically]
603+
* xref:security-jpa.adoc#programmatic-set-up[Set up Basic authentication with Jakarta Persistence programmatically]
603604

604605
[[standard-security-annotations]]
605606
== Authorization using annotations

docs/src/main/asciidoc/security-jpa.adoc

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,64 @@ For more information about proactive authentication, see the Quarkus xref:securi
222222

223223
include::{generated-dir}/config/quarkus-security-jpa.adoc[opts=optional, leveloffset=+2]
224224

225+
[[programmatic-set-up]]
226+
== Set up Security JPA programmatically
227+
228+
If you need to combine authentication mechanisms with different identity providers or persistence units, you can leverage the `io.quarkus.vertx.http.security.HttpSecurity` CDI event.
229+
For example, if you combine the built-in Basic and the Form-based authentication mechanisms, you can configure different persistence unit for each of mechanism like in the example below:
230+
231+
[source,java]
232+
----
233+
package org.acme.http.security;
234+
235+
import static io.quarkus.security.jpa.SecurityJpa.jpa;
236+
237+
import io.quarkus.vertx.http.security.Form;
238+
import io.quarkus.vertx.http.security.HttpSecurity;
239+
import jakarta.enterprise.event.Observes;
240+
241+
class HttpSecurityConfiguration {
242+
243+
void configure(@Observes HttpSecurity httpSecurity) {
244+
httpSecurity
245+
.basic(jpa().persistence("basic-pu")) <1>
246+
.mechanism(Form.create(), jpa().persistence("form-pu")); <2>
247+
}
248+
249+
}
250+
----
251+
<1> Configure the Basic authentication to store the users information in the `basic-pu` persistence unit.
252+
<2> Also use the Security JPA identity providers, but this time with the `form-pu` persistence unit.
253+
254+
Another situation when you can use the `io.quarkus.security.jpa.SecurityJpa` API is when you need to apply different `SecurityIdentityAugmentor` to a different authentication mechanism:
255+
256+
[source,java]
257+
----
258+
package org.acme.http.security;
259+
260+
import static io.quarkus.security.jpa.SecurityJpa.jpa;
261+
262+
import io.quarkus.vertx.http.security.Form;
263+
import io.quarkus.vertx.http.security.HttpSecurity;
264+
import jakarta.enterprise.event.Observes;
265+
266+
class HttpSecurityConfiguration {
267+
268+
void configure(@Observes HttpSecurity httpSecurity) {
269+
httpSecurity
270+
.mechanism(Form.create(), jpa().augmentor((securityIdentity, ignored) -> {
271+
// augment the SecurityIdentity produced by the Form-based authentication
272+
return Uni.createFrom().item(securityIdentity);
273+
}))
274+
.basic(jpa().augmentor((securityIdentity, ignored) -> {
275+
// augment the SecurityIdentity produced by the Basic authentication
276+
return Uni.createFrom().item(securityIdentity);
277+
}));
278+
}
279+
280+
}
281+
----
282+
225283
== References
226284

227285
* xref:security-getting-started-tutorial.adoc[Getting started with Security by using Basic authentication and Jakarta Persistence]

extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/QuarkusSecurityJpaCommonProcessor.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
package io.quarkus.security.jpa.common.deployment;
22

33
import static io.quarkus.security.jpa.common.deployment.JpaSecurityIdentityUtil.getSingleAnnotatedElement;
4+
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
5+
import static org.objectweb.asm.Opcodes.ACC_STATIC;
46

57
import java.util.List;
8+
import java.util.Optional;
69

710
import org.jboss.jandex.AnnotationInstance;
811
import org.jboss.jandex.AnnotationTarget;
912
import org.jboss.jandex.ClassInfo;
1013
import org.jboss.jandex.DotName;
1114

15+
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
1216
import io.quarkus.deployment.annotations.BuildProducer;
1317
import io.quarkus.deployment.annotations.BuildStep;
1418
import io.quarkus.deployment.builditem.ApplicationIndexBuildItem;
19+
import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem;
20+
import io.quarkus.gizmo.ClassTransformer;
21+
import io.quarkus.gizmo.MethodDescriptor;
1522
import io.quarkus.security.jpa.Password;
1623
import io.quarkus.security.jpa.Roles;
1724
import io.quarkus.security.jpa.UserDefinition;
1825
import io.quarkus.security.jpa.Username;
26+
import io.quarkus.security.jpa.common.runtime.JpaIdentityProviderUtil;
1927

2028
class QuarkusSecurityJpaCommonProcessor {
2129

@@ -48,4 +56,50 @@ void provideJpaSecurityDefinition(ApplicationIndexBuildItem index, PanacheEntity
4856
}
4957
}
5058

59+
/**
60+
* This method produces {@link io.quarkus.security.jpa.SecurityJpa} CDI bean producer for either Hibernate ORM
61+
* or Hibernate Reactive version of the Quarkus Security JPA.
62+
*/
63+
@BuildStep
64+
void registerSecurityJpaImplCdiBean(BuildProducer<BytecodeTransformerBuildItem> bytecodeTransformerProducer,
65+
BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
66+
Optional<SecurityJpaProviderInfoBuildItem> securityJpaProviderClassBuildItem) {
67+
if (securityJpaProviderClassBuildItem.isPresent()) {
68+
var item = securityJpaProviderClassBuildItem.get();
69+
additionalBeanProducer.produce(AdditionalBeanBuildItem.unremovableOf(item.securityJpaProviderClass));
70+
bytecodeTransformerProducer
71+
.produce(new BytecodeTransformerBuildItem(item.securityJpaProviderClass.getName(), (cls, classVisitor) -> {
72+
var newInstanceUtilMethodDesc = MethodDescriptor.ofMethod(JpaIdentityProviderUtil.class, "newInstance",
73+
Object.class, Class.class);
74+
var classTransformer = new ClassTransformer(cls);
75+
classTransformer.removeMethod("newJpaIdentityProvider", item.jpaIdentityProviderClass);
76+
try (var mc = classTransformer.addMethod("newJpaIdentityProvider", item.jpaIdentityProviderClass)) {
77+
mc.setModifiers(ACC_PRIVATE | ACC_STATIC);
78+
// generates method similar to:
79+
// private static JpaIdentityProvider newJpaIdentityProvider() {
80+
// return JpaIdentityProviderUtil.newInstance(MyEntity__JpaIdentityProviderImpl.class);
81+
// }
82+
var clazz = mc.loadClassFromTCCL(item.jpaIdentityProviderImplName);
83+
// we don't use "new MyEntity__JpaIdentityProviderImpl()" because the generated class needs TCCL
84+
var newInstance = mc.invokeStaticMethod(newInstanceUtilMethodDesc, clazz);
85+
mc.returnValue(newInstance);
86+
}
87+
classTransformer.removeMethod("newJpaTrustedIdentityProvider", item.jpaTrustedIdentityProviderClass);
88+
try (var mc = classTransformer.addMethod("newJpaTrustedIdentityProvider",
89+
item.jpaTrustedIdentityProviderClass)) {
90+
mc.setModifiers(ACC_PRIVATE | ACC_STATIC);
91+
// generates method similar to:
92+
// private static JpaTrustedIdentityProvider newJpaTrustedIdentityProvider() {
93+
// return JpaIdentityProviderUtil.newInstance(MyEntity__JpaTrustedIdentityProviderImpl.class);
94+
// }
95+
var clazz = mc.loadClassFromTCCL(item.jpaTrustedIdentityProviderImplName);
96+
// we don't use "new MyEntity__JpaTrustedIdentityProviderImpl()",
97+
// because the generated class needs TCCL
98+
var newInstance = mc.invokeStaticMethod(newInstanceUtilMethodDesc, clazz);
99+
mc.returnValue(newInstance);
100+
}
101+
return classTransformer.applyTo(classVisitor);
102+
}));
103+
}
104+
}
51105
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.quarkus.security.jpa.common.deployment;
2+
3+
import java.util.Objects;
4+
5+
import io.quarkus.builder.item.SimpleBuildItem;
6+
7+
/**
8+
* Registers {@link io.quarkus.security.jpa.SecurityJpa} CDI bean producer class and generated identity provider names.
9+
*/
10+
public final class SecurityJpaProviderInfoBuildItem extends SimpleBuildItem {
11+
12+
final Class<?> securityJpaProviderClass;
13+
final String jpaIdentityProviderImplName;
14+
final String jpaTrustedIdentityProviderImplName;
15+
final Class<?> jpaIdentityProviderClass;
16+
final Class<?> jpaTrustedIdentityProviderClass;
17+
18+
public SecurityJpaProviderInfoBuildItem(Class<?> securityJpaProviderClass, String jpaIdentityProviderImplName,
19+
String jpaTrustedIdentityProviderImplName, Class<?> jpaIdentityProviderClass,
20+
Class<?> jpaTrustedIdentityProviderClass) {
21+
this.securityJpaProviderClass = Objects.requireNonNull(securityJpaProviderClass);
22+
this.jpaIdentityProviderImplName = jpaIdentityProviderImplName;
23+
this.jpaTrustedIdentityProviderImplName = jpaTrustedIdentityProviderImplName;
24+
this.jpaIdentityProviderClass = jpaIdentityProviderClass;
25+
this.jpaTrustedIdentityProviderClass = jpaTrustedIdentityProviderClass;
26+
}
27+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.quarkus.security.jpa;
2+
3+
import io.quarkus.arc.Arc;
4+
import io.quarkus.security.identity.IdentityProvider;
5+
import io.quarkus.security.identity.SecurityIdentityAugmentor;
6+
import io.quarkus.security.spi.runtime.IdentityProviderBuilder;
7+
import io.smallrye.common.annotation.Experimental;
8+
9+
/**
10+
* A CDI bean used to build Quarkus Security JPA {@link IdentityProvider}s programmatically.
11+
* This bean should be used together with the CDI event 'HttpSecurity' in following situations:
12+
* <ul>
13+
* <li>
14+
* You want to configure the Basic authentication to use the Quarkus Security JPA {@link IdentityProvider},
15+
* while other authentication mechanism is using different {@link IdentityProvider}:
16+
*
17+
* <pre>
18+
* {@code
19+
* import static io.quarkus.security.jpa.SecurityJpa.jpa;
20+
* import io.quarkus.vertx.http.security.Form;
21+
* import io.quarkus.vertx.http.security.HttpSecurity;
22+
* import jakarta.enterprise.event.Observes;
23+
*
24+
* public class HttpSecurityConfiguration {
25+
*
26+
* void configure(@Observes HttpSecurity httpSecurity) {
27+
* httpSecurity
28+
* .basic(jpa())
29+
* .mechanism(Form.create(), createCustomIdentityProviders());
30+
* }
31+
* }
32+
* }
33+
* </pre>
34+
*
35+
* </li>
36+
* <li>
37+
* If you want to store the identity information in a named datasource determined during the runtime:
38+
*
39+
* <pre>
40+
* {@code
41+
* import static io.quarkus.security.jpa.SecurityJpa.jpa;
42+
* import io.quarkus.vertx.http.security.Form;
43+
* import io.quarkus.vertx.http.security.HttpSecurity;
44+
* import jakarta.enterprise.event.Observes;
45+
* import org.eclipse.microprofile.config.inject.ConfigProperty;
46+
*
47+
* public class HttpSecurityConfiguration {
48+
*
49+
* void configure(@Observes HttpSecurity httpSecurity, @ConfigProperty(name = "named-pu") String namedPersistenceUnit) {
50+
* httpSecurity.basic(jpa().persistence(namedPersistenceUnit));
51+
* // or maybe you need to use 2 different persistence units for each mechanism
52+
* httpSecurity.mechanism(Form.create(), jpa().persistence("different-persistence-unit"));
53+
* }
54+
* }
55+
* }
56+
* </pre>
57+
*
58+
* </li>
59+
* </ul>
60+
*/
61+
@Experimental("This API is currently experimental and might get changed")
62+
public interface SecurityJpa extends IdentityProviderBuilder {
63+
64+
/**
65+
* Selects the persistence unit used by the Security JPA identity providers.
66+
*
67+
* @param persistenceUnitName persistence unit name
68+
* @return
69+
*/
70+
SecurityJpa persistence(String persistenceUnitName);
71+
72+
/**
73+
* Specifies the {@link SecurityIdentityAugmentor} used to augment the {@link io.quarkus.security.identity.SecurityIdentity}
74+
* produced by the Security JPA. When this method is used, only augmentors specified this way will be applied.
75+
*
76+
* @param securityIdentityAugmentor {@link SecurityIdentityAugmentor}
77+
* @return
78+
*/
79+
SecurityJpa augmentor(SecurityIdentityAugmentor securityIdentityAugmentor);
80+
81+
/**
82+
* Looks up the {@link SecurityJpa} builder and returns it.
83+
*
84+
* @return {@link SecurityJpa}
85+
*/
86+
static SecurityJpa jpa() {
87+
return Arc.requireContainer().instance(SecurityJpa.class).get();
88+
}
89+
}

extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.security.jpa.common.runtime;
22

3+
import java.lang.reflect.InvocationTargetException;
34
import java.security.spec.InvalidKeySpecException;
45
import java.util.List;
56
import java.util.UUID;
@@ -82,4 +83,13 @@ public static void passwordAction(PasswordType type) {
8283
BcryptUtil.bcryptHash(uuid);
8384
}
8485
}
86+
87+
@SuppressWarnings("unchecked")
88+
public static <T> T newInstance(Class<T> clazz) {
89+
try {
90+
return (T) clazz.getConstructors()[0].newInstance();
91+
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
92+
throw new RuntimeException(e);
93+
}
94+
}
8595
}

0 commit comments

Comments
 (0)