Skip to content

Commit

Permalink
Select TenantIdentityProvider with a @Tenant annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed May 25, 2024
1 parent edf4d5f commit 2deb8b3
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1297,7 +1297,7 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.Tenant;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.ConsumeEvent;
Expand All @@ -1306,7 +1306,7 @@ import io.smallrye.common.annotation.Blocking;
@ApplicationScoped
public class OrderService {
@TenantFeature("tenantId")
@Tenant("tenantId")
@Inject
TenantIdentityProvider identityProvider;
Expand All @@ -1323,14 +1323,14 @@ public class OrderService {
}
----
<1> For the default tenant, the `TenantFeature` qualifier is optional.
<1> For the default tenant, the `Tenant` qualifier is optional.
<2> Executes token verification and converts the token to a `SecurityIdentity`.

[NOTE]
====
When the provider is used during an HTTP request, the tenant configuration can be resolved as described in

Check warning on line 1331 in docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc", "range": {"start": {"line": 1331, "column": 92}}}, "severity": "INFO"}
the xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect Multi-Tenancy] guide.
However, when there is no active HTTP request, you must select the tenant explicitly with the `io.quarkus.oidc.TenantFeature` qualifier.
However, when there is no active HTTP request, you must select the tenant explicitly with the `io.quarkus.oidc.Tenant` qualifier.
====

[WARNING]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import static io.quarkus.arc.processor.BuiltinScope.APPLICATION;
import static io.quarkus.arc.processor.DotNames.DEFAULT;
import static io.quarkus.arc.processor.DotNames.NAMED;
import static io.quarkus.oidc.common.runtime.OidcConstants.BEARER_SCHEME;
import static io.quarkus.oidc.common.runtime.OidcConstants.CODE_FLOW_CODE;
import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID;
import static org.jboss.jandex.AnnotationTarget.Kind.CLASS;
import static org.jboss.jandex.AnnotationTarget.Kind.METHOD;

import java.util.List;
import java.util.Map;
Expand All @@ -16,16 +18,23 @@

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem;
import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem;
import io.quarkus.arc.deployment.InjectionPointTransformerBuildItem;
import io.quarkus.arc.deployment.QualifierRegistrarBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.processor.InjectionPointInfo;
import io.quarkus.arc.processor.InjectionPointsTransformer;
import io.quarkus.arc.processor.QualifierRegistrar;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
Expand Down Expand Up @@ -151,6 +160,7 @@ ExtensionSslNativeSupportBuildItem enableSslInNative() {
QualifierRegistrarBuildItem addQualifiers() {
// this seems to be necessary; I think it's because sometimes we only access beans
// annotated with @TenantFeature programmatically and no injection point is annotated with it
// TODO: drop @TenantFeature qualifier when 'TenantFeatureFinder' stop using this annotation as a qualifier
return new QualifierRegistrarBuildItem(new QualifierRegistrar() {
@Override
public Map<DotName, Set<String>> getAdditionalQualifiers() {
Expand All @@ -159,52 +169,94 @@ public Map<DotName, Set<String>> getAdditionalQualifiers() {
});
}

@BuildStep
InjectionPointTransformerBuildItem makeTenantIdentityProviderInjectionPointsNamed() {
// @Tenant annotation cannot be a qualifier as it is used on resource methods and lead to illegal states
return new InjectionPointTransformerBuildItem(new InjectionPointsTransformer() {
@Override
public boolean appliesTo(Type requiredType) {
return requiredType.name().equals(TENANT_IDENTITY_PROVIDER_NAME);
}

@Override
public void transform(TransformationContext ctx) {
if (ctx.getTarget().kind() == METHOD) {
ctx
.getAllAnnotations()
.stream()
.filter(a -> TENANT_NAME.equals(a.name()))
.forEach(a -> {
var annotationValue = new AnnotationValue[] {
AnnotationValue.createStringValue("value", a.value().asString()) };
ctx
.transform()
.add(AnnotationInstance.create(NAMED, a.target(), annotationValue))
.done();
});
} else {
// field
var tenantAnnotation = Annotations.find(ctx.getAllAnnotations(), TENANT_NAME);
if (tenantAnnotation != null && tenantAnnotation.value() != null) {
ctx
.transform()
.add(NAMED, AnnotationValue.createStringValue("value", tenantAnnotation.value().asString()))
.done();
}
}
}
});
}

/**
* Produce {@link OidcIdentityProvider} with already selected tenant for each {@link OidcIdentityProvider}
* injection point annotated with {@link TenantFeature} annotation.
* For example, we produce {@link OidcIdentityProvider} with pre-selected tenant 'my-tenant' for injection point:
* Produce {@link TenantIdentityProvider} with already selected tenant for each {@link TenantIdentityProvider}
* injection point annotated with {@link Tenant} annotation.
* For example, we produce {@link TenantIdentityProvider} with pre-selected tenant 'my-tenant' for injection point:
*
* <code>
* &#064;Inject
* &#064;TenantFeature("my-tenant")
* OidcIdentityProvider identityProvider;
* &#064;Tenant("my-tenant")
* TenantIdentityProvider identityProvider;
* </code>
*/
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void produceTenantIdentityProviders(BuildProducer<SyntheticBeanBuildItem> syntheticBeanProducer,
OidcRecorder recorder, BeanDiscoveryFinishedBuildItem beans, CombinedIndexBuildItem combinedIndex) {
// create TenantIdentityProviders for tenants selected with @TenantFeature like: @TenantFeature("my-tenant")
if (!combinedIndex.getIndex().getAnnotations(TENANT_FEATURE_NAME).isEmpty()) {
// create TenantIdentityProviders for tenants selected with @TenantFeature like: @TenantFeature("my-tenant")
if (!combinedIndex.getIndex().getAnnotations(TENANT_NAME).isEmpty()) {
// create TenantIdentityProviders for tenants selected with @Tenant like: @Tenant("my-tenant")
beans
.getInjectionPoints()
.stream()
.filter(ip -> ip.getRequiredQualifier(TENANT_FEATURE_NAME) != null)
.filter(OidcBuildStep::isTenantIdentityProviderType)
.map(ip -> ip.getRequiredQualifier(TENANT_FEATURE_NAME).value().asString())
.filter(ip -> ip.getRequiredQualifier(NAMED) != null)
.map(ip -> ip.getRequiredQualifier(NAMED).value().asString())
.distinct()
.forEach(tenantName -> syntheticBeanProducer.produce(
SyntheticBeanBuildItem
.configure(TenantIdentityProvider.class)
.addQualifier().annotation(TENANT_FEATURE_NAME).addValue("value", tenantName).done()
.named(tenantName)
.scope(APPLICATION.getInfo())
.supplier(recorder.createTenantIdentityProvider(tenantName))
.unremovable()
.done()));
}
// create TenantIdentityProvider for default tenant when tenant is not explicitly selected via @TenantFeature
// create TenantIdentityProvider for default tenant when tenant is not explicitly selected via @Tenant
boolean createTenantIdentityProviderForDefaultTenant = beans
.getInjectionPoints()
.stream()
.filter(InjectionPointInfo::hasDefaultedQualifier)
.filter(ip -> ip.getRequiredQualifier(NAMED) == null)
.anyMatch(OidcBuildStep::isTenantIdentityProviderType);
if (createTenantIdentityProviderForDefaultTenant) {
syntheticBeanProducer.produce(
SyntheticBeanBuildItem
.configure(TenantIdentityProvider.class)
.addQualifier(DEFAULT)
.scope(APPLICATION.getInfo())
.addQualifier(DEFAULT)
// named beans are implicitly default according to the specs
// when no other qualifiers are present other than @Named and @Any
// which means we need to handle ambiguous resolution
.alternative(true)
.priority(1)
.supplier(recorder.createTenantIdentityProvider(DEFAULT_TENANT_ID))
.unremovable()
.done());
Expand Down Expand Up @@ -243,8 +295,17 @@ public void registerTenantResolverInterceptor(Capabilities capabilities, OidcRec
BuildProducer<SystemPropertyBuildItem> systemPropertyProducer) {
if (!buildTimeConfig.auth.proactive
&& (capabilities.isPresent(Capability.RESTEASY_REACTIVE) || capabilities.isPresent(Capability.RESTEASY))) {
var annotationInstances = combinedIndexBuildItem.getIndex().getAnnotations(TENANT_NAME);
if (!annotationInstances.isEmpty()) {
boolean foundTenantResolver = combinedIndexBuildItem
.getIndex()
.getAnnotations(TENANT_NAME)
.stream()
.map(AnnotationInstance::target)
// ignored field injection points and injection setters
// as we don't want to count in the TenantIdentityProvider injection point
.filter(t -> t.kind() == METHOD)
.map(AnnotationTarget::asMethod)
.anyMatch(m -> !m.isConstructor() && !m.hasAnnotation(DotNames.INJECT));
if (foundTenantResolver) {
// register method interceptor that will be run before security checks
bindingProducer.produce(
new EagerSecurityInterceptorBindingBuildItem(recorder.tenantResolverInterceptorCreator(), TENANT_NAME));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.oidc;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;

import java.lang.annotation.Retention;
Expand All @@ -9,8 +11,10 @@

/**
* Annotation which can be used to associate OIDC tenant configurations with Jakarta REST resources and resource methods.
* When placed on injection points, this annotation can be used to select a tenant associated
* with the {@link TenantIdentityProvider}.
*/
@Target({ TYPE, METHOD })
@Target({ TYPE, METHOD, FIELD, PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface Tenant {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

/**
* Tenant-specific {@link SecurityIdentity} provider. Associated tenant configuration needs to be selected
* with the {@link TenantFeature} qualifier. When injection point is not annotated with the {@link TenantFeature}
* with the {@link Tenant} qualifier. When injection point is not annotated with the {@link Tenant}
* qualifier, default tenant is selected.
*/
public interface TenantIdentityProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import jakarta.inject.Inject;

import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.Tenant;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.ConsumeEvent;
Expand All @@ -21,7 +21,7 @@ public class OrderService {
@Inject
SecurityIdentity identity;

@TenantFeature("bearer")
@Tenant("bearer")
@Inject
TenantIdentityProvider identityProvider;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
import java.util.concurrent.ConcurrentHashMap;

import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.Tenant;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.security.AuthenticationFailedException;
Expand All @@ -27,16 +28,18 @@ public class StartupService {

private static final String ISSUER = "https://server.example.com";

@TenantFeature("bearer")
@Inject
@Tenant("bearer")
TenantIdentityProvider identityProviderBearer;

@TenantFeature("bearer-role-claim-path")
@Inject
@Tenant("bearer-role-claim-path")
TenantIdentityProvider identityProviderBearerRoleClaimPath;

private final Map<String, Map<String, Set<String>>> tenantToIdentityWithRole = new ConcurrentHashMap<>();

void onStartup(@Observes StartupEvent event,
@TenantFeature(DEFAULT_TENANT_ID) TenantIdentityProvider defaultTenantProvider,
@Tenant(DEFAULT_TENANT_ID) TenantIdentityProvider defaultTenantProvider,
TenantIdentityProvider defaultTenantProviderDefaultQualifier) {
assertDefaultTenantProviderInjection(defaultTenantProvider);
assertDefaultTenantProviderInjection(defaultTenantProviderDefaultQualifier);
Expand Down

0 comments on commit 2deb8b3

Please sign in to comment.