Tenant Resolver Extension
Warp to the code: Quarkus Tenant Resolver
What is it Link to heading
Simple quarkus extension, implementing TenantConfigResolver. It uses passed regex string to extract tenant ID from request parameters.
Task description Link to heading
At some moment got an idea to add multitenancy support for project in development. Core requirements:
- all tenants are authenticated against one OIDC server
- each tenant has own realm
- tenant name is used within business logic, so should be easily accessible in RequestScope
- amount of tenants was not pre-defined
In common that is rather straightforward task… when you know how the URL will look like. At the moment of starting implementation of multitenancy support there was no yet agreement on how the URL will look like. Will the tenant information passed as a subdomain? Or specific query parameter? Or path part? It was resolved by setting a technical meeting with involved parties and making decision how it should be. However, it also rised an idea to try to implement something that can be switched with minimal efforts. And that how this project appeared..
Implementation Link to heading
This project is quarkus extension implementation. It provides one main class RegexTenantResolver class that implements TenantConfigResolver. When instantiated it will intercept requests and try to extract tenant information, based on passed parameters. The outcome of interception is creation of custom OidcTenantConfig with set tenant Id, if any is found.
Exact behavior of resolver depends on passed parameters.
Usage Link to heading
Implementation do not define any producer or annotation to auto create bean. Thus to instantiate it - create a producer like:
@ApplicationScoped
public class RegexTenantResolverProducer {
@ConfigProperty(name = "dev.shebur.tenant-resolver.tenant.source")
protected TenantSource tenantSource;
@ConfigProperty(name = "dev.shebur.tenant-resolver.tenant.regex")
protected String regex;
@Produces
@ApplicationScoped
public RegexTenantResolver regexTenantResolver() {
return new RegexTenantResolver(
tenantSource,
regex
);
}
}
…or simply copy code and edit it as needed.
Configuration Link to heading
Parameters that should be passed into resolver:
- tenantSource - enum value that define what part of request URL should be processed for tenant Id:
- HOST - tenant is part of host
https://tenantA.service.my/api/...
- PATH - tenant is part of path
https://service.my/tenantA/api/...
- QUERY - tenant is passed as query paremeter
https://service.my/api/?tenant=tenantA
- NONE - there is no tenant Id to be parsed
- HOST - tenant is part of host
- regex - regex string, used to identify how to get tenant information. Resolver expects that first matched group will represent tenant Id
Extending Link to heading
Default implementation will try to extract tenant information and if succeeded will generate a new OidcTenantConfig with extracted tenant Id set. For more advanced use cases it is possible to extend the default implementation. There are 2 main points for overriding:
- createOidcTenantConfig - place where OidcTenantConfig is created, thus it can be overwritten to make more customizations to OIDC config. For example, calculate custom auth server URL
- onTenantParsed - generic callback method, executed when tenant information was successfully parsed.
Example of ExtendedRegexTenantResolver can be found under ExtendedRegexTenantResolver.java. Default implementation is improved to:
- Calculate custom auth server URL, where used realm name equals to tenant name
@Override
protected Uni<OidcTenantConfig> createOidcTenantConfig(String tenant) {
if (Strings.isNullOrEmpty(tenant)) {
return Uni.createFrom().nullItem();
}
final OidcTenantConfig config = new OidcTenantConfig();
config.setAuthServerUrl(oidcUrlTemplate.replace(oidcUrlTenantPlaceholder, tenant));
config.setTenantId(tenant);
return Uni.createFrom().item(config);
}
- Put tenant information into RoutingContext, so it can be easily extracted from endpoints implementations
@Override
protected void onTenantParsed(RoutingContext context, String tenant) {
context.put("tenant", tenant);
}
Regex examples Link to heading
Regex string can be edited and checked using some online tool like regex101
Remember about escaping special characters
^([^&.]*)
- extract first part of host before dot. Gets tenantA from tenantA.service.my^/([^&]*)/
- extract first part of path. Gets tenantA from /tenantA/some/servicetenant=([^&]*)
- extract value for query parameter named tenant. Gets tenantA from ?o=1&tenant=tenantA&t=2