/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.boot.actuate.autoconfigure;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.crsh.auth.AuthenticationPlugin;
import org.crsh.plugin.CRaSHPlugin;
import org.crsh.plugin.PluginContext;
import org.crsh.plugin.PluginDiscovery;
import org.crsh.plugin.PluginLifeCycle;
import org.crsh.plugin.PropertyDescriptor;
import org.crsh.plugin.ServiceLoaderDiscovery;
import org.crsh.vfs.FS;
import org.crsh.vfs.spi.AbstractFSDriver;
import org.crsh.vfs.spi.FSDriver;

import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.autoconfigure.ShellProperties.CrshShellAuthenticationProperties;
import org.springframework.boot.actuate.autoconfigure.ShellProperties.CrshShellProperties;
import org.springframework.boot.actuate.autoconfigure.ShellProperties.JaasAuthenticationProperties;
import org.springframework.boot.actuate.autoconfigure.ShellProperties.KeyAuthenticationProperties;
import org.springframework.boot.actuate.autoconfigure.ShellProperties.SimpleAuthenticationProperties;
import org.springframework.boot.actuate.autoconfigure.ShellProperties.SpringAuthenticationProperties;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.SpringVersion;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for embedding an extensible shell
 * into a Spring Boot enabled application. By default a SSH daemon is started on port
 * 2000. If the CRaSH Telnet plugin is available on the classpath, Telnet daemon will be
 * launched on port 5000.
 * <p>
 * The default shell authentication method uses a username and password combination. If no
 * configuration is provided the default username is 'user' and the password will be
 * printed to console during application startup. Those default values can be overridden
 * by using {@code management.shell.auth.simple.username} and
 * {@code management.shell.auth.simple.password}.
 * <p>
 * If a Spring Security {@link AuthenticationManager} is detected, this configuration will
 * create a {@link CRaSHPlugin} to forward shell authentication requests to Spring
 * Security. This authentication method will get enabled if
 * {@code management.shell.auth.type} is set to {@code spring} or if no explicit
 * {@code management.shell.auth} is provided and a {@link AuthenticationManager} is
 * available. In the latter case shell access will be restricted to users having roles
 * that match those configured in {@link ManagementServerProperties}. Required roles can
 * be overridden by {@code management.shell.auth.spring.roles}.
 * <p>
 * To add customizations to the shell simply define beans of type {@link CRaSHPlugin} in
 * the application context. Those beans will get auto detected during startup and
 * registered with the underlying shell infrastructure. To configure plugins and the CRaSH
 * infrastructure add beans of type {@link CrshShellProperties} to the application
 * context.
 * <p>
 * Additional shell commands can be implemented using the guide and documentation at
 * <a href="https://www.crashub.org">crashub.org</a>. By default Boot will search for
 * commands using the following classpath scanning pattern {@code classpath*:/commands/**}
 * . To add different locations or override the default use
 * {@code management.shell.command-path-patterns} in your application configuration.
 *
 * @author Christian Dupuis
 * @author Matt Benson
 * @see ShellProperties
 * @deprecated as of 1.5 since CRaSH is not actively maintained
 */
@Configuration
@ConditionalOnClass(PluginLifeCycle.class)
@EnableConfigurationProperties(ShellProperties.class)
@AutoConfigureAfter({ SecurityAutoConfiguration.class,
		ManagementWebSecurityAutoConfiguration.class })
@Deprecated
public class CrshAutoConfiguration {

	public static final String AUTH_PREFIX = ShellProperties.SHELL_PREFIX + ".auth";

	private final ShellProperties properties;

	public CrshAutoConfiguration(ShellProperties properties) {
		this.properties = properties;
	}

	@Bean
	@ConditionalOnMissingBean(PluginLifeCycle.class)
	public CrshBootstrapBean shellBootstrap() {
		CrshBootstrapBean bootstrapBean = new CrshBootstrapBean();
		bootstrapBean.setConfig(this.properties.asCrshShellConfig());
		return bootstrapBean;
	}

	@Configuration
	@Deprecated
	static class CrshAdditionalPropertiesConfiguration {

		@Bean
		@ConditionalOnProperty(prefix = AUTH_PREFIX, name = "type", havingValue = "jaas")
		@ConditionalOnMissingBean(CrshShellAuthenticationProperties.class)
		public JaasAuthenticationProperties jaasAuthenticationProperties() {
			return new JaasAuthenticationProperties();
		}

		@Bean
		@ConditionalOnProperty(prefix = AUTH_PREFIX, name = "type", havingValue = "key")
		@ConditionalOnMissingBean(CrshShellAuthenticationProperties.class)
		public KeyAuthenticationProperties keyAuthenticationProperties() {
			return new KeyAuthenticationProperties();
		}

		@Bean
		@ConditionalOnProperty(prefix = AUTH_PREFIX, name = "type",
				havingValue = "simple", matchIfMissing = true)
		@ConditionalOnMissingBean(CrshShellAuthenticationProperties.class)
		public SimpleAuthenticationProperties simpleAuthenticationProperties() {
			return new SimpleAuthenticationProperties();
		}

	}

	/**
	 * Class to configure CRaSH to authenticate against Spring Security.
	 */
	@Configuration
	@ConditionalOnProperty(prefix = AUTH_PREFIX, name = "type", havingValue = "spring",
			matchIfMissing = true)
	@ConditionalOnBean(AuthenticationManager.class)
	@Deprecated
	public static class AuthenticationManagerAdapterConfiguration {

		private final ManagementServerProperties management;

		public AuthenticationManagerAdapterConfiguration(
				ObjectProvider<ManagementServerProperties> management) {
			this.management = management.getIfAvailable();
		}

		@Bean
		public AuthenticationManagerAdapter shellAuthenticationManager() {
			return new AuthenticationManagerAdapter();
		}

		@Bean
		@ConditionalOnMissingBean(CrshShellAuthenticationProperties.class)
		public SpringAuthenticationProperties springAuthenticationProperties() {
			// In case no management.shell.auth.type property is provided fall back to
			// Spring Security based authentication and get role to access shell from
			// ManagementServerProperties.
			// In case management.shell.auth.type is set to spring and roles are
			// configured using shell.auth.spring.roles the below default role will be
			// overridden by ConfigurationProperties.
			SpringAuthenticationProperties authenticationProperties = new SpringAuthenticationProperties();
			if (this.management != null) {
				List<String> roles = this.management.getSecurity().getRoles();
				authenticationProperties
						.setRoles(roles.toArray(new String[roles.size()]));
			}
			return authenticationProperties;
		}

	}

	/**
	 * Spring Bean used to bootstrap the CRaSH shell.
	 */
	public static class CrshBootstrapBean extends PluginLifeCycle {

		@Autowired
		private ListableBeanFactory beanFactory;

		@Autowired
		private Environment environment;

		@Autowired
		private ShellProperties properties;

		@Autowired
		private ResourcePatternResolver resourceLoader;

		@PreDestroy
		public void destroy() {
			stop();
		}

		@PostConstruct
		public void init() {
			FS commandFileSystem = createFileSystem(
					this.properties.getCommandPathPatterns(),
					this.properties.getDisabledCommands());
			FS configurationFileSystem = createFileSystem(
					this.properties.getConfigPathPatterns(), new String[0]);

			PluginDiscovery discovery = new BeanFactoryFilteringPluginDiscovery(
					this.resourceLoader.getClassLoader(), this.beanFactory,
					this.properties.getDisabledPlugins());

			PluginContext context = new PluginContext(discovery,
					createPluginContextAttributes(), commandFileSystem,
					configurationFileSystem, this.resourceLoader.getClassLoader());

			context.refresh();
			start(context);
		}

		protected FS createFileSystem(String[] pathPatterns, String[] filterPatterns) {
			Assert.notNull(pathPatterns, "PathPatterns must not be null");
			Assert.notNull(filterPatterns, "FilterPatterns must not be null");
			FS fileSystem = new FS();
			for (String pathPattern : pathPatterns) {
				try {
					fileSystem.mount(new SimpleFileSystemDriver(new DirectoryHandle(
							pathPattern, this.resourceLoader, filterPatterns)));
				}
				catch (IOException ex) {
					throw new IllegalStateException(
							"Failed to mount file system for '" + pathPattern + "'", ex);
				}
			}
			return fileSystem;
		}

		protected Map<String, Object> createPluginContextAttributes() {
			Map<String, Object> attributes = new HashMap<String, Object>();
			String bootVersion = CrshAutoConfiguration.class.getPackage()
					.getImplementationVersion();
			if (bootVersion != null) {
				attributes.put("spring.boot.version", bootVersion);
			}
			attributes.put("spring.version", SpringVersion.getVersion());
			if (this.beanFactory != null) {
				attributes.put("spring.beanfactory", this.beanFactory);
			}
			if (this.environment != null) {
				attributes.put("spring.environment", this.environment);
			}
			return attributes;
		}

	}

	/**
	 * Adapts a Spring Security {@link AuthenticationManager} for use with CRaSH.
	 */
	@SuppressWarnings("rawtypes")
	private static class AuthenticationManagerAdapter extends
			CRaSHPlugin<AuthenticationPlugin> implements AuthenticationPlugin<String> {

		private static final PropertyDescriptor<String> ROLES = PropertyDescriptor.create(
				"auth.spring.roles", "ACTUATOR",
				"Comma separated list of roles required to access the shell");

		@Autowired
		private AuthenticationManager authenticationManager;

		@Autowired(required = false)
		@Qualifier("shellAccessDecisionManager")
		private AccessDecisionManager accessDecisionManager;

		private String[] roles = new String[] { "ACTUATOR" };

		@Override
		public boolean authenticate(String username, String password) throws Exception {
			Authentication token = new UsernamePasswordAuthenticationToken(username,
					password);
			try {
				// Authenticate first to make sure credentials are valid
				token = this.authenticationManager.authenticate(token);
			}
			catch (AuthenticationException ex) {
				return false;
			}

			// Test access rights if a Spring Security AccessDecisionManager is installed
			if (this.accessDecisionManager != null && token.isAuthenticated()
					&& this.roles != null) {
				try {
					this.accessDecisionManager.decide(token, this,
							SecurityConfig.createList(this.roles));
				}
				catch (AccessDeniedException ex) {
					return false;
				}
			}
			return token.isAuthenticated();
		}

		@Override
		public Class<String> getCredentialType() {
			return String.class;
		}

		@Override
		public AuthenticationPlugin<String> getImplementation() {
			return this;
		}

		@Override
		public String getName() {
			return "spring";
		}

		@Override
		public void init() {
			String rolesPropertyValue = getContext().getProperty(ROLES);
			if (rolesPropertyValue != null) {
				this.roles = StringUtils
						.commaDelimitedListToStringArray(rolesPropertyValue);
			}
		}

		@Override
		protected Iterable<PropertyDescriptor<?>> createConfigurationCapabilities() {
			return Arrays.<PropertyDescriptor<?>>asList(ROLES);
		}

	}

	/**
	 * {@link ServiceLoaderDiscovery} to expose {@link CRaSHPlugin} Beans from Spring and
	 * deal with filtering disabled plugins.
	 */
	private static class BeanFactoryFilteringPluginDiscovery
			extends ServiceLoaderDiscovery {

		private final ListableBeanFactory beanFactory;

		private final String[] disabledPlugins;

		BeanFactoryFilteringPluginDiscovery(ClassLoader classLoader,
				ListableBeanFactory beanFactory, String[] disabledPlugins)
				throws NullPointerException {
			super(classLoader);
			this.beanFactory = beanFactory;
			this.disabledPlugins = disabledPlugins;
		}

		@Override
		@SuppressWarnings("rawtypes")
		public Iterable<CRaSHPlugin<?>> getPlugins() {
			List<CRaSHPlugin<?>> plugins = new ArrayList<CRaSHPlugin<?>>();

			for (CRaSHPlugin<?> p : super.getPlugins()) {
				if (isEnabled(p)) {
					plugins.add(p);
				}
			}

			Collection<CRaSHPlugin> pluginBeans = this.beanFactory
					.getBeansOfType(CRaSHPlugin.class).values();
			for (CRaSHPlugin<?> pluginBean : pluginBeans) {
				if (isEnabled(pluginBean)) {
					plugins.add(pluginBean);
				}
			}

			return plugins;
		}

		protected boolean isEnabled(CRaSHPlugin<?> plugin) {
			Assert.notNull(plugin, "Plugin must not be null");

			if (ObjectUtils.isEmpty(this.disabledPlugins)) {
				return true;
			}

			Set<Class<?>> pluginClasses = ClassUtils.getAllInterfacesAsSet(plugin);
			pluginClasses.add(plugin.getClass());

			for (Class<?> pluginClass : pluginClasses) {
				if (!isEnabled(pluginClass)) {
					return false;
				}
			}
			return true;
		}

		private boolean isEnabled(Class<?> pluginClass) {
			for (String disabledPlugin : this.disabledPlugins) {
				if (ClassUtils.getShortName(pluginClass).equalsIgnoreCase(disabledPlugin)
						|| ClassUtils.getQualifiedName(pluginClass)
								.equalsIgnoreCase(disabledPlugin)) {
					return false;
				}
			}
			return true;
		}

	}

	/**
	 * {@link FSDriver} to wrap Spring's {@link Resource} abstraction to CRaSH.
	 */
	private static class SimpleFileSystemDriver extends AbstractFSDriver<ResourceHandle> {

		private final ResourceHandle root;

		SimpleFileSystemDriver(ResourceHandle handle) {
			this.root = handle;
		}

		@Override
		public Iterable<ResourceHandle> children(ResourceHandle handle)
				throws IOException {
			if (handle instanceof DirectoryHandle) {
				return ((DirectoryHandle) handle).members();
			}
			return Collections.emptySet();
		}

		@Override
		public long getLastModified(ResourceHandle handle) throws IOException {
			if (handle instanceof FileHandle) {
				return ((FileHandle) handle).getLastModified();
			}
			return -1;
		}

		@Override
		public boolean isDir(ResourceHandle handle) throws IOException {
			return handle instanceof DirectoryHandle;
		}

		@Override
		public String name(ResourceHandle handle) throws IOException {
			return handle.getName();
		}

		@Override
		public Iterator<InputStream> open(ResourceHandle handle) throws IOException {
			if (handle instanceof FileHandle) {
				return Collections.singletonList(((FileHandle) handle).openStream())
						.iterator();
			}
			return Collections.<InputStream>emptyList().iterator();
		}

		@Override
		public ResourceHandle root() throws IOException {
			return this.root;
		}

	}

	/**
	 * Base for handles to Spring {@link Resource}s.
	 */
	private abstract static class ResourceHandle {

		private final String name;

		ResourceHandle(String name) {
			this.name = name;
		}

		public String getName() {
			return this.name;
		}

	}

	/**
	 * {@link ResourceHandle} for a directory.
	 */
	private static class DirectoryHandle extends ResourceHandle {

		private final ResourcePatternResolver resourceLoader;

		private final String[] filterPatterns;

		private final AntPathMatcher matcher = new AntPathMatcher();

		DirectoryHandle(String name, ResourcePatternResolver resourceLoader,
				String[] filterPatterns) {
			super(name);
			this.resourceLoader = resourceLoader;
			this.filterPatterns = filterPatterns;
		}

		public List<ResourceHandle> members() throws IOException {
			Resource[] resources = this.resourceLoader.getResources(getName());
			List<ResourceHandle> files = new ArrayList<ResourceHandle>();
			for (Resource resource : resources) {
				if (!resource.getURL().getPath().endsWith("/")
						&& !shouldFilter(resource)) {
					files.add(new FileHandle(resource.getFilename(), resource));
				}
			}
			return files;
		}

		private boolean shouldFilter(Resource resource) {
			for (String filterPattern : this.filterPatterns) {
				if (this.matcher.match(filterPattern, resource.getFilename())) {
					return true;
				}
			}
			return false;
		}

	}

	/**
	 * {@link ResourceHandle} for a file backed by a Spring {@link Resource}.
	 */
	private static class FileHandle extends ResourceHandle {

		private final Resource resource;

		FileHandle(String name, Resource resource) {
			super(name);
			this.resource = resource;
		}

		public InputStream openStream() throws IOException {
			return this.resource.getInputStream();
		}

		public long getLastModified() {
			try {
				return this.resource.lastModified();
			}
			catch (IOException ex) {
				return -1;
			}
		}

	}

}
