/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.drill.exec.server.rest; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper; import com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; import org.apache.drill.shaded.guava.com.google.common.base.Strings; import freemarker.cache.ClassTemplateLoader; import freemarker.cache.FileTemplateLoader; import freemarker.cache.MultiTemplateLoader; import freemarker.cache.TemplateLoader; import freemarker.cache.WebappTemplateLoader; import freemarker.core.HTMLOutputFormat; import freemarker.template.Configuration; import io.netty.channel.ChannelPromise; import io.netty.channel.DefaultChannelPromise; import io.netty.util.concurrent.EventExecutor; import org.apache.drill.common.config.DrillConfig; import org.apache.drill.exec.ExecConstants; import org.apache.drill.exec.memory.BufferAllocator; import org.apache.drill.exec.proto.UserBitShared; import org.apache.drill.exec.rpc.user.UserSession; import org.apache.drill.exec.server.Drillbit; import org.apache.drill.exec.server.DrillbitContext; import org.apache.drill.exec.server.rest.WebUserConnection.AnonWebUserConnection; import org.apache.drill.exec.server.rest.auth.AuthDynamicFeature; import org.apache.drill.exec.server.rest.auth.DrillUserPrincipal; import org.apache.drill.exec.server.rest.auth.DrillUserPrincipal.AnonDrillUserPrincipal; import org.apache.drill.exec.server.rest.profile.ProfileResources; import org.apache.drill.exec.store.StoragePluginRegistry; import org.apache.drill.exec.store.sys.PersistentStoreProvider; import org.apache.drill.exec.work.WorkManager; import org.glassfish.hk2.api.Factory; import org.glassfish.hk2.utilities.binding.AbstractBinder; import org.glassfish.jersey.CommonProperties; import org.glassfish.jersey.internal.util.PropertiesHelper; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.process.internal.RequestScoped; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.ServerProperties; import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; import org.glassfish.jersey.server.mvc.freemarker.FreemarkerMvcFeature; import javax.inject.Inject; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.Principal; import java.util.ArrayList; import java.util.List; public class DrillRestServer extends ResourceConfig { static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DrillRestServer.class); public DrillRestServer(final WorkManager workManager, final ServletContext servletContext, final Drillbit drillbit) { register(DrillRoot.class); register(StatusResources.class); register(StorageResources.class); register(ProfileResources.class); register(QueryResources.class); register(MetricsResources.class); register(ThreadsResources.class); register(LogsResources.class); property(FreemarkerMvcFeature.TEMPLATE_OBJECT_FACTORY, getFreemarkerConfiguration(servletContext)); register(FreemarkerMvcFeature.class); register(MultiPartFeature.class); property(ServerProperties.METAINF_SERVICES_LOOKUP_DISABLE, true); final boolean isAuthEnabled = workManager.getContext().getConfig().getBoolean(ExecConstants.USER_AUTHENTICATION_ENABLED); if (isAuthEnabled) { register(LogInLogOutResources.class); register(AuthDynamicFeature.class); register(RolesAllowedDynamicFeature.class); } //disable moxy so it doesn't conflict with jackson. final String disableMoxy = PropertiesHelper.getPropertyNameForRuntime(CommonProperties.MOXY_JSON_FEATURE_DISABLE, getConfiguration().getRuntimeType()); property(disableMoxy, true); register(JsonParseExceptionMapper.class); register(JsonMappingExceptionMapper.class); register(GenericExceptionMapper.class); JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider(); provider.setMapper(workManager.getContext().getLpPersistence().getMapper()); register(provider); // Get an EventExecutor out of the BitServer EventLoopGroup to notify listeners for WebUserConnection. For // actual connections between Drillbits this EventLoopGroup is used to handle network related events. Though // there is no actual network connection associated with WebUserConnection but we need a CloseFuture in // WebSessionResources, so we are using EvenExecutor from network EventLoopGroup pool. final EventExecutor executor = workManager.getContext().getBitLoopGroup().next(); register(new AbstractBinder() { @Override protected void configure() { bind(drillbit).to(Drillbit.class); bind(workManager).to(WorkManager.class); bind(executor).to(EventExecutor.class); bind(workManager.getContext().getLpPersistence().getMapper()).to(ObjectMapper.class); bind(workManager.getContext().getStoreProvider()).to(PersistentStoreProvider.class); bind(workManager.getContext().getStorage()).to(StoragePluginRegistry.class); bind(new UserAuthEnabled(isAuthEnabled)).to(UserAuthEnabled.class); if (isAuthEnabled) { bindFactory(DrillUserPrincipalProvider.class).to(DrillUserPrincipal.class); bindFactory(AuthWebUserConnectionProvider.class).to(WebUserConnection.class); } else { bindFactory(AnonDrillUserPrincipalProvider.class).to(DrillUserPrincipal.class); bindFactory(AnonWebUserConnectionProvider.class).to(WebUserConnection.class); } } }); } /** * Creates freemarker configuration settings, * default output format to trigger auto-escaping policy * and template loaders. * * @param servletContext servlet context * @return freemarker configuration settings */ private Configuration getFreemarkerConfiguration(ServletContext servletContext) { Configuration configuration = new Configuration(Configuration.VERSION_2_3_26); configuration.setOutputFormat(HTMLOutputFormat.INSTANCE); List<TemplateLoader> loaders = new ArrayList<>(); loaders.add(new WebappTemplateLoader(servletContext)); loaders.add(new ClassTemplateLoader(DrillRestServer.class, "/")); try { loaders.add(new FileTemplateLoader(new File("/"))); } catch (IOException e) { logger.error("Could not set up file template loader.", e); } configuration.setTemplateLoader(new MultiTemplateLoader(loaders.toArray(new TemplateLoader[loaders.size()]))); return configuration; } public static class AuthWebUserConnectionProvider implements Factory<WebUserConnection> { @Inject HttpServletRequest request; @Inject WorkManager workManager; @Inject EventExecutor executor; @Override public WebUserConnection provide() { final HttpSession session = request.getSession(); final Principal sessionUserPrincipal = request.getUserPrincipal(); // If there is no valid principal this means user is not logged in yet. if (sessionUserPrincipal == null) { return null; } // User is logged in, get/set the WebSessionResources attribute WebSessionResources webSessionResources = (WebSessionResources) session.getAttribute(WebSessionResources.class.getSimpleName()); if (webSessionResources == null) { // User is login in for the first time final DrillbitContext drillbitContext = workManager.getContext(); final DrillConfig config = drillbitContext.getConfig(); final UserSession drillUserSession = UserSession.Builder.newBuilder() .withCredentials(UserBitShared.UserCredentials.newBuilder() .setUserName(sessionUserPrincipal.getName()) .build()) .withOptionManager(drillbitContext.getOptionManager()) .setSupportComplexTypes(config.getBoolean(ExecConstants.CLIENT_SUPPORT_COMPLEX_TYPES)) .build(); // Only try getting remote address in first login since it's a costly operation. SocketAddress remoteAddress = null; try { // This can be slow as the underlying library will try to resolve the address remoteAddress = new InetSocketAddress(InetAddress.getByName(request.getRemoteAddr()), request.getRemotePort()); session.setAttribute(SocketAddress.class.getSimpleName(), remoteAddress); } catch (Exception ex) { //no-op logger.trace("Failed to get the remote address of the http session request", ex); } // Create per session BufferAllocator and set it in session final String sessionAllocatorName = String.format("WebServer:AuthUserSession:%s", session.getId()); final BufferAllocator sessionAllocator = workManager.getContext().getAllocator().newChildAllocator( sessionAllocatorName, config.getLong(ExecConstants.HTTP_SESSION_MEMORY_RESERVATION), config.getLong(ExecConstants.HTTP_SESSION_MEMORY_MAXIMUM)); // Create a dummy close future which is needed by Foreman only. Foreman uses this future to add a close // listener to known about channel close event from underlying layer. We use this future to notify Foreman // listeners when the Web session (not connection) between Web Client and WebServer is closed. This will help // Foreman to cancel all the running queries for this Web Client. final ChannelPromise closeFuture = new DefaultChannelPromise(null, executor); // Create a WebSessionResource instance which owns the lifecycle of all the session resources. // Set this instance as an attribute of HttpSession, since it will be used until session is destroyed webSessionResources = new WebSessionResources(sessionAllocator, remoteAddress, drillUserSession, closeFuture); session.setAttribute(WebSessionResources.class.getSimpleName(), webSessionResources); } // Create a new WebUserConnection for the request return new WebUserConnection(webSessionResources); } @Override public void dispose(WebUserConnection instance) { } } public static class AnonWebUserConnectionProvider implements Factory<WebUserConnection> { @Inject HttpServletRequest request; @Inject WorkManager workManager; @Inject EventExecutor executor; @Override public WebUserConnection provide() { final DrillbitContext drillbitContext = workManager.getContext(); final DrillConfig config = drillbitContext.getConfig(); // Create an allocator here for each request final BufferAllocator sessionAllocator = drillbitContext.getAllocator() .newChildAllocator("WebServer:AnonUserSession", config.getLong(ExecConstants.HTTP_SESSION_MEMORY_RESERVATION), config.getLong(ExecConstants.HTTP_SESSION_MEMORY_MAXIMUM)); final Principal sessionUserPrincipal = createSessionUserPrincipal(config, request); // Create new UserSession for each request from non-authenticated user final UserSession drillUserSession = UserSession.Builder.newBuilder() .withCredentials(UserBitShared.UserCredentials.newBuilder() .setUserName(sessionUserPrincipal.getName()) .build()) .withOptionManager(drillbitContext.getOptionManager()) .setSupportComplexTypes(drillbitContext.getConfig().getBoolean(ExecConstants.CLIENT_SUPPORT_COMPLEX_TYPES)) .build(); // Try to get the remote Address but set it to null in case of failure. SocketAddress remoteAddress = null; try { // This can be slow as the underlying library will try to resolve the address remoteAddress = new InetSocketAddress(InetAddress.getByName(request.getRemoteAddr()), request.getRemotePort()); } catch (Exception ex) { // no-op logger.trace("Failed to get the remote address of the http session request", ex); } // Create a dummy close future which is needed by Foreman only. Foreman uses this future to add a close // listener to known about channel close event from underlying layer. // // The invocation of this close future is no-op as it will be triggered after query completion in unsecure case. // But we need this close future as it's expected by Foreman. final ChannelPromise closeFuture = new DefaultChannelPromise(null, executor); final WebSessionResources webSessionResources = new WebSessionResources(sessionAllocator, remoteAddress, drillUserSession, closeFuture); // Create a AnonWenUserConnection for this request return new AnonWebUserConnection(webSessionResources); } @Override public void dispose(WebUserConnection instance) { } /** * Creates session user principal. If impersonation is enabled without authentication and User-Name header is present and valid, * will create session user principal with provided user name, otherwise anonymous user name will be used. * In both cases session user principal will have admin rights. * * @param config drill config * @param request client request * @return session user principal */ private Principal createSessionUserPrincipal(DrillConfig config, HttpServletRequest request) { if (WebServer.isOnlyImpersonationEnabled(config)) { final String userName = request.getHeader("User-Name"); if (!Strings.isNullOrEmpty(userName)) { return new DrillUserPrincipal(userName, true); } } return new AnonDrillUserPrincipal(); } } // Provider which injects DrillUserPrincipal directly instead of getting it from SecurityContext and typecasting public static class DrillUserPrincipalProvider implements Factory<DrillUserPrincipal> { @Inject HttpServletRequest request; @Override public DrillUserPrincipal provide() { return (DrillUserPrincipal) request.getUserPrincipal(); } @Override public void dispose(DrillUserPrincipal principal) { // No-Op } } // Provider which creates and cleanups DrillUserPrincipal for anonymous (auth disabled) mode public static class AnonDrillUserPrincipalProvider implements Factory<DrillUserPrincipal> { @RequestScoped @Override public DrillUserPrincipal provide() { return new AnonDrillUserPrincipal(); } @Override public void dispose(DrillUserPrincipal principal) { // If this worked it would have been clean to free the resources here, but there are various scenarios // where dispose never gets called due to bugs in jersey. } } // Returns whether auth is enabled or not in config public static class UserAuthEnabled { private boolean value; public UserAuthEnabled(boolean value) { this.value = value; } public boolean get() { return value; } } }