Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JPA security : allow @UserDefinition to work with multitenant datasource #16700

Closed
laurentperez opened this issue Apr 21, 2021 · 13 comments · Fixed by #36759
Closed

JPA security : allow @UserDefinition to work with multitenant datasource #16700

laurentperez opened this issue Apr 21, 2021 · 13 comments · Fixed by #36759
Assignees
Labels
area/security kind/enhancement New feature or request
Milestone

Comments

@laurentperez
Copy link
Contributor

Description

Hi, posting as enhancement but this may be a bug or missing feature.
We're are using quarkus.hibernate-orm.multitenant=DATABASE. Our custom TenantResolver or TenantConnectionResolver implementations work fine, in a @RequestScoped resolving tenant

However, we're facing a problem with JPA, https://quarkus.io/guides/security-jpa . A io.quarkus.security.jpa.UserDefinition is needed to trigger the io.quarkus.security.jpa.runtime.JpaIdentityProvider

The problem is that it will fail with the following at startup, because TenantConnectionResolver will not be bound. So the @UserDefinition does not understand how to get the tenant.

Is there a technique to set the tenant in the use case of a secured JPA @UserDefinition ?

    org.hibernate.HibernateException: SessionFactory configured for multi-tenancy, but no tenant identifier specified
        at org.hibernate.internal.AbstractSharedSessionContract.<init>(AbstractSharedSessionContract.java:172)
        at org.hibernate.internal.AbstractSessionImpl.<init>(AbstractSessionImpl.java:29)
        at org.hibernate.internal.SessionImpl.<init>(SessionImpl.java:221)
    ....
        at io.quarkus.security.jpa.runtime.JpaIdentityProvider$1.get(JpaIdentityProvider.java:35)
        at io.quarkus.security.runtime.QuarkusIdentityProviderManagerImpl$1$1$1$1.run(QuarkusIdentityProviderManagerImpl.java:58)

Implementation ideas

To reproduce the exception, set quarkus.hibernate-orm.multitenant=DATABASE and annotate a single entity with @UserDefinition as per https://quarkus.io/guides/security-jpa

@laurentperez laurentperez added the kind/enhancement New feature or request label Apr 21, 2021
@quarkus-bot
Copy link

quarkus-bot bot commented Apr 21, 2021

/cc @sberyozkin

@sberyozkin
Copy link
Member

CC @gsmet @FroMage

@FroMage
Copy link
Member

FroMage commented Apr 22, 2021

Yeah, this is not supported ATM, IIRC we're loading the entity via the default session, and we should actually set the right PU in JpaTrustedIdentityProvider when we look up the EntityManagerFactory.

I'm not sure I'll have time to work on this soon, though, so if you're game for providing a PR, that could be faster :)

@laurentperez
Copy link
Contributor Author

I forked to attempt a PR, but my knowledge of the code is still quite limited and it's a bit hard for me atm to get the "big picture", as in #12400

I understand Quarkus is a big project too, documentation exists, but its internals has a steep learning curve, for me at least.

So at best I can only provide ideas atm, for example maybe quarkus.hibernate-orm.multitenant should be quarkus.hibernate-orm."dataSourceName".multitenant ?

The thing is that the lookup does not seem trivial, but again, I'm not that familiar with the codebase. Since JpaIdentityProvider starts at boot time, I don't see how to bind to a tenant which can be a dynamic one set by https://github.com/quarkusio/quarkus/blob/main/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/TenantResolver.java

@FroMage
Copy link
Member

FroMage commented Apr 23, 2021

Oh right, this is not just about specifying a data source, but also resolving the tenant.

Yeah, this won't be trivial :(

@FroMage
Copy link
Member

FroMage commented Apr 23, 2021

What do you use ATM to resolve the tenant for the normal requests?

@laurentperez
Copy link
Contributor Author

We're in a fortunate scenario where all of our calls are HTTP so we're using a custom implementation of a @RequestScoped https://github.com/quarkusio/quarkus/blob/main/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/TenantResolver.java and for the dataSources a custom https://github.com/quarkusio/quarkus/blob/main/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/tenant/TenantConnectionResolver.java because we have dynamic DataSources

We found an alternative to authenticate without a JPA @UserDefinition so this issue is not a blocker.

Maybe the resolved tenant could be captured with a vertx ThreadContext (https://quarkus.io/guides/context-propagation#usage-example-for-completionstage) to propagate it to a JPA identity resolver in the same context.

@jonjanisch
Copy link

@laurentperez we're having the same issue. Can you provide more details on your workaround?

@laurentperez
Copy link
Contributor Author

@laurentperez we're having the same issue. Can you provide more details on your workaround?

well we dropped JPA (for tenant datasource resolution). also because our tenants are not known at build time, ours are dynamic ones.

I guess once you manage to resolve your tenant, context propagation or maybe EventBus should work to let other parts of your codebase know the tenant

@FroMage
Copy link
Member

FroMage commented May 24, 2022

@jonjanisch I can probably help, if you can give me a minimal reproducer.

@jonjanisch
Copy link

@FroMage thanks I've created a minimal Quarkus app here: https://github.com/jonjanisch/multitenant

It requires a MySQL database with two simple tables for the user and role (see README.md). You can likely use any datasource type. I inserted a user "admin" with password "password" (quarkus uses bcrypt with 10 rounds by default):

INSERT INTO sys_user
 (user_id,password)
VALUES
('admin', '$2a$10$adViu9K6wtbOUd0rIHkoCuI1DNPDg6/4RbyK97tbkKUs04BHyAK26');

This is the default Quarkus hello world GET endpoint:
http://localhost:8080/hello

This is a simple API endpoint that returns a list of userIds:
http://localhost:8080/api/tenant/tenant1/hello

The path /api/* is protected and the TenantResolver extracts the tenant from the request path. In this case it extracts tenant1 which is the only tenant I've configured in the sample app:

quarkus.datasource.tenant1.db-kind = mysql
quarkus.datasource.tenant1.username = quarkus
quarkus.datasource.tenant1.password = quarkus
quarkus.datasource.tenant1.jdbc.url = jdbc:mysql://127.0.0.1:3306/multi

Attempting to access the above resource will redirect you to login.html. Typing "admin" and "password" and hitting "Sign In" will throw the error:

2022-05-24 14:21:36,205 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-0) HTTP Request to /j_security_check failed, error id: df89d3ee-1912-4a7f-86e2-ba90287c3228-2: org.hibernate.HibernateException: SessionFactory configured for multi-tenancy, but no tenant identifier specified
	at org.hibernate.internal.AbstractSharedSessionContract.<init>(AbstractSharedSessionContract.java:172)
	at org.hibernate.internal.AbstractSessionImpl.<init>(AbstractSessionImpl.java:29)
	at org.hibernate.internal.SessionImpl.<init>(SessionImpl.java:230)
	at org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl.openSession(SessionFactoryImpl.java:1334)
	at org.hibernate.internal.SessionFactoryImpl.buildEntityManager(SessionFactoryImpl.java:651)
	at org.hibernate.internal.SessionFactoryImpl.createEntityManager(SessionFactoryImpl.java:637)
	at org.hibernate.internal.SessionFactoryImpl.createEntityManager(SessionFactoryImpl.java:158)
	at org.hibernate.SessionFactory_12eda9c48804599c3f7735587b95a719257e2891_Synthetic_ClientProxy.createEntityManager(Unknown Source)
	at io.quarkus.security.jpa.runtime.JpaIdentityProvider$1.get(JpaIdentityProvider.java:38)
	at io.quarkus.security.jpa.runtime.JpaIdentityProvider$1.get(JpaIdentityProvider.java:35)
	at io.quarkus.security.runtime.QuarkusIdentityProviderManagerImpl$1$1$1$1.run(QuarkusIdentityProviderManagerImpl.java:58)
	at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:550)
	at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)
	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478)
	at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
	at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:833)

If you go into application.properties and comment out the auth config the multi-tenancy works fine and the above endpoint will display the users for the given tenant.

#quarkus.http.auth.form.enabled = true
#quarkus.http.auth.permission.authenticated1.paths=/api/*
#quarkus.http.auth.permission.authenticated1.policy=authenticated
#quarkus.http.auth.permission.authenticated1.auth-mechanism=form

@michalvavrik
Copy link
Member

I wrote couple of tests on top of current main, basically this task is about optional activating request scope (or doing more things manually inside Security JPA, which I think is unnecessary). I think we can just detect when user defined TenantResolver with request scope and active it if proactive auth is disabled. It won't work for proactive auth, but it's IMO not a big deal.

@michalvavrik
Copy link
Member

michalvavrik commented Oct 27, 2023

Correction: I was testing it with RESTEasy Classic and proactive auth and works when I active request context, the issue was when I requested RoutingContext in TenantResolver as it is set later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/security kind/enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants