-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Intercept JPAIdentityProvider to set the required role
- Loading branch information
1 parent
116e978
commit 9d21977
Showing
4 changed files
with
129 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
...nd/src/main/java/io/hyperfoil/tools/horreum/server/JpaIdentityProviderRolesExtension.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package io.hyperfoil.tools.horreum.server; | ||
|
||
import io.hyperfoil.tools.horreum.svc.Roles; | ||
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; | ||
import io.quarkus.arc.processor.AnnotationsTransformer; | ||
import io.quarkus.deployment.annotations.BuildStep; | ||
import io.quarkus.security.jpa.runtime.JpaIdentityProvider; | ||
import jakarta.annotation.Priority; | ||
import jakarta.inject.Inject; | ||
import jakarta.interceptor.AroundInvoke; | ||
import jakarta.interceptor.Interceptor; | ||
import jakarta.interceptor.InterceptorBinding; | ||
import jakarta.interceptor.InvocationContext; | ||
import org.jboss.jandex.DotName; | ||
import org.jboss.jandex.MethodInfo; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Inherited; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
import static jakarta.interceptor.Interceptor.Priority.LIBRARY_BEFORE; | ||
|
||
/** | ||
* Enhance the security-jpa {@link JpaIdentityProvider} to work with row level security. | ||
* Creates a build step that adds an annotation to a target method on the JpaIdentityProvider class. | ||
* The procedure is done at build time since the identity provider is invoked so early in the request processing pipeline. | ||
* That annotation is the binding for an interceptor that is invoked around that method, that fetches the username/password from the database. | ||
* The interceptor sets the necessary role to comply with row level security. | ||
*/ | ||
public class JpaIdentityProviderRolesExtension { | ||
|
||
private static final DotName IDENTITY_PROVIDER_DOT_NAME = DotName.createSimple(JpaIdentityProvider.class); | ||
|
||
private static boolean isTargetMethod(MethodInfo method) { | ||
// could use one of the authenticate() methods instead, but we hook into getSingleUser() method as it is unique to JPAIdentityProvider | ||
return IDENTITY_PROVIDER_DOT_NAME.equals(method.declaringClass().name()) && "getSingleUser".equals(method.name()); | ||
} | ||
|
||
@BuildStep AnnotationsTransformerBuildItem transform() { | ||
return new AnnotationsTransformerBuildItem( | ||
AnnotationsTransformer.appliedToMethod() | ||
.whenMethod(JpaIdentityProviderRolesExtension::isTargetMethod) | ||
.thenTransform(t -> t.add(WithJpaIdentityProviderRole.class)) | ||
); | ||
} | ||
|
||
@Inherited | ||
@InterceptorBinding | ||
@Target({ ElementType.METHOD, ElementType.TYPE }) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
private @interface WithJpaIdentityProviderRole { | ||
} | ||
|
||
@Interceptor @Priority(LIBRARY_BEFORE) @WithJpaIdentityProviderRole public static class JpaIdentityProviderInterceptor { | ||
|
||
@Inject RoleManager roleManager; | ||
|
||
@AroundInvoke public Object intercept(InvocationContext ctx) throws Exception { | ||
String previous = roleManager.setRoles(Roles.HORREUM_SYSTEM); | ||
try { | ||
return ctx.proceed(); | ||
} finally { | ||
roleManager.setRoles(previous); | ||
} | ||
} | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/BasicAuthTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package io.hyperfoil.tools.horreum.svc; | ||
|
||
import io.hyperfoil.tools.horreum.api.internal.services.UserService; | ||
import io.hyperfoil.tools.horreum.test.DatabaseRolesTestProfile; | ||
import io.quarkus.logging.Log; | ||
import io.quarkus.test.junit.QuarkusTest; | ||
import io.quarkus.test.junit.TestProfile; | ||
import io.quarkus.test.security.TestSecurity; | ||
import jakarta.inject.Inject; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import static io.restassured.RestAssured.given; | ||
import static org.apache.http.HttpStatus.SC_OK; | ||
import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; | ||
|
||
@QuarkusTest | ||
@TestProfile(DatabaseRolesTestProfile.class) | ||
public class BasicAuthTest { | ||
|
||
@Inject UserServiceImpl userService; | ||
|
||
@TestSecurity(user = "admin", roles = { Roles.ADMIN }) | ||
@Test void basicAuthTest() { | ||
String USERNAME = "botAccount", PASSWORD = "botPassword"; | ||
|
||
// HTTP request for the non-existing user should fail | ||
given().auth().preemptive().basic(USERNAME, PASSWORD).get("api/user/roles").then().statusCode(SC_UNAUTHORIZED); | ||
|
||
// create user account | ||
UserService.NewUser newUser = new UserService.NewUser(); | ||
newUser.user = new UserService.UserData("", USERNAME, "Bot", "Account", "[email protected]"); | ||
newUser.password = PASSWORD; | ||
userService.createUser(newUser); | ||
Log.infov("Created test user {0} with password {1}", USERNAME, PASSWORD); | ||
|
||
// user should be able to authenticate now | ||
given().auth().preemptive().basic(USERNAME, PASSWORD).get("api/user/roles").then().statusCode(SC_OK); | ||
|
||
// request with bad password | ||
given().auth().preemptive().basic(USERNAME, PASSWORD.substring(1)).get("api/user/roles").then().statusCode(SC_UNAUTHORIZED); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters