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

org.restheart.plugins.PluginsRegistryImpl Maven / Gradle / Ivy

/*-
 * ========================LICENSE_START=================================
 * restheart-core
 * %%
 * Copyright (C) 2014 - 2024 SoftInstigate
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 * =========================LICENSE_END==================================
 */
package org.restheart.plugins;

import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.restheart.cache.CacheFactory;
import org.restheart.cache.LoadingCache;
import org.restheart.configuration.ConfigurationException;
import org.restheart.exchange.ByteArrayProxyRequest;
import org.restheart.exchange.ByteArrayProxyResponse;
import org.restheart.exchange.PipelineInfo;
import static org.restheart.exchange.PipelineInfo.PIPELINE_TYPE.SERVICE;
import org.restheart.handlers.BeforeExchangeInitInterceptorsExecutor;
import org.restheart.handlers.CORSHandler;
import org.restheart.handlers.ConfigurableEncodingHandler;
import org.restheart.handlers.ErrorHandler;
import org.restheart.handlers.PipelinedHandler;
import static org.restheart.handlers.PipelinedHandler.pipe;
import org.restheart.handlers.PipelinedWrappingHandler;
import org.restheart.handlers.QueryStringRebuilder;
import org.restheart.handlers.RequestInterceptorsExecutor;
import org.restheart.handlers.RequestLogger;
import org.restheart.handlers.ResponseInterceptorsExecutor;
import org.restheart.handlers.ResponseSender;
import org.restheart.handlers.ServiceExchangeInitializer;
import org.restheart.handlers.TracingInstrumentationHandler;
import org.restheart.handlers.WorkingThreadsPoolDispatcher;
import org.restheart.handlers.injectors.PipelineInfoInjector;
import static org.restheart.plugins.InterceptPoint.REQUEST_AFTER_AUTH;
import static org.restheart.plugins.InterceptPoint.REQUEST_BEFORE_AUTH;
import org.restheart.plugins.RegisterPlugin.MATCH_POLICY;
import org.restheart.plugins.security.AuthMechanism;
import org.restheart.plugins.security.Authenticator;
import org.restheart.plugins.security.Authorizer;
import org.restheart.plugins.security.TokenManager;
import org.restheart.security.BaseAclPermissionTransformer;
import org.restheart.security.authorizers.FullAuthorizer;
import org.restheart.security.handlers.SecurityHandler;
import org.restheart.utils.PluginUtils;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import static io.undertow.Handlers.path;
import io.undertow.server.handlers.PathHandler;
import io.undertow.util.PathMatcher;

/**
 *
 * @author Andrea Di Cesare {@literal }
 */
public class PluginsRegistryImpl implements PluginsRegistry {

    private static final PathHandler ROOT_PATH_HANDLER = path();
    private static final PathMatcher PIPELINE_INFOS = new PathMatcher<>();

    public static PluginsRegistryImpl getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private Set> authMechanisms;

    private Set> authenticators;

    private Set permissionTransformers;

    private Set> authorizers;

    private Optional> tokenManager;

    private Set>> providers;

    private final Set>> services = new LinkedHashSet<>();
    // keep track of service initialization, to allow initializers to add services
    // before actual scannit. this is used for intance by PolyglotDeployer
    private boolean servicesInitialized = false;

    private Set> initializers;

    private Set>> interceptors;

    private PluginsRegistryImpl() {
    }

    /**
     * force plugin objects instantiation
     */
    public void instantiateAll() {
        var factory = PluginsFactory.getInstance();

        factory.providers(); // providers must be invoked first
        factory.initializers();
        factory.authMechanisms();
        factory.authorizers();
        factory.tokenManager();
        factory.authenticators();
        factory.interceptors();
        factory.services();

        factory.injectDependencies();
    }

    /**
     * @return the authMechanisms
     */
    @Override
    public Set> getAuthMechanisms() {
        if (this.authMechanisms == null) {
            this.authMechanisms = new LinkedHashSet<>();
            this.authMechanisms.addAll(PluginsFactory.getInstance().authMechanisms());
        }

        return Collections.unmodifiableSet(this.authMechanisms);
    }

    /**
     * @return the authenticators
     */
    @Override
    public Set> getAuthenticators() {
        if (this.authenticators == null) {
            this.authenticators = new LinkedHashSet<>();
            this.authenticators.addAll(PluginsFactory.getInstance().authenticators());
        }

        return Collections.unmodifiableSet(this.authenticators);
    }

    /**
     *
     * @param name the name of the authenticator
     * @return the authenticator
     * @throws org.restheart.configuration.ConfigurationException
     */
    @Override
    public PluginRecord getAuthenticator(String name) throws ConfigurationException {

        var auth = getAuthenticators().stream().filter(p -> name.equals(p.getName())).findFirst();

        if (auth != null && auth.isPresent()) {
            return auth.get();
        } else {
            throw new ConfigurationException("Authenticator " + name + " not found");
        }
    }

    /**
     * Can be used by some authenticators to allowo modifyign the permissions with custom logic
     *
     * @return the permission transformers
     */
    @Override
    public Set getPermissionTransformers() {
        if (this.permissionTransformers == null) {
            this.permissionTransformers = Sets.newLinkedHashSet();
        }

        return this.permissionTransformers;
    }

    /**
     * @return the authenticators
     */
    @Override
    public PluginRecord getTokenManager() {
        if (this.tokenManager == null) {
            var tm = PluginsFactory.getInstance().tokenManager();

            this.tokenManager = tm == null ? Optional.empty() : Optional.of(tm);
        }

        return this.tokenManager.isPresent() ? this.tokenManager.get() : null;
    }

    /**
     * @return the authenticators
     */
    @Override
    public Set> getAuthorizers() {
        if (this.authorizers == null) {
            this.authorizers = PluginsFactory.getInstance().authorizers();
        }

        return Collections.unmodifiableSet(this.authorizers);
    }

    /**
     * @return the initializers
     */
    @Override
    public Set> getInitializers() {
        if (this.initializers == null) {
            this.initializers = new LinkedHashSet<>();
            this.initializers.addAll(PluginsFactory.getInstance().initializers());
        }

        return Collections.unmodifiableSet(this.initializers);
    }

    /**
     * note, this is cached to speed up requests
     * @return the interceptors
     */
    @Override
    public Set>> getInterceptors() {
        if (this.interceptors == null) {
            this.interceptors = new LinkedHashSet<>();
            this.interceptors.addAll(PluginsFactory.getInstance().interceptors());
        }

        return Collections.unmodifiableSet(this.interceptors);
    }

     /**
     * @return the authenticators
     */
    @Override
    public Set>> getProviders() {
        if (this.providers == null) {
            this.providers = PluginsFactory.getInstance().providers();
        }

        return Collections.unmodifiableSet(this.providers);
    }

    @Override
    public void addInterceptor(PluginRecord> i) {
        this.SRV_INTERCEPTORS_CACHE.invalidateAll();

        if (this.interceptors == null) {
            // avoid NPE if not already initialized
            getInterceptors();
        }

        this.interceptors.add(i);
    }

    @Override
    public boolean removeInterceptorIf(java.util.function.Predicate>> filter) {
        this.SRV_INTERCEPTORS_CACHE.invalidateAll();
        return this.interceptors.removeIf(filter);
    }

    private final LoadingCache, List>> SRV_INTERCEPTORS_CACHE = CacheFactory
        .createHashMapLoadingCache((key) -> __interceptors(key.getKey(), key.getValue()));

    private List> __interceptors(String serviceName, InterceptPoint interceptPoint) {
        Optional>> _service = serviceName == null ? Optional.empty() : getServices().stream().filter(pr -> serviceName.equals(pr.getName())).findFirst();

        var _interceptors = getInterceptors();

        if (_service.isPresent()) {
            // if the request is handled by a service set to not execute interceptors
            // at this interceptPoint, appy only required interceptor
            // var vip = PluginUtils.dontIntercept(PluginsRegistryImpl.getInstance(), exchange);
            var vip = PluginUtils.dontIntercept(_service.get().getInstance());
            if (Arrays.stream(vip).anyMatch(interceptPoint::equals) || Arrays.stream(vip).anyMatch(InterceptPoint.ANY::equals) ) {
                _interceptors = _interceptors.stream().filter(i ->  PluginUtils.requiredinterceptor(i.getInstance())).collect(Collectors.toSet());
            }
        }

        return _interceptors
            .stream()
            .filter(ri -> ri.isEnabled())
            .map(ri -> ri.getInstance())
            // IMPORTANT: An interceptor can intercept:
            // - service requests handled by a Service when its request and response
            //   types are equal to the ones declared by the Service
            // - request handled by a Service when its request and response
            //   are WildcardRequest and WildcardResponse
            // - request handled by a Proxy when its request and response
            //   are ByteArrayProxyRequest and ByteArrayProxyResponse
            .filter(ri
                -> (_service.isPresent()
                    && PluginUtils.cachedRequestType(ri).equals(PluginUtils.cachedRequestType(_service.get().getInstance()))
                    && PluginUtils.cachedResponseType(ri).equals(PluginUtils.cachedResponseType(_service.get().getInstance())))
                || (_service.isPresent() && ri instanceof WildcardInterceptor)
                || (_service.isEmpty()
                    && PluginUtils.cachedRequestType(ri).equals(ByteArrayProxyRequest.type())
                    && PluginUtils.cachedResponseType(ri).equals(ByteArrayProxyResponse.type())))
            .filter(ri -> interceptPoint == PluginUtils.interceptPoint(ri))
            .collect(Collectors.toList());
    }

    /**
     * @return the interceptors of the service srv
     * @param srv
     * @param interceptPoint
     *
     */
    @Override
    public List> getServiceInterceptors(Service srv, InterceptPoint interceptPoint) {
        Objects.requireNonNull(srv);
        Objects.requireNonNull(interceptPoint);

        var serviceName = PluginUtils.name(srv);

        var _ret =  SRV_INTERCEPTORS_CACHE.getLoading(new AbstractMap.SimpleEntry<>(serviceName, interceptPoint));

        return _ret.isPresent() ? _ret.get() : Lists.newArrayList();
    }

    /**
     * @return the interceptors of the proxy
     * @param interceptPoint
     *
     */
    @Override
    public List> getProxyInterceptors(InterceptPoint interceptPoint) {
        var _ret =  SRV_INTERCEPTORS_CACHE.getLoading(new AbstractMap.SimpleEntry<>(null, interceptPoint));

        return _ret.isPresent() ? _ret.get() : Lists.newArrayList();
    }

    /**
     * @return the services
     */
    @Override
    public Set>> getServices() {
        if (!servicesInitialized) {
            this.services.addAll(PluginsFactory.getInstance().services());
            this.servicesInitialized = true;
        }

        return Collections.unmodifiableSet(this.services);
    }

    @Override
    public PathHandler getRootPathHandler() {
        return ROOT_PATH_HANDLER;
    }

    @Override
    public void plugPipeline(String path, PipelinedHandler handler, PipelineInfo info) {
        if (info.getUriMatchPolicy() == MATCH_POLICY.PREFIX) {
            ROOT_PATH_HANDLER.addPrefixPath(path, handler);
            PIPELINE_INFOS.addPrefixPath(path, info);
        } else {
            ROOT_PATH_HANDLER.addExactPath(path, handler);
            PIPELINE_INFOS.addExactPath(path, info);
        }
    }

    @Override
    public PipelineInfo getPipelineInfo(String path) {
        var m = PIPELINE_INFOS.match(path);

        return m.getValue();
    }

    @Override
    public void plugService(PluginRecord> srv, final String uri, MATCH_POLICY mp, boolean secured) {
        SecurityHandler securityHandler;

        var _mechanisms = getAuthMechanisms();
        var _authorizers = getAuthorizers();
        var _tokenManager = getTokenManager();

        if (secured) {
            securityHandler = new SecurityHandler(_mechanisms, _authorizers, _tokenManager);
        } else {
            var _fauthorizers = new LinkedHashSet>();

            var _fauthorizer = new PluginRecord(
                "fullAuthorizer",
                "authorize any operation to any user",
                false, // secure, only applies to services
                true,
                FullAuthorizer.class.getName(),
                new FullAuthorizer(false),
                null
            );

            _fauthorizers.add(_fauthorizer);

            securityHandler = new SecurityHandler(_mechanisms, _fauthorizers, _tokenManager);
        }

        var blockingSrv = PluginUtils.blocking(srv.getInstance());

        var _srv = pipe(
            // if service is blocking (i.e. @RegisterPlugin(blocking=true))
            // add WorkingThreadsPoolDispatcher to the pipe
            blockingSrv ? new WorkingThreadsPoolDispatcher() : null,
            new ErrorHandler(),
            new PipelineInfoInjector(),
            new TracingInstrumentationHandler(),
            new RequestLogger(),
            new BeforeExchangeInitInterceptorsExecutor(),
            new ServiceExchangeInitializer(),
            new CORSHandler(),
            new RequestInterceptorsExecutor(REQUEST_BEFORE_AUTH),
            new QueryStringRebuilder(),
            securityHandler,
            new RequestInterceptorsExecutor(REQUEST_AFTER_AUTH),
            new QueryStringRebuilder(),
            PipelinedWrappingHandler.wrap(new ConfigurableEncodingHandler(PipelinedWrappingHandler.wrap(srv.getInstance()))),
            new ResponseInterceptorsExecutor(),
            new ResponseSender()
        );

        plugPipeline(uri, _srv, new PipelineInfo(SERVICE, uri, mp, srv.getName()));

        this.services.add(srv);

        // service list changed, invalidate cache
        this.SRV_INTERCEPTORS_CACHE.invalidateAll();
    }

    /**
     * unplugs an handler from the root handler
     *
     * @param uri
     * @param mp
     */
    @Override
    public void unplug(String uri, MATCH_POLICY mp) {
        var pi = getPipelineInfo(uri);

        this.services.removeIf(s -> s.getName().equals(pi.getName()));

        if (mp == MATCH_POLICY.PREFIX) {
            ROOT_PATH_HANDLER.removePrefixPath(uri);
            PIPELINE_INFOS.removePrefixPath(uri);
        } else {
            ROOT_PATH_HANDLER.removeExactPath(uri);
            PIPELINE_INFOS.removeExactPath(uri);
        }

        // service list changed, invalidate cache
        this.SRV_INTERCEPTORS_CACHE.invalidateAll();
    }

    private static class SingletonHolder {
        private static final PluginsRegistryImpl INSTANCE;

        // make sure the Singleton is a Singleton even in a multi-classloader environment
        // credits to https://stackoverflow.com/users/145989/ondra-Žižka
        // https://stackoverflow.com/a/47445573/4481670
        static {
            // There should be just one system class loader object in the whole JVM.
            synchronized(ClassLoader.getSystemClassLoader()) {
                var sysProps = System.getProperties();
                // The key is a String, because the .class object would be different across classloaders.
                var singleton = (PluginsRegistryImpl) sysProps.get(PluginsRegistryImpl.class.getName());

                // Some other class loader loaded PluginsRegistryImpl earlier.
                if (singleton != null) {
                    INSTANCE = singleton;
                }
                else {
                    // Otherwise this classloader is the first one, let's create a singleton.
                    // Make sure not to do any locking within this.
                    INSTANCE = new PluginsRegistryImpl();
                    System.getProperties().put(PluginsRegistryImpl.class.getName(), INSTANCE);
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy