Skip to content

Commit

Permalink
Intercept JPAIdentityProvider to set the required role
Browse files Browse the repository at this point in the history
  • Loading branch information
barreiro authored and johnaohara committed May 16, 2024
1 parent 116e978 commit 9d21977
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 2 deletions.
18 changes: 16 additions & 2 deletions horreum-backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
<excludeTags>CiTests</excludeTags>
</properties>



<dependencies>
<dependency>
<groupId>io.hyperfoil.tools</groupId>
Expand Down Expand Up @@ -92,6 +90,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
Expand Down Expand Up @@ -252,6 +254,18 @@
<version>${version.maven.compiler}</version>
<configuration>
<forceJavacCompilerUse>true</forceJavacCompilerUse>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.version}</version>
</path>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-common</artifactId>
<version>${quarkus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
Expand Down
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);
}
}
}
}
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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class DatabaseRolesTestProfile extends HorreumTestProfile {
Map<String, String> configOverrides = new HashMap<>(super.getConfigOverrides());
configOverrides.put("horreum.roles.provider", "database");
configOverrides.put("horreum.roles.database.override", "false");
configOverrides.put("quarkus.http.auth.basic", "true");
return configOverrides;
}
}

0 comments on commit 9d21977

Please sign in to comment.