Skip to content

Commit 3d9f827

Browse files
committed
feat(security): add fluent API for Security JPA
1 parent f542c6a commit 3d9f827

File tree

16 files changed

+855
-15
lines changed

16 files changed

+855
-15
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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,31 @@ 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 Basic authentication with Jakarta Persistence programmatically
227+
228+
The `io.quarkus.vertx.http.security.HttpSecurity` CDI event allows to configure the Basic authentication mechanism programmatically.
229+
If there is more than one `IdentityProvider` handling the `io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest` request, you can configure the Basic authentication to use the Jakarta Persistence `IdentityProvider` 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 jakarta.enterprise.event.Observes;
238+
239+
import io.quarkus.vertx.http.security.HttpSecurity;
240+
241+
public class HttpSecurityConfiguration {
242+
243+
void configure(@Observes HttpSecurity httpSecurity) {
244+
httpSecurity.basic(jpa());
245+
}
246+
247+
}
248+
----
249+
225250
== References
226251

227252
* xref:security-getting-started-tutorial.adoc[Getting started with Security by using Basic authentication and Jakarta Persistence]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.quarkus.security.jpa;
2+
3+
import java.util.Collection;
4+
5+
import io.quarkus.arc.Arc;
6+
import io.quarkus.security.identity.IdentityProvider;
7+
8+
/**
9+
* A CDI beans used to retrieve generated Quarkus Security JPA beans.
10+
*/
11+
public interface SecurityJpa {
12+
13+
/**
14+
* @return Quarkus Security JPA {@link IdentityProvider}s
15+
*/
16+
Collection<IdentityProvider<?>> getIdentityProviders();
17+
18+
/**
19+
* Looks up the {@link SecurityJpa} CDI bean and returns the Quarkus Security JPA {@link IdentityProvider}s.
20+
*
21+
* @return Quarkus Security JPA {@link IdentityProvider}s
22+
*/
23+
static Collection<IdentityProvider<?>> jpa() {
24+
try (var securityJpaInstance = Arc.requireContainer().instance(SecurityJpa.class)) {
25+
return securityJpaInstance.get().getIdentityProviders();
26+
}
27+
}
28+
}

extensions/security-jpa-reactive/deployment/src/main/java/io/quarkus/security/jpa/reactive/deployment/QuarkusSecurityJpaReactiveProcessor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.jboss.jandex.DotName;
2727
import org.jboss.jandex.Index;
2828

29+
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
2930
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
3031
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
3132
import io.quarkus.deployment.Feature;
@@ -49,6 +50,7 @@
4950
import io.quarkus.security.jpa.common.deployment.PanacheEntityPredicateBuildItem;
5051
import io.quarkus.security.jpa.reactive.runtime.JpaReactiveIdentityProvider;
5152
import io.quarkus.security.jpa.reactive.runtime.JpaReactiveTrustedIdentityProvider;
53+
import io.quarkus.security.jpa.reactive.runtime.SecurityJpaReactiveImpl;
5254
import io.smallrye.mutiny.Uni;
5355

5456
class QuarkusSecurityJpaReactiveProcessor {
@@ -82,6 +84,11 @@ PanacheEntityPredicateBuildItem panacheEntityPredicate(List<PanacheEntityClasses
8284
return new PanacheEntityPredicateBuildItem(collectPanacheEntities(panacheEntityClasses));
8385
}
8486

87+
@BuildStep
88+
AdditionalBeanBuildItem registerSecurityJpaReactiveImplCdiBean() {
89+
return AdditionalBeanBuildItem.unremovableOf(SecurityJpaReactiveImpl.class);
90+
}
91+
8592
private static Set<String> collectPanacheEntities(List<PanacheEntityClassesBuildItem> panacheEntityClassesBuildItems) {
8693
Set<String> modelClasses = new HashSet<>();
8794
for (PanacheEntityClassesBuildItem panacheEntityClasses : panacheEntityClassesBuildItems) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package io.quarkus.security.jpa.reactive;
2+
3+
import static org.hamcrest.Matchers.containsString;
4+
import static org.hamcrest.Matchers.equalTo;
5+
import static org.hamcrest.Matchers.notNullValue;
6+
7+
import java.time.Duration;
8+
9+
import jakarta.enterprise.event.Observes;
10+
11+
import org.jboss.shrinkwrap.api.ShrinkWrap;
12+
import org.jboss.shrinkwrap.api.asset.StringAsset;
13+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.extension.RegisterExtension;
16+
17+
import io.quarkus.security.jpa.SecurityJpa;
18+
import io.quarkus.test.QuarkusUnitTest;
19+
import io.quarkus.vertx.http.security.Form;
20+
import io.quarkus.vertx.http.security.HttpSecurity;
21+
import io.restassured.RestAssured;
22+
import io.restassured.filter.cookie.CookieFilter;
23+
24+
public class SecurityJpaReactiveFluentApiTest {
25+
26+
@RegisterExtension
27+
static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap
28+
.create(JavaArchive.class)
29+
.addClasses(TestApplication.class, MinimalUserEntity.class, SecurityJpaConfiguration.class,
30+
SingleRoleSecuredResource.class)
31+
.addAsResource("minimal-config/import.sql", "import.sql")
32+
.addAsResource(new StringAsset("""
33+
quarkus.datasource.db-kind=postgresql
34+
quarkus.datasource.username=${postgres.reactive.username}
35+
quarkus.datasource.password=${postgres.reactive.password}
36+
quarkus.datasource.reactive=true
37+
quarkus.datasource.reactive.url=${postgres.reactive.url}
38+
quarkus.hibernate-orm.sql-load-script=import.sql
39+
quarkus.hibernate-orm.schema-management.strategy=drop-and-create
40+
"""), "application.properties"));
41+
42+
@Test
43+
void testFormBasedAuthentication() {
44+
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
45+
CookieFilter cookies = new CookieFilter();
46+
RestAssured
47+
.given()
48+
.filter(cookies)
49+
.redirects().follow(false)
50+
.when()
51+
.get("/jaxrs-secured/user-secured")
52+
.then()
53+
.assertThat()
54+
.statusCode(302)
55+
.header("location", containsString("/login"))
56+
.cookie("quarkus-redirect-location", containsString("/user-secured"));
57+
58+
// test with a non-existent user
59+
RestAssured
60+
.given()
61+
.filter(cookies)
62+
.redirects().follow(false)
63+
.when()
64+
.formParam("j_username", "dummy")
65+
.formParam("j_password", "dummy")
66+
.post("/j_security_check")
67+
.then()
68+
.assertThat()
69+
.statusCode(302);
70+
71+
RestAssured
72+
.given()
73+
.filter(cookies)
74+
.redirects().follow(false)
75+
.when()
76+
.formParam("j_username", "user")
77+
.formParam("j_password", "user")
78+
.post("/j_security_check")
79+
.then()
80+
.assertThat()
81+
.statusCode(302)
82+
.header("location", containsString("/user-secured"))
83+
.cookie("laitnederc-sukrauq", notNullValue());
84+
85+
RestAssured
86+
.given()
87+
.filter(cookies)
88+
.redirects().follow(false)
89+
.when()
90+
.get("/jaxrs-secured/user-secured")
91+
.then()
92+
.assertThat()
93+
.statusCode(200)
94+
.body(equalTo("A secured message"));
95+
}
96+
97+
@Test
98+
void testBasicAuthentication() {
99+
RestAssured
100+
.given()
101+
.auth().preemptive().basic("user", "wrong-password")
102+
.get("/jaxrs-secured/user-secured")
103+
.then()
104+
.statusCode(401);
105+
RestAssured
106+
.given()
107+
.auth().preemptive().basic("user", "user")
108+
.get("/jaxrs-secured/user-secured")
109+
.then()
110+
.statusCode(200)
111+
.body(equalTo("A secured message"));
112+
}
113+
114+
public static class SecurityJpaConfiguration {
115+
116+
void configure(@Observes HttpSecurity httpSecurity) {
117+
var form = Form.builder()
118+
.loginPage("login")
119+
.errorPage("error")
120+
.landingPage("landing")
121+
.cookieName("laitnederc-sukrauq")
122+
.newCookieInterval(Duration.ofSeconds(5))
123+
.timeout(Duration.ofSeconds(5))
124+
.encryptionKey("CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT")
125+
.build();
126+
var jpa = SecurityJpa.jpa();
127+
httpSecurity.mechanism(form, jpa).basic(jpa);
128+
}
129+
130+
}
131+
132+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.quarkus.security.jpa.reactive.runtime;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
import java.util.Collections;
6+
7+
import jakarta.enterprise.inject.Instance;
8+
import jakarta.inject.Inject;
9+
10+
import io.quarkus.security.identity.IdentityProvider;
11+
import io.quarkus.security.jpa.SecurityJpa;
12+
13+
public final class SecurityJpaReactiveImpl implements SecurityJpa {
14+
15+
@Inject
16+
Instance<JpaReactiveIdentityProvider> jpaReactiveIdentityProvider;
17+
18+
@Inject
19+
Instance<JpaReactiveTrustedIdentityProvider> jpaReactiveTrustedIdentityProvider;
20+
21+
@Override
22+
public Collection<IdentityProvider<?>> getIdentityProviders() {
23+
final Collection<IdentityProvider<?>> result = new ArrayList<>();
24+
if (jpaReactiveIdentityProvider.isResolvable()) {
25+
result.add(jpaReactiveIdentityProvider.get());
26+
}
27+
if (jpaReactiveTrustedIdentityProvider.isResolvable()) {
28+
result.add(jpaReactiveTrustedIdentityProvider.get());
29+
}
30+
return Collections.unmodifiableCollection(result);
31+
}
32+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.jboss.jandex.Index;
2727
import org.jboss.jandex.Type;
2828

29+
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
2930
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
3031
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
3132
import io.quarkus.arc.deployment.InjectionPointTransformerBuildItem;
@@ -55,6 +56,7 @@
5556
import io.quarkus.security.jpa.common.deployment.PanacheEntityPredicateBuildItem;
5657
import io.quarkus.security.jpa.runtime.JpaIdentityProvider;
5758
import io.quarkus.security.jpa.runtime.JpaTrustedIdentityProvider;
59+
import io.quarkus.security.jpa.runtime.SecurityJpaImpl;
5860

5961
class QuarkusSecurityJpaProcessor {
6062

@@ -118,6 +120,11 @@ PanacheEntityPredicateBuildItem panacheEntityPredicate(List<PanacheEntityClasses
118120
return new PanacheEntityPredicateBuildItem(collectPanacheEntities(panacheEntityClasses));
119121
}
120122

123+
@BuildStep
124+
AdditionalBeanBuildItem registerSecurityJpaImplCdiBean() {
125+
return AdditionalBeanBuildItem.unremovableOf(SecurityJpaImpl.class);
126+
}
127+
121128
private Set<String> collectPanacheEntities(List<PanacheEntityClassesBuildItem> panacheEntityClassesBuildItems) {
122129
Set<String> modelClasses = new HashSet<>();
123130
for (PanacheEntityClassesBuildItem panacheEntityClasses : panacheEntityClassesBuildItems) {

0 commit comments

Comments
 (0)