io.quarkus.security.deployment.PermissionSecurityChecks Maven / Gradle / Ivy
The newest version!
package io.quarkus.security.deployment;
import static io.quarkus.arc.processor.DotNames.BOOLEAN;
import static io.quarkus.arc.processor.DotNames.STRING;
import static io.quarkus.arc.processor.DotNames.UNI;
import static io.quarkus.gizmo.Type.classType;
import static io.quarkus.gizmo.Type.parameterizedType;
import static io.quarkus.security.PermissionsAllowed.AUTODETECTED;
import static io.quarkus.security.PermissionsAllowed.PERMISSION_TO_ACTION_SEPARATOR;
import static io.quarkus.security.deployment.DotNames.PERMISSIONS_ALLOWED;
import static io.quarkus.security.deployment.SecurityProcessor.isPublicNonStaticNonConstructor;
import java.lang.annotation.RetentionPolicy;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Modifier;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.PrimitiveType.Primitive;
import org.jboss.jandex.Type;
import org.jboss.jandex.VoidType;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.DescriptorUtils;
import io.quarkus.gizmo.FieldDescriptor;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.gizmo.SignatureBuilder;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.security.PermissionChecker;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.StringPermission;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusPermission;
import io.quarkus.security.runtime.SecurityCheckRecorder;
import io.quarkus.security.runtime.interceptor.PermissionsAllowedInterceptor;
import io.quarkus.security.spi.PermissionsAllowedMetaAnnotationBuildItem;
import io.quarkus.security.spi.runtime.SecurityCheck;
import io.smallrye.common.annotation.Blocking;
interface PermissionSecurityChecks {
DotName PERMISSION_CHECKER_NAME = DotName.createSimple(PermissionChecker.class);
DotName BLOCKING = DotName.createSimple(Blocking.class);
Map getMethodSecurityChecks();
Map getClassNameSecurityChecks();
Set permissionClasses();
final class PermissionSecurityChecksBuilder {
private static final DotName STRING_PERMISSION = DotName.createSimple(StringPermission.class);
private static final DotName PERMISSIONS_ALLOWED_INTERCEPTOR = DotName
.createSimple(PermissionsAllowedInterceptor.class);
private static final String PERMISSION_ATTR = "permission";
private static final String IS_GRANTED_UNI = "isGrantedUni";
private static final String IS_GRANTED = "isGranted";
private static final DotName SECURITY_IDENTITY_NAME = DotName.createSimple(SecurityIdentity.class);
private static final String SECURED_METHOD_PARAMETER = "securedMethodParameter";
private final Map>> targetToPermissionKeys = new HashMap<>();
private final Map targetToPredicate = new HashMap<>();
private final Map classSignatureToConstructor = new HashMap<>();
private final IndexView index;
private final List permissionInstances;
private final Map permissionNameToChecker;
private volatile SecurityCheckRecorder recorder;
private volatile PermissionConverterGenerator paramConverterGenerator;
PermissionSecurityChecksBuilder(IndexView index, PermissionsAllowedMetaAnnotationBuildItem metaAnnotationItem) {
this.index = index;
var instances = getPermissionsAllowedInstances(index, metaAnnotationItem);
// make sure we process annotations on methods first
instances.sort(new Comparator() {
@Override
public int compare(AnnotationInstance o1, AnnotationInstance o2) {
if (o1.target().kind() != o2.target().kind()) {
return o1.target().kind() == AnnotationTarget.Kind.METHOD ? -1 : 1;
}
// variable 'instances' won't be modified
return 0;
}
});
// this needs to be immutable as build steps that gather security checks
// and produce permission augmenter can and did in past run concurrently
this.permissionInstances = Collections.unmodifiableList(instances);
this.permissionNameToChecker = Collections.unmodifiableMap(getPermissionCheckers(index));
}
private static Map getPermissionCheckers(IndexView index) {
int permissionCheckerIndex = 0; // this ensures generated QuarkusPermission name is unique
var permissionCheckers = new HashMap();
for (var annotationInstance : index.getAnnotations(PERMISSION_CHECKER_NAME)) {
var checkerMethod = annotationInstance.target().asMethod();
if (Modifier.isPrivate(checkerMethod.flags())) {
// we generate QuarkusPermission in the same package as where the @PermissionChecker is detected
// so the checker method must be either public or package-private
throw new RuntimeException("Private method '" + toString(checkerMethod)
+ "' cannot be annotated with the @PermissionChecker annotation");
}
if (Modifier.isStatic(checkerMethod.flags())) {
// checkers must be CDI bean member methods for now, so the checker method must not be static
throw new RuntimeException("Static method '" + toString(checkerMethod)
+ "' cannot be annotated with the @PermissionChecker annotation");
}
boolean isReactive = isUniBoolean(checkerMethod);
if (!isReactive && !isPrimitiveBoolean(checkerMethod)) {
throw new RuntimeException(("@PermissionChecker method '%s' has return type '%s', but only " +
"supported return types are 'boolean' and 'Uni'. ")
.formatted(toString(checkerMethod), checkerMethod.returnType().name()));
}
var permissionToActions = parsePermissionToActions(annotationInstance.value().asString(), new HashMap<>())
.entrySet().iterator().next();
var permissionName = permissionToActions.getKey();
if (permissionName.isBlank()) {
throw new IllegalArgumentException(
"@PermissionChecker annotation placed on the '%s' attribute 'value' must not be blank"
.formatted(toString(checkerMethod)));
}
var permissionActions = permissionToActions.getValue();
if (permissionActions != null && !permissionActions.isEmpty()) {
throw new IllegalArgumentException("""
@PermissionChecker annotation instance placed on the '%s' has attribute 'value' with
permission name '%s' and actions '%s', however actions are currently not supported
""".formatted(toString(checkerMethod), permissionName, permissionActions));
}
boolean isBlocking = checkerMethod.hasDeclaredAnnotation(BLOCKING);
if (isBlocking && isReactive) {
throw new IllegalArgumentException("""
@PermissionChecker annotation instance placed on the '%s' returns 'Uni' and is
annotated with the @Blocking annotation; if you need to block, please return 'boolean'
""".formatted(toString(checkerMethod)));
}
var generatedPermissionClassName = getGeneratedPermissionName(checkerMethod, permissionCheckerIndex++);
var methodParamMappers = new MethodParameterMapper[checkerMethod.parametersCount()];
var generatedPermissionConstructor = getGeneratedPermissionConstructor(checkerMethod, methodParamMappers);
var checkerMetadata = new PermissionCheckerMetadata(checkerMethod, generatedPermissionClassName,
isReactive, generatedPermissionConstructor, methodParamMappers, isBlocking);
if (permissionCheckers.containsKey(permissionName)) {
throw new IllegalArgumentException("""
Detected two @PermissionChecker annotations with same value '%s', annotated methods are:
- %s
- %s
"""
.formatted(annotationInstance.value().asString(), toString(checkerMethod),
toString(permissionCheckers.get(permissionName).checkerMethod())));
}
permissionCheckers.put(permissionName, checkerMetadata);
}
return permissionCheckers;
}
private static boolean isUniBoolean(MethodInfo checkerMethod) {
if (checkerMethod.returnType().kind() == Type.Kind.PARAMETERIZED_TYPE) {
var parametrizedType = checkerMethod.returnType().asParameterizedType();
boolean returnsUni = UNI.equals(parametrizedType.name());
boolean booleanArg = parametrizedType.arguments().size() == 1
&& BOOLEAN.equals(parametrizedType.arguments().get(0).name());
return returnsUni && booleanArg;
}
return false;
}
private static boolean isPrimitiveBoolean(MethodInfo checkerMethod) {
return checkerMethod.returnType().kind() == Type.Kind.PRIMITIVE
&& Primitive.BOOLEAN.equals(checkerMethod.returnType().asPrimitiveType().primitive());
}
private static MethodInfo getGeneratedPermissionConstructor(MethodInfo checkerMethod,
MethodParameterMapper[] paramMappers) {
if (!checkerMethod.exceptions().isEmpty()) {
throw new RuntimeException("@PermissionChecker method '%s' declares checked exceptions which is not allowed"
.formatted(toString(checkerMethod)));
}
if (checkerMethod.parametersCount() == 0) {
throw new RuntimeException(
"@PermissionChecker method '%s' must have at least one parameter".formatted(toString(checkerMethod)));
}
// Permission constructor: permission name, <>...
// Permission checker method: [optionally at any place SecurityIdentity], <>...
// that is constructor param length great or equal to checker method param length
int constructorParameterCount = checkerMethod.parametersCount() + (hasSecurityIdentityParam(checkerMethod) ? 0 : 1);
final Type[] constructorParameterTypes = new Type[constructorParameterCount];
final String[] constructorParameterNames = new String[constructorParameterCount];
constructorParameterNames[0] = "permissionName";
constructorParameterTypes[0] = Type.create(String.class);
for (int i = 0, j = 1; i < checkerMethod.parametersCount(); i++) {
var parameterType = checkerMethod.parameterType(i);
if (SECURITY_IDENTITY_NAME.equals(parameterType.name())) {
paramMappers[i] = new MethodParameterMapper(i, MethodParameterMapper.SECURITY_IDENTITY_IDX);
} else {
constructorParameterTypes[j] = parameterType;
constructorParameterNames[j] = checkerMethod.parameterName(i);
paramMappers[i] = new MethodParameterMapper(i, j);
j++;
}
}
return MethodInfo.create(checkerMethod.declaringClass(), "", constructorParameterNames,
constructorParameterTypes, VoidType.VOID, (short) Modifier.PUBLIC, null, null);
}
private static boolean hasSecurityIdentityParam(MethodInfo checkerMethod) {
return checkerMethod
.parameterTypes()
.stream()
.filter(t -> t.kind() == Type.Kind.CLASS)
.map(Type::name)
.anyMatch(SECURITY_IDENTITY_NAME::equals);
}
private static String getGeneratedPermissionName(MethodInfo checkerMethod, int i) {
return checkerMethod.declaringClass() + "_QuarkusPermission_" + checkerMethod.name() + "_" + i;
}
boolean foundPermissionsAllowedInstances() {
return !permissionInstances.isEmpty();
}
PermissionSecurityChecksBuilder prepareParamConverterGenerator(SecurityCheckRecorder recorder,
BuildProducer generatedClassesProducer,
BuildProducer reflectiveClassesProducer) {
this.recorder = recorder;
this.paramConverterGenerator = new PermissionConverterGenerator(generatedClassesProducer, reflectiveClassesProducer,
recorder, index);
return this;
}
PermissionSecurityChecks build() {
paramConverterGenerator.close();
final Map cache = new HashMap<>();
final Map methodToCheck = new HashMap<>();
final Map classNameToCheck = new HashMap<>();
for (var targetToPredicate : targetToPredicate.entrySet()) {
SecurityCheck check = cache.computeIfAbsent(targetToPredicate.getValue(), this::createSecurityCheck);
var annotationTarget = targetToPredicate.getKey();
if (annotationTarget.kind() == AnnotationTarget.Kind.CLASS) {
DotName className = annotationTarget.asClass().name();
classNameToCheck.put(className, check);
} else {
MethodInfo securedMethod = annotationTarget.asMethod();
methodToCheck.put(securedMethod, check);
}
}
return new PermissionSecurityChecks() {
@Override
public Map getMethodSecurityChecks() {
return Map.copyOf(methodToCheck);
}
@Override
public Map getClassNameSecurityChecks() {
return Map.copyOf(classNameToCheck);
}
@Override
public Set permissionClasses() {
return classSignatureToConstructor.keySet();
}
};
}
/**
* Creates predicate for each secured method. Predicates are cached if possible.
* What we call predicate here is combination of (possibly computed) {@link Permission}s joined with
* logical operators 'AND' or 'OR'.
*
* For example, combination of following 2 annotation instances:
*
*
* @PermissionsAllowed({"createResource", "createAll"})
* @PermissionsAllowed({"updateResource", "updateAll"})
* public void createOrUpdate() {
* ...
* }
*
*
* leads to (pseudocode): (createResource OR createAll) AND (updateResource OR updateAll)
*
* @return PermissionSecurityChecksBuilder
*/
PermissionSecurityChecksBuilder createPermissionPredicates() {
Map permissionCache = new HashMap<>();
for (var entry : targetToPermissionKeys.entrySet()) {
final AnnotationTarget securedTarget = entry.getKey();
final LogicalAndPermissionPredicate predicate = new LogicalAndPermissionPredicate();
// 'AND' operands
for (List permissionKeys : entry.getValue()) {
final boolean inclusive = isInclusive(permissionKeys);
// inclusive = false => permission1 OR permission2
// inclusive = true => permission1 AND permission2
if (inclusive) {
// 'AND' operands
for (PermissionKey permissionKey : permissionKeys) {
var permission = createPermission(permissionKey, securedTarget, permissionCache);
if (permission.isComputed()) {
predicate.markAsComputed();
}
// OR predicate with single operand is identity function
predicate.and(new LogicalOrPermissionPredicate().or(permission));
}
} else {
// 'OR' operands
var orPredicate = new LogicalOrPermissionPredicate();
predicate.and(orPredicate);
for (PermissionKey permissionKey : permissionKeys) {
var permission = createPermission(permissionKey, securedTarget, permissionCache);
if (permission.isComputed()) {
predicate.markAsComputed();
}
orPredicate.or(permission);
}
}
}
targetToPredicate.put(securedTarget, predicate);
}
return this;
}
private boolean isInclusive(List permissionKeys) {
// decide whether relation between permission specified via one annotation instance is 'AND' or 'OR'
// all PermissionKeys in the list 'permissionKeys' comes from same annotation, therefore we can
// safely pick flag from the first one
if (permissionKeys.isEmpty()) {
// permission keys should never ever be empty, this is just to stay on the safe side (avoid NPE)
return false;
}
return permissionKeys.get(0).inclusive;
}
PermissionSecurityChecksBuilder validatePermissionClasses() {
var permissionCheckers = this.permissionNameToChecker.entrySet().stream()
.map(e -> Map.entry(e.getValue(), e.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
for (List> keyLists : targetToPermissionKeys.values()) {
for (List keyList : keyLists) {
for (PermissionKey key : keyList) {
if (!classSignatureToConstructor.containsKey(key.classSignature())) {
if (key.permissionChecker != null) {
// QuarkusPermission we generated for the @PermissionChecker
// won't be in the index and as we generated it, we don't need
// to validate it
classSignatureToConstructor.put(key.classSignature(),
key.permissionChecker.quarkusPermissionConstructor());
permissionCheckers.remove(key.permissionChecker);
continue;
}
// validate permission class
final ClassInfo clazz = index.getClassByName(key.clazz.name());
Objects.requireNonNull(clazz);
if (clazz.constructors().size() != 1) {
throw new RuntimeException(
String.format("Permission class '%s' has %d constructors, exactly one is allowed",
key.classSignature(), clazz.constructors().size()));
}
var constructor = clazz.constructors().get(0);
// first constructor parameter must be permission name
if (constructor.parametersCount() == 0 || !STRING.equals(constructor.parameterType(0).name())) {
throw new RuntimeException(
String.format("Permission constructor '%s' first argument must be '%s'",
clazz.name().toString(), String.class.getName()));
}
// rest of validation needs to be done for computed classes only and per each secured method
// therefore we do it later
// cache validation result
classSignatureToConstructor.put(key.classSignature(), constructor);
}
}
}
}
if (!permissionCheckers.isEmpty()) {
if (permissionCheckers.size() > 1) {
throw new RuntimeException("""
Found @PermissionChecker annotation instances that authorize the '%s' permissions, however
no @PermissionsAllowed annotation instance requires these permissions
""".formatted(String.join(",", permissionCheckers.values())));
} else {
throw new RuntimeException("""
Found @PermissionChecker annotation instance that authorize the '%s' permission, however
no @PermissionsAllowed annotation instance requires this permission
""".formatted(permissionCheckers.values().iterator().next()));
}
}
return this;
}
PermissionSecurityChecksBuilder gatherPermissionsAllowedAnnotations(
Map alreadyCheckedMethods,
Map alreadyCheckedClasses,
List additionalClassInstances,
Predicate hasAdditionalSecurityAnnotations) {
List cache = new ArrayList<>();
Map>> classMethodToPermissionKeys = new HashMap<>();
for (AnnotationInstance instance : permissionInstances) {
AnnotationTarget target = instance.target();
if (target.kind() == AnnotationTarget.Kind.METHOD) {
// method annotation
final MethodInfo methodInfo = target.asMethod();
// we don't allow combining @PermissionsAllowed with other security annotations as @DenyAll, ...
if (alreadyCheckedMethods.containsKey(methodInfo) || hasAdditionalSecurityAnnotations.test(methodInfo)) {
throw new IllegalStateException(
String.format("Method %s of class %s is annotated with multiple security annotations",
methodInfo.name(), methodInfo.declaringClass()));
}
gatherPermissionKeys(instance, methodInfo, cache, targetToPermissionKeys);
} else {
// class annotation
// add permissions for the class annotation if respective method haven't already been annotated
if (target.kind() == AnnotationTarget.Kind.CLASS) {
final ClassInfo clazz = target.asClass();
// ignore PermissionsAllowedInterceptor in security module
// we also need to check string as long as duplicate "PermissionsAllowedInterceptor" exists
// in RESTEasy Reactive, however this workaround should be removed when the interceptor is dropped
if (isPermissionsAllowedInterceptor(clazz)) {
continue;
}
if (clazz.isAnnotation()) {
// meta-annotations are handled separately
continue;
}
// check that class wasn't annotated with other security annotation
final AnnotationInstance existingClassInstance = alreadyCheckedClasses.get(clazz);
if (existingClassInstance == null) {
for (MethodInfo methodInfo : clazz.methods()) {
if (!isPublicNonStaticNonConstructor(methodInfo)) {
continue;
}
if (hasAdditionalSecurityAnnotations.test(methodInfo)) {
continue;
}
// ignore method annotated with other security annotation
boolean noMethodLevelSecurityAnnotation = !alreadyCheckedMethods.containsKey(methodInfo);
// ignore method annotated with method-level @PermissionsAllowed
boolean noMethodLevelPermissionsAllowed = !targetToPermissionKeys.containsKey(methodInfo);
if (noMethodLevelSecurityAnnotation && noMethodLevelPermissionsAllowed) {
gatherPermissionKeys(instance, methodInfo, cache, classMethodToPermissionKeys);
}
}
} else {
// we do not allow combining @PermissionsAllowed with other security annotations as @Authenticated
throw new IllegalStateException(
String.format("Class %s is annotated with multiple security annotations %s and %s", clazz,
instance.name(), existingClassInstance.name()));
}
}
}
}
targetToPermissionKeys.putAll(classMethodToPermissionKeys);
for (var instance : additionalClassInstances) {
gatherPermissionKeys(instance, instance.target(), cache, targetToPermissionKeys);
}
// for validation purposes, so that we detect correctly combinations with other security annotations
var targetInstances = new ArrayList<>(permissionInstances);
targetInstances.addAll(additionalClassInstances);
targetToPermissionKeys.keySet().forEach(at -> {
if (at.kind() == AnnotationTarget.Kind.CLASS) {
var classInfo = at.asClass();
alreadyCheckedClasses.put(classInfo, getAnnotationInstance(classInfo, targetInstances));
} else {
var methodInfo = at.asMethod();
var methodLevelAnn = getAnnotationInstance(methodInfo, targetInstances);
if (methodLevelAnn != null) {
alreadyCheckedMethods.put(methodInfo, methodLevelAnn);
} else {
var classInfo = methodInfo.declaringClass();
alreadyCheckedClasses.put(classInfo, getAnnotationInstance(classInfo, targetInstances));
}
}
});
return this;
}
static boolean isPermissionsAllowedInterceptor(ClassInfo clazz) {
return PERMISSIONS_ALLOWED_INTERCEPTOR.equals(clazz.name())
|| clazz.name().toString().endsWith("PermissionsAllowedInterceptor");
}
private static ArrayList getPermissionsAllowedInstances(IndexView index,
PermissionsAllowedMetaAnnotationBuildItem item) {
var instances = getPermissionsAllowedInstances(index);
if (!item.getTransitiveInstances().isEmpty()) {
instances.addAll(item.getTransitiveInstances());
}
return instances;
}
static ArrayList getPermissionsAllowedInstances(IndexView index) {
return new ArrayList<>(
index.getAnnotationsWithRepeatable(PERMISSIONS_ALLOWED, index));
}
static PermissionsAllowedMetaAnnotationBuildItem movePermFromMetaAnnToMetaTarget(IndexView index) {
var permissionsAllowed = getPermissionsAllowedInstances(index)
.stream()
.filter(ai -> ai.target().kind() == AnnotationTarget.Kind.CLASS)
.filter(ai -> ai.target().asClass().isAnnotation())
.toList();
final List metaAnnotationNames = new ArrayList<>();
var newInstances = permissionsAllowed
.stream()
.flatMap(instanceOnMetaAnn -> {
var metaAnnotationName = instanceOnMetaAnn.target().asClass().name();
metaAnnotationNames.add(metaAnnotationName);
return index.getAnnotations(metaAnnotationName).stream()
.map(ai -> AnnotationInstance.create(PERMISSIONS_ALLOWED, ai.target(),
instanceOnMetaAnn.values()));
})
.toList();
return new PermissionsAllowedMetaAnnotationBuildItem(newInstances, metaAnnotationNames);
}
private static AnnotationInstance getAnnotationInstance(ClassInfo classInfo,
List annotationInstances) {
return annotationInstances.stream()
.filter(ai -> ai.target().kind() == AnnotationTarget.Kind.CLASS)
.filter(ai -> ai.target().asClass().name().equals(classInfo.name()))
.findFirst().orElseThrow();
}
private static AnnotationInstance getAnnotationInstance(MethodInfo methodInfo,
List annotationInstances) {
return annotationInstances.stream()
.filter(ai -> ai.target().kind() == AnnotationTarget.Kind.METHOD)
.filter(ai -> ai.target().asMethod().name().equals(methodInfo.name()))
.findFirst()
.orElse(null);
}
private void gatherPermissionKeys(AnnotationInstance instance, T annotationTarget,
List cache, Map>> targetToPermissionKeys) {
// @PermissionsAllowed value is in format permission:action, permission2:action, permission:action2, permission3
// here we transform it to permission -> actions
final var permissionToActions = new HashMap>();
for (String permissionToAction : instance.value().asStringArray()) {
parsePermissionToActions(permissionToAction, permissionToActions);
}
if (permissionToActions.isEmpty()) {
if (annotationTarget.kind() == AnnotationTarget.Kind.METHOD) {
throw new RuntimeException(String.format(
"Method '%s' was annotated with '@PermissionsAllowed', but no valid permission was provided",
annotationTarget.asMethod().name()));
} else {
throw new RuntimeException(String.format(
"Class '%s' was annotated with '@PermissionsAllowed', but no valid permission was provided",
annotationTarget.asClass().name()));
}
}
// permissions specified via @PermissionsAllowed has 'one of' relation, therefore we put them in one list
final List orPermissions = new ArrayList<>();
final String[] params = instance.value("params") == null ? new String[] { PermissionsAllowed.AUTODETECTED }
: instance.value("params").asStringArray();
final Type classType = getPermissionClass(instance);
final boolean inclusive = instance.value("inclusive") != null && instance.value("inclusive").asBoolean();
for (var permissionToAction : permissionToActions.entrySet()) {
final var permissionName = permissionToAction.getKey();
final var permissionActions = permissionToAction.getValue();
final var permissionChecker = findPermissionChecker(permissionName, permissionActions);
final var key = new PermissionKey(permissionName, permissionActions, params, classType, inclusive,
permissionChecker, annotationTarget);
final int i = cache.indexOf(key);
if (i == -1) {
orPermissions.add(key);
cache.add(key);
} else {
orPermissions.add(cache.get(i));
}
}
// store annotation value as permission keys
targetToPermissionKeys
.computeIfAbsent(annotationTarget, at -> new ArrayList<>())
.add(List.copyOf(orPermissions));
}
private static HashMap> parsePermissionToActions(String permissionToAction,
HashMap> permissionToActions) {
if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) {
// expected format: permission:action
final String[] permissionToActionArr = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR);
if (permissionToActionArr.length != 2) {
throw new RuntimeException(String.format(
"PermissionsAllowed value '%s' contains more than one separator '%2$s', expected format is 'permissionName%2$saction'",
permissionToAction, PERMISSION_TO_ACTION_SEPARATOR));
}
final String permissionName = permissionToActionArr[0];
final String action = permissionToActionArr[1];
if (permissionToActions.containsKey(permissionName)) {
permissionToActions.get(permissionName).add(action);
} else {
final Set actions = new HashSet<>();
actions.add(action);
permissionToActions.put(permissionName, actions);
}
} else {
// expected format: permission
if (!permissionToActions.containsKey(permissionToAction)) {
permissionToActions.put(permissionToAction, new HashSet<>());
}
}
return permissionToActions;
}
private PermissionCheckerMetadata findPermissionChecker(String permissionName, Set permissionActions) {
if (permissionActions != null && !permissionActions.isEmpty()) {
// only permission name is supported for now
return null;
}
return permissionNameToChecker.get(permissionName);
}
private static Type getPermissionClass(AnnotationInstance instance) {
return instance.value(PERMISSION_ATTR) == null ? Type.create(STRING_PERMISSION, Type.Kind.CLASS)
: instance.value(PERMISSION_ATTR).asClass();
}
boolean foundPermissionChecker() {
return !permissionNameToChecker.isEmpty();
}
List getPermissionCheckers() {
return permissionNameToChecker.values().stream().map(PermissionCheckerMetadata::checkerMethod).toList();
}
/**
* This method for each detected {@link PermissionChecker} annotation instance generate following class:
*
*
* {@code
* public final class GeneratedQuarkusPermission extends QuarkusPermission {
*
* private final SomeDto securedMethodParameter1;
*
* public GeneratedQuarkusPermission(String permissionName, SomeDto securedMethodParameter1) {
* super("io.quarkus.security.runtime.GeneratedQuarkusPermission");
* this.securedMethodParameter1 = securedMethodParameter1;
* }
*
* @Override
* protected final boolean isGranted(SecurityIdentity securityIdentity) {
* return getBean().hasPermission(securityIdentity, securedMethodParameter1);
* }
*
* // or same method with Uni depending on the 'hasPermission' return type
* @Override
* protected final Uni isGrantedUni(SecurityIdentity securityIdentity) {
* return getBean().hasPermission(securityIdentity, securedMethodParameter1);
* }
*
* @Override
* protected final Class getBeanClass() {
* return io.quarkus.security.runtime.GeneratedQuarkusPermission.class;
* }
*
* @Override
* protected final boolean isBlocking() {
* return false; // true when checker method annotated with @Blocking
* }
*
* @Override
* protected final boolean isReactive() {
* return false; // true when checker method returns Uni
* }
*
* }
* }
*
*
* The {@code CheckerBean} in question can look like this:
*
*
* {@code
* @Singleton
* public class CheckerBean {
*
* @PermissionChecker("permission-name")
* boolean isGranted(SecurityIdentity securityIdentity, SomeDto someDto) {
* return false;
* }
*
* }
* }
*
*/
void generatePermissionCheckers(BuildProducer generatedClassProducer) {
permissionNameToChecker.values().forEach(checkerMetadata -> {
var declaringCdiBean = checkerMetadata.checkerMethod().declaringClass();
var declaringCdiBeanType = classType(declaringCdiBean.name());
var generatedClassName = checkerMetadata.generatedClassName();
try (var classCreator = ClassCreator.builder()
.classOutput(new GeneratedClassGizmoAdaptor(generatedClassProducer, true))
.setFinal(true)
.className(generatedClassName)
.signature(SignatureBuilder
.forClass()
// extends QuarkusPermission
// XYZ == @PermissionChecker declaring class
.setSuperClass(parameterizedType(classType(QuarkusPermission.class), declaringCdiBeanType)))
.build()) {
record SecuredMethodParamDesc(FieldDescriptor fieldDescriptor, int ctorParamIdx) {
SecuredMethodParamDesc() {
this(null, -1);
}
boolean isNotSecurityIdentity() {
return fieldDescriptor != null;
}
}
SecuredMethodParamDesc[] securedMethodParams = new SecuredMethodParamDesc[checkerMetadata
.methodParamMappers().length];
for (int i = 0; i < checkerMetadata.methodParamMappers.length; i++) {
var paramMapper = checkerMetadata.methodParamMappers[i];
if (paramMapper.isSecurityIdentity()) {
securedMethodParams[i] = new SecuredMethodParamDesc();
} else {
// GENERATED CODE: private final SomeDto securedMethodParameter1;
var fieldName = SECURED_METHOD_PARAMETER + paramMapper.securedMethodIdx();
var ctorParamIdx = paramMapper.permConstructorIdx();
var fieldTypeName = checkerMetadata.quarkusPermissionConstructor().parameterType(ctorParamIdx)
.name();
var fieldCreator = classCreator.getFieldCreator(fieldName, fieldTypeName.toString());
fieldCreator.setModifiers(Modifier.PRIVATE | Modifier.FINAL);
securedMethodParams[i] = new SecuredMethodParamDesc(fieldCreator.getFieldDescriptor(),
ctorParamIdx);
}
}
// public GeneratedQuarkusPermission(String permissionName, SomeDto securedMethodParameter1) {
// super("io.quarkus.security.runtime.GeneratedQuarkusPermission");
// this.securedMethodParameter1 = securedMethodParameter1;
// }
// How many 'securedMethodParameterXYZ' are there depends on the secured method
var ctorParams = Stream.concat(Stream.of(String.class.getName()), Arrays
.stream(securedMethodParams)
.filter(SecuredMethodParamDesc::isNotSecurityIdentity)
.map(SecuredMethodParamDesc::fieldDescriptor)
.map(FieldDescriptor::getType)).toArray(String[]::new);
try (var ctor = classCreator.getConstructorCreator(ctorParams)) {
ctor.setModifiers(Modifier.PUBLIC);
// GENERATED CODE: super("io.quarkus.security.runtime.GeneratedQuarkusPermission");
// why not to propagate permission name to the java.security.Permission ?
// if someone declares @PermissionChecker("permission-name-1") we expect that required permission
// @PermissionAllowed("permission-name-1") is only granted by the checker method and accidentally some
// user-defined augmentor won't grant it based on permission name match in case they misunderstand docs
var superCtorDesc = MethodDescriptor.ofConstructor(classCreator.getSuperClass(), String.class);
ctor.invokeSpecialMethod(superCtorDesc, ctor.getThis(), ctor.load(generatedClassName));
// GENERATED CODE: this.securedMethodParameterXYZ = securedMethodParameterXYZ;
for (var securedMethodParamDesc : securedMethodParams) {
if (securedMethodParamDesc.isNotSecurityIdentity()) {
var field = securedMethodParamDesc.fieldDescriptor();
var constructorParameter = ctor.getMethodParam(securedMethodParamDesc.ctorParamIdx());
ctor.writeInstanceField(field, ctor.getThis(), constructorParameter);
}
}
ctor.returnVoid();
}
// @Override
// protected final boolean isGranted(SecurityIdentity securityIdentity) {
// return getBean().hasPermission(securityIdentity, securedMethodParameter1);
// }
// or when user-defined permission checker returns Uni:
// @Override
// protected final Uni isGrantedUni(SecurityIdentity securityIdentity) {
// return getBean().hasPermission(securityIdentity, securedMethodParameter1);
// }
var isGrantedName = checkerMetadata.reactive() ? IS_GRANTED_UNI : IS_GRANTED;
var isGrantedReturn = DescriptorUtils.typeToString(checkerMetadata.checkerMethod().returnType());
try (var methodCreator = classCreator.getMethodCreator(isGrantedName, isGrantedReturn,
SecurityIdentity.class)) {
methodCreator.setModifiers(Modifier.PROTECTED | Modifier.FINAL);
methodCreator.addAnnotation(Override.class.getName(), RetentionPolicy.CLASS);
// getBean()
var getBeanDescriptor = MethodDescriptor.ofMethod(generatedClassName, "getBean", Object.class);
var cdiBean = methodCreator.invokeVirtualMethod(getBeanDescriptor, methodCreator.getThis());
// <>.hasPermission(securityIdentity, securedMethodParameter1)
var isGrantedDescriptor = MethodDescriptor.of(checkerMetadata.checkerMethod());
var securedMethodParamHandles = new ResultHandle[securedMethodParams.length];
for (int i = 0; i < securedMethodParams.length; i++) {
var securedMethodParam = securedMethodParams[i];
if (securedMethodParam.isNotSecurityIdentity()) {
// QuarkusPermission field assigned in the permission constructor
// for example: this.securedMethodParameter1
securedMethodParamHandles[i] = methodCreator
.readInstanceField(securedMethodParam.fieldDescriptor(), methodCreator.getThis());
} else {
// SecurityIdentity from QuarkusPermission#isGranted method parameter
securedMethodParamHandles[i] = methodCreator.getMethodParam(0);
}
}
final ResultHandle result;
if (checkerMetadata.checkerMethod.isDefault()) {
result = methodCreator.invokeInterfaceMethod(isGrantedDescriptor, cdiBean,
securedMethodParamHandles);
} else {
result = methodCreator.invokeVirtualMethod(isGrantedDescriptor, cdiBean, securedMethodParamHandles);
}
// return 'hasPermission' result
methodCreator.returnValue(result);
}
var alwaysFalseName = checkerMetadata.reactive() ? IS_GRANTED : IS_GRANTED_UNI;
var alwaysFalseType = checkerMetadata.reactive() ? boolean.class.getName() : UNI.toString();
try (var methodCreator = classCreator.getMethodCreator(alwaysFalseName, alwaysFalseType,
SecurityIdentity.class)) {
methodCreator.setModifiers(Modifier.PROTECTED | Modifier.FINAL);
methodCreator.addAnnotation(Override.class.getName(), RetentionPolicy.CLASS);
if (checkerMetadata.reactive()) {
methodCreator.returnValue(methodCreator.load(false));
} else {
var accessDenied = methodCreator.invokeStaticMethod(
MethodDescriptor.ofMethod(QuarkusPermission.class, "accessDenied", UNI.toString()));
methodCreator.returnValue(accessDenied);
}
}
// @Override
// protected final Class getBeanClass() {
// return io.quarkus.security.runtime.GeneratedQuarkusPermission.class;
// }
try (var methodCreator = classCreator.getMethodCreator("getBeanClass", Class.class)) {
methodCreator.setModifiers(Modifier.PROTECTED | Modifier.FINAL);
methodCreator.addAnnotation(Override.class.getName(), RetentionPolicy.CLASS);
methodCreator.returnValue(methodCreator.loadClassFromTCCL(declaringCdiBean.name().toString()));
}
// @Override
// protected final boolean isBlocking() {
// return false; // or true
// }
try (var methodCreator = classCreator.getMethodCreator("isBlocking", boolean.class)) {
methodCreator.setModifiers(Modifier.PROTECTED | Modifier.FINAL);
methodCreator.addAnnotation(Override.class.getName(), RetentionPolicy.CLASS);
methodCreator.returnValue(methodCreator.load(checkerMetadata.blocking()));
}
// @Override
// protected final boolean isReactive() {
// return false; // true when checker method returns Uni
// }
try (var methodCreator = classCreator.getMethodCreator("isReactive", boolean.class)) {
methodCreator.setModifiers(Modifier.PROTECTED | Modifier.FINAL);
methodCreator.addAnnotation(Override.class.getName(), RetentionPolicy.CLASS);
methodCreator.returnValue(methodCreator.load(checkerMetadata.reactive()));
}
}
});
}
private static String toString(AnnotationTarget annotationTarget) {
if (annotationTarget.kind() == AnnotationTarget.Kind.METHOD) {
var method = annotationTarget.asMethod();
return method.declaringClass().toString() + "#" + method.name();
}
return annotationTarget.asClass().name().toString();
}
private SecurityCheck createSecurityCheck(LogicalAndPermissionPredicate andPredicate) {
final SecurityCheck securityCheck;
final boolean isSinglePermissionGroup = andPredicate.operands.size() == 1;
if (isSinglePermissionGroup) {
final LogicalOrPermissionPredicate orPredicate = andPredicate.operands.iterator().next();
final boolean isSinglePermission = orPredicate.operands.size() == 1;
if (isSinglePermission) {
// single permission
final PermissionWrapper permissionWrapper = orPredicate.operands.iterator().next();
securityCheck = recorder.permissionsAllowed(permissionWrapper.computedPermission,
permissionWrapper.permission);
} else {
// multiple OR operands (permission OR permission OR ...)
if (andPredicate.atLeastOnePermissionIsComputed) {
securityCheck = recorder.permissionsAllowed(orPredicate.asComputedPermissions(recorder), null);
} else {
securityCheck = recorder.permissionsAllowed(null, orPredicate.asPermissions());
}
}
} else {
// permission group AND permission group AND permission group AND ...
// permission group = (permission OR permission OR permission OR ...)
if (andPredicate.atLeastOnePermissionIsComputed) {
final List>> computedPermissionGroups = new ArrayList<>();
for (LogicalOrPermissionPredicate permissionGroup : andPredicate.operands) {
computedPermissionGroups.add(permissionGroup.asComputedPermissions(recorder));
}
securityCheck = recorder.permissionsAllowedGroups(computedPermissionGroups, null);
} else {
final List>> permissionGroups = new ArrayList<>();
for (LogicalOrPermissionPredicate permissionGroup : andPredicate.operands) {
permissionGroups.add(permissionGroup.asPermissions());
}
securityCheck = recorder.permissionsAllowedGroups(null, permissionGroups);
}
}
return securityCheck;
}
private PermissionWrapper createPermission(PermissionKey permissionKey, AnnotationTarget securedTarget,
Map cache) {
var constructor = classSignatureToConstructor.get(permissionKey.classSignature());
return cache.computeIfAbsent(
new PermissionCacheKey(permissionKey, securedTarget, constructor, paramConverterGenerator),
new Function() {
@Override
public PermissionWrapper apply(PermissionCacheKey permissionCacheKey) {
if (permissionCacheKey.computed) {
return new PermissionWrapper(createComputedPermission(permissionCacheKey), null);
} else {
final RuntimeValue permission;
if (permissionCacheKey.isStringPermission()) {
permission = createStringPermission(permissionCacheKey.permissionKey);
} else {
permission = createCustomPermission(permissionCacheKey);
}
return new PermissionWrapper(null, permission);
}
}
});
}
private Function