io.quarkus.security.deployment.SecurityProcessor Maven / Gradle / Ivy
The newest version!
package io.quarkus.security.deployment;
import static io.quarkus.arc.processor.DotNames.NO_CLASS_INTERCEPTORS;
import static io.quarkus.gizmo.MethodDescriptor.ofMethod;
import static io.quarkus.security.deployment.DotNames.AUTHENTICATED;
import static io.quarkus.security.deployment.DotNames.DENY_ALL;
import static io.quarkus.security.deployment.DotNames.INHERITED;
import static io.quarkus.security.deployment.DotNames.PERMISSIONS_ALLOWED;
import static io.quarkus.security.deployment.DotNames.PERMIT_ALL;
import static io.quarkus.security.deployment.DotNames.ROLES_ALLOWED;
import static io.quarkus.security.deployment.PermissionSecurityChecks.BLOCKING;
import static io.quarkus.security.deployment.PermissionSecurityChecks.PERMISSION_CHECKER_NAME;
import static io.quarkus.security.deployment.PermissionSecurityChecks.PermissionSecurityChecksBuilder.movePermFromMetaAnnToMetaTarget;
import static io.quarkus.security.runtime.SecurityProviderUtils.findProviderIndex;
import static io.quarkus.security.spi.SecurityTransformerUtils.findFirstStandardSecurityAnnotation;
import static io.quarkus.security.spi.SecurityTransformerUtils.hasSecurityAnnotation;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.security.Provider;
import java.security.Security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import jakarta.annotation.security.DenyAll;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Singleton;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTransformation;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem;
import io.quarkus.arc.deployment.InterceptorBindingRegistrarBuildItem;
import io.quarkus.arc.deployment.SynthesisFinishedBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem;
import io.quarkus.arc.processor.AnnotationStore;
import io.quarkus.arc.processor.BuildExtension;
import io.quarkus.arc.processor.BuiltinScope;
import io.quarkus.arc.processor.ObserverInfo;
import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.builder.item.SimpleBuildItem;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.deployment.builditem.GeneratedNativeImageClassBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem;
import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem;
import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
import io.quarkus.deployment.builditem.nativeimage.JPMSExportBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageSecurityProviderBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem;
import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem;
import io.quarkus.deployment.pkg.NativeConfig;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.gizmo.CatchBlockCreator;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.gizmo.TryBlock;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.security.deployment.PermissionSecurityChecks.PermissionSecurityChecksBuilder;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.IdentityProviderManagerCreator;
import io.quarkus.security.runtime.QuarkusSecurityRolesAllowedConfigBuilder;
import io.quarkus.security.runtime.SecurityBuildTimeConfig;
import io.quarkus.security.runtime.SecurityCheckRecorder;
import io.quarkus.security.runtime.SecurityIdentityAssociation;
import io.quarkus.security.runtime.SecurityIdentityProxy;
import io.quarkus.security.runtime.SecurityProviderRecorder;
import io.quarkus.security.runtime.SecurityProviderUtils;
import io.quarkus.security.runtime.X509IdentityProvider;
import io.quarkus.security.runtime.interceptor.AuthenticatedInterceptor;
import io.quarkus.security.runtime.interceptor.DenyAllInterceptor;
import io.quarkus.security.runtime.interceptor.PermissionsAllowedInterceptor;
import io.quarkus.security.runtime.interceptor.PermitAllInterceptor;
import io.quarkus.security.runtime.interceptor.RolesAllowedInterceptor;
import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder;
import io.quarkus.security.runtime.interceptor.SecurityConstrainer;
import io.quarkus.security.runtime.interceptor.SecurityHandler;
import io.quarkus.security.spi.AdditionalSecuredClassesBuildItem;
import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem;
import io.quarkus.security.spi.AdditionalSecurityAnnotationBuildItem;
import io.quarkus.security.spi.AdditionalSecurityConstrainerEventPropsBuildItem;
import io.quarkus.security.spi.ClassSecurityCheckAnnotationBuildItem;
import io.quarkus.security.spi.ClassSecurityCheckStorageBuildItem;
import io.quarkus.security.spi.ClassSecurityCheckStorageBuildItem.ClassStorageBuilder;
import io.quarkus.security.spi.DefaultSecurityCheckBuildItem;
import io.quarkus.security.spi.PermissionsAllowedMetaAnnotationBuildItem;
import io.quarkus.security.spi.RolesAllowedConfigExpResolverBuildItem;
import io.quarkus.security.spi.SecurityTransformerUtils;
import io.quarkus.security.spi.runtime.AuthorizationController;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.security.spi.runtime.DevModeDisabledAuthorizationController;
import io.quarkus.security.spi.runtime.MethodDescription;
import io.quarkus.security.spi.runtime.SecurityCheck;
import io.quarkus.security.spi.runtime.SecurityCheckStorage;
public class SecurityProcessor {
private static final Logger log = Logger.getLogger(SecurityProcessor.class);
private static final DotName STARTUP_EVENT_NAME = DotName.createSimple(StartupEvent.class.getName());
SecurityConfig security;
/**
* Create JCAProviderBuildItems for any configured provider names
*/
@BuildStep
void produceJcaSecurityProviders(BuildProducer jcaProviders,
BuildProducer bouncyCastleProvider,
BuildProducer bouncyCastleJsseProvider) {
Set providers = security.securityProviders().orElse(Set.of());
for (String providerName : providers) {
if (SecurityProviderUtils.BOUNCYCASTLE_PROVIDER_NAME.equals(providerName)) {
bouncyCastleProvider.produce(new BouncyCastleProviderBuildItem());
} else if (SecurityProviderUtils.BOUNCYCASTLE_JSSE_PROVIDER_NAME.equals(providerName)) {
bouncyCastleJsseProvider.produce(new BouncyCastleJsseProviderBuildItem());
} else if (SecurityProviderUtils.BOUNCYCASTLE_FIPS_PROVIDER_NAME.equals(providerName)) {
bouncyCastleProvider.produce(new BouncyCastleProviderBuildItem(true));
} else if (SecurityProviderUtils.BOUNCYCASTLE_FIPS_JSSE_PROVIDER_NAME.equals(providerName)) {
bouncyCastleJsseProvider.produce(new BouncyCastleJsseProviderBuildItem(true));
} else {
jcaProviders
.produce(new JCAProviderBuildItem(providerName, security.securityProviderConfig().get(providerName)));
}
log.debugf("Added providerName: %s", providerName);
}
}
/**
* Register the classes for reflection in the requested named providers
*
* @param classes - ReflectiveClassBuildItem producer
* @param jcaProviders - JCAProviderBuildItem for requested providers
* @throws URISyntaxException
* @throws MalformedURLException
*/
@BuildStep
void registerJCAProvidersForReflection(BuildProducer classes,
List jcaProviders,
BuildProducer additionalProviders) throws IOException, URISyntaxException {
for (JCAProviderBuildItem provider : jcaProviders) {
List providerClasses = registerProvider(provider.getProviderName(), provider.getProviderConfig(),
additionalProviders);
for (String className : providerClasses) {
classes.produce(ReflectiveClassBuildItem.builder(className).methods().fields().build());
log.debugf("Register JCA class: %s", className);
}
}
}
@BuildStep
void prepareBouncyCastleProviders(CurateOutcomeBuildItem curateOutcomeBuildItem,
BuildProducer reflection,
BuildProducer runtimeReInitialized,
List bouncyCastleProviders,
List bouncyCastleJsseProviders) throws Exception {
Optional bouncyCastleJsseProvider = getOne(bouncyCastleJsseProviders);
if (bouncyCastleJsseProvider.isPresent()) {
reflection.produce(
ReflectiveClassBuildItem.builder(SecurityProviderUtils.BOUNCYCASTLE_JSSE_PROVIDER_CLASS_NAME).methods()
.fields().build());
reflection.produce(
ReflectiveClassBuildItem.builder("org.bouncycastle.jsse.provider.DefaultSSLContextSpi$LazyManagers")
.methods().fields().build());
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem(
"org.bouncycastle.jsse.provider.DefaultSSLContextSpi$LazyManagers"));
prepareBouncyCastleProvider(curateOutcomeBuildItem, reflection, runtimeReInitialized,
bouncyCastleJsseProvider.get().isInFipsMode());
} else {
Optional bouncyCastleProvider = getOne(bouncyCastleProviders);
if (bouncyCastleProvider.isPresent()) {
prepareBouncyCastleProvider(curateOutcomeBuildItem, reflection, runtimeReInitialized,
bouncyCastleProvider.get().isInFipsMode());
}
}
}
private static void prepareBouncyCastleProvider(CurateOutcomeBuildItem curateOutcomeBuildItem,
BuildProducer reflection,
BuildProducer runtimeReInitialized, boolean isFipsMode) {
reflection
.produce(
ReflectiveClassBuildItem
.builder(isFipsMode ? SecurityProviderUtils.BOUNCYCASTLE_FIPS_PROVIDER_CLASS_NAME
: SecurityProviderUtils.BOUNCYCASTLE_PROVIDER_CLASS_NAME)
.methods().fields().build());
if (curateOutcomeBuildItem.getApplicationModel().getDependencies().stream().anyMatch(
x -> x.getGroupId().equals("org.bouncycastle") && x.getArtifactId().startsWith("bcprov-"))) {
reflection.produce(ReflectiveClassBuildItem.builder("org.bouncycastle.jcajce.provider.symmetric.AES",
"org.bouncycastle.jcajce.provider.symmetric.AES$CBC",
"org.bouncycastle.crypto.paddings.PKCS7Padding",
"org.bouncycastle.jcajce.provider.asymmetric.ec.KeyFactorySpi",
"org.bouncycastle.jcajce.provider.asymmetric.ec.KeyFactorySpi$EC",
"org.bouncycastle.jcajce.provider.asymmetric.ec.KeyFactorySpi$ECDSA",
"org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi",
"org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi$EC",
"org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi$ECDSA",
"org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyFactorySpi",
"org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyPairGeneratorSpi",
"org.bouncycastle.jcajce.provider.asymmetric.rsa.PSSSignatureSpi",
"org.bouncycastle.jcajce.provider.asymmetric.rsa.PSSSignatureSpi$SHA256withRSA").methods().fields()
.build());
}
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.crypto.CryptoServicesRegistrar"));
if (!isFipsMode) {
reflection.produce(ReflectiveClassBuildItem.builder("org.bouncycastle.jcajce.provider.drbg.DRBG$Default")
.methods().fields().build());
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.jcajce.provider.drbg.DRBG$Default"));
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.jcajce.provider.drbg.DRBG$NonceAndIV"));
// URLSeededEntropySourceProvider.seedStream may contain a reference to a 'FileInputStream' which includes
// references to FileDescriptors which aren't allowed in the image heap
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem(
"org.bouncycastle.jcajce.provider.drbg.DRBG$URLSeededEntropySourceProvider"));
} else {
reflection.produce(ReflectiveClassBuildItem.builder("org.bouncycastle.crypto.general.AES")
.methods().fields().build());
runtimeReInitialized.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.crypto.general.AES"));
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem(
"org.bouncycastle.crypto.asymmetric.NamedECDomainParameters"));
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.crypto.asymmetric.CustomNamedCurves"));
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.asn1.ua.DSTU4145NamedCurves"));
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.asn1.sec.SECNamedCurves"));
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.asn1.cryptopro.ECGOST3410NamedCurves"));
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.asn1.x9.X962NamedCurves"));
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.asn1.x9.ECNamedCurveTable"));
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.asn1.anssi.ANSSINamedCurves"));
runtimeReInitialized
.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves"));
runtimeReInitialized.produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.jcajce.spec.ECUtil"));
}
// Reinitialize class because it embeds a java.lang.ref.Cleaner instance in the image heap
runtimeReInitialized.produce(new RuntimeReinitializedClassBuildItem("sun.security.pkcs11.P11Util"));
}
@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void recordBouncyCastleProviders(SecurityProviderRecorder recorder,
List bouncyCastleProviders,
List bouncyCastleJsseProviders) {
Optional bouncyCastleJsseProvider = getOne(bouncyCastleJsseProviders);
if (bouncyCastleJsseProvider.isPresent()) {
if (bouncyCastleJsseProvider.get().isInFipsMode()) {
recorder.addBouncyCastleFipsJsseProvider();
} else {
recorder.addBouncyCastleJsseProvider();
}
} else {
Optional bouncyCastleProvider = getOne(bouncyCastleProviders);
if (bouncyCastleProvider.isPresent()) {
recorder.addBouncyCastleProvider(bouncyCastleProvider.get().isInFipsMode());
}
}
}
@BuildStep
NativeImageFeatureBuildItem bouncyCastleFeature(NativeConfig nativeConfig,
List bouncyCastleProviders,
List bouncyCastleJsseProviders) {
if (nativeConfig.enabled()) {
Optional bouncyCastleJsseProvider = getOne(bouncyCastleJsseProviders);
Optional bouncyCastleProvider = getOne(bouncyCastleProviders);
if (bouncyCastleJsseProvider.isPresent() || bouncyCastleProvider.isPresent()) {
return new NativeImageFeatureBuildItem("io.quarkus.security.BouncyCastleFeature");
}
}
return null;
}
@BuildStep
void addBouncyCastleProvidersToNativeImage(
BuildProducer nativeImageClass,
BuildProducer additionalProviders,
List bouncyCastleProviders,
List bouncyCastleJsseProviders) {
Optional bouncyCastleJsseProvider = getOne(bouncyCastleJsseProviders);
Optional bouncyCastleProvider = getOne(bouncyCastleProviders);
if (bouncyCastleJsseProvider.isPresent() || bouncyCastleProvider.isPresent()) {
// Prepare BouncyCastle AutoFeature generation
ClassCreator file = new ClassCreator(new ClassOutput() {
@Override
public void write(String s, byte[] bytes) {
nativeImageClass.produce(new GeneratedNativeImageClassBuildItem(s, bytes));
}
}, "io.quarkus.security.BouncyCastleFeature", null, Object.class.getName(),
org.graalvm.nativeimage.hosted.Feature.class.getName());
MethodCreator afterRegistration = file.getMethodCreator("afterRegistration", "V",
org.graalvm.nativeimage.hosted.Feature.AfterRegistrationAccess.class.getName());
TryBlock overallCatch = afterRegistration.tryBlock();
if (bouncyCastleJsseProvider.isPresent()) {
// BCJSSE or BCJSSEFIPS
additionalProviders.produce(
new NativeImageSecurityProviderBuildItem(SecurityProviderUtils.BOUNCYCASTLE_JSSE_PROVIDER_CLASS_NAME));
if (!bouncyCastleJsseProvider.get().isInFipsMode()) {
final int sunJsseIndex = findProviderIndex(SecurityProviderUtils.SUN_JSSE_PROVIDER_NAME);
ResultHandle bcProvider = overallCatch
.newInstance(
MethodDescriptor.ofConstructor(SecurityProviderUtils.BOUNCYCASTLE_PROVIDER_CLASS_NAME));
ResultHandle bcJsseProvider = overallCatch.newInstance(
MethodDescriptor.ofConstructor(SecurityProviderUtils.BOUNCYCASTLE_JSSE_PROVIDER_CLASS_NAME));
overallCatch.invokeStaticMethod(
MethodDescriptor.ofMethod(Security.class, "insertProviderAt", int.class, Provider.class,
int.class),
bcProvider, overallCatch.load(sunJsseIndex));
overallCatch.invokeStaticMethod(
MethodDescriptor.ofMethod(Security.class, "insertProviderAt", int.class, Provider.class,
int.class),
bcJsseProvider, overallCatch.load(sunJsseIndex + 1));
} else {
final int sunIndex = findProviderIndex(SecurityProviderUtils.SUN_PROVIDER_NAME);
ResultHandle bcFipsProvider = overallCatch
.newInstance(MethodDescriptor
.ofConstructor(SecurityProviderUtils.BOUNCYCASTLE_FIPS_PROVIDER_CLASS_NAME));
MethodDescriptor bcJsseProviderConstructor = MethodDescriptor.ofConstructor(
SecurityProviderUtils.BOUNCYCASTLE_JSSE_PROVIDER_CLASS_NAME, "boolean",
Provider.class.getName());
ResultHandle bcJsseProvider = overallCatch.newInstance(bcJsseProviderConstructor,
overallCatch.load(true), bcFipsProvider);
overallCatch.invokeStaticMethod(
MethodDescriptor.ofMethod(Security.class, "insertProviderAt", int.class, Provider.class,
int.class),
bcFipsProvider, overallCatch.load(sunIndex));
overallCatch.invokeStaticMethod(
MethodDescriptor.ofMethod(Security.class, "insertProviderAt", int.class, Provider.class,
int.class),
bcJsseProvider, overallCatch.load(sunIndex + 1));
}
} else {
// BC or BCFIPS
final String providerName = bouncyCastleProvider.get().isInFipsMode()
? SecurityProviderUtils.BOUNCYCASTLE_FIPS_PROVIDER_CLASS_NAME
: SecurityProviderUtils.BOUNCYCASTLE_PROVIDER_CLASS_NAME;
ResultHandle bcProvider = overallCatch.newInstance(MethodDescriptor.ofConstructor(providerName));
overallCatch.invokeStaticMethod(
MethodDescriptor.ofMethod(Security.class, "addProvider", int.class, Provider.class),
bcProvider);
}
// Complete BouncyCastle AutoFeature generation
CatchBlockCreator print = overallCatch.addCatch(Throwable.class);
print.invokeVirtualMethod(ofMethod(Throwable.class, "printStackTrace", void.class), print.getCaughtException());
afterRegistration.returnValue(null);
file.close();
}
}
// Work around https://github.com/quarkusio/quarkus/issues/21374
@BuildStep
void addBouncyCastleExportsToNativeImage(BuildProducer jpmsExports,
List bouncyCastleProviders,
List bouncyCastleJsseProviders) {
boolean isInFipsMode;
Optional bouncyCastleJsseProvider = getOne(bouncyCastleJsseProviders);
if (bouncyCastleJsseProvider.isPresent()) {
isInFipsMode = bouncyCastleJsseProvider.get().isInFipsMode();
} else {
Optional bouncyCastleProvider = getOne(bouncyCastleProviders);
isInFipsMode = bouncyCastleProvider.isPresent() && bouncyCastleProvider.get().isInFipsMode();
}
if (isInFipsMode) {
jpmsExports.produce(new JPMSExportBuildItem("java.base", "sun.security.internal.spec"));
jpmsExports.produce(new JPMSExportBuildItem("java.base", "sun.security.provider"));
}
}
private static Optional getOne(List items) {
if (items.size() > 1) {
throw new IllegalStateException("Only a single Bouncy Castle registration can be provided.");
}
return items.stream().findFirst();
}
/**
* Determine the classes that make up the provider and its services
*
* @param providerName - JCA provider name
* @return class names that make up the provider and its services
*/
private static List registerProvider(String providerName,
String providerConfig,
BuildProducer additionalProviders) {
List providerClasses = new ArrayList<>();
Provider provider = Security.getProvider(providerName);
if (provider != null) {
providerClasses.add(provider.getClass().getName());
for (Provider.Service service : provider.getServices()) {
providerClasses.add(service.getClassName());
// Need to pull in the key classes
String supportedKeyClasses = service.getAttribute("SupportedKeyClasses");
if (supportedKeyClasses != null) {
providerClasses.addAll(Arrays.asList(supportedKeyClasses.split("\\|")));
}
}
if (providerConfig != null) {
Provider configuredProvider = provider.configure(providerConfig);
if (configuredProvider != null) {
Security.addProvider(configuredProvider);
providerClasses.add(configuredProvider.getClass().getName());
}
}
}
if (SecurityProviderUtils.SUN_PROVIDERS.containsKey(providerName)) {
additionalProviders.produce(
new NativeImageSecurityProviderBuildItem(SecurityProviderUtils.SUN_PROVIDERS.get(providerName)));
}
return providerClasses;
}
@Consume(RuntimeConfigSetupCompleteBuildItem.class)
@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
void recordRuntimeConfigReady(SecurityCheckRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem,
LaunchModeBuildItem launchModeBuildItem) {
recorder.setRuntimeConfigReady();
if (launchModeBuildItem.getLaunchMode() == LaunchMode.DEVELOPMENT) {
recorder.unsetRuntimeConfigReady(shutdownContextBuildItem);
}
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void registerSecurityInterceptors(BuildProducer registrars,
BuildProducer beans,
BuildProducer syntheticBeanProducer, SecurityCheckRecorder recorder,
Optional additionalSecurityConstrainerEventsItem) {
registrars.produce(new InterceptorBindingRegistrarBuildItem(new SecurityAnnotationsRegistrar()));
Class>[] interceptors = { AuthenticatedInterceptor.class, DenyAllInterceptor.class, PermitAllInterceptor.class,
RolesAllowedInterceptor.class, PermissionsAllowedInterceptor.class };
beans.produce(new AdditionalBeanBuildItem(interceptors));
beans.produce(new AdditionalBeanBuildItem(SecurityHandler.class));
var additionalEventsSupplier = additionalSecurityConstrainerEventsItem
.map(AdditionalSecurityConstrainerEventPropsBuildItem::getAdditionalEventPropsSupplier)
.orElse(null);
syntheticBeanProducer.produce(SyntheticBeanBuildItem
.configure(SecurityConstrainer.class)
.unremovable()
.scope(Singleton.class)
.supplier(recorder.createSecurityConstrainer(additionalEventsSupplier))
.done());
}
/**
* Transform deprecated {@link AdditionalSecuredClassesBuildItem} to {@link AdditionalSecuredMethodsBuildItem}.
*/
@BuildStep
void transformAdditionalSecuredClassesToMethods(List additionalSecuredClassesBuildItems,
BuildProducer additionalSecuredMethodsBuildItemBuildProducer) {
for (AdditionalSecuredClassesBuildItem additionalSecuredClassesBuildItem : additionalSecuredClassesBuildItems) {
final Collection securedMethods = new ArrayList<>();
for (ClassInfo additionalSecuredClass : additionalSecuredClassesBuildItem.additionalSecuredClasses) {
for (MethodInfo method : additionalSecuredClass.methods()) {
if (isPublicNonStaticNonConstructor(method)) {
securedMethods.add(method);
}
}
}
additionalSecuredMethodsBuildItemBuildProducer.produce(
new AdditionalSecuredMethodsBuildItem(securedMethods, additionalSecuredClassesBuildItem.rolesAllowed));
}
}
/*
* The annotation store is not meant to be generally supported for security annotation.
* It is only used here in order to be able to register the DenyAllInterceptor for
* methods that don't have a security annotation
*/
@BuildStep
void transformSecurityAnnotations(BuildProducer transformers,
List additionalSecuredMethods,
SecurityBuildTimeConfig config) {
if (config.denyUnannotated()) {
transformers.produce(new AnnotationsTransformerBuildItem(AnnotationTransformation
.forClasses()
.whenClass(new DenyUnannotatedPredicate())
.transform(ctx -> ctx.add(DenyAll.class))));
}
if (!additionalSecuredMethods.isEmpty()) {
for (AdditionalSecuredMethodsBuildItem securedMethods : additionalSecuredMethods) {
Collection additionalSecured = new HashSet<>();
for (MethodInfo additionalSecuredMethod : securedMethods.additionalSecuredMethods) {
additionalSecured.add(createMethodDescription(additionalSecuredMethod));
}
if (securedMethods.rolesAllowed.isPresent()) {
var additionalRolesAllowedTransformer = new AdditionalRolesAllowedTransformer(additionalSecured,
securedMethods.rolesAllowed.get());
transformers.produce(new AnnotationsTransformerBuildItem(AnnotationTransformation
.forMethods()
.whenMethod(additionalRolesAllowedTransformer)
.transform(additionalRolesAllowedTransformer)));
} else {
var additionalDenyingUnannotatedTransformer = new AdditionalDenyingUnannotatedTransformer(
additionalSecured);
transformers.produce(new AnnotationsTransformerBuildItem(AnnotationTransformation
.forMethods()
.whenMethod(additionalDenyingUnannotatedTransformer)
.transform(additionalDenyingUnannotatedTransformer)));
}
}
}
}
/*
* Transform all security annotations to be {@code @Inherited}
*/
@BuildStep
void makeSecurityAnnotationsInherited(BuildProducer transformer) {
Set securityAnnotationNames = Set.of(PERMIT_ALL, DENY_ALL, AUTHENTICATED, PERMISSIONS_ALLOWED, ROLES_ALLOWED);
transformer.produce(new AnnotationsTransformerBuildItem(AnnotationTransformation.forClasses()
.whenClass(c -> securityAnnotationNames.contains(c.name()))
.transform(c -> c.add(AnnotationInstance.builder(INHERITED).build()))));
}
@BuildStep
PermissionsAllowedMetaAnnotationBuildItem transformPermissionsAllowedMetaAnnotations(
BeanArchiveIndexBuildItem beanArchiveBuildItem,
BuildProducer transformers,
List classAnnotationItems) {
var index = beanArchiveBuildItem.getIndex();
var item = movePermFromMetaAnnToMetaTarget(index);
// add @PermissionsAllowed to meta-annotation method target
item.getTransitiveInstances()
.stream()
.filter(i -> i.target().kind() == AnnotationTarget.Kind.METHOD)
.forEach(i -> {
var method = i.target().asMethod();
var targetClassName = method.declaringClass().name();
transformers.produce(
new AnnotationsTransformerBuildItem(
AnnotationTransformation
.forMethods()
.whenMethod(targetClassName, method.name())
.transform(tc -> tc.add(i))));
});
// extensions WebSockets Next doesn't want CDI interceptors to prevent repeated checks
var additionalClassAnnotations = classAnnotationItems.stream()
.map(ClassSecurityCheckAnnotationBuildItem::getClassAnnotation).collect(Collectors.toSet());
final Predicate hasNoAdditionalClassAnnotation;
if (additionalClassAnnotations.isEmpty()) {
hasNoAdditionalClassAnnotation = ai -> true;
} else {
hasNoAdditionalClassAnnotation = ai -> {
for (var declaredAnnotation : ai.target().asClass().declaredAnnotations()) {
if (additionalClassAnnotations.contains(declaredAnnotation.name())) {
return false;
}
}
return true;
};
}
// add @PermissionsAllowed to meta-annotation class target
item.getTransitiveInstances()
.stream()
.filter(i -> i.target().kind() == AnnotationTarget.Kind.CLASS)
.filter(hasNoAdditionalClassAnnotation)
.forEach(i -> {
var clazz = i.target().asClass();
transformers.produce(
new AnnotationsTransformerBuildItem(
AnnotationTransformation
.forClasses()
.whenClass(clazz.name())
.transform(tc -> tc.add(i))));
});
return item;
}
@BuildStep
PermissionSecurityChecksBuilderBuildItem createPermissionSecurityChecksBuilder(
BeanArchiveIndexBuildItem beanArchiveBuildItem,
PermissionsAllowedMetaAnnotationBuildItem metaAnnotationItem) {
return new PermissionSecurityChecksBuilderBuildItem(
new PermissionSecurityChecksBuilder(beanArchiveBuildItem.getIndex(), metaAnnotationItem));
}
@BuildStep
UnremovableBeanBuildItem makePermissionCheckerClassBeansUnremovable() {
// this won't do the trick for checkers on abstract classes or beans producer fields and methods
return new UnremovableBeanBuildItem(bi -> {
if (bi.isRemovable() && bi.isClassBean()) {
return bi.getTarget().map(t -> t.hasAnnotation(PERMISSION_CHECKER_NAME)).orElse(false);
}
return false;
});
}
@BuildStep
ExecutionModelAnnotationsAllowedBuildItem supportBlockingExecutionOfPermissionChecks() {
return new ExecutionModelAnnotationsAllowedBuildItem(
methodInfo -> methodInfo.hasDeclaredAnnotation(PERMISSION_CHECKER_NAME)
&& methodInfo.hasDeclaredAnnotation(BLOCKING));
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void configurePermissionCheckers(PermissionSecurityChecksBuilderBuildItem checkerBuilder,
BuildProducer syntheticBeanProducer, SecurityCheckRecorder recorder,
BeanDiscoveryFinishedBuildItem beanDiscoveryFinishedBuildItem,
BuildProducer generatedClassProducer) {
if (checkerBuilder.instance.foundPermissionChecker()) {
// ==== produce SecurityIdentityAugmentor that checks QuarkusPermissions
// why do we use synthetic bean?
// - this processor relies on the bean archive index (cycle: idx -> additional bean -> idx)
// - we have injection points (=> better validation from Arc) as checker beans are only requested from this augmentor
var syntheticBeanConfigurator = SyntheticBeanBuildItem
.configure(SecurityIdentityAugmentor.class)
// ATM we do get augmentors from CDI once, no need to keep the instance in the CDI container
.scope(Dependent.class)
.unremovable()
.addInjectionPoint(Type.create(BlockingSecurityExecutor.class))
.createWith(recorder.createPermissionAugmentor());
checkerBuilder.instance.getPermissionCheckers().stream().forEach(checkerMethod -> {
var checkerClassType = Type.create(checkerMethod.declaringClass().name(), Type.Kind.CLASS);
// validate permission checker method's declaring class is a CDI bean
// synthetic beans are not taken into consideration which makes them not supported
var matchingBeans = beanDiscoveryFinishedBuildItem.beanStream().assignableTo(checkerClassType).collect();
if (matchingBeans.isEmpty()) {
throw new RuntimeException(
"""
@PermissionChecker declared on method '%s', but no matching CDI bean could be found for the declaring class '%s'.
"""
.formatted(checkerMethod.name(), checkerClassType.name()));
}
// Using @Dependent is problematic because we would have to destroy beans manually at some point (which?)
matchingBeans.stream().filter(b -> BuiltinScope.DEPENDENT.getInfo().equals(b.getScope())).findFirst()
.ifPresent(bi -> {
throw new RuntimeException(
"""
Found @PermissionChecker annotation instance declared on the CDI bean method '%s#%s'.
The CDI bean is a dependent scoped bean, but only the '@Singleton' bean or normal scoped beans are supported
"""
.formatted(checkerMethod.name(), checkerClassType.name()));
});
syntheticBeanConfigurator.addInjectionPoint(checkerClassType);
});
syntheticBeanProducer.produce(syntheticBeanConfigurator.done());
// ==== Generate QuarkusPermission for each @PermissionChecker annotation instance
checkerBuilder.instance.generatePermissionCheckers(generatedClassProducer);
}
}
@BuildStep
@Record(ExecutionTime.STATIC_INIT)
MethodSecurityChecks gatherSecurityChecks(
BuildProducer configExpSecurityCheckProducer,
List rolesAllowedConfigExpResolverBuildItems,
BeanArchiveIndexBuildItem beanArchiveBuildItem,
BuildProducer classPredicate,
BuildProducer configBuilderProducer,
List additionalSecuredMethods,
SecurityCheckRecorder recorder, List additionalSecurityAnnotationItems,
BuildProducer classSecurityCheckStorageProducer,
List registerClassSecurityCheckBuildItems,
BuildProducer reflectiveClassBuildItemBuildProducer,
List additionalSecurityChecks, SecurityBuildTimeConfig config,
PermissionSecurityChecksBuilderBuildItem permissionSecurityChecksBuilderBuildItem,
BuildProducer generatedClassesProducer,
BuildProducer reflectiveClassesProducer) {
var hasAdditionalSecAnn = hasAdditionalSecurityAnnotation(additionalSecurityAnnotationItems.stream()
.map(AdditionalSecurityAnnotationBuildItem::getSecurityAnnotationName).collect(Collectors.toSet()));
classPredicate.produce(new ApplicationClassPredicateBuildItem(new SecurityCheckStorageAppPredicate()));
final Map additionalSecured = new HashMap<>();
for (AdditionalSecuredMethodsBuildItem securedMethods : additionalSecuredMethods) {
for (MethodInfo m : securedMethods.additionalSecuredMethods) {
additionalSecured.putIfAbsent(createMethodDescription(m),
new AdditionalSecured(m, securedMethods.rolesAllowed));
}
}
IndexView index = beanArchiveBuildItem.getIndex();
Map securityChecks = gatherSecurityAnnotations(index, configExpSecurityCheckProducer,
additionalSecured.values(), config.denyUnannotated(), recorder, configBuilderProducer,
reflectiveClassBuildItemBuildProducer, rolesAllowedConfigExpResolverBuildItems,
registerClassSecurityCheckBuildItems, classSecurityCheckStorageProducer, hasAdditionalSecAnn,
additionalSecurityAnnotationItems, permissionSecurityChecksBuilderBuildItem.instance,
generatedClassesProducer, reflectiveClassesProducer);
for (AdditionalSecurityCheckBuildItem additionalSecurityCheck : additionalSecurityChecks) {
securityChecks.put(additionalSecurityCheck.getMethodInfo(),
additionalSecurityCheck.getSecurityCheck());
}
return new MethodSecurityChecks(securityChecks);
}
@Consume(Capabilities.class) // make sure extension combinations are validated before default security check
@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void createSecurityCheckStorage(BuildProducer syntheticBeans,
BuildProducer classPredicate,
SecurityCheckRecorder recorder, MethodSecurityChecks securityChecksItem,
List defaultSecurityCheckBuildItem) {
classPredicate.produce(new ApplicationClassPredicateBuildItem(new SecurityCheckStorageAppPredicate()));
RuntimeValue builder = recorder.newBuilder();
for (Map.Entry methodEntry : securityChecksItem.securityChecks
.entrySet()) {
MethodInfo method = methodEntry.getKey();
String[] params = new String[method.parametersCount()];
for (int i = 0; i < method.parametersCount(); ++i) {
params[i] = method.parameterType(i).name().toString();
}
recorder.addMethod(builder, method.declaringClass().name().toString(), method.name(), params,
methodEntry.getValue());
}
if (!defaultSecurityCheckBuildItem.isEmpty()) {
if (defaultSecurityCheckBuildItem.size() > 1) {
int itemCount = defaultSecurityCheckBuildItem.size();
throw new IllegalStateException("Found %d DefaultSecurityCheckBuildItem items, ".formatted(itemCount)
+ "please make sure the item is produced exactly once");
}
var roles = defaultSecurityCheckBuildItem.get(0).getRolesAllowed();
if (roles == null) {
recorder.registerDefaultSecurityCheck(builder, recorder.denyAll());
} else {
recorder.registerDefaultSecurityCheck(builder, recorder.rolesAllowed(roles.toArray(new String[0])));
}
}
syntheticBeans.produce(
SyntheticBeanBuildItem.configure(SecurityCheckStorage.class)
.scope(ApplicationScoped.class)
.unremovable()
.runtimeProxy(recorder.create(builder))
.done());
}
@Consume(RuntimeConfigSetupCompleteBuildItem.class)
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
public void resolveConfigExpressionRoles(Optional configExpRolesChecks,
SecurityCheckRecorder recorder) {
if (configExpRolesChecks.isPresent()) {
// we created supplier security check for each role set with at least one config expression
// now we need to resolve config expression so that if there are any failures they happen when app starts
// rather than first time request is checked (which would be more likely to affect end user)
recorder.resolveRolesAllowedConfigExpRoles();
}
}
private static Map gatherSecurityAnnotations(IndexView index,
BuildProducer configExpSecurityCheckProducer,
Collection additionalSecuredMethods, boolean denyUnannotated, SecurityCheckRecorder recorder,
BuildProducer configBuilderProducer,
BuildProducer reflectiveClassBuildItemBuildProducer,
List rolesAllowedConfigExpResolverBuildItems,
List registerClassSecurityCheckBuildItems,
BuildProducer classSecurityCheckStorageProducer,
Predicate hasAdditionalSecurityAnnotations,
List additionalSecurityAnnotationItems,
PermissionSecurityChecksBuilder permissionCheckBuilder,
BuildProducer generatedClassesProducer,
BuildProducer reflectiveClassesProducer) {
Map methodToInstanceCollector = new HashMap<>();
Map classAnnotations = new HashMap<>();
Map result = new HashMap<>();
var permitAllGatherer = new SecurityAnnotationGatherer(index.getAnnotations(PERMIT_ALL), methodToInstanceCollector,
((m, i) -> result.put(m, recorder.permitAll())), classAnnotations, hasAdditionalSecurityAnnotations);
var authenticatedGatherer = new SecurityAnnotationGatherer(index.getAnnotations(DotNames.AUTHENTICATED),
methodToInstanceCollector, ((m, i) -> result.put(m, recorder.authenticated())), classAnnotations,
hasAdditionalSecurityAnnotations);
var denyAllGatherer = new SecurityAnnotationGatherer(index.getAnnotations(DENY_ALL), methodToInstanceCollector,
((m, i) -> result.put(m, recorder.denyAll())), classAnnotations, hasAdditionalSecurityAnnotations);
// here we just collect all methods annotated with @RolesAllowed
Map methodToRoles = new HashMap<>();
var rolesAllowedGatherer = new SecurityAnnotationGatherer(index.getAnnotations(ROLES_ALLOWED),
methodToInstanceCollector,
((methodInfo, instance) -> methodToRoles.put(methodInfo, instance.value().asStringArray())), classAnnotations,
hasAdditionalSecurityAnnotations);
// gather method-level instances for @Authenticated, @RolesAllowed, @PermitAll, @DenyAll
permitAllGatherer.gatherMethodSecurityAnnotations();
authenticatedGatherer.gatherMethodSecurityAnnotations();
denyAllGatherer.gatherMethodSecurityAnnotations();
rolesAllowedGatherer.gatherMethodSecurityAnnotations();
// gather @PermissionsAllowed security checks
final Map classNameToPermCheck;
if (permissionCheckBuilder.foundPermissionsAllowedInstances()) {
var additionalClassInstances = registerClassSecurityCheckBuildItems
.stream()
.filter(i -> PERMISSIONS_ALLOWED.equals(i.securityAnnotationInstance.name()))
.map(i -> i.securityAnnotationInstance)
.toList();
var securityChecks = permissionCheckBuilder
.prepareParamConverterGenerator(recorder, generatedClassesProducer, reflectiveClassesProducer)
.gatherPermissionsAllowedAnnotations(methodToInstanceCollector, classAnnotations, additionalClassInstances,
hasAdditionalSecurityAnnotations)
.validatePermissionClasses()
.createPermissionPredicates()
.build();
result.putAll(securityChecks.getMethodSecurityChecks());
classNameToPermCheck = securityChecks.getClassNameSecurityChecks();
// register used permission classes for reflection
for (String permissionClass : securityChecks.permissionClasses()) {
reflectiveClassBuildItemBuildProducer
.produce(ReflectiveClassBuildItem.builder(permissionClass).constructors().fields().methods().build());
log.debugf("Register Permission class for reflection: %s", permissionClass);
}
} else {
classNameToPermCheck = Map.of();
}
// gather class-level instances for @Authenticated, @RolesAllowed, @PermitAll, @DenyAll
permitAllGatherer.gatherClassSecurityAnnotations();
authenticatedGatherer.gatherClassSecurityAnnotations();
denyAllGatherer.gatherClassSecurityAnnotations();
rolesAllowedGatherer.gatherClassSecurityAnnotations();
// validate additional annotations on class level are not accompanied by standard security annotations
additionalSecurityAnnotationItems
.stream()
.map(AdditionalSecurityAnnotationBuildItem::getSecurityAnnotationName)
.forEach(additionalSecAnnName -> index
.getAnnotations(additionalSecAnnName)
.stream()
.filter(ai -> ai.target().kind() == AnnotationTarget.Kind.CLASS)
.map(ai -> ai.target().asClass())
.filter(SecurityTransformerUtils::hasSecurityAnnotation)
.findFirst()
.ifPresent(ci -> {
var securityAnnotation = findFirstStandardSecurityAnnotation(ci).get().name();
throw new RuntimeException("""
Class '%s' is annotated with '%s' and '%s' security annotations,
however security annotations cannot be combined.
""".formatted(ci.name(), additionalSecAnnName, securityAnnotation));
}));
/*
* Handle additional secured methods by adding the denyAll/rolesAllowed check to all public non-static methods
* that don't have same security annotations
*/
for (AdditionalSecured additionalSecuredMethod : additionalSecuredMethods) {
if (!isPublicNonStaticNonConstructor(additionalSecuredMethod.methodInfo)) {
continue;
}
if (hasAdditionalSecurityAnnotations.test(additionalSecuredMethod.methodInfo)) {
continue;
}
AnnotationInstance alreadyExistingInstance = methodToInstanceCollector.get(additionalSecuredMethod.methodInfo);
if (additionalSecuredMethod.rolesAllowed.isPresent()) {
if (alreadyExistingInstance == null) {
methodToRoles.put(additionalSecuredMethod.methodInfo,
additionalSecuredMethod.rolesAllowed.get().toArray(String[]::new));
} else if (alreadyHasAnnotation(alreadyExistingInstance, ROLES_ALLOWED)) {
// we should not try to add second @RolesAllowed
throw new IllegalStateException("Method " + additionalSecuredMethod.methodInfo.declaringClass() + "#"
+ additionalSecuredMethod.methodInfo.name() + " should not have been added as an additional "
+ "secured method as it's already annotated with @RolesAllowed.");
}
} else {
if (alreadyExistingInstance == null) {
result.put(additionalSecuredMethod.methodInfo, recorder.denyAll());
} else if (alreadyHasAnnotation(alreadyExistingInstance, DENY_ALL)) {
// we should not try to add second @DenyAll
throw new IllegalStateException("Method " + additionalSecuredMethod.methodInfo.declaringClass() + "#"
+ additionalSecuredMethod.methodInfo.name() + " should not have been added as an additional "
+ "secured method as it's already annotated with @DenyAll.");
}
}
}
// create roles allowed security checks
// we create only one security check for each role set
Map, SecurityCheck> cache = new HashMap<>();
final AtomicInteger keyIndex = new AtomicInteger(0);
final AtomicBoolean hasRolesAllowedCheckWithConfigExp = new AtomicBoolean(false);
for (Map.Entry entry : methodToRoles.entrySet()) {
final MethodInfo methodInfo = entry.getKey();
result.put(methodInfo,
computeRolesAllowedCheck(cache, hasRolesAllowedCheckWithConfigExp, keyIndex, recorder, entry.getValue()));
}
if (!registerClassSecurityCheckBuildItems.isEmpty()) {
var classStorageBuilder = new ClassStorageBuilder();
registerClassSecurityCheckBuildItems.forEach(item -> {
var securityAnnotationName = item.securityAnnotationInstance.name();
final SecurityCheck securityCheck;
if (DENY_ALL.equals(securityAnnotationName)) {
securityCheck = recorder.denyAll();
} else if (PERMIT_ALL.equals(securityAnnotationName)) {
securityCheck = recorder.permitAll();
} else if (AUTHENTICATED.equals(securityAnnotationName)) {
securityCheck = recorder.authenticated();
} else if (ROLES_ALLOWED.equals(securityAnnotationName)) {
var allowedRoles = item.securityAnnotationInstance.value().asStringArray();
securityCheck = computeRolesAllowedCheck(cache, hasRolesAllowedCheckWithConfigExp, keyIndex, recorder,
allowedRoles);
} else if (PERMISSIONS_ALLOWED.equals(securityAnnotationName)) {
securityCheck = Objects.requireNonNull(classNameToPermCheck.get(item.className));
} else {
throw new IllegalStateException("Found unknown security annotation: " + securityAnnotationName);
}
classStorageBuilder.addSecurityCheck(item.className, securityCheck);
});
classSecurityCheckStorageProducer.produce(classStorageBuilder.build());
}
final boolean registerRolesAllowedConfigSource;
// way to resolve roles allowed configuration expressions specified via annotations to configuration values
if (!rolesAllowedConfigExpResolverBuildItems.isEmpty()) {
registerRolesAllowedConfigSource = true;
for (RolesAllowedConfigExpResolverBuildItem item : rolesAllowedConfigExpResolverBuildItems) {
recorder.recordRolesAllowedConfigExpression(item.getRoleConfigExpr(), keyIndex.getAndIncrement(),
item.getConfigValueRecorder());
}
} else {
registerRolesAllowedConfigSource = hasRolesAllowedCheckWithConfigExp.get();
}
if (hasRolesAllowedCheckWithConfigExp.get()) {
// make sure config expressions are eagerly resolved inside security checks when app starts
configExpSecurityCheckProducer
.produce(new ConfigExpRolesAllowedSecurityCheckBuildItem());
}
if (registerRolesAllowedConfigSource) {
// register config source with the Config system
configBuilderProducer
.produce(new RunTimeConfigBuilderBuildItem(QuarkusSecurityRolesAllowedConfigBuilder.class.getName()));
}
/*
* If we need to add the denyAll security check to all unannotated methods, we simply go through all secured methods,
* collect the declaring classes, then go through all methods of the classes and add the necessary check
*/
if (denyUnannotated) {
Set allClassesWithSecurityChecks = new HashSet<>(methodToInstanceCollector.keySet().size());
for (MethodInfo methodInfo : methodToInstanceCollector.keySet()) {
allClassesWithSecurityChecks.add(methodInfo.declaringClass());
}
for (ClassInfo classWithSecurityCheck : allClassesWithSecurityChecks) {
for (MethodInfo methodInfo : classWithSecurityCheck.methods()) {
if (!isPublicNonStaticNonConstructor(methodInfo)) {
continue;
}
if (methodToInstanceCollector.containsKey(methodInfo)) { // the method already has a security check
continue;
}
if (hasAdditionalSecurityAnnotations.test(methodInfo)) {
continue;
}
result.put(methodInfo, recorder.denyAll());
}
}
}
return result;
}
private static SecurityCheck computeRolesAllowedCheck(Map, SecurityCheck> cache,
AtomicBoolean hasRolesAllowedCheckWithConfigExp, AtomicInteger keyIndex, SecurityCheckRecorder recorder,
String[] allowedRoles) {
return cache.computeIfAbsent(getSetForKey(allowedRoles), new Function, SecurityCheck>() {
@Override
public SecurityCheck apply(Set allowedRolesSet) {
final int[] configExpressionPositions = configExpressionPositions(allowedRoles);
if (configExpressionPositions.length > 0) {
// we need to use supplier check as security checks are created during static init
// while config expressions are resolved during runtime
hasRolesAllowedCheckWithConfigExp.set(true);
// we don't create security check for each method, therefore we need artificial keys
// we can safely use numbers as RolesAllowed config source prefix all keys
final int[] configKeys = new int[configExpressionPositions.length];
for (int i = 0; i < configExpressionPositions.length; i++) {
// now we just collect artificial keys, but
// before we add the property to the Config system, we prefix it, e.g.
// @RolesAllowed("${admin}") -> QuarkusSecurityRolesAllowedConfigSource.property-0=${admin}
configKeys[i] = keyIndex.getAndIncrement();
}
return recorder.rolesAllowedSupplier(allowedRoles, configExpressionPositions, configKeys);
}
return recorder.rolesAllowed(allowedRoles);
}
});
}
public static int[] configExpressionPositions(String[] allowedRoles) {
final Set expPositions = new HashSet<>();
for (int i = 0; i < allowedRoles.length; i++) {
final int exprStart = allowedRoles[i].indexOf("${");
if (exprStart >= 0 && allowedRoles[i].indexOf('}', exprStart + 2) > 0) {
expPositions.add(i);
}
}
if (expPositions.isEmpty()) {
return new int[0];
}
return expPositions.stream().mapToInt(Integer::intValue).toArray();
}
private static Set getSetForKey(String[] allowedRoles) {
if (allowedRoles.length == 0) { // shouldn't happen, but let's be on the safe side
return Collections.emptySet();
} else if (allowedRoles.length == 1) {
return Collections.singleton(allowedRoles[0]);
}
// use a set in order to avoid caring about the order of elements
return new HashSet<>(Arrays.asList(allowedRoles));
}
private static boolean alreadyHasAnnotation(AnnotationInstance alreadyExistingInstance, DotName annotationName) {
return alreadyExistingInstance.target().kind() == AnnotationTarget.Kind.METHOD
&& alreadyExistingInstance.name().equals(annotationName);
}
static boolean isPublicNonStaticNonConstructor(MethodInfo methodInfo) {
return Modifier.isPublic(methodInfo.flags()) && !Modifier.isStatic(methodInfo.flags())
&& !"".equals(methodInfo.name());
}
@BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem(Feature.SECURITY);
}
@BuildStep
void registerAdditionalBeans(BuildProducer beans) {
beans.produce(AdditionalBeanBuildItem.unremovableOf(SecurityIdentityAssociation.class));
beans.produce(AdditionalBeanBuildItem.unremovableOf(IdentityProviderManagerCreator.class));
beans.produce(AdditionalBeanBuildItem.unremovableOf(SecurityIdentityProxy.class));
beans.produce(AdditionalBeanBuildItem.unremovableOf(X509IdentityProvider.class));
}
@BuildStep
AdditionalBeanBuildItem authorizationController(LaunchModeBuildItem launchMode) {
Class extends AuthorizationController> controllerClass = AuthorizationController.class;
if (launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT && !security.authorizationEnabledInDevMode()) {
controllerClass = DevModeDisabledAuthorizationController.class;
}
return AdditionalBeanBuildItem.builder().addBeanClass(controllerClass).build();
}
@BuildStep
void validateStartUpObserversNotSecured(SynthesisFinishedBuildItem synthesisFinished,
ValidationPhaseBuildItem validationPhase,
BeanArchiveIndexBuildItem beanArchiveIndexBuildItem,
BuildProducer validationErrorProducer) {
AnnotationStore annotationStore = validationPhase.getContext().get(BuildExtension.Key.ANNOTATION_STORE);
synthesisFinished
.getObservers()
.stream()
.map(ObserverInfo::asObserver)
.filter(observer -> observer.getObservedType().name().equals(STARTUP_EVENT_NAME))
.map(ObserverInfo::getObserverMethod)
.filter(Objects::nonNull) // synthetic observer method created for @Startup is null and not secured
.forEach(mi -> {
if (hasSecurityAnnotation(annotationStore.getAnnotations(mi))
|| hasClassLevelStandardSecurityAnnotation(mi, annotationStore)) {
var declaringClass = mi.declaringClass();
findFirstStandardSecurityAnnotation(annotationStore.getAnnotations(mi))
.or(() -> findFirstStandardSecurityAnnotation(
annotationStore.getAnnotations(declaringClass)))
.map(AnnotationInstance::name)
.filter(name -> !name.equals(PERMIT_ALL))
.ifPresent(securityAnnotation -> {
var errorMsg = String.format(
"Method '%s#%s' cannot observe '%s' as the method is secured with the '%s' annotation",
declaringClass.name(), mi.name(), STARTUP_EVENT_NAME, securityAnnotation);
validationErrorProducer
.produce(new ValidationErrorBuildItem(new ConfigurationException(errorMsg)));
});
}
});
}
@BuildStep
void gatherClassSecurityChecks(BuildProducer producer,
BeanArchiveIndexBuildItem indexBuildItem, PermissionsAllowedMetaAnnotationBuildItem permsMetaAnnotationsItem,
List classAnnotationItems) {
if (!classAnnotationItems.isEmpty()) {
var index = indexBuildItem.getIndex();
classAnnotationItems
.stream()
.map(ClassSecurityCheckAnnotationBuildItem::getClassAnnotation)
.map(index::getAnnotations)
.flatMap(Collection::stream)
.filter(ai -> ai.target().kind() == AnnotationTarget.Kind.CLASS)
.map(ai -> ai.target().asClass())
.filter(cl -> SecurityTransformerUtils.hasSecurityAnnotation(cl)
|| permsMetaAnnotationsItem.hasPermissionsAllowed(cl))
.map(c -> new RegisterClassSecurityCheckBuildItem(c.name(), findFirstStandardSecurityAnnotation(c)
.or(() -> permsMetaAnnotationsItem.findPermissionsAllowedInstance(c))
.get()))
.forEach(producer::produce);
}
}
private static boolean hasClassLevelStandardSecurityAnnotation(MethodInfo method, AnnotationStore annotationStore) {
return applyClassLevenInterceptor(method, annotationStore)
&& hasSecurityAnnotation(annotationStore.getAnnotations(method.declaringClass()));
}
private static boolean applyClassLevenInterceptor(MethodInfo method, AnnotationStore store) {
// whether class-level business method interceptors (@AroundInvoke) are applied
return !method.isConstructor() && Modifier.isPublic(method.flags())
&& !store.hasAnnotation(method, NO_CLASS_INTERCEPTORS);
}
static MethodDescription createMethodDescription(MethodInfo additionalSecuredMethod) {
String[] paramTypes = new String[additionalSecuredMethod.parametersCount()];
for (int i = 0; i < additionalSecuredMethod.parametersCount(); i++) {
paramTypes[i] = additionalSecuredMethod.parameterTypes().get(i).name().toString();
}
return new MethodDescription(additionalSecuredMethod.declaringClass().name().toString(), additionalSecuredMethod.name(),
paramTypes);
}
static class AdditionalSecured {
final MethodInfo methodInfo;
final Optional> rolesAllowed;
AdditionalSecured(MethodInfo methodInfo, Optional> rolesAllowed) {
this.methodInfo = methodInfo;
this.rolesAllowed = rolesAllowed;
}
}
static class SecurityCheckStorageAppPredicate implements Predicate {
@Override
public boolean test(String s) {
return s.equals(SecurityCheckStorage.class.getName());
}
}
static final class MethodSecurityChecks extends SimpleBuildItem {
Map securityChecks;
MethodSecurityChecks(Map securityChecks) {
this.securityChecks = securityChecks;
}
}
private static Predicate hasAdditionalSecurityAnnotation(Set additionalSecAnnotations) {
return new Predicate() {
@Override
public boolean test(MethodInfo methodInfo) {
return additionalSecAnnotations.stream().anyMatch(methodInfo::hasDeclaredAnnotation);
}
};
}
private static final class SecurityAnnotationGatherer {
private final Collection annotationInstances;
private final Map alreadyCheckedMethods;
private final BiConsumer putResult;
private final Map classLevelAnnotations;
private final Predicate hasAdditionalSecurityAnnotation;
private SecurityAnnotationGatherer(Collection annotationInstances,
Map alreadyCheckedMethods, BiConsumer putResult,
Map classLevelAnnotations,
Predicate hasAdditionalSecurityAnnotation) {
this.annotationInstances = annotationInstances;
this.alreadyCheckedMethods = alreadyCheckedMethods;
this.putResult = putResult;
this.classLevelAnnotations = classLevelAnnotations;
this.hasAdditionalSecurityAnnotation = hasAdditionalSecurityAnnotation;
}
private void gatherClassSecurityAnnotations() {
// now add the class annotations to methods if they haven't already been annotated
for (AnnotationInstance instance : annotationInstances) {
AnnotationTarget target = instance.target();
if (target.kind() == AnnotationTarget.Kind.CLASS) {
List methods = target.asClass().methods();
AnnotationInstance existingClassInstance = classLevelAnnotations.get(target.asClass());
if (existingClassInstance == null) {
classLevelAnnotations.put(target.asClass(), instance);
for (MethodInfo methodInfo : methods) {
AnnotationInstance alreadyExistingInstance = alreadyCheckedMethods.get(methodInfo);
if ((alreadyExistingInstance == null) && !hasAdditionalSecurityAnnotation.test(methodInfo)) {
putResult.accept(methodInfo, instance);
}
}
} else {
throw new IllegalStateException(
"Class " + target.asClass() + " is annotated with multiple security annotations "
+ instance.name()
+ " and " + existingClassInstance.name());
}
}
}
}
private void gatherMethodSecurityAnnotations() {
// make sure we process annotations on methods first
for (AnnotationInstance instance : annotationInstances) {
AnnotationTarget target = instance.target();
if (target.kind() == AnnotationTarget.Kind.METHOD) {
MethodInfo methodInfo = target.asMethod();
if (alreadyCheckedMethods.containsKey(methodInfo) || hasAdditionalSecurityAnnotation.test(methodInfo)) {
throw new IllegalStateException(
"Method " + methodInfo.name() + " of class " + methodInfo.declaringClass()
+ " is annotated with multiple security annotations");
}
alreadyCheckedMethods.put(methodInfo, instance);
putResult.accept(methodInfo, instance);
}
}
}
}
}