All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkus.security.runtime.SecurityCheckRecorder Maven / Gradle / Ivy

There is a newer version: 3.17.5
Show newest version
package io.quarkus.security.runtime;

import static io.quarkus.security.runtime.QuarkusSecurityRolesAllowedConfigBuilder.transformToKey;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.jboss.logging.Logger;

import io.quarkus.arc.Arc;
import io.quarkus.arc.SyntheticCreationalContext;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.ShutdownContext;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.StringPermission;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder;
import io.quarkus.security.runtime.interceptor.SecurityConstrainer;
import io.quarkus.security.runtime.interceptor.check.AuthenticatedCheck;
import io.quarkus.security.runtime.interceptor.check.DenyAllCheck;
import io.quarkus.security.runtime.interceptor.check.PermissionSecurityCheck;
import io.quarkus.security.runtime.interceptor.check.PermitAllCheck;
import io.quarkus.security.runtime.interceptor.check.RolesAllowedCheck;
import io.quarkus.security.runtime.interceptor.check.SupplierRolesAllowedCheck;
import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.security.spi.runtime.SecurityCheck;
import io.quarkus.security.spi.runtime.SecurityCheckStorage;
import io.smallrye.config.Expressions;
import io.smallrye.config.common.utils.StringUtil;

@Recorder
public class SecurityCheckRecorder {

    private static final Logger LOGGER = Logger.getLogger(SecurityCheckRecorder.class);
    private static final Set configExpRolesAllowedChecks = ConcurrentHashMap.newKeySet();
    private static volatile boolean runtimeConfigReady = false;

    public SecurityCheck denyAll() {
        return DenyAllCheck.INSTANCE;
    }

    public SecurityCheck permitAll() {
        return PermitAllCheck.INSTANCE;
    }

    public SecurityCheck rolesAllowed(String... roles) {
        return RolesAllowedCheck.of(roles);
    }

    public SecurityCheck rolesAllowedSupplier(String[] allowedRoles, int[] configExpIndexes, int[] configKeys) {

        // here we add generated keys and values with the property expressions to the config source,
        // the config source will be registered with the Config system,
        // and we get all features available from Config
        for (int i = 0; i < configExpIndexes.length; i++) {
            QuarkusSecurityRolesAllowedConfigBuilder.addProperty(configKeys[i], allowedRoles[configExpIndexes[i]]);
        }

        final var check = new SupplierRolesAllowedCheck(
                resolveRolesAllowedConfigExp(allowedRoles, configExpIndexes, configKeys));
        configExpRolesAllowedChecks.add(check);
        return check;
    }

    /* STATIC INIT */
    public void recordRolesAllowedConfigExpression(String configExpression, int configKeyIndex,
            BiConsumer> configValueRecorder) {
        QuarkusSecurityRolesAllowedConfigBuilder.addProperty(configKeyIndex, configExpression);
        // one configuration expression resolves to string array because the expression can be list treated as list
        Supplier configValSupplier = resolveRolesAllowedConfigExp(new String[] { configExpression },
                new int[] { 0 }, new int[] { configKeyIndex });
        configValueRecorder.accept(configExpression, configValSupplier);
    }

    private static Supplier resolveRolesAllowedConfigExp(String[] allowedRoles, int[] configExpIndexes,
            int[] configKeys) {

        final List roles = new ArrayList<>(Arrays.asList(allowedRoles));
        return new Supplier() {
            @Override
            public String[] get() {
                final var config = ConfigProviderResolver.instance().getConfig(Thread.currentThread().getContextClassLoader());
                if (config.getOptionalValue(Config.PROPERTY_EXPRESSIONS_ENABLED, Boolean.class).orElse(Boolean.TRUE)
                        && Expressions.isEnabled()) {
                    // property expressions are enabled
                    for (int i = 0; i < configExpIndexes.length; i++) {
                        // resolve configuration expressions specified as value of the @RolesAllowed annotation
                        var strVal = config.getValue(transformToKey(configKeys[i]), String.class);

                        // treat config value that contains collection separator as a list
                        // @RolesAllowed({"${my.roles}"}) => my.roles=one,two <=> @RolesAllowed({"one", "two"})
                        if (strVal != null && strVal.contains(",")) {
                            var strArr = StringUtil.split(strVal);
                            if (strArr.length >= 1) {
                                // role order is irrelevant as logical operator between them is OR

                                // first role will go to the original place, double escaped comma will be parsed correctly
                                strVal = strArr[0];

                                if (strArr.length > 1) {
                                    // the rest of the roles will be appended at the end
                                    for (int i1 = 1; i1 < strArr.length; i1++) {
                                        roles.add(strArr[i1]);
                                    }
                                }
                            }
                        }

                        roles.set(configExpIndexes[i], strVal);
                    }
                }
                return roles.toArray(String[]::new);
            }
        };
    }

    public SecurityCheck authenticated() {
        return AuthenticatedCheck.INSTANCE;
    }

    /**
     * Creates {@link SecurityCheck} for a single permission.
     *
     * @return SecurityCheck
     */
    public SecurityCheck permissionsAllowed(Function computedPermission,
            RuntimeValue permissionRuntimeValue) {
        final Permission permission;
        if (computedPermission == null) {
            Objects.requireNonNull(permissionRuntimeValue);
            permission = permissionRuntimeValue.getValue();
        } else {
            permission = null;
        }
        return PermissionSecurityCheck.of(permission, computedPermission);
    }

    /**
     * Creates {@link SecurityCheck} for a permission set. User must have at least one of security check permissions.
     *
     * @return SecurityCheck
     */
    public SecurityCheck permissionsAllowed(List> computedPermissions,
            List> permissionsRuntimeValue) {
        final Permission[] permissions;
        final Function computedPermissionsAggregator;
        if (computedPermissions == null) {

            // plain permissions
            Objects.requireNonNull(permissionsRuntimeValue);
            computedPermissionsAggregator = null;
            permissions = new Permission[permissionsRuntimeValue.size()];
            for (int i = 0; i < permissionsRuntimeValue.size(); i++) {
                // assign permission
                permissions[i] = Objects.requireNonNull(permissionsRuntimeValue.get(i).getValue());
            }
        } else {

            // computed permissions
            permissions = null;
            computedPermissionsAggregator = new Function<>() {
                @Override
                public Permission[] apply(Object[] securedMethodParameters) {

                    // compute permissions
                    Permission[] result = new Permission[computedPermissions.size()];
                    for (int i = 0; i < computedPermissions.size(); i++) {
                        // instantiate Permission with actual method arguments
                        result[i] = computedPermissions.get(i).apply(securedMethodParameters);
                    }
                    return result;
                }
            };
        }

        return PermissionSecurityCheck.of(permissions, computedPermissionsAggregator);
    }

    /**
     * Creates {@link SecurityCheck} for a permission groups.
     * User must have at least one of security check permissions from each permission group.
     *
     * @return SecurityCheck
     */
    public SecurityCheck permissionsAllowedGroups(List>> computedPermissionGroups,
            List>> permissionGroupsRuntimeValue) {
        final Function computedPermissionGroupAggregator;
        final Permission[][] permissionGroups;
        if (computedPermissionGroups == null) {

            // plain permission groups
            Objects.requireNonNull(permissionGroupsRuntimeValue);
            computedPermissionGroupAggregator = null;
            permissionGroups = new Permission[permissionGroupsRuntimeValue.size()][];

            // collect runtime values
            for (int i = 0; i < permissionGroupsRuntimeValue.size(); i++) {
                var groupRuntimeValue = permissionGroupsRuntimeValue.get(i);
                permissionGroups[i] = new Permission[groupRuntimeValue.size()];
                for (int j = 0; j < groupRuntimeValue.size(); j++) {
                    // assign permission
                    permissionGroups[i][j] = groupRuntimeValue.get(j).getValue();
                }
            }
        } else {

            // computed permission groups
            permissionGroups = null;
            computedPermissionGroupAggregator = new Function<>() {
                @Override
                public Permission[][] apply(Object[] securedMethodParams) {

                    // compute permissions
                    Permission[][] permissionGroups = new Permission[computedPermissionGroups.size()][];
                    for (int i = 0; i < computedPermissionGroups.size(); i++) {
                        var computedPermissionGroup = computedPermissionGroups.get(i);
                        permissionGroups[i] = new Permission[computedPermissionGroup.size()];
                        for (int j = 0; j < computedPermissionGroup.size(); j++) {
                            // instantiate Permission with actual method arguments
                            permissionGroups[i][j] = computedPermissionGroup.get(j).apply(securedMethodParams);
                        }
                    }

                    return permissionGroups;
                }
            };
        }

        return PermissionSecurityCheck.of(permissionGroups, computedPermissionGroupAggregator);
    }

    public Function toComputedPermission(RuntimeValue permissionRuntimeVal) {
        return new Function<>() {
            @Override
            public Permission apply(Object[] objects) {
                return permissionRuntimeVal.getValue();
            }
        };
    }

    public RuntimeValue createStringPermission(String name, String[] actions) {
        return new RuntimeValue<>(new StringPermission(name, actions));
    }

    /**
     * Creates permission.
     *
     * @param name permission name
     * @param clazz permission class
     * @param actions nullable actions
     * @param passActionsToConstructor flag signals whether Permission constructor accepts (name) or (name, actions)
     * @return {@link RuntimeValue}
     */
    public RuntimeValue createPermission(String name, String clazz, String[] actions,
            boolean passActionsToConstructor) {
        final Permission permission;
        try {
            if (passActionsToConstructor) {
                permission = (Permission) loadClass(clazz).getConstructors()[0].newInstance(name, actions);
            } else {
                permission = (Permission) loadClass(clazz).getConstructors()[0].newInstance(name);
            }
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | RuntimeException e) {
            LOGGER.errorf(e, "Failed to create Permission - class '%s', name '%s', actions '%s', access will be denied",
                    clazz, name, Arrays.toString(actions));
            throw new ForbiddenException();
        }
        return new RuntimeValue<>(permission);
    }

    /**
     * Creates function that transform arguments of a method annotated with {@link io.quarkus.security.PermissionsAllowed}
     * to custom {@link Permission}.
     *
     * @param permissionName permission name
     * @param clazz permission class
     * @param actions permission actions
     * @param passActionsToConstructor flag signals whether Permission constructor accepts (name) or (name, actions)
     * @param formalParamIndexes indexes of secured method params that should be passed to permission constructor
     * @param formalParamConverters converts method parameter to constructor parameter; most of the time, this will be
     *        either identity function or a method calling method parameter getter
     * @return computed permission
     */
    public Function createComputedPermission(String permissionName, String clazz, String[] actions,
            boolean passActionsToConstructor, int[] formalParamIndexes, String[] formalParamConverters,
            Map> converterNameToMethodHandle) {
        final int addActions = (passActionsToConstructor ? 1 : 0);
        final int argsCount = 1 + addActions + formalParamIndexes.length;
        final int methodArgsStart = 1 + addActions;
        final var permissionClassConstructor = loadClass(clazz).getConstructors()[0];
        return new Function<>() {
            @Override
            public Permission apply(Object[] securedMethodArgs) {
                try {
                    final Object[] initArgs = initArgs(securedMethodArgs);
                    return (Permission) permissionClassConstructor.newInstance(initArgs);
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException | RuntimeException e) {
                    LOGGER.errorf(e,
                            "Failed to create computed Permission - class '%s', name '%s', actions '%s', access will be denied",
                            clazz, permissionName, Arrays.toString(actions));
                    throw new ForbiddenException();
                }
            }

            private Object[] initArgs(Object[] methodArgs) {
                // Permission constructor init args are: permission name, possibly actions, selected secured method args
                final Object[] initArgs = new Object[argsCount];
                initArgs[0] = permissionName;
                if (passActionsToConstructor) {
                    initArgs[1] = actions;
                }
                for (int i = 0; i < formalParamIndexes.length; i++) {
                    var methodArg = methodArgs[formalParamIndexes[i]];
                    if (formalParamConverters == null || formalParamConverters[i] == null) {
                        initArgs[methodArgsStart + i] = methodArg;
                    } else {
                        var convertedValue = convertMethodParamToPermParam(i, methodArg, converterNameToMethodHandle,
                                formalParamConverters);
                        initArgs[methodArgsStart + i] = convertedValue;
                    }
                }
                return initArgs;
            }
        };
    }

    public RuntimeValue newBuilder() {
        return new RuntimeValue<>(new SecurityCheckStorageBuilder());
    }

    public void addMethod(RuntimeValue builder, String className,
            String methodName,
            String[] parameterTypes,
            SecurityCheck securityCheck) {
        builder.getValue().registerCheck(className, methodName, parameterTypes, securityCheck);
    }

    public SecurityCheckStorage create(RuntimeValue builder) {
        return builder.getValue().create();
    }

    public void resolveRolesAllowedConfigExpRoles() {
        if (!configExpRolesAllowedChecks.isEmpty()) {
            for (SupplierRolesAllowedCheck configExpRolesAllowedCheck : configExpRolesAllowedChecks) {
                configExpRolesAllowedCheck.resolveAllowedRoles();
            }
            configExpRolesAllowedChecks.clear();
        }
    }

    private Class loadClass(String className) {
        try {
            return Thread.currentThread().getContextClassLoader().loadClass(className);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Unable to load class '" + className + "' for creating permission", e);
        }
    }

    public void registerDefaultSecurityCheck(RuntimeValue builder, SecurityCheck securityCheck) {
        builder.getValue().registerDefaultSecurityCheck(securityCheck);
    }

    public Supplier createSecurityConstrainer(Supplier> additionalEventPropsSupplier) {
        return new Supplier() {
            @Override
            public SecurityConstrainer get() {
                var container = Arc.container();
                var beanManager = container.beanManager();
                var eventPropsSupplier = additionalEventPropsSupplier == null ? new Supplier>() {
                    @Override
                    public Map get() {
                        return Map.of();
                    }
                } : additionalEventPropsSupplier;
                return new SecurityConstrainer(container.instance(SecurityCheckStorage.class).get(),
                        beanManager, beanManager.getEvent().select(AuthorizationFailureEvent.class),
                        beanManager.getEvent().select(AuthorizationSuccessEvent.class), runtimeConfigReady,
                        container.select(SecurityIdentityAssociation.class), eventPropsSupplier);
            }
        };
    }

    public void setRuntimeConfigReady() {
        runtimeConfigReady = true;
    }

    public void unsetRuntimeConfigReady(ShutdownContext shutdownContext) {
        shutdownContext.addShutdownTask(new Runnable() {
            @Override
            public void run() {
                runtimeConfigReady = false;
            }
        });
    }

    public RuntimeValue createPermissionMethodConverter(String methodName, RuntimeValue> clazz) {
        try {
            var handle = MethodHandles.publicLookup().findStatic(clazz.getValue(), methodName,
                    MethodType.methodType(Object.class, Object.class));
            return new RuntimeValue<>(handle);
        } catch (NoSuchMethodException | IllegalAccessException e) {
            throw new RuntimeException("Failed to create Permission constructor method parameter converter", e);
        }
    }

    public RuntimeValue> loadClassRuntimeVal(String className) {
        return new RuntimeValue<>(loadClass(className));
    }

    private static Object convertMethodParamToPermParam(int i, Object methodArg,
            Map> converterNameToMethodHandle, String[] formalParamConverters) {
        var converter = converterNameToMethodHandle.get(formalParamConverters[i]).getValue();
        try {
            return converter.invokeExact(methodArg);
        } catch (Throwable e) {
            throw new RuntimeException(
                    "Failed to convert method argument '%s' to Permission constructor parameter".formatted(methodArg), e);
        }
    }

    public Function, SecurityIdentityAugmentor> createPermissionAugmentor() {
        return new Function, SecurityIdentityAugmentor>() {
            @Override
            public SecurityIdentityAugmentor apply(SyntheticCreationalContext ctx) {
                return new QuarkusPermissionSecurityIdentityAugmentor(ctx.getInjectedReference(BlockingSecurityExecutor.class));
            }
        };
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy