diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index 0838b2efdf9..0bc11243e5f 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -72,6 +72,11 @@ tomcat-embed-core true + + org.crashub + crash.embed.spring + true + org.springframework diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/CrshAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/CrshAutoConfiguration.java new file mode 100644 index 00000000000..61c24ce0d37 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/CrshAutoConfiguration.java @@ -0,0 +1,462 @@ +/* + * Copyright 2013 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 + * + * 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.springframework.boot.actuate.autoconfigure; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +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.Properties; +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.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.properties.CrshProperties; +import org.springframework.boot.actuate.properties.CrshProperties.AuthenticationProperties; +import org.springframework.boot.actuate.properties.CrshProperties.JaasAuthenticationProperties; +import org.springframework.boot.actuate.properties.CrshProperties.KeyAuthenticationProperties; +import org.springframework.boot.actuate.properties.CrshProperties.SimpleAuthenticationProperties; +import org.springframework.boot.actuate.properties.CrshProperties.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.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +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.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.Assert; +import org.springframework.util.ClassUtils; +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 with a default username + * user and password (default password is logged during application startup). + * + *

+ * This configuration will auto detect the existence of a Spring Security {@link AuthenticationManager} + * and will delegate authentication requests for shell access to this detected instance. + * + *

+ * 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. + * + *

+ * Additional shell commands can be implemented using the guide and documentation at + * crashub.org. By default Boot will search for commands using + * the following classpath scanning pattern classpath*:/commands/**. To add different + * locations or override the default use shell.command_path_patterns in your application + * configuration. + * + * @author Christian Dupuis + */ +@Configuration +@ConditionalOnClass({ PluginLifeCycle.class }) +@EnableConfigurationProperties({ CrshProperties.class }) +@AutoConfigureAfter(SecurityAutoConfiguration.class) +public class CrshAutoConfiguration { + + @Autowired + private CrshProperties properties; + + + @Bean + @ConditionalOnExpression("#{environment['shell.auth'] == 'jaas'}") + @ConditionalOnMissingBean({ AuthenticationProperties.class }) + public AuthenticationProperties jaasAuthenticationProperties() { + return new JaasAuthenticationProperties(); + } + + @Bean + @ConditionalOnExpression("#{environment['shell.auth'] == 'key'}") + @ConditionalOnMissingBean({ AuthenticationProperties.class }) + public AuthenticationProperties keyAuthenticationProperties() { + return new KeyAuthenticationProperties(); + } + + @Bean + @ConditionalOnExpression("#{environment['shell.auth'] == 'simple'}") + @ConditionalOnMissingBean({ AuthenticationProperties.class }) + public AuthenticationProperties simpleAuthenticationProperties() { + return new SimpleAuthenticationProperties(); + } + + @Bean + @ConditionalOnExpression("#{environment['shell.auth'] == 'spring'}") + @ConditionalOnMissingBean({ AuthenticationProperties.class }) + public AuthenticationProperties SpringAuthenticationProperties() { + return new SpringAuthenticationProperties(); + } + + @Bean + @ConditionalOnBean({ AuthenticationManager.class }) + public CRaSHPlugin shellAuthenticationManager() { + return new AuthenticationManagerAdapter(); + } + + @Bean + @ConditionalOnMissingBean({ PluginLifeCycle.class }) + public PluginLifeCycle shellBootstrap() { + CrshBootstrap bs = new CrshBootstrap(); + bs.setConfig(properties.mergeProperties(new Properties())); + return bs; + } + + + public static class CrshBootstrap extends PluginLifeCycle { + + @Autowired + private ListableBeanFactory beanFactory; + + @Autowired + private CrshProperties properties; + + @Autowired + private ResourcePatternResolver resourceLoader; + + + @PreDestroy + public void destroy() { + stop(); + } + + @PostConstruct + public void init() throws Exception { + FS commandFileSystem = createFileSystem(properties.getCommandPathPatterns()); + FS confFileSystem = createFileSystem(properties.getConfigPathPatterns()); + + PluginDiscovery discovery = new BeanFactoryFilteringPluginDiscovery(resourceLoader.getClassLoader(), + beanFactory, properties.getDisabledPlugins()); + + PluginContext context = new PluginContext(discovery, createPluginContextAttributes(), + commandFileSystem, confFileSystem, resourceLoader.getClassLoader()); + + context.refresh(); + start(context); + } + + + protected FS createFileSystem(String[] pathPatterns) throws IOException, URISyntaxException { + Assert.notNull(pathPatterns); + FS cmdFS = new FS(); + for (String pathPattern : pathPatterns) { + cmdFS.mount(new SimpleFileSystemDriver(new DirectoryHandle(pathPattern, resourceLoader))); + } + return cmdFS; + } + + protected Map createPluginContextAttributes() { + Map attributes = new HashMap(); + String bootVersion = CrshAutoConfiguration.class.getPackage().getImplementationVersion(); + if (bootVersion != null) { + attributes.put("spring.boot.version", bootVersion); + } + attributes.put("spring.version", SpringVersion.getVersion()); + if (beanFactory != null) { + attributes.put("spring.beanfactory", beanFactory); + } + return attributes; + } + + } + + + @SuppressWarnings("rawtypes") + private static class AuthenticationManagerAdapter extends CRaSHPlugin implements + AuthenticationPlugin { + + private static final PropertyDescriptor ROLES = PropertyDescriptor.create( + "auth.spring.roles", "ADMIN", "Comma separated list of roles required to access the shell"); + + + @Autowired(required=false) + private AccessDecisionManager accessDecisionManager; + + @Autowired + private AuthenticationManager authenticationManager; + + private String[] roles = new String[] { "ROLE_ADMIN" }; + + + @Override + public boolean authenticate(String username, String password) throws Exception { + // Authenticate first to make credentials are valid + Authentication token = new UsernamePasswordAuthenticationToken(username, password); + try { + token = authenticationManager.authenticate(token); + } + catch (AuthenticationException ae) { + return false; + } + + // Test access rights if a Spring Security AccessDecisionManager is installed + if (accessDecisionManager != null && token.isAuthenticated() && roles != null) { + try { + accessDecisionManager.decide(token, this, SecurityConfig.createList(roles)); + } + catch (AccessDeniedException e) { + return false; + } + } + return token.isAuthenticated(); + } + + @Override + public Class getCredentialType() { + return String.class; + } + + @Override + public AuthenticationPlugin 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> createConfigurationCapabilities() { + return Arrays.>asList(ROLES); + } + + } + + + private static class BeanFactoryFilteringPluginDiscovery extends ServiceLoaderDiscovery { + + private ListableBeanFactory beanFactory; + + private String[] disabledPlugins; + + + public BeanFactoryFilteringPluginDiscovery(ClassLoader classLoader, ListableBeanFactory beanFactory, + String[] disabledPlugins) + throws NullPointerException { + super(classLoader); + this.beanFactory = beanFactory; + this.disabledPlugins = disabledPlugins; + } + + + @Override + @SuppressWarnings("rawtypes") + public Iterable> getPlugins() { + List> plugins = new ArrayList>(); + + for (CRaSHPlugin p : super.getPlugins()) { + if (!shouldFilter(p)) { + plugins.add(p); + } + } + + Collection springPlugins = beanFactory.getBeansOfType(CRaSHPlugin.class).values(); + for (CRaSHPlugin p : springPlugins) { + if (!shouldFilter(p)) { + plugins.add(p); + } + } + + return plugins; + } + + + @SuppressWarnings("rawtypes") + protected boolean shouldFilter(CRaSHPlugin plugin) { + Assert.notNull(plugin); + + Set classes = ClassUtils.getAllInterfacesAsSet(plugin); + classes.add(plugin.getClass()); + + for (Class clazz : classes) { + if (disabledPlugins != null && disabledPlugins.length > 0) { + for (String disabledPlugin : disabledPlugins) { + if (ClassUtils.getShortName(clazz).equalsIgnoreCase(disabledPlugin) + || ClassUtils.getQualifiedName(clazz).equalsIgnoreCase(disabledPlugin)) { + return true; + } + } + } + } + return false; + } + + } + + + private static class SimpleFileSystemDriver extends AbstractFSDriver { + + private ResourceHandle root; + + + public SimpleFileSystemDriver(ResourceHandle handle) { + this.root = handle; + } + + + @Override + public Iterable 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 open(ResourceHandle handle) throws IOException { + if (handle instanceof FileHandle) { + return Collections.singletonList(((FileHandle) handle).openStream()).iterator(); + } + return Collections.emptyList().iterator(); + } + + @Override + public ResourceHandle root() throws IOException { + return root; + } + + } + + + private static class DirectoryHandle extends ResourceHandle { + + private ResourcePatternResolver resourceLoader; + + + public DirectoryHandle(String name, ResourcePatternResolver resourceLoader) { + super(name); + this.resourceLoader = resourceLoader; + } + + + public List members() throws IOException { + Resource[] resources = resourceLoader.getResources(getName()); + List files = new ArrayList(); + for (Resource resource : resources) { + if (!resource.getURL().getPath().endsWith("/")) { + files.add(new FileHandle(resource.getFilename(), resource)); + } + } + return files; + } + + } + + + private static class FileHandle extends ResourceHandle { + + private Resource resource; + + + public 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 e) {} + return -1; + } + + } + + + private abstract static class ResourceHandle { + + private String name; + + + public ResourceHandle(String name) { + this.name = name; + } + + + public String getName() { + return name; + } + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/properties/CrshProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/properties/CrshProperties.java new file mode 100644 index 00000000000..cc0c8f2e6ff --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/properties/CrshProperties.java @@ -0,0 +1,355 @@ +/* + * Copyright 2013 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 + * + * 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.springframework.boot.actuate.properties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.UUID; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Configuration properties for the shell subsystem. + * + * @author Christian Dupuis + */ +@ConfigurationProperties(name = "shell", ignoreUnknownFields = true) +public class CrshProperties { + + protected static final String CRASH_AUTH = "crash.auth"; + protected static final String CRASH_AUTH_JAAS_DOMAIN = "crash.auth.jaas.domain"; + protected static final String CRASH_AUTH_KEY_PATH = "crash.auth.key.path"; + protected static final String CRASH_AUTH_SIMPLE_PASSWORD = "crash.auth.simple.password"; + protected static final String CRASH_AUTH_SIMPLE_USERNAME = "crash.auth.simple.username"; + protected static final String CRASH_AUTH_SPRING_ROLES = "crash.auth.spring.roles"; + protected static final String CRASH_SSH_KEYPATH = "crash.ssh.keypath"; + protected static final String CRASH_SSH_PORT = "crash.ssh.port"; + protected static final String CRASH_TELNET_PORT = "crash.telnet.port"; + protected static final String CRASH_VFS_REFRESH_PERIOD = "crash.vfs.refresh_period"; + + private String auth = "simple"; + + @Autowired(required = false) + private AuthenticationProperties authenticationProperties; + + private int commandRefreshInterval = -1; + + private String[] commandPathPatterns = new String[] { "classpath*:/commands/**", + "classpath*:/crash/commands/**" }; + + private String[] configPathPatterns = new String[] { "classpath*:/crash/*" }; + + private String[] disabledPlugins = new String[0]; + + private Ssh ssh = new Ssh(); + + private Telnet telnet = new Telnet(); + + + public String getAuth() { + return this.auth; + } + + public AuthenticationProperties getAuthenticationProperties() { + return this.authenticationProperties; + } + + public int getCommandRefreshInterval() { + return this.commandRefreshInterval; + } + + public String[] getCommandPathPatterns() { + return this.commandPathPatterns; + } + + public String[] getConfigPathPatterns() { + return this.configPathPatterns; + } + + public String[] getDisabledPlugins() { + return this.disabledPlugins; + } + + public Ssh getSsh() { + return this.ssh; + } + + public Telnet getTelnet() { + return this.telnet; + } + + public Properties mergeProperties(Properties properties) { + properties = ssh.mergeProperties(properties); + properties = telnet.mergeProperties(properties); + + properties.put(CRASH_AUTH, auth); + if (authenticationProperties != null) { + properties = authenticationProperties.mergeProperties(properties); + } + + if (this.commandRefreshInterval > 0) { + properties.put(CRASH_VFS_REFRESH_PERIOD, String.valueOf(this.commandRefreshInterval)); + } + + // special handling for disabling Ssh and Telnet support + List dp = new ArrayList(Arrays.asList(this.disabledPlugins)); + if (!ssh.isEnabled()) { + dp.add("org.crsh.ssh.SSHPlugin"); + } + if (!telnet.isEnabled()) { + dp.add("org.crsh.telnet.TelnetPlugin"); + } + this.disabledPlugins = dp.toArray(new String[dp.size()]); + + return properties; + } + + public void setAuth(String auth) { + Assert.hasLength(auth); + this.auth = auth; + } + + public void setAuthenticationProperties(AuthenticationProperties authenticationProperties) { + Assert.notNull(authenticationProperties); + this.authenticationProperties = authenticationProperties; + } + + public void setCommandRefreshInterval(int commandRefreshInterval) { + this.commandRefreshInterval = commandRefreshInterval; + } + + public void setCommandPathPatterns(String[] commandPathPatterns) { + Assert.notEmpty(commandPathPatterns); + this.commandPathPatterns = commandPathPatterns; + } + + public void setConfigPathPatterns(String[] configPathPatterns) { + Assert.notEmpty(configPathPatterns); + this.configPathPatterns = configPathPatterns; + } + + public void setDisabledPlugins(String[] disabledPlugins) { + Assert.notEmpty(disabledPlugins); + this.disabledPlugins = disabledPlugins; + } + + public void setSsh(Ssh ssh) { + Assert.notNull(ssh); + this.ssh = ssh; + } + + public void setTelnet(Telnet telnet) { + Assert.notNull(telnet); + this.telnet = telnet; + } + + + public interface AuthenticationProperties extends PropertiesProvider { + } + + + @ConfigurationProperties(name = "shell.auth.jaas", ignoreUnknownFields = false) + public static class JaasAuthenticationProperties implements AuthenticationProperties { + + private String domain = "my-domain"; + + + @Override + public Properties mergeProperties(Properties properties) { + properties.put(CRASH_AUTH_JAAS_DOMAIN, this.domain); + return properties; + } + + public void setDomain(String domain) { + Assert.hasText(domain); + this.domain = domain; + } + + } + + + @ConfigurationProperties(name = "shell.auth.key", ignoreUnknownFields = false) + public static class KeyAuthenticationProperties implements AuthenticationProperties { + + private String path; + + + @Override + public Properties mergeProperties(Properties properties) { + if (this.path != null) { + properties.put(CRASH_AUTH_KEY_PATH, this.path); + } + return properties; + } + + public void setPath(String path) { + Assert.hasText(path); + this.path = path; + } + + } + + + public interface PropertiesProvider { + Properties mergeProperties(Properties properties); + } + + + @ConfigurationProperties(name = "shell.auth.simple", ignoreUnknownFields = false) + public static class SimpleAuthenticationProperties implements AuthenticationProperties { + + private static Log logger = LogFactory.getLog(SimpleAuthenticationProperties.class); + + + private String username = "user"; + + private String password = UUID.randomUUID().toString(); + + private boolean defaultPassword = true; + + + public boolean isDefaultPassword() { + return this.defaultPassword; + } + + @Override + public Properties mergeProperties(Properties properties) { + properties.put(CRASH_AUTH_SIMPLE_USERNAME, this.username); + properties.put(CRASH_AUTH_SIMPLE_PASSWORD, this.password); + if (this.defaultPassword) { + logger.info("Using default password for shell access: " + this.password); + } + return properties; + } + + public void setPassword(String password) { + if (password.startsWith("${") && password.endsWith("}") || !StringUtils.hasLength(password)) { + return; + } + this.password = password; + this.defaultPassword = false; + } + + public void setUsername(String username) { + Assert.hasLength(username); + this.username = username; + } + + } + + + @ConfigurationProperties(name = "shell.auth.spring", ignoreUnknownFields = false) + public static class SpringAuthenticationProperties implements AuthenticationProperties { + + private String[] roles = new String[] { "ROLE_ADMIN" }; + + + @Override + public Properties mergeProperties(Properties properties) { + if (this.roles != null) { + properties.put(CRASH_AUTH_SPRING_ROLES, StringUtils.arrayToCommaDelimitedString(this.roles)); + } + return properties; + } + + public void setRoles(String[] roles) { + Assert.notNull(roles); + this.roles = roles; + } + + } + + + public static class Ssh implements PropertiesProvider { + + private boolean enabled = true; + + private String keyPath = null; + + private String port = "2000"; + + + public boolean isEnabled() { + return this.enabled; + } + + @Override + public Properties mergeProperties(Properties properties) { + if (this.enabled) { + properties.put(CRASH_SSH_PORT, this.port); + if (this.keyPath != null) { + properties.put(CRASH_SSH_KEYPATH, this.keyPath); + } + } + return properties; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setKeyPath(String keyPath) { + Assert.hasText(keyPath); + this.keyPath = keyPath; + } + + public void setPort(Integer port) { + Assert.notNull(port); + this.port = port.toString(); + } + + } + + + public static class Telnet implements PropertiesProvider { + + private boolean enabled = false; + + private String port = "5000"; + + + public boolean isEnabled() { + return this.enabled; + } + + @Override + public Properties mergeProperties(Properties properties) { + if (this.enabled) { + properties.put(CRASH_TELNET_PORT, this.port); + } + return properties; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setPort(Integer port) { + Assert.notNull(port); + this.port = port.toString(); + } + + } + +} diff --git a/spring-boot-actuator/src/main/resources/META-INF/spring.factories b/spring-boot-actuator/src/main/resources/META-INF/spring.factories index 55cabc4f408..89736607c7d 100644 --- a/spring-boot-actuator/src/main/resources/META-INF/spring.factories +++ b/spring-boot-actuator/src/main/resources/META-INF/spring.factories @@ -1,5 +1,6 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.CrshAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.ErrorMvcAutoConfiguration,\ diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CrshAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CrshAutoConfigurationTests.java new file mode 100644 index 00000000000..1843535e2f2 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CrshAutoConfigurationTests.java @@ -0,0 +1,333 @@ +/* + * Copyright 2013 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 + * + * 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.springframework.boot.actuate.autoconfigure; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.crsh.auth.AuthenticationPlugin; +import org.crsh.auth.JaasAuthenticationPlugin; +import org.crsh.lang.groovy.GroovyREPL; +import org.crsh.plugin.PluginContext; +import org.crsh.plugin.PluginLifeCycle; +import org.crsh.plugin.ResourceKind; +import org.crsh.processor.term.ProcessorIOHandler; +import org.crsh.vfs.Resource; +import org.junit.After; +import org.junit.Test; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.vote.RoleVoter; +import org.springframework.security.access.vote.UnanimousBased; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +/** + * Tests for {@link CrshAutoConfiguration}. + * + * @author Christian Dupuis + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class CrshAutoConfigurationTests { + + private AnnotationConfigWebApplicationContext context; + + @After + public void tearDown() { + if (this.context != null) { + this.context.close(); + this.context = null; + } + } + + @Test + public void testDisabledPlugins() throws Exception { + MockEnvironment env = new MockEnvironment(); + env.setProperty("shell.disabled_plugins", "GroovyREPL, termIOHandler, org.crsh.auth.AuthenticationPlugin"); + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setEnvironment(env); + this.context.register(CrshAutoConfiguration.class); + this.context.refresh(); + + PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class); + assertNotNull(lifeCycle); + + assertNull(lifeCycle.getContext().getPlugin(GroovyREPL.class)); + assertNull(lifeCycle.getContext().getPlugin(ProcessorIOHandler.class)); + assertNull(lifeCycle.getContext().getPlugin(JaasAuthenticationPlugin.class)); + } + + @Test + public void testAttributes() throws Exception { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.register(CrshAutoConfiguration.class); + this.context.refresh(); + + PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class); + + Map attributes = lifeCycle.getContext().getAttributes(); + assertTrue(attributes.containsKey("spring.version")); + assertTrue(attributes.containsKey("spring.beanfactory")); + assertEquals(this.context.getBeanFactory(), attributes.get("spring.beanfactory")); + } + + @Test + public void testSshConfiguration() { + MockEnvironment env = new MockEnvironment(); + env.setProperty("shell.ssh.enabled", "true"); + env.setProperty("shell.ssh.port", "3333"); + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setEnvironment(env); + this.context.register(CrshAutoConfiguration.class); + this.context.refresh(); + + PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class); + + assertEquals(lifeCycle.getConfig().getProperty("crash.ssh.port"), "3333"); + } + + @Test + public void testSshConfigurationWithKeyPath() { + MockEnvironment env = new MockEnvironment(); + env.setProperty("shell.ssh.enabled", "true"); + env.setProperty("shell.ssh.key_path", "~/.ssh/id.pem"); + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setEnvironment(env); + this.context.register(CrshAutoConfiguration.class); + this.context.refresh(); + + PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class); + + assertEquals(lifeCycle.getConfig().getProperty("crash.ssh.keypath"), "~/.ssh/id.pem"); + } + + @Test + public void testCommandResolution() { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.register(CrshAutoConfiguration.class); + this.context.refresh(); + + PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class); + + int count = 0; + Iterator resources = lifeCycle.getContext().loadResources("login", ResourceKind.LIFECYCLE).iterator(); + while (resources.hasNext()) { + count++; + resources.next(); + } + assertEquals(1, count); + + count = 0; + resources = lifeCycle.getContext().loadResources("help.java", ResourceKind.COMMAND).iterator(); + while (resources.hasNext()) { + count++; + resources.next(); + } + assertEquals(1, count); + } + + @Test + public void testAuthenticationProvidersAreInstalled() { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setServletContext(new MockServletContext()); + this.context.register(SecurityConfiguration.class); + this.context.register(CrshAutoConfiguration.class); + this.context.refresh(); + + PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class); + PluginContext pluginContext = lifeCycle.getContext(); + + int count = 0; + Iterator plugins = pluginContext.getPlugins(AuthenticationPlugin.class).iterator(); + while (plugins.hasNext()) { + count++; + plugins.next(); + } + assertEquals(3, count); + } + + @Test + public void testJaasAuthenticationProvider() { + MockEnvironment env = new MockEnvironment(); + env.setProperty("shell.auth", "jaas"); + env.setProperty("shell.auth.jaas.domain", "my-test-domain"); + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setEnvironment(env); + this.context.setServletContext(new MockServletContext()); + this.context.register(SecurityConfiguration.class); + this.context.register(CrshAutoConfiguration.class); + this.context.refresh(); + + PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class); + assertEquals(lifeCycle.getConfig().get("crash.auth"), "jaas"); + assertEquals(lifeCycle.getConfig().get("crash.auth.jaas.domain"), "my-test-domain"); + } + + @Test + public void testKeyAuthenticationProvider() { + MockEnvironment env = new MockEnvironment(); + env.setProperty("shell.auth", "key"); + env.setProperty("shell.auth.key.path", "~/test.pem"); + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setEnvironment(env); + this.context.setServletContext(new MockServletContext()); + this.context.register(SecurityConfiguration.class); + this.context.register(CrshAutoConfiguration.class); + this.context.refresh(); + + PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class); + assertEquals(lifeCycle.getConfig().get("crash.auth"), "key"); + assertEquals(lifeCycle.getConfig().get("crash.auth.key.path"), "~/test.pem"); + } + + @Test + public void testSimpleAuthenticationProvider() { + MockEnvironment env = new MockEnvironment(); + env.setProperty("shell.auth", "simple"); + env.setProperty("shell.auth.simple.username", "user"); + env.setProperty("shell.auth.simple.password", "password"); + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setEnvironment(env); + this.context.setServletContext(new MockServletContext()); + this.context.register(SecurityConfiguration.class); + this.context.register(CrshAutoConfiguration.class); + this.context.refresh(); + + PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class); + assertEquals(lifeCycle.getConfig().get("crash.auth"), "simple"); + + AuthenticationPlugin authenticationPlugin = null; + String authentication = lifeCycle.getConfig().getProperty("crash.auth"); + assertNotNull(authentication); + for (AuthenticationPlugin plugin : lifeCycle.getContext().getPlugins(AuthenticationPlugin.class)) { + if (authentication.equals(plugin.getName())) { + authenticationPlugin = plugin; + break; + } + } + assertNotNull(authenticationPlugin); + try { + assertTrue(authenticationPlugin.authenticate("user", "password")); + } + catch (Exception e) { + fail(); + } + + try { + assertFalse(authenticationPlugin.authenticate(UUID.randomUUID().toString(), + "password")); + } + catch (Exception e) { + fail(); + } + } + + @Test + public void testSpringAuthenticationProvider() { + MockEnvironment env = new MockEnvironment(); + env.setProperty("shell.auth", "spring"); + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setEnvironment(env); + this.context.setServletContext(new MockServletContext()); + this.context.register(SecurityConfiguration.class); + this.context.register(CrshAutoConfiguration.class); + this.context.refresh(); + + PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class); + + AuthenticationPlugin authenticationPlugin = null; + String authentication = lifeCycle.getConfig().getProperty("crash.auth"); + assertNotNull(authentication); + for (AuthenticationPlugin plugin : lifeCycle.getContext().getPlugins(AuthenticationPlugin.class)) { + if (authentication.equals(plugin.getName())) { + authenticationPlugin = plugin; + break; + } + } + assertNotNull(authenticationPlugin); + try { + assertTrue(authenticationPlugin.authenticate(SecurityConfiguration.USERNAME, + SecurityConfiguration.PASSWORD)); + } + catch (Exception e) { + fail(); + } + + try { + assertFalse(authenticationPlugin.authenticate(UUID.randomUUID().toString(), + SecurityConfiguration.PASSWORD)); + } + catch (Exception e) { + fail(); + } + } + + @Configuration + public static class SecurityConfiguration { + + public static final String USERNAME = UUID.randomUUID().toString(); + + public static final String PASSWORD = UUID.randomUUID().toString(); + + @Bean + public AuthenticationManager authenticationManager() { + return new AuthenticationManager() { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (authentication.getName().equals(USERNAME) && authentication.getCredentials().equals(PASSWORD)) { + authentication = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), + authentication.getCredentials(), Collections.singleton(new SimpleGrantedAuthority("ROLE_ADMIN"))); + } + else { + throw new BadCredentialsException("Invalid username and password"); + } + return authentication; + } + }; + } + + @Bean + public AccessDecisionManager accessDecisionManager() { + List voters = new ArrayList(); + voters.add(new RoleVoter()); + AccessDecisionManager result = new UnanimousBased(voters); + return result; + } + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/properties/CrshPropertiesTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/properties/CrshPropertiesTests.java new file mode 100644 index 00000000000..d00cabcf2f1 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/properties/CrshPropertiesTests.java @@ -0,0 +1,282 @@ +/* + * Copyright 2013 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 + * + * 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.springframework.boot.actuate.properties; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.boot.actuate.properties.CrshProperties.JaasAuthenticationProperties; +import org.springframework.boot.actuate.properties.CrshProperties.KeyAuthenticationProperties; +import org.springframework.boot.actuate.properties.CrshProperties.SimpleAuthenticationProperties; +import org.springframework.boot.actuate.properties.CrshProperties.SpringAuthenticationProperties; +import org.springframework.boot.bind.RelaxedDataBinder; +import org.springframework.core.convert.support.DefaultConversionService; + +/** + * Tests for {@link CrshProperties}. + * + * @author Christian Dupuis + */ +public class CrshPropertiesTests { + + @Test + public void testBindingAuth() { + CrshProperties props = new CrshProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell"); + binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.auth", "spring"))); + assertFalse(binder.getBindingResult().hasErrors()); + assertEquals("spring", props.getAuth()); + } + + @Test + public void testBindingAuthIfEmpty() { + CrshProperties props = new CrshProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell"); + binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.auth", ""))); + assertTrue(binder.getBindingResult().hasErrors()); + assertEquals("simple", props.getAuth()); + } + + @Test + public void testBindingCommandRefreshInterval() { + CrshProperties props = new CrshProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell"); + binder.setConversionService(new DefaultConversionService()); + binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.command_refresh_interval", "1"))); + assertFalse(binder.getBindingResult().hasErrors()); + assertEquals(1, props.getCommandRefreshInterval()); + } + + @Test + public void testBindingCommandPathPatterns() { + CrshProperties props = new CrshProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell"); + binder.setConversionService(new DefaultConversionService()); + binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.command_path_patterns", + "pattern1, pattern2"))); + assertFalse(binder.getBindingResult().hasErrors()); + assertEquals(2, props.getCommandPathPatterns().length); + Assert.assertArrayEquals(new String[] { "pattern1", "pattern2" }, props.getCommandPathPatterns()); + } + + @Test + public void testBindingConfigPathPatterns() { + CrshProperties props = new CrshProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell"); + binder.setConversionService(new DefaultConversionService()); + binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.config_path_patterns", + "pattern1, pattern2"))); + assertFalse(binder.getBindingResult().hasErrors()); + assertEquals(2, props.getConfigPathPatterns().length, 2); + Assert.assertArrayEquals(new String[] { "pattern1", "pattern2" }, props.getConfigPathPatterns()); + } + + @Test + public void testBindingDisabledPlugins() { + CrshProperties props = new CrshProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell"); + binder.setConversionService(new DefaultConversionService()); + binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.disabled_plugins", + "pattern1, pattern2"))); + assertFalse(binder.getBindingResult().hasErrors()); + assertEquals(2, props.getDisabledPlugins().length, 2); + assertArrayEquals(new String[] { "pattern1", "pattern2" }, props.getDisabledPlugins()); + } + + @Test + public void testBindingSsh() { + CrshProperties props = new CrshProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell"); + binder.setConversionService(new DefaultConversionService()); + Map map = new HashMap(); + map.put("shell.ssh.enabled", "true"); + map.put("shell.ssh.port", "2222"); + map.put("shell.ssh.key_path", "~/.ssh/test.pem"); + binder.bind(new MutablePropertyValues(map)); + assertFalse(binder.getBindingResult().hasErrors()); + + Properties p = new Properties(); + p = props.mergeProperties(p); + + assertEquals("2222", p.get(CrshProperties.CRASH_SSH_PORT)); + assertEquals("~/.ssh/test.pem", p.get(CrshProperties.CRASH_SSH_KEYPATH)); + } + + @Test + public void testBindingSshIgnored() { + CrshProperties props = new CrshProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell"); + binder.setConversionService(new DefaultConversionService()); + Map map = new HashMap(); + map.put("shell.ssh.enabled", "false"); + map.put("shell.ssh.port", "2222"); + map.put("shell.ssh.key_path", "~/.ssh/test.pem"); + binder.bind(new MutablePropertyValues(map)); + assertFalse(binder.getBindingResult().hasErrors()); + + Properties p = new Properties(); + p = props.mergeProperties(p); + + assertNull(p.get(CrshProperties.CRASH_SSH_PORT)); + assertNull(p.get(CrshProperties.CRASH_SSH_KEYPATH)); + } + + @Test + public void testBindingTelnet() { + CrshProperties props = new CrshProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell"); + binder.setConversionService(new DefaultConversionService()); + Map map = new HashMap(); + map.put("shell.telnet.enabled", "true"); + map.put("shell.telnet.port", "2222"); + binder.bind(new MutablePropertyValues(map)); + assertFalse(binder.getBindingResult().hasErrors()); + + Properties p = new Properties(); + p = props.mergeProperties(p); + + assertEquals("2222", p.get(CrshProperties.CRASH_TELNET_PORT)); + } + + @Test + public void testBindingTelnetIgnored() { + CrshProperties props = new CrshProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell"); + binder.setConversionService(new DefaultConversionService()); + Map map = new HashMap(); + map.put("shell.telnet.enabled", "false"); + map.put("shell.telnet.port", "2222"); + binder.bind(new MutablePropertyValues(map)); + assertFalse(binder.getBindingResult().hasErrors()); + + Properties p = new Properties(); + p = props.mergeProperties(p); + + assertNull(p.get(CrshProperties.CRASH_TELNET_PORT)); + } + + @Test + public void testBindingJaas() { + JaasAuthenticationProperties props = new JaasAuthenticationProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell.auth.jaas"); + binder.setConversionService(new DefaultConversionService()); + Map map = new HashMap(); + map.put("shell.auth.jaas.domain", "my-test-domain"); + binder.bind(new MutablePropertyValues(map)); + assertFalse(binder.getBindingResult().hasErrors()); + + Properties p = new Properties(); + p = props.mergeProperties(p); + + assertEquals("my-test-domain", p.get(CrshProperties.CRASH_AUTH_JAAS_DOMAIN)); + } + + @Test + public void testBindingKey() { + KeyAuthenticationProperties props = new KeyAuthenticationProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell.auth.key"); + binder.setConversionService(new DefaultConversionService()); + Map map = new HashMap(); + map.put("shell.auth.key.path", "~/.ssh/test.pem"); + binder.bind(new MutablePropertyValues(map)); + assertFalse(binder.getBindingResult().hasErrors()); + + Properties p = new Properties(); + p = props.mergeProperties(p); + + assertEquals("~/.ssh/test.pem", p.get(CrshProperties.CRASH_AUTH_KEY_PATH)); + } + + @Test + public void testBindingKeyIgnored() { + KeyAuthenticationProperties props = new KeyAuthenticationProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell.auth.key"); + binder.setConversionService(new DefaultConversionService()); + Map map = new HashMap(); + binder.bind(new MutablePropertyValues(map)); + assertFalse(binder.getBindingResult().hasErrors()); + + Properties p = new Properties(); + p = props.mergeProperties(p); + + assertNull(p.get(CrshProperties.CRASH_AUTH_KEY_PATH)); + } + + @Test + public void testBindingSimple() { + SimpleAuthenticationProperties props = new SimpleAuthenticationProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell.auth.simple"); + binder.setConversionService(new DefaultConversionService()); + Map map = new HashMap(); + map.put("shell.auth.simple.username", "username123"); + map.put("shell.auth.simple.password", "password123"); + binder.bind(new MutablePropertyValues(map)); + assertFalse(binder.getBindingResult().hasErrors()); + + Properties p = new Properties(); + p = props.mergeProperties(p); + + assertEquals("username123", p.get(CrshProperties.CRASH_AUTH_SIMPLE_USERNAME)); + assertEquals("password123", p.get(CrshProperties.CRASH_AUTH_SIMPLE_PASSWORD)); + } + + @Test + public void testDefaultPasswordAutogeneratedIfUnresolovedPlaceholder() { + SimpleAuthenticationProperties security = new SimpleAuthenticationProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(security, "security"); + binder.bind(new MutablePropertyValues(Collections.singletonMap( + "shell.auth.simple.password", "${ADMIN_PASSWORD}"))); + assertFalse(binder.getBindingResult().hasErrors()); + assertTrue(security.isDefaultPassword()); + } + + @Test + public void testDefaultPasswordAutogeneratedIfEmpty() { + SimpleAuthenticationProperties security = new SimpleAuthenticationProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(security, "security"); + binder.bind(new MutablePropertyValues(Collections.singletonMap( + "shell.auth.simple.password", ""))); + assertFalse(binder.getBindingResult().hasErrors()); + assertTrue(security.isDefaultPassword()); + } + + @Test + public void testBindingSpring() { + SpringAuthenticationProperties props = new SpringAuthenticationProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell.auth.spring"); + binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.auth.spring.roles", "role1, role2"))); + assertFalse(binder.getBindingResult().hasErrors()); + + Properties p = new Properties(); + p = props.mergeProperties(p); + + assertEquals("role1, role2", p.get(CrshProperties.CRASH_AUTH_SPRING_ROLES)); + } + + +} diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index e14a5f02db2..14b3f94395a 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -47,6 +47,7 @@ 2.0.1 1.1.3 7.0.42 + 1.3.0-beta8 @@ -481,6 +482,31 @@ geronimo-jms_1.1_spec 1.1 + + org.crashub + crash.cli + ${crashub.version} + + + org.crashub + crash.connectors.ssh + ${crashub.version} + + + org.crashub + crash.connectors.telnet + ${crashub.version} + + + org.crashub + crash.embed.spring + ${crashub.version} + + + org.crashub + crash.shell + ${crashub.version} + diff --git a/spring-boot-samples/spring-boot-sample-actuator/pom.xml b/spring-boot-samples/spring-boot-sample-actuator/pom.xml index dc284647db7..a52e16a8a26 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/pom.xml +++ b/spring-boot-samples/spring-boot-sample-actuator/pom.xml @@ -32,6 +32,10 @@ ${project.groupId} spring-boot-starter-security + + ${project.groupId} + spring-boot-starter-shell-crsh + diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties index 8691d54080d..a21f25c56b8 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/application.properties @@ -6,4 +6,12 @@ server.port: 8080 server.tomcat.basedir: target/tomcat server.tomcat.access_log_pattern: %h %t "%r" %s %b security.require_ssl: false -service.name: Phil \ No newline at end of file +service.name: Phil +shell.ssh.enabled: true +shell.ssh.port: 2222 +shell.telnet.enabled: false +#shell.telnet.port: 1111 +shell.auth: spring +#shell.auth: key +#shell.auth.key.path: ${user.home}/test/id_rsa.pub.pem +#shell.auth: simple \ No newline at end of file diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml index a72623787b1..23d801f9d72 100644 --- a/spring-boot-starters/pom.xml +++ b/spring-boot-starters/pom.xml @@ -26,6 +26,7 @@ spring-boot-starter-actuator spring-boot-starter-parent spring-boot-starter-security + spring-boot-starter-shell-crsh spring-boot-starter-test spring-boot-starter-tomcat spring-boot-starter-web diff --git a/spring-boot-starters/spring-boot-starter-parent/pom.xml b/spring-boot-starters/spring-boot-starter-parent/pom.xml index e579f380e99..e4b52badc9d 100644 --- a/spring-boot-starters/spring-boot-starter-parent/pom.xml +++ b/spring-boot-starters/spring-boot-starter-parent/pom.xml @@ -98,6 +98,11 @@ spring-boot-starter-security 0.5.0.BUILD-SNAPSHOT + + org.springframework.boot + spring-boot-starter-shell-crsh + 0.5.0.BUILD-SNAPSHOT + org.springframework.boot spring-boot-starter-websocket diff --git a/spring-boot-starters/spring-boot-starter-shell-crsh/pom.xml b/spring-boot-starters/spring-boot-starter-shell-crsh/pom.xml new file mode 100644 index 00000000000..2d159c569b2 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-shell-crsh/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starters + 0.5.0.BUILD-SNAPSHOT + + spring-boot-starter-shell-crsh + jar + + ${basedir}/../.. + + + + ${project.groupId} + spring-boot-starter + ${project.version} + + + org.crashub + crash.cli + + + org.crashub + crash.connectors.ssh + + + org.crashub + crash.connectors.telnet + + + javax.servlet + servlet-api + + + log4j + log4j + + + commons-logging + commons-logging + + + true + + + org.crashub + crash.embed.spring + + + org.crashub + crash.shell + + + org.codehaus.groovy + groovy-all + + + + + org.codehaus.groovy + groovy + + + diff --git a/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/help.groovy b/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/help.groovy new file mode 100644 index 00000000000..4ea73636cac --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/help.groovy @@ -0,0 +1,58 @@ +package crash.commands.base; + +import org.crsh.cli.Command; +import org.crsh.cli.Usage; +import org.crsh.command.BaseCommand; +import org.crsh.command.DescriptionFormat; +import org.crsh.command.InvocationContext; +import org.crsh.command.ShellCommand; +import org.crsh.shell.impl.command.CRaSH; +import org.crsh.text.Color; +import org.crsh.text.Decoration; +import org.crsh.text.Style; +import org.crsh.text.ui.LabelElement; +import org.crsh.text.ui.RowElement; +import org.crsh.text.ui.TableElement; + +import java.io.IOException; + +/** @author Julien Viet */ +public class help extends BaseCommand { + + @Usage("provides basic help") + @Command + public void main(InvocationContext context) throws IOException { + + // + TableElement table = new TableElement().rightCellPadding(1); + table.add( + new RowElement(). + add(new LabelElement("NAME").style(Style.style(Decoration.bold))). + add(new LabelElement("DESCRIPTION"))); + + // + CRaSH crash = (CRaSH)context.getSession().get("crash"); + Iterable names = crash.getCommandNames(); + for (String name : names) { + try { + ShellCommand cmd = crash.getCommand(name); + if (cmd != null) { + String desc = cmd.describe(name, DescriptionFormat.DESCRIBE); + if (desc == null) { + desc = ""; + } + table.add( + new RowElement(). + add(new LabelElement(name).style(Style.style(Color.red))). + add(new LabelElement(desc))); + } + } catch (Exception ignore) { + // + } + } + + // + context.provide(new LabelElement("Try one of these commands with the -h or --help switch:\n")); + context.provide(table); + } +} diff --git a/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/jmx.groovy b/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/jmx.groovy new file mode 100644 index 00000000000..64bded8bc71 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/jmx.groovy @@ -0,0 +1,123 @@ +package crash.commands.base; + +import org.crsh.cli.Argument; +import org.crsh.cli.Command; +import org.crsh.cli.Option; +import org.crsh.cli.Usage; +import org.crsh.command.BaseCommand; +import org.crsh.command.InvocationContext; +import org.crsh.command.PipeCommand; +import org.crsh.command.ScriptException; + +import javax.management.JMException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanServer; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** @author Julien Viet */ +@Usage("Java Management Extensions") +public class jmx extends BaseCommand { + + @Usage("find mbeans") + @Command + public void find( + InvocationContext context, + @Usage("The object name pattern") + @Option(names = {"p", "pattern"}) + String pattern) throws Exception { + + // + ObjectName patternName = pattern != null ? ObjectName.getInstance(pattern) : null; + MBeanServer server = ManagementFactory.getPlatformMBeanServer(); + Set instances = server.queryMBeans(patternName, null); + for (ObjectInstance instance : instances) { + context.provide(instance.getObjectName()); + } +/* + if (context.piped) { + } else { + UIBuilder ui = new UIBuilder() + ui.table(columns: [1,3]) { + row(bold: true, fg: black, bg: white) { + label("CLASS NAME"); label("OBJECT NAME") + } + instances.each { instance -> + row() { + label(foreground: red, instance.getClassName()); label(instance.objectName) + } + } + } + out << ui; + } +*/ + } + + @Command + @Usage("return the attributes info of an MBean") + public void attributes(InvocationContext context, @Argument ObjectName name) throws IOException { + MBeanServer server = ManagementFactory.getPlatformMBeanServer(); + try { + MBeanInfo info = server.getMBeanInfo(name); + for (MBeanAttributeInfo attributeInfo : info.getAttributes()) { + HashMap tuple = new HashMap(); + tuple.put("name", attributeInfo.getName()); + tuple.put("type", attributeInfo.getType()); + tuple.put("description", attributeInfo.getDescription()); + context.provide(tuple); + } + } + catch (JMException e) { + throw new ScriptException("Could not access MBean meta data", e); + } + } + + @Usage("get attributes of an MBean") + @Command + public PipeCommand get(@Argument final List attributes) { + + // Determine common attributes from all names + if (attributes == null || attributes.isEmpty()) { + throw new ScriptException("Must provide JMX attributes"); + } + + // + return new PipeCommand() { + + /** . */ + private MBeanServer server; + + @Override + public void open() throws ScriptException { + server = ManagementFactory.getPlatformMBeanServer(); + } + + @Override + public void provide(ObjectName name) throws IOException { + try { + HashMap tuple = new HashMap(); + for (String attribute : attributes) { + String prop = name.getKeyProperty(attribute); + if (prop != null) { + tuple.put(attribute, prop); + } + else { + tuple.put(attribute, server.getAttribute(name, attribute)); + } + } + context.provide(tuple); + } + catch (JMException ignore) { + // + } + } + }; + } +} diff --git a/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/login.groovy b/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/login.groovy new file mode 100644 index 00000000000..21d4dc5b65a --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/login.groovy @@ -0,0 +1,22 @@ +welcome = { -> + def hostName; + try { + hostName = java.net.InetAddress.getLocalHost().getHostName(); + } catch (java.net.UnknownHostException ignore) { + hostName = "localhost"; + } + def version = crash.context.attributes.get("spring.boot.version") + return """\ + . ____ _ __ _ _ + /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\ +( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\ + \\\\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v$version) on $hostName +"""; +} + +prompt = { -> + return "> "; +} diff --git a/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/metrics.groovy b/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/metrics.groovy new file mode 100644 index 00000000000..3fffed92f3b --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-shell-crsh/src/main/resources/commands/crash/metrics.groovy @@ -0,0 +1,49 @@ +package commands + +import org.crsh.text.ui.UIBuilder +import org.springframework.boot.actuate.endpoint.MetricsEndpoint + +class metrics { + + @Usage("Display metrics provided by Spring Boot") + @Command + public void main(InvocationContext context) { + + context.takeAlternateBuffer(); + try { + while (!Thread.interrupted()) { + out.cls() + out.show(new UIBuilder().table(columns:[1]) { + header { + table(columns:[1], separator: dashed) { + header(bold: true, fg: black, bg: white) { label("metrics"); } + } + } + row { + table(columns:[1, 1]) { + header(bold: true, fg: black, bg: white) { + label("NAME") + label("VALUE") + } + + context.attributes['spring.beanfactory'].getBeansOfType(MetricsEndpoint.class).each { name, metrics -> + metrics.invoke().each { k, v -> + row { + label(k) + label(v) + } + } + } + } + } + } + ); + out.flush(); + Thread.sleep(1000); + } + } + finally { + context.releaseAlternateBuffer(); + } + } +} \ No newline at end of file