diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000000000000000000000000000000000000..f202d3031e2c69a6aa27c2cda1ad9fe29c690889 --- /dev/null +++ b/lombok.config @@ -0,0 +1,4 @@ +# tells Lombok that this is the root directory and that it shouldn’t search parent directories for more configuration files +config.stopBubbling = true +# tells Lombok to add @lombok.Generated annotation to all generated methods +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9baf244d9b0038a664e3a2e2daeba27b7d4b3eca..5652f0fb061e960d85468c929295018d46978d7f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,10 +3,10 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> - <groupId>org.geant</groupId> - <artifactId>looking-glass-service</artifactId> - <version>2.0.14-SNAPSHOT</version> - <packaging>jar</packaging> + <groupId>org.geant</groupId> + <artifactId>looking-glass-service</artifactId> + <version>2.0.14-SNAPSHOT</version> + <packaging>jar</packaging> <name>looking-glass-service</name> <description>Looking Glass Demo</description> @@ -14,7 +14,7 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>2.0.5.RELEASE</version> + <version>2.2.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> @@ -55,6 +55,11 @@ <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> @@ -69,16 +74,6 @@ <artifactId>spring-security-saml2-core</artifactId> <version>1.0.3.RELEASE</version> </dependency> - <dependency> - <groupId>io.springfox</groupId> - <artifactId>springfox-swagger2</artifactId> - <version>2.7.0</version> - </dependency> - <dependency> - <groupId>io.springfox</groupId> - <artifactId>springfox-swagger-ui</artifactId> - <version>2.7.0</version> - </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> @@ -87,6 +82,50 @@ <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-statsd</artifactId> </dependency> + <dependency> + <groupId>com.squareup.retrofit2</groupId> + <artifactId>retrofit</artifactId> + <version>2.3.0</version> + </dependency> + <dependency> + <groupId>com.squareup.retrofit2</groupId> + <artifactId>converter-gson</artifactId> + <version>2.3.0</version> + </dependency> + <dependency> + <groupId>com.squareup.retrofit2</groupId> + <artifactId>converter-jackson</artifactId> + <version>2.8.0</version> + </dependency> + <dependency> + <groupId>com.tngtech.jgiven</groupId> + <artifactId>jgiven-junit</artifactId> + <version>0.18.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.tngtech.jgiven</groupId> + <artifactId>jgiven-spring</artifactId> + <version>0.18.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <version>3.14.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.alexlovett</groupId> + <artifactId>jgiven-extension</artifactId> + <version>0.0.1-SNAPSHOT</version> + </dependency> + <dependency> + <groupId>com.github.tomakehurst</groupId> + <artifactId>wiremock-jre8</artifactId> + <version>2.26.3</version> + <scope>test</scope> + </dependency> </dependencies> <build> @@ -106,7 +145,9 @@ <line>After=syslog.target</line> <line>[Service]</line> <line>User=${artifactId}</line> - <line>ExecStart=/opt/${artifactId}/latest/bin/${artifactId}.${project.packaging} --spring.config.location=file:/opt/${artifactId}/${version}/config/application.yml</line> + <line>ExecStart=/opt/${artifactId}/latest/bin/${artifactId}.${project.packaging} + --spring.config.location=file:/opt/${artifactId}/${version}/config/application.yml + </line> <line>StandardOutput=syslog</line> <line>StandardError=syslog</line> <line>SuccessExitStatus=143</line> @@ -156,13 +197,15 @@ <version>0.8.5</version> <executions> <execution> + <id>pre-integration-test</id> + <phase>test-compile</phase> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> - <phase>prepare-package</phase> + <phase>post-integration-test</phase> <goals> <goal>report</goal> </goals> @@ -245,8 +288,11 @@ <ruleset>default</ruleset> </entry> <entry> - <name>/opt/${artifactId}/${version}/bin/${project.build.finalName}.${project.packaging}</name> - <file>${project.build.directory}/${project.build.finalName}.${project.packaging}</file> + <name> + /opt/${artifactId}/${version}/bin/${project.build.finalName}.${project.packaging} + </name> + <file>${project.build.directory}/${project.build.finalName}.${project.packaging} + </file> <ruleset>default</ruleset> </entry> <entry> @@ -256,7 +302,9 @@ </entry> <entry> <name>/opt/${artifactId}/${version}/bin/${artifactId}.${project.packaging}</name> - <linkTo>/opt/${artifactId}/${version}/bin/${project.build.finalName}.${project.packaging}</linkTo> + <linkTo> + /opt/${artifactId}/${version}/bin/${project.build.finalName}.${project.packaging} + </linkTo> <ruleset>default</ruleset> </entry> <entry> @@ -269,6 +317,34 @@ </execution> </executions> </plugin> + <plugin> + <artifactId>maven-failsafe-plugin</artifactId> + <version>2.18.1</version> + <executions> + <execution> + <goals> + <goal>integration-test</goal> + <goal>verify</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>com.tngtech.jgiven</groupId> + <artifactId>jgiven-maven-plugin</artifactId> + <version>0.18.2</version> + <executions> + <execution> + <phase>post-integration-test</phase> + <goals> + <goal>report</goal> + </goals> + </execution> + </executions> + <configuration> + <format>html</format> + </configuration> + </plugin> </plugins> </build> diff --git a/saml/keystore.jks b/saml/keystore.jks deleted file mode 100644 index 402e7ab6eaa52384af67825a7d9bf9fd26183237..0000000000000000000000000000000000000000 Binary files a/saml/keystore.jks and /dev/null differ diff --git a/src/main/java/org/geant/lgservice/LookingGlassServiceApplication.java b/src/main/java/org/geant/lgservice/LookingGlassServiceApplication.java index 2d37ef6559f361750cfab987561033de78b46497..0e3e273e48f9f2339af39553b651762cd9cebcf5 100644 --- a/src/main/java/org/geant/lgservice/LookingGlassServiceApplication.java +++ b/src/main/java/org/geant/lgservice/LookingGlassServiceApplication.java @@ -1,32 +1,19 @@ package org.geant.lgservice; +import org.geant.lgservice.infrastructure.rest.inventoryprovider.Config; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; -import springfox.documentation.builders.ApiInfoBuilder; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; - -@EnableSwagger2 @SpringBootApplication +@Import({ + org.geant.lgservice.infrastructure.rest.inventoryprovider.Config.class, + org.geant.lgservice.infrastructure.ssh.Config.class +}) public class LookingGlassServiceApplication { public static void main(String[] args) { SpringApplication.run(LookingGlassServiceApplication.class, args); } - @Bean - public Docket productApi() { - return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select() - .apis(RequestHandlerSelectors.basePackage("org.geant.lgservice")).build(); - } - - private ApiInfo apiInfo() { - return new ApiInfoBuilder().title("Looking Glass Rest APIs") - .description("This page lists all the rest apis for Looking Glass Application.").build(); - } } diff --git a/src/main/java/org/geant/lgservice/config/CommandsParser.java b/src/main/java/org/geant/lgservice/config/CommandsParser.java index 47d92ceab8fe70307bdb00b7bc47e6a60613ed47..f3051fa7b626ae16504d368d797dde0c84d80012 100644 --- a/src/main/java/org/geant/lgservice/config/CommandsParser.java +++ b/src/main/java/org/geant/lgservice/config/CommandsParser.java @@ -1,17 +1,22 @@ package org.geant.lgservice.config; -import java.io.InputStream; -import java.util.List; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Unmarshaller; - +import com.google.common.collect.ImmutableMap; +import lombok.Builder; +import lombok.Getter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.geant.lgservice.pojos.Group; import org.geant.lgservice.security.LGUser; -import org.geant.lgservice.utils.LGRole; +import org.springframework.security.core.GrantedAuthority; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import java.io.InputStream; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; public class CommandsParser { @@ -38,25 +43,35 @@ public class CommandsParser { return commands.getGroups(); } + @Builder + @Getter + private static class FileName { + private final String accessLevel; + private final int rank; + } + + private static final Map<Predicate<Collection<GrantedAuthority>>, FileName> FILE_NAME_PREDICATES = ImmutableMap + .<Predicate<Collection<GrantedAuthority>>, FileName>builder() + .put( + grantedAuthorities -> grantedAuthorities.contains(LGUser.Group.INTERNAL), + FileName.builder().accessLevel("internal").rank(0).build()) + .put( + grantedAuthorities -> grantedAuthorities.contains(LGUser.Group.EXTERNAL), + FileName.builder().accessLevel("external").rank(1).build()) + .build(); + public String resolveFileName(final String fileName, final LGUser user) { - String newFileName = fileName + "_" + LGRole.PUBLIC.getName(); + String accessLevel = "public"; if (user != null) { - switch (user.getRole()) { - case PUBLIC: { - newFileName = fileName + "_" + LGRole.PUBLIC.getName(); - break; - } - case INTERNAL: { - newFileName = fileName + "_" + LGRole.INTERNAL.getName(); - break; - } - case EXTERNAL: { - newFileName = fileName + "_" + LGRole.EXTERNAL.getName(); - break; - } - } + accessLevel = FILE_NAME_PREDICATES.entrySet() + .stream() + .filter(entry -> entry.getKey().test(user.getAuthorities())) + .map(Map.Entry::getValue) + .reduce((fileName1, fileName2) -> fileName1.getRank() < fileName2.getRank() ? fileName1 : fileName2) + .map(FileName::getAccessLevel) + .orElse("public"); } - newFileName = newFileName + ".xml"; - return newFileName; + + return String.format("%s_%s.xml", fileName, accessLevel); } } diff --git a/src/main/java/org/geant/lgservice/domain/Router.java b/src/main/java/org/geant/lgservice/domain/Router.java new file mode 100644 index 0000000000000000000000000000000000000000..5421521b49b14cfa705c7821d7a86f2dd6ea4dfc --- /dev/null +++ b/src/main/java/org/geant/lgservice/domain/Router.java @@ -0,0 +1,18 @@ +package org.geant.lgservice.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class Router { + private final String hostName; + private final Access access; + + public enum Access { + INTERNAL, + PUBLIC + } +} diff --git a/src/main/java/org/geant/lgservice/domain/RouterRepository.java b/src/main/java/org/geant/lgservice/domain/RouterRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..8177f0a98076a6d2e4b3d4d0ddccc4064e4ddb3f --- /dev/null +++ b/src/main/java/org/geant/lgservice/domain/RouterRepository.java @@ -0,0 +1,7 @@ +package org.geant.lgservice.domain; + +import java.util.Optional; + +public interface RouterRepository { + Optional<Router> getByHostName(String hostname); +} diff --git a/src/main/java/org/geant/lgservice/ecmr/CallableCommandExecutor.java b/src/main/java/org/geant/lgservice/ecmr/CallableCommandExecutor.java deleted file mode 100644 index afb29f3678965113917353734b39ee963b3b4929..0000000000000000000000000000000000000000 --- a/src/main/java/org/geant/lgservice/ecmr/CallableCommandExecutor.java +++ /dev/null @@ -1,122 +0,0 @@ -package org.geant.lgservice.ecmr; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.concurrent.Callable; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.apache.commons.lang.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.geant.lgservice.pojos.Command; -import org.geant.lgservice.pojos.CommandOutput; -import org.geant.lgservice.pojos.Credentials; -import org.geant.lgservice.pojos.Router; - -import ch.ethz.ssh2.Connection; -import ch.ethz.ssh2.Session; - -public class CallableCommandExecutor implements Callable<CommandOutput> { - - private static final Logger logger = LogManager.getLogger(CallableCommandExecutor.class); - - private Router router; - - private Command command; - - private String arguments; - - private boolean displayAsXML; - - private Credentials credentials; - - CallableCommandExecutor(final Router router, final Command command, final String arguments, - final boolean displayAsXML, final Credentials credentials) { - this.router = router; - this.command = command; - this.arguments = arguments; - this.displayAsXML = displayAsXML; - this.credentials = credentials; - } - - @Override - public CommandOutput call() { - long start = System.currentTimeMillis(); - StringBuilder output = new StringBuilder(); - CommandOutput finalOutput = new CommandOutput(); - - Connection conn = new Connection(router.getName()); - try { - try { - conn.connect(null, 30000, 30000); // Should be configurable but this code is due a rewrite / pulled out - } catch (IOException e) { - logger.error("Error while connecting to router " + router.getName(), e); - return new CommandOutput(router.getName(), "Error while connecting to router " + router.getName(), null); - } - boolean isAuthenticated; - try { - isAuthenticated = conn.authenticateWithPublicKey(credentials.getUsername(), - new File(credentials.getKeyFile()), credentials.getPassword()); - } catch (IOException e) { - logger.error("Error while authenticating the user for router " + router.getName(), e); - return new CommandOutput(router.getName(), - "Error while authenticating the user for router " + router.getName(), null); - } - - if (isAuthenticated) { - Session session; - try { - session = conn.openSession(); - } catch (IOException e) { - logger.error("Error while opening session to router " + router.getName(), e); - return new CommandOutput(router.getName(), "Error while opening session to router " + router.getName(), - null); - } - InputStream in = session.getStdout(); - BufferedReader br = new BufferedReader(new InputStreamReader(in)); - - try { - session.execCommand(getCommandWithArguments()); - } catch (IOException e) { - logger.error("Error while executing command " + command.getValue(), e); - return new CommandOutput(router.getName(), "Error while executing command " + command.getValue(), null); - } - - while (true) { - String line = ""; - try { - line = br.readLine(); - if (line == null) - break; - output.append(line + "\n"); - } catch (IOException e) { - return new CommandOutput(router.getName(), "Error while reading line", null); - } - } - long finish = System.currentTimeMillis(); - finalOutput.setRouterName(router.getName()); - finalOutput.setCommandResult(output.toString()); - finalOutput.setExecutionTime(finish - start); - - return finalOutput; - } - return new CommandOutput(router.getName(), "Unable to authenticate user for router " + router.getName(), null); - } finally { - conn.close(); - } - } - - public String getCommandWithArguments() { - return Stream.of( - this.command.getValue(), - this.arguments, - this.displayAsXML ? "| display xml" : "") - .filter(StringUtils::isNotBlank) - .collect(Collectors.joining(" ")); - } - -} diff --git a/src/main/java/org/geant/lgservice/ecmr/CommandExecutor.java b/src/main/java/org/geant/lgservice/ecmr/CommandExecutor.java deleted file mode 100644 index 551969153187b00044026905fbd4b717302e3668..0000000000000000000000000000000000000000 --- a/src/main/java/org/geant/lgservice/ecmr/CommandExecutor.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.geant.lgservice.ecmr; - -import org.geant.lgservice.config.AppConfig; -import org.geant.lgservice.pojos.Command; -import org.geant.lgservice.pojos.CommandOutput; -import org.geant.lgservice.pojos.Credentials; -import org.geant.lgservice.pojos.Router; -import org.springframework.stereotype.Component; - -@Component -public class CommandExecutor { - - private final Credentials credentials; - - public CommandExecutor(AppConfig config) { - credentials = Credentials.builder() - .username(config.getUsername()) - .password(config.getPassword()) - .keyFile(config.getKeyFile()) - .build(); - } - - public CommandOutput execute(Router router, Command command, String arguments, boolean asXml) { - return new CallableCommandExecutor(router, command, arguments, asXml, credentials).call(); - } -} diff --git a/src/main/java/org/geant/lgservice/exceptions/BusinessException.java b/src/main/java/org/geant/lgservice/exceptions/BusinessException.java new file mode 100644 index 0000000000000000000000000000000000000000..68926f80859f45b5670daff8afb1f0e0b4729897 --- /dev/null +++ b/src/main/java/org/geant/lgservice/exceptions/BusinessException.java @@ -0,0 +1,33 @@ +package org.geant.lgservice.exceptions; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import static lombok.AccessLevel.PRIVATE; + +@RequiredArgsConstructor(access = PRIVATE) +public class BusinessException extends RuntimeException { + + @Getter + private final Reason reason; + + @AllArgsConstructor + public enum Reason { + UNAUTHORIZED("User is not logged in"), + FORBIDDEN("User does not have access rights"), + NOT_FOUND("Could not find router"); + + @Getter + private final String description; + } + + public static BusinessException withReason(Reason reason) { + return new BusinessException(reason); + } + + @Override + public String getMessage() { + return reason.getDescription(); + } +} diff --git a/src/main/java/org/geant/lgservice/exceptions/TechnicalException.java b/src/main/java/org/geant/lgservice/exceptions/TechnicalException.java new file mode 100644 index 0000000000000000000000000000000000000000..e18b63ae5faa0b9fbbfd2422f72fb3d68042ec93 --- /dev/null +++ b/src/main/java/org/geant/lgservice/exceptions/TechnicalException.java @@ -0,0 +1,11 @@ +package org.geant.lgservice.exceptions; + +public class TechnicalException extends RuntimeException { + public TechnicalException(Throwable t) { + super(t); + } + + public TechnicalException(String message) { + super(message); + } +} diff --git a/src/main/java/org/geant/lgservice/infrastructure/rest/inventoryprovider/Config.java b/src/main/java/org/geant/lgservice/infrastructure/rest/inventoryprovider/Config.java new file mode 100644 index 0000000000000000000000000000000000000000..46b88e30f924457baece8e105b0b605135286c7f --- /dev/null +++ b/src/main/java/org/geant/lgservice/infrastructure/rest/inventoryprovider/Config.java @@ -0,0 +1,16 @@ +package org.geant.lgservice.infrastructure.rest.inventoryprovider; + +import org.geant.lgservice.domain.RouterRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class Config { + + @Bean + public RouterRepository routerRepository(@Value("${inventoryprovider.baseUrl}")String baseUrl) { + return InventoryProviderRouterRepository.builder().baseUrl(baseUrl).build(); + } + +} diff --git a/src/main/java/org/geant/lgservice/infrastructure/rest/inventoryprovider/InventoryProviderRouterRepository.java b/src/main/java/org/geant/lgservice/infrastructure/rest/inventoryprovider/InventoryProviderRouterRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..7eabb49d91d965defbdb4268224a70fa74e37ba0 --- /dev/null +++ b/src/main/java/org/geant/lgservice/infrastructure/rest/inventoryprovider/InventoryProviderRouterRepository.java @@ -0,0 +1,107 @@ +package org.geant.lgservice.infrastructure.rest.inventoryprovider; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import org.geant.lgservice.domain.Router; +import org.geant.lgservice.domain.RouterRepository; +import org.geant.lgservice.exceptions.TechnicalException; +import org.springframework.http.HttpStatus; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.HttpStatusCodeException; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; +import retrofit2.http.GET; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +import static java.nio.charset.Charset.defaultCharset; +import static org.geant.lgservice.domain.Router.Access.INTERNAL; +import static org.geant.lgservice.domain.Router.Access.PUBLIC; + +@Slf4j +public class InventoryProviderRouterRepository implements RouterRepository { + + private final InventoryProvider inventoryProvider; + + @Override + public Optional<Router> getByHostName(String hostname) { + try { + Response<Collection<InventoryProvider.InventoryProviderRouter>> response = inventoryProvider.getRouters() + .execute(); + if (!response.isSuccessful()) { + HttpStatus status = HttpStatus.valueOf(response.code()); + HttpStatusCodeException rootException = status.is4xxClientError() ? + new HttpClientErrorException(status, response.message(), response.errorBody() + .bytes(), defaultCharset()) : + new HttpServerErrorException(status, response.message(), response.errorBody() + .bytes(), defaultCharset()); + log.error(response.message(), rootException); + throw new TechnicalException(rootException); + } + + return response.body() + .stream() + .filter(router -> router.getEquipmentName().equals(hostname)) + .map(InventoryProvider.InventoryProviderRouter::toDomain) + .findFirst(); + } catch (IOException e) { + throw new TechnicalException(e); + } + } + + @Builder + public InventoryProviderRouterRepository(String baseUrl, OkHttpClient client) { + this.inventoryProvider = new Retrofit.Builder() + .baseUrl(baseUrl) + .client(client != null ? client : new OkHttpClient()) + .addConverterFactory(JacksonConverterFactory.create()) + .build() + .create(InventoryProvider.class); + } + + interface InventoryProvider { + @GET("routers/all") + Call<Collection<InventoryProviderRouter>> getRouters(); + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + class InventoryProviderRouter { + private final String equipmentName; + private final String type; + + @JsonCreator + public InventoryProviderRouter( + @JsonProperty("equipment name") String equipmentName, + @JsonProperty("type") String type + ) { + this.equipmentName = equipmentName; + this.type = type; + } + + private static final Map<String, Router.Access> ACCESS_MAPPINGS = + ImmutableMap.<String, Router.Access>builder() + .put("INTERNAL", INTERNAL) + .build(); + + public Router toDomain() { + return Router.builder() + .hostName(equipmentName) + .access(ACCESS_MAPPINGS.getOrDefault(type, PUBLIC)) + .build(); + } + } + + } +} diff --git a/src/main/java/org/geant/lgservice/infrastructure/ssh/Config.java b/src/main/java/org/geant/lgservice/infrastructure/ssh/Config.java new file mode 100644 index 0000000000000000000000000000000000000000..de7917939699e080eb5804697bf7296ce4318545 --- /dev/null +++ b/src/main/java/org/geant/lgservice/infrastructure/ssh/Config.java @@ -0,0 +1,22 @@ +package org.geant.lgservice.infrastructure.ssh; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.File; + +@Configuration +public class Config { + + @Bean + public ConnectionFactory connectionFactory( + @Value("${configuration.publickey.username}") String username, + @Value("${configuration.publickey.keyfile}") File keyFile, + @Value("${configuration.publickey.password}") String password, + @Value("${configuration.ssh.connection.timeout:30000}") int timeout + ) { + return new ConnectionFactory(username, password, keyFile, timeout); + } + +} diff --git a/src/main/java/org/geant/lgservice/infrastructure/ssh/ConnectionFactory.java b/src/main/java/org/geant/lgservice/infrastructure/ssh/ConnectionFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..46fa347a6f95c2d371f47333c158cf0b38a3e1ac --- /dev/null +++ b/src/main/java/org/geant/lgservice/infrastructure/ssh/ConnectionFactory.java @@ -0,0 +1,36 @@ +package org.geant.lgservice.infrastructure.ssh; + +import ch.ethz.ssh2.Connection; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.geant.lgservice.exceptions.TechnicalException; + +import java.io.File; +import java.io.IOException; + +import static java.lang.String.format; + +@RequiredArgsConstructor +@Slf4j +public class ConnectionFactory { + + private final String username; + private final String password; + private final File keyFile; + private final int timeout; + + public Connection createConnection(String hostname) { + log.info("Creating connection for {}", hostname); + Connection connection = new Connection(hostname); + try { + connection.connect(null, timeout, timeout); + if (!connection.authenticateWithPublicKey(username, keyFile, password)){ + throw new TechnicalException(format("Could not authenticate ssh connection with %s", hostname)); + } + } catch (IOException e) { + throw new TechnicalException(e); + } + log.info("Connection for {} created"); + return connection; + } +} diff --git a/src/main/java/org/geant/lgservice/infrastructure/ssh/ConnectionManager.java b/src/main/java/org/geant/lgservice/infrastructure/ssh/ConnectionManager.java new file mode 100644 index 0000000000000000000000000000000000000000..0f77845bb5a6a7ef01b37b5e2f26adb3f53d54f7 --- /dev/null +++ b/src/main/java/org/geant/lgservice/infrastructure/ssh/ConnectionManager.java @@ -0,0 +1,43 @@ +package org.geant.lgservice.infrastructure.ssh; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.geant.lgservice.infrastructure.ssh.SSHHost.ConnectionStateListener.State.DISCONNECTED; + +@Slf4j +@Component +@AllArgsConstructor +public class ConnectionManager implements SSHHost.ConnectionStateListener { + + private final ConnectionFactory connectionFactory; + + private final Map<String, SSHHost> hosts = new ConcurrentHashMap<>(); + + public SSHHost getHost(String hostname) { + log.info("retrieving host {}", hostname); + synchronized (this) { + return hosts + .computeIfAbsent( + hostname, + host -> { + log.info("Host [{}] connection does not exist", hostname); + return SSHHost.fromConnection(connectionFactory.createConnection(host)) + .withConnectionStateListener(this); + }); + } + } + + @Override + public void stateChanged(SSHHost host, State state) { + synchronized (this) { + if (state.equals(DISCONNECTED)) { + hosts.remove(host); + } + } + } +} diff --git a/src/main/java/org/geant/lgservice/infrastructure/ssh/SSHCommandExecutor.java b/src/main/java/org/geant/lgservice/infrastructure/ssh/SSHCommandExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..35f532db31c73edc1ef157da219d7616114f8095 --- /dev/null +++ b/src/main/java/org/geant/lgservice/infrastructure/ssh/SSHCommandExecutor.java @@ -0,0 +1,31 @@ +package org.geant.lgservice.infrastructure.ssh; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.geant.lgservice.pojos.CommandOutput; +import org.springframework.stereotype.Component; +import org.springframework.util.StopWatch; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SSHCommandExecutor { + + private final ConnectionManager connectionManager; + + public CommandOutput execute(String hostname, String command) { + log.info("Executing command [{}] on host [{}]", command, hostname); + StopWatch timer = new StopWatch(); + timer.start(); + String response = connectionManager.getHost(hostname) + .execute(command); + timer.stop(); + log.info("Command completed on host [{}] in [{}]ms with result [{}]", hostname, timer.getTotalTimeMillis(), response); + return CommandOutput.builder() + .routerName(hostname) + .commandResult(response) + .executionTime(timer.getTotalTimeMillis()) + .build(); + } + +} diff --git a/src/main/java/org/geant/lgservice/infrastructure/ssh/SSHHost.java b/src/main/java/org/geant/lgservice/infrastructure/ssh/SSHHost.java new file mode 100644 index 0000000000000000000000000000000000000000..8eba9473978cc53240b8b5a0b2ff08c198a9c688 --- /dev/null +++ b/src/main/java/org/geant/lgservice/infrastructure/ssh/SSHHost.java @@ -0,0 +1,88 @@ +package org.geant.lgservice.infrastructure.ssh; + +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.ConnectionMonitor; +import ch.ethz.ssh2.Session; +import lombok.extern.slf4j.Slf4j; +import org.geant.lgservice.exceptions.TechnicalException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Collection; +import java.util.HashSet; +import java.util.stream.Collectors; + +import static ch.ethz.ssh2.ChannelCondition.EXIT_STATUS; +import static org.geant.lgservice.infrastructure.ssh.SSHHost.ConnectionStateListener.State.DISCONNECTED; + +@Slf4j +public class SSHHost implements ConnectionMonitor { + + private final Connection connection; + private final Collection<ConnectionStateListener> connectionStateListeners = new HashSet<>(); + + private SSHHost(Connection connection) { + this.connection = connection; + connection.addConnectionMonitor(this); + } + + public static SSHHost fromConnection(Connection connection) { + if (!connection.isAuthenticationComplete()) { + throw new TechnicalException("Connection is not authenticated"); + } + return new SSHHost(connection); + } + + public String execute(String command) { + Session session = null; + try { + session = connection.openSession(); + + log.debug("sending command [{}] to host [{}]", command, hostname()); + session.execCommand(command); + + session.waitForCondition(EXIT_STATUS, 0); + int exitStatus = session.getExitStatus(); + log.debug("Exit status [{}] recieved", exitStatus); + + InputStream response = exitStatus == 0 ? session.getStdout() : session.getStderr(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(response))) { + return reader.lines().collect(Collectors.joining("\n")); + } + } catch (IOException e) { + throw new TechnicalException(e); + } finally { + if (session != null) { + session.close(); + } + } + } + + public String hostname() { + return connection.getHostname(); + } + + public SSHHost withConnectionStateListener(ConnectionStateListener listener) { + connectionStateListeners.add(listener); + return this; + } + + @Override + public void connectionLost(Throwable reason) { + log.warn("Host [{}] lost connection: ", connection.getHostname(), reason); + connectionStateListeners + .forEach(connectionStateListener -> connectionStateListener.stateChanged(this, DISCONNECTED)); + } + + @FunctionalInterface + public interface ConnectionStateListener { + + enum State { + DISCONNECTED + } + + void stateChanged(SSHHost host, State state); + } +} diff --git a/src/main/java/org/geant/lgservice/interfaces/rest/BusinessExceptionHandler.java b/src/main/java/org/geant/lgservice/interfaces/rest/BusinessExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..a67f39942644af7bbedf38e2b830558a1d79af68 --- /dev/null +++ b/src/main/java/org/geant/lgservice/interfaces/rest/BusinessExceptionHandler.java @@ -0,0 +1,19 @@ +package org.geant.lgservice.interfaces.rest; + +import org.geant.lgservice.exceptions.BusinessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class BusinessExceptionHandler extends ResponseEntityExceptionHandler { + + + + @ExceptionHandler(BusinessException.class) + protected ResponseEntity<Object> handleBusinessException (BusinessException ex) { + return ResponseEntity.status(HttpStatus.valueOf(ex.getReason().name())).build(); + } +} diff --git a/src/main/java/org/geant/lgservice/interfaces/rest/TechnicalExceptionHandler.java b/src/main/java/org/geant/lgservice/interfaces/rest/TechnicalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..bf184cc96a76572005d456cd6f4c84dd2fed0925 --- /dev/null +++ b/src/main/java/org/geant/lgservice/interfaces/rest/TechnicalExceptionHandler.java @@ -0,0 +1,21 @@ +package org.geant.lgservice.interfaces.rest; + +import lombok.extern.slf4j.Slf4j; +import org.geant.lgservice.exceptions.TechnicalException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +@ControllerAdvice +@Slf4j +public class TechnicalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(TechnicalException.class) + protected ResponseEntity<Object> handleBusinessException(TechnicalException ex) { + log.error("Caught Technical Exception: ", ex); + return ResponseEntity.status(INTERNAL_SERVER_ERROR).build(); + } +} diff --git a/src/main/java/org/geant/lgservice/pojos/Command.java b/src/main/java/org/geant/lgservice/pojos/Command.java index 674fdc65cd166998a69fa5b080e03bd89c1702aa..265d84425583960b6155ec275484ea4a2cc02a3e 100644 --- a/src/main/java/org/geant/lgservice/pojos/Command.java +++ b/src/main/java/org/geant/lgservice/pojos/Command.java @@ -1,16 +1,16 @@ package org.geant.lgservice.pojos; -import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; -@Data @NoArgsConstructor -@AllArgsConstructor +@Setter public class Command { private String name; + @Getter private String value; private String description; diff --git a/src/main/java/org/geant/lgservice/pojos/CommandOutput.java b/src/main/java/org/geant/lgservice/pojos/CommandOutput.java index 89b252988870a6a51b3384ac0649e1c60fe29352..c4073ba2c1dc43f891e4968721e9cbfd2d215273 100644 --- a/src/main/java/org/geant/lgservice/pojos/CommandOutput.java +++ b/src/main/java/org/geant/lgservice/pojos/CommandOutput.java @@ -1,16 +1,14 @@ package org.geant.lgservice.pojos; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.Builder; +import lombok.Getter; -@Data -@NoArgsConstructor -@AllArgsConstructor +@Getter +@Builder public class CommandOutput { - private String routerName; - private String commandResult; - private Long executionTime; + private final String routerName; + private final String commandResult; + private final Long executionTime; } diff --git a/src/main/java/org/geant/lgservice/pojos/Coordinates.java b/src/main/java/org/geant/lgservice/pojos/Coordinates.java deleted file mode 100644 index 622c71d4ad288808db47f9f768b50c39291c4bf4..0000000000000000000000000000000000000000 --- a/src/main/java/org/geant/lgservice/pojos/Coordinates.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.geant.lgservice.pojos; - -import javax.xml.bind.annotation.XmlAttribute; - -public class Coordinates { - - private String latitude; - - private String longitude; - - public Coordinates() { - - } - - public Coordinates(String latitude, String longitude) { - super(); - this.latitude = latitude; - this.longitude = longitude; - } - - public String getLatitude() { - return latitude; - } - - @XmlAttribute(name = "lat") - public void setLatitude(String latitude) { - this.latitude = latitude; - } - - public String getLongitude() { - return longitude; - } - - @XmlAttribute(name = "long") - public void setLongitude(String longitude) { - this.longitude = longitude; - } - -} diff --git a/src/main/java/org/geant/lgservice/pojos/Credentials.java b/src/main/java/org/geant/lgservice/pojos/Credentials.java deleted file mode 100644 index 05f6defe925b12f3bff7d4e19a171609e14f2615..0000000000000000000000000000000000000000 --- a/src/main/java/org/geant/lgservice/pojos/Credentials.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.geant.lgservice.pojos; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class Credentials { - - private String username; - - private String password; - - private String keyFile; - -} diff --git a/src/main/java/org/geant/lgservice/pojos/QueryInputRequest.java b/src/main/java/org/geant/lgservice/pojos/QueryInputRequest.java index 6ff035763a9c62acfc716e5198fd7a7218ccbc50..3f9b2abf1c66052a151a5867a61662568d0c7197 100644 --- a/src/main/java/org/geant/lgservice/pojos/QueryInputRequest.java +++ b/src/main/java/org/geant/lgservice/pojos/QueryInputRequest.java @@ -1,13 +1,11 @@ package org.geant.lgservice.pojos; -import java.util.List; - -import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.Getter; import lombok.NoArgsConstructor; -@Data -@AllArgsConstructor +import java.util.List; + +@Getter @NoArgsConstructor public class QueryInputRequest { diff --git a/src/main/java/org/geant/lgservice/pojos/QueryResponse.java b/src/main/java/org/geant/lgservice/pojos/QueryResponse.java index 9ded0504bae9655840e85312b76ff20c5027090b..4d6584d66232ec7b864d18d7305c8b3fddb5feea 100644 --- a/src/main/java/org/geant/lgservice/pojos/QueryResponse.java +++ b/src/main/java/org/geant/lgservice/pojos/QueryResponse.java @@ -1,14 +1,12 @@ package org.geant.lgservice.pojos; -import java.util.Map; +import lombok.Builder; +import lombok.Getter; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import java.util.Map; -@Data -@NoArgsConstructor -@AllArgsConstructor +@Getter +@Builder public class QueryResponse { Map<String, CommandOutput> output; diff --git a/src/main/java/org/geant/lgservice/rest/LookingGlassRestController.java b/src/main/java/org/geant/lgservice/rest/LookingGlassRestController.java index 94973e050b746a00b63e0956d5e8908e7ce8b8eb..39b98d64aeec92340a0d03a9cc779b5dc6a00dbb 100644 --- a/src/main/java/org/geant/lgservice/rest/LookingGlassRestController.java +++ b/src/main/java/org/geant/lgservice/rest/LookingGlassRestController.java @@ -1,11 +1,7 @@ package org.geant.lgservice.rest; -import java.util.List; -import java.util.Map; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.geant.lgservice.exceptions.ConnectionException; import org.geant.lgservice.pojos.CommandOutput; import org.geant.lgservice.pojos.Group; import org.geant.lgservice.pojos.QueryInputRequest; @@ -16,6 +12,7 @@ import org.geant.lgservice.services.LookingGlassService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.info.BuildProperties; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; @@ -24,7 +21,10 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import io.swagger.annotations.ApiOperation; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.toMap; @RestController @RequestMapping("/rest") @@ -63,19 +63,16 @@ public class LookingGlassRestController { @PostMapping(value = "/submit") @CrossOrigin - public ResponseEntity<QueryResponse> submit(@RequestBody QueryInputRequest input) throws ConnectionException { - QueryResponse response = new QueryResponse(); - Map<String, CommandOutput> result = service.submitQuery(input.getSelectedRouters(), input.getSelectedCommand(), - input.getArguments(), input.isDisplayAsXML()); - if (result != null && !result.isEmpty()) - response.setOutput(result); - else - throw new ConnectionException(); - return ResponseEntity.ok(response); + public QueryResponse submit(@RequestBody QueryInputRequest input, @AuthenticationPrincipal LGUser user) { + + Map<String, CommandOutput> result = input.getSelectedRouters().stream() + .map(router -> service.submitQuery(router.getName(), input.getSelectedCommand(), input.getArguments(), input.isDisplayAsXML(), user)) + .collect(toMap(CommandOutput::getRouterName, output -> output)); + + return QueryResponse.builder().output(result).build(); } @GetMapping(value = "/build") - @ApiOperation(value = "Get build information", response = String.class) public ResponseEntity<?> getBuildInfo() { BuildProperties buildProperties = buildService.getBuildInfo(); return ResponseEntity.ok(buildProperties); diff --git a/src/main/java/org/geant/lgservice/security/LGUser.java b/src/main/java/org/geant/lgservice/security/LGUser.java index 54859a3faf55f4e499e8c96e380d26575a5940cd..340631cda86d2750ae7dead3b22ef295621bcee7 100644 --- a/src/main/java/org/geant/lgservice/security/LGUser.java +++ b/src/main/java/org/geant/lgservice/security/LGUser.java @@ -1,103 +1,97 @@ package org.geant.lgservice.security; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import org.geant.lgservice.utils.LGRole; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; -public class LGUser extends User { +import java.util.Collection; +import java.util.Optional; +import java.util.stream.Stream; - private static final long serialVersionUID = 1L; +import static java.util.stream.Collectors.toSet; - private String federatedUser; - private String givenName; - private String surname; - private String mail; - private LGRole role; - private Collection<? extends GrantedAuthority> groups; +@Getter +public class LGUser implements UserDetails { - public static final String INTERNAL_GROUP = "GEANT Staff:All"; - public static final String EXTERNAL_GROUP = "GEANT_CO:GEANT Services:NREN Svc Mgmt:GEANT NRENs:members_GEANT NRENs"; + private final String federatedUser; + private final String givenName; + private final String surname; + private final String mail; + private final Collection<GrantedAuthority> authorities; - public LGUser(String federatedUser, String givenName, String surname, String mail, - Collection<? extends GrantedAuthority> groups) { + @RequiredArgsConstructor + public enum Group implements GrantedAuthority{ + INTERNAL(new SimpleGrantedAuthority("GEANT Staff:All")), + EXTERNAL(new SimpleGrantedAuthority("GEANT_CO:GEANT Services:NREN Svc Mgmt:GEANT NRENs:members_GEANT NRENs")); - super(federatedUser, "DUMMY PASSWORD", true, true, true, true, groups); + private final GrantedAuthority authority; - this.federatedUser = federatedUser; - this.givenName = givenName; - this.surname = surname; - this.mail = mail; - this.groups = groups; - - List<String> groupValues = new ArrayList<>(); - for (GrantedAuthority grantedAuthority : groups) { - groupValues.add(grantedAuthority.getAuthority()); - } - - if (!groupValues.contains(INTERNAL_GROUP) && !groupValues.contains(EXTERNAL_GROUP)) { - this.role = LGRole.PUBLIC; + public static Optional<Group> fromGroup(String group) { + return Stream.of(Group.values()) + .filter(value -> value.authority.getAuthority().equals(group)) + .findFirst(); } - if (groupValues.contains(INTERNAL_GROUP)) { - this.role = LGRole.INTERNAL; + public static Optional<Group> fromAuthority(GrantedAuthority authority) { + return Stream.of(Group.values()) + .filter(authority::equals) + .findFirst(); } - if (groupValues.contains(EXTERNAL_GROUP)) { - this.role = LGRole.EXTERNAL; + @Override + public String getAuthority() { + return name(); } - } - public String getSurname() { - return surname; - } + @Builder + public LGUser(String federatedUser, String givenName, String surname, String mail, + String ...groups) { - public void setSurname(String surname) { + this.federatedUser = federatedUser; + this.givenName = givenName; this.surname = surname; - } + this.mail = mail; + this.authorities = Stream.of(groups) + .map(Group::fromGroup) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toSet()); - public String getGivenName() { - return givenName; - } - public void setGivenName(String givenName) { - this.givenName = givenName; } - public String getMail() { - return mail; + @Override + public String getPassword() { + return null; } - public void setMail(String mail) { - this.mail = mail; - } - - public String getFederatedUser() { + @Override + public String getUsername() { return federatedUser; } - public void setFederatedUser(String federatedUser) { - this.federatedUser = federatedUser; - } - - public Collection<? extends GrantedAuthority> getGroups() { - return groups; + @Override + public boolean isAccountNonExpired() { + return true; } - public void setGroups(Collection<? extends GrantedAuthority> groups) { - this.groups = groups; + @Override + public boolean isAccountNonLocked() { + return true; } - public LGRole getRole() { - return role; + @Override + public boolean isCredentialsNonExpired() { + return true; } - public void setRole(LGRole role) { - this.role = role; + @Override + public boolean isEnabled() { + return true; } } diff --git a/src/main/java/org/geant/lgservice/security/LGUserService.java b/src/main/java/org/geant/lgservice/security/LGUserService.java index 506512f993831a945769920196dac7303b1cfe35..a0ac88e9bc512327909cdaf52c8f52d6fad5358e 100644 --- a/src/main/java/org/geant/lgservice/security/LGUserService.java +++ b/src/main/java/org/geant/lgservice/security/LGUserService.java @@ -1,12 +1,7 @@ package org.geant.lgservice.security; -import java.util.ArrayList; -import java.util.List; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.saml.SAMLCredential; import org.springframework.security.saml.userdetails.SAMLUserDetailsService; @@ -23,17 +18,15 @@ public class LGUserService implements SAMLUserDetailsService { String givenName = credential.getAttributeAsString("givenName"); String surname = credential.getAttributeAsString("sn"); String mail = credential.getAttributeAsString("mail"); - - List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); String[] groupNames = credential.getAttributeAsStringArray("isMemberOf"); - if (groupNames != null) { - for (String role : groupNames) { - GrantedAuthority authority = new SimpleGrantedAuthority(role); - authorities.add(authority); - } - } + LOGGER.info("Current user : " + givenName + ", " + surname); - LGUser currentUser = new LGUser(federatedUser, givenName, surname, mail, authorities); - return currentUser; + return LGUser.builder() + .federatedUser(federatedUser) + .givenName(givenName) + .surname(surname) + .mail(mail) + .groups(groupNames) + .build(); } } diff --git a/src/main/java/org/geant/lgservice/services/LookingGlassService.java b/src/main/java/org/geant/lgservice/services/LookingGlassService.java index ee92c9d8d3438672e02b30bf82456a99f36e6390..aa8e955f9aa0a7f5b8a79cd9cbd0bd4c705ceddf 100644 --- a/src/main/java/org/geant/lgservice/services/LookingGlassService.java +++ b/src/main/java/org/geant/lgservice/services/LookingGlassService.java @@ -1,18 +1,17 @@ package org.geant.lgservice.services; -import java.util.List; -import java.util.Map; - import org.geant.lgservice.pojos.Command; import org.geant.lgservice.pojos.CommandOutput; import org.geant.lgservice.pojos.Group; -import org.geant.lgservice.pojos.Router; import org.geant.lgservice.security.LGUser; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.List; public interface LookingGlassService { List<Group> getAllCommands(final LGUser user); - Map<String, CommandOutput> submitQuery(final List<Router> selectedRouters, final Command selectedCommand, - final String arguments, final boolean displayAsXML); + CommandOutput submitQuery(final String hostname, final Command selectedCommand, + final String arguments, final boolean displayAsXML, UserDetails user); } diff --git a/src/main/java/org/geant/lgservice/services/LookingGlassServiceImpl.java b/src/main/java/org/geant/lgservice/services/LookingGlassServiceImpl.java index ab6ac1a706b2acd1873ff488e2ae47631a1d4439..be982e9769f993464cd323216760a391eead0540 100644 --- a/src/main/java/org/geant/lgservice/services/LookingGlassServiceImpl.java +++ b/src/main/java/org/geant/lgservice/services/LookingGlassServiceImpl.java @@ -1,21 +1,27 @@ package org.geant.lgservice.services; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - import lombok.RequiredArgsConstructor; -import org.geant.lgservice.ecmr.CommandExecutor; +import org.apache.commons.lang.StringUtils; +import org.geant.lgservice.domain.Router; +import org.geant.lgservice.domain.RouterRepository; +import org.geant.lgservice.exceptions.BusinessException; +import org.geant.lgservice.infrastructure.ssh.SSHCommandExecutor; import org.geant.lgservice.pojos.Command; import org.geant.lgservice.pojos.CommandOutput; import org.geant.lgservice.pojos.Group; -import org.geant.lgservice.pojos.Router; import org.geant.lgservice.security.LGUser; import org.geant.lgservice.utils.LookingGlassHelper; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; -import static java.util.stream.Collectors.toMap; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.geant.lgservice.domain.Router.Access.INTERNAL; +import static org.geant.lgservice.exceptions.BusinessException.Reason.FORBIDDEN; +import static org.geant.lgservice.exceptions.BusinessException.Reason.NOT_FOUND; +import static org.geant.lgservice.exceptions.BusinessException.Reason.UNAUTHORIZED; @Service @RequiredArgsConstructor @@ -23,7 +29,9 @@ public class LookingGlassServiceImpl implements LookingGlassService { private final LookingGlassHelper lookingGlassHelper; - private final CommandExecutor commandExecutor; + private final RouterRepository routerRepository; + + private final SSHCommandExecutor sshCommandExecutor; @Override public List<Group> getAllCommands(final LGUser user) { @@ -31,10 +39,29 @@ public class LookingGlassServiceImpl implements LookingGlassService { } @Override - public Map<String, CommandOutput> submitQuery(final List<Router> selectedRouters, final Command selectedCommand, - final String arguments, final boolean displayAsXML) { - return selectedRouters.stream() - .map(router -> commandExecutor.execute(router, selectedCommand, arguments, displayAsXML)) - .collect(toMap(CommandOutput::getRouterName, output -> output)); + public CommandOutput submitQuery(final String hostname, final Command selectedCommand, + final String arguments, final boolean displayAsXML, UserDetails user) { + + Router router = routerRepository.getByHostName(hostname) + .orElseThrow(() -> BusinessException.withReason(NOT_FOUND)); + + if (router.getAccess().equals(INTERNAL)) { + if (user == null){ + throw BusinessException.withReason(UNAUTHORIZED); + }else if (!user.getAuthorities().contains(LGUser.Group.INTERNAL)){ + throw BusinessException.withReason(FORBIDDEN); + } + } + + return sshCommandExecutor.execute(hostname, getCommandWithArguments(selectedCommand, arguments, displayAsXML)); + } + + private String getCommandWithArguments(Command command, String arguments, boolean displayAsXML) { + return Stream.of( + command.getValue(), + arguments, + displayAsXML ? "| display xml" : "") + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining(" ")); } } diff --git a/src/test/java/org/geant/lgservice/ecmr/CallableCommandExecutorTest.java b/src/test/java/org/geant/lgservice/ecmr/CallableCommandExecutorTest.java deleted file mode 100644 index 34da57adcd13b53426bba0be22694099b5ec5151..0000000000000000000000000000000000000000 --- a/src/test/java/org/geant/lgservice/ecmr/CallableCommandExecutorTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.geant.lgservice.ecmr; - -import static org.junit.Assert.assertEquals; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.SneakyThrows; -import org.geant.lgservice.pojos.Command; -import org.geant.lgservice.pojos.Router; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -public class CallableCommandExecutorTest { - - public static final String TEST_ROUTER = "test-router.geant.net"; - public static final String TEST_COMMAND = "ping 25"; - public static final String TEST_ROUTER_JSON = String.format("{\"name\":\"%s\"}", TEST_ROUTER); - private static final ObjectMapper MAPPER = new ObjectMapper(); - private Router router; - - @Before - @SneakyThrows - public void setup() { - router = MAPPER.readValue(TEST_ROUTER_JSON, Router.class); - } - - @Test - public void testGetCommandWithArgumentsXML() { - Command command = new Command(); - command.setValue(TEST_COMMAND); - command.setRequiresParams(true); - - CallableCommandExecutor callable = new CallableCommandExecutor(router, command, "param1", true, null); - assertEquals("ping 25 param1 | display xml", callable.getCommandWithArguments()); - } - - @Test - public void testGetCommandWithoutArgumentsXML() { - Command command = new Command(); - command.setValue(TEST_COMMAND); - command.setRequiresParams(false); - - CallableCommandExecutor callable = new CallableCommandExecutor(router, command, null, true, null); - assertEquals("ping 25 | display xml", callable.getCommandWithArguments()); - } - - @Test - public void testGetCommandWithoutArguments() { - Command command = new Command(); - command.setValue(TEST_COMMAND); - command.setRequiresParams(false); - - CallableCommandExecutor callable = new CallableCommandExecutor(router, command, null, false, null); - assertEquals("ping 25", callable.getCommandWithArguments()); - } - -} diff --git a/src/test/java/org/geant/lgservice/infrastructure/ssh/SSHHostTest.java b/src/test/java/org/geant/lgservice/infrastructure/ssh/SSHHostTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9adef25bb14684f4c67041e91925ec77ff647c8e --- /dev/null +++ b/src/test/java/org/geant/lgservice/infrastructure/ssh/SSHHostTest.java @@ -0,0 +1,110 @@ +package org.geant.lgservice.infrastructure.ssh; + +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.Session; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.SneakyThrows; +import org.geant.lgservice.exceptions.TechnicalException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.geant.lgservice.infrastructure.ssh.SSHHost.ConnectionStateListener.State.DISCONNECTED; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +public class SSHHostTest { + + private static final String HOSTNAME = "localhost"; + private static final String RESPONSE_STRING = "A Response String"; + + private Connection connection = mock(Connection.class); + private Session session = mock(Session.class); + private SSHHost subject; + + private PipedInputStream response; + private PipedOutputStream responseSource; + + @Before + @SneakyThrows + public void setup() { + reset(connection); + when(connection.isAuthenticationComplete()).thenReturn(true); + when(connection.openSession()).thenReturn(session); + when(connection.getHostname()).thenReturn(HOSTNAME); + + responseSource = new PipedOutputStream(); + response = new PipedInputStream(responseSource); + when(session.getStdout()).thenReturn(response); + when(session.getStderr()).thenReturn(response); + + responseSource.write(RESPONSE_STRING.getBytes()); + responseSource.close(); + + subject = SSHHost.fromConnection(connection); + } + + @Test + public void commandExecutedSuccessfully() { + when(session.getExitStatus()).thenReturn(0); + assertThat(subject.execute("some command")).isEqualTo(RESPONSE_STRING); + } + + @Test + public void commandExitedWithNonZeroStatus() { + when(session.getExitStatus()).thenReturn(1); + assertThat(subject.execute("some command")).isEqualTo(RESPONSE_STRING); + } + + @Test(expected = TechnicalException.class) + public void createWithUnAuthenticatedConnection() { + reset(connection); + when(connection.isAuthenticationComplete()).thenReturn(false); + SSHHost.fromConnection(connection); + fail("subject should have thrown an exception"); + } + + @Test(expected = TechnicalException.class) + public void sessionThrowsIOException() { + try { + Mockito.doThrow(new IOException()).when(session).execCommand(any(String.class)); + } catch (IOException e) { + fail("Mock should not throw IOException", e); + } + subject.execute("some command"); + fail("subject should have thrown an exception"); + } + + @Getter + @Setter + @NoArgsConstructor + private static class StateListenerOutput { + private String host; + private SSHHost.ConnectionStateListener.State state; + } + + @Test + public void connectionStateListenersTriggered() { + final StateListenerOutput listenerOutput = new StateListenerOutput(); + + subject.withConnectionStateListener((host, state) -> { + listenerOutput.setHost(host.hostname()); + listenerOutput.setState(state); + }); + + subject.connectionLost(new IOException()); + + assertThat(listenerOutput.getHost()).isEqualTo(HOSTNAME); + assertThat(listenerOutput.getState()).isEqualTo(DISCONNECTED); + } +} diff --git a/src/test/java/org/geant/lgservice/integration/BaseIT.java b/src/test/java/org/geant/lgservice/integration/BaseIT.java new file mode 100644 index 0000000000000000000000000000000000000000..a0ab1a60a811290c37d943d90002562dfc2566b8 --- /dev/null +++ b/src/test/java/org/geant/lgservice/integration/BaseIT.java @@ -0,0 +1,97 @@ +package org.geant.lgservice.integration; + +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.Session; +import com.alexlovett.jgivenextension.ExtendedScenarioTest; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.tngtech.jgiven.integration.spring.SpringStageCreator; +import lombok.SneakyThrows; +import org.geant.lgservice.LookingGlassServiceApplication; +import org.geant.lgservice.infrastructure.ssh.ConnectionFactory; +import org.geant.lgservice.infrastructure.ssh.ConnectionManager; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.lang.reflect.Field; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.reset; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_CLASS; + +@RunWith(SpringRunner.class) +@SpringBootTest( + webEnvironment = RANDOM_PORT, + classes = {LookingGlassServiceApplication.class, Config.class} +) +@TestPropertySource(properties = { + "server.ssl.enabled=false" +}) +@DirtiesContext(classMode = BEFORE_CLASS) +public abstract class BaseIT <GIVEN, WHEN, THEN> extends ExtendedScenarioTest<GIVEN, WHEN, THEN> implements BeanFactoryAware { + @LocalServerPort + protected int port; + + @Autowired + private PortSink portSink; + + @Autowired + private ConnectionFactory connectionFactory; + + @Autowired + private ConnectionManager connectionManager; + + @Autowired + private Connection connection; + + @Autowired + private Session session; + + interface PortSink { + void sink(int port); + } + + @ClassRule + public static final WireMockClassRule INVENTORY_PROVIDER = new WireMockClassRule(wireMockConfig().dynamicPort()); + + @Before + @SneakyThrows + public void setup() { + portSink.sink(port); + INVENTORY_PROVIDER.resetAll(); + reset(connectionFactory); + reset(connection); + reset(session); + Mockito.when(connectionFactory.createConnection(any(String.class))).thenReturn(connection); + Mockito.when(connection.openSession()).thenReturn(session); + Mockito.when(connection.isAuthenticationComplete()).thenReturn(true); + + Field hostField = ConnectionManager.class.getDeclaredField("hosts"); + hostField.setAccessible(true); + Map hosts = (Map)hostField.get(connectionManager); + hosts.clear(); + } + + @Override + public void setBeanFactory( BeanFactory beanFactory ) { + getScenario().setStageCreator( beanFactory.getBean( SpringStageCreator.class ) ); + } + + @BeforeClass + public static void setupClass() { + System.setProperty("inventoryprovider.baseUrl", String.format("http://localhost:%s/lg/", INVENTORY_PROVIDER.port())); + } +} diff --git a/src/test/java/org/geant/lgservice/integration/Config.java b/src/test/java/org/geant/lgservice/integration/Config.java new file mode 100644 index 0000000000000000000000000000000000000000..137c9338bedb4d01bc112ffe364a5bc9bc4b3721 --- /dev/null +++ b/src/test/java/org/geant/lgservice/integration/Config.java @@ -0,0 +1,99 @@ +package org.geant.lgservice.integration; + +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.Session; +import com.tngtech.jgiven.integration.spring.EnableJGiven; +import org.geant.lgservice.infrastructure.ssh.ConnectionFactory; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.support.HttpRequestWrapper; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; +import java.net.URI; + +import static java.util.Optional.of; +import static org.geant.lgservice.integration.Context.withDefaults; + +@Configuration +@EnableJGiven +@ComponentScan(basePackages = { "org.geant.lgservice.integration"}) +@MockBean({ + ConnectionFactory.class, + Connection.class, + Session.class +}) +public class Config { + + @Bean + public RestTemplate restTemplate(PortInjectingInterceptor interceptor) { + return new RestTemplateBuilder() + .rootUri("http://localhost") + .interceptors(interceptor) + .messageConverters(new MappingJackson2HttpMessageConverter()) + .build(); + } + + @Bean + PortInjectingInterceptor portInjector() { + return new PortInjectingInterceptor(); + } + + static class PortInjectingInterceptor implements ClientHttpRequestInterceptor, BaseIT.PortSink { + + private int port; + + @Override + public ClientHttpResponse intercept( + HttpRequest httpRequest, + byte[] bytes, + ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { + + HttpRequest req = new HttpRequestWrapper(httpRequest) { + @Override + public URI getURI() { + return of(port) + .map(port -> UriComponentsBuilder.fromUri(super.getURI()) + .host("localhost") + .port(port) + .build() + .toUri()) + .get(); + } + }; + + return clientHttpRequestExecution.execute(req, bytes); + } + + @Override + public void sink(int port) { + this.port = port; + } + } + + @Bean + public Context context(){ + return withDefaults(); + } + + @Bean + public MockMvc mockMvc(WebApplicationContext context) { + return MockMvcBuilders.webAppContextSetup(context) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build(); + } + +} diff --git a/src/test/java/org/geant/lgservice/integration/Context.java b/src/test/java/org/geant/lgservice/integration/Context.java new file mode 100644 index 0000000000000000000000000000000000000000..f97e2afd50709426ae60d282815d67645bff758c --- /dev/null +++ b/src/test/java/org/geant/lgservice/integration/Context.java @@ -0,0 +1,151 @@ +package org.geant.lgservice.integration; + +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.Session; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.stubbing.StubMapping; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.geant.lgservice.security.LGUser; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static org.geant.lgservice.integration.Context.Router.INTERNAL; +import static org.geant.lgservice.integration.Context.Router.PUBLIC; +import static org.mockito.Mockito.mock; + +@Getter +@Builder +public class Context { + + @Getter + @RequiredArgsConstructor + enum Router { + PUBLIC("mx2.tal.ee.geant.net"), + INTERNAL("mx1.cbg.uk.geant.net"); + + private final String host; + } + + private Router router; + private String command; + private LGUser user; + + private StubMapping inventoryProviderMapping; + + private int responseStatus; + private SubmitResponseBody submitResponseBody; + private Collection<CommandGroup> commandResponseBody; + + public Context withPublicRouter() { + this.router = PUBLIC; + return this; + } + + public Context withInternalRouter() { + this.router = INTERNAL; + return this; + } + + public Context withCommand(String command) { + this.command = command; + return this; + } + + public Context withUser(LGUser user) { + this.user = user; + return this; + } + + public Context withInventoryProviderStub(StubMapping mapping) { + this.inventoryProviderMapping = mapping; + return this; + } + + public Context withResponseStatus(int responseStatus) { + this.responseStatus = responseStatus; + return this; + } + + public Context withSubmitResponseBody(SubmitResponseBody submitResponseBody) { + this.submitResponseBody = submitResponseBody; + return this; + } + + public Context withCommandResponseBody(Collection<CommandGroup> commandResponseBody) { + this.commandResponseBody = commandResponseBody; + return this; + } + + public String getRouterHost() { + return router.getHost(); + } + + @SneakyThrows + public static Context withDefaults() { + return Context.builder() + .inventoryProviderMapping(stubFor(get("/lg/routers/all") + .willReturn(WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBodyFile("routers.json")))) + .build() + .withPublicRouter() + .withCommand("show config"); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SubmitResponseBody { + + private final Map<String, CommandResult> output; + + @JsonCreator + public SubmitResponseBody(@JsonProperty("output") Map<String, CommandResult> output) { + this.output = output; + } + + public CommandResult result() { + return output.values() + .stream() + .findFirst() + .orElseThrow(() -> new AssertionError("There should be command result in the response")); + } + + @Getter + public static class CommandResult { + + private String routerName; + @JsonProperty("commandResult") + private String response; + private Long executionTime; + } + } + + @Getter + public static class CommandGroup { + + private List<Command> commands; + private String name; + + @Getter + public static class Command { + private String name; + private String value; + private String description; + private boolean requiresParams; + } + } + + +} diff --git a/src/test/java/org/geant/lgservice/integration/scenario/GetCommandsIT.java b/src/test/java/org/geant/lgservice/integration/scenario/GetCommandsIT.java new file mode 100644 index 0000000000000000000000000000000000000000..da1b79cd83491c2f084654001b4a04a78c33cac5 --- /dev/null +++ b/src/test/java/org/geant/lgservice/integration/scenario/GetCommandsIT.java @@ -0,0 +1,66 @@ +package org.geant.lgservice.integration.scenario; + +import org.geant.lgservice.integration.BaseIT; +import org.junit.Test; + +import static org.geant.lgservice.integration.scenario.Then.ThenResponse.ThenCommandResponse.UserType.EXTERNAL; +import static org.geant.lgservice.integration.scenario.Then.ThenResponse.ThenCommandResponse.UserType.INTERNAL; +import static org.geant.lgservice.integration.scenario.Then.ThenResponse.ThenCommandResponse.UserType.PUBLIC; + +public class GetCommandsIT extends BaseIT<Given, When, Then> { + + @Test + public void anonymous_user_requests_commands() { + given() + .a().user() + .that().is().anonymous(); + + when() + .a().request_for_available_commands_is_sent(); + + then() + .the().response() + .is().ok() + .and().has_commands() + .that().are_for_$_users(PUBLIC) + .and().are_not_for_$_users(EXTERNAL) + .and().are_not_for_$_users(INTERNAL); + } + + @Test + public void external_user_requests_commands() { + given() + .a().user() + .that().is().logged_in() + .but().does_not_have_internal_access(); + + when() + .a().request_for_available_commands_is_sent(); + + then() + .the().response() + .is().ok() + .and().has_commands() + .that().are_for_$_users(PUBLIC) + .and().are_for_$_users(EXTERNAL) + .and().are_not_for_$_users(INTERNAL); + } + + @Test + public void internal_user_requests_commands() { + given() + .a().user() + .that().is().logged_in(); + + when() + .a().request_for_available_commands_is_sent(); + + then() + .the().response() + .is().ok() + .and().has_commands() + .that().are_for_$_users(PUBLIC) + .and().are_for_$_users(EXTERNAL) + .and().are_for_$_users(INTERNAL); + } +} diff --git a/src/test/java/org/geant/lgservice/integration/scenario/Given.java b/src/test/java/org/geant/lgservice/integration/scenario/Given.java new file mode 100644 index 0000000000000000000000000000000000000000..272ee36db013c224e86fb0226cd245fcd9d1cea9 --- /dev/null +++ b/src/test/java/org/geant/lgservice/integration/scenario/Given.java @@ -0,0 +1,161 @@ +package org.geant.lgservice.integration.scenario; + +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.Session; +import com.alexlovett.jgivenextension.ExtendedStage; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.http.ResponseDefinition; +import com.github.tomakehurst.wiremock.stubbing.StubMapping; +import com.tngtech.jgiven.annotation.AfterStage; +import com.tngtech.jgiven.annotation.BeforeScenario; +import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import com.tngtech.jgiven.annotation.ProvidedScenarioState; +import com.tngtech.jgiven.integration.spring.JGivenStage; +import lombok.SneakyThrows; +import org.geant.lgservice.infrastructure.ssh.ConnectionFactory; +import org.geant.lgservice.integration.BaseIT; +import org.geant.lgservice.integration.Context; +import org.geant.lgservice.security.LGUser; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.PipedInputStream; +import java.io.PipedOutputStream; + +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static org.geant.lgservice.integration.Context.withDefaults; +import static org.mockito.ArgumentMatchers.any; + +@JGivenStage +public class Given extends ExtendedStage<Given> { + + @ProvidedScenarioState + private Context context; + + @Autowired + @ProvidedScenarioState + private ConnectionFactory connectionFactory; + + @Autowired + @ProvidedScenarioState + private Session session; + + @BeforeScenario + public void setup() { + context = withDefaults(); + } + + public GivenUser user() { + return nest(GivenUser.class); + } + + public static class GivenUser extends NestedStage<GivenUser, Given> { + + @ExpectedScenarioState + private Context context; + + public GivenUser anonymous() { + return self(); + } + + public GivenUser logged_in() { + context.withUser(new LGUser( + "1", + "barry", + "scott", + "barry.scott@geant.org", + "GEANT Staff:All")); + return self(); + } + + public GivenUser does_not_have_internal_access() { + LGUser user = context.getUser(); + context.withUser( new LGUser( + user.getFederatedUser(), + user.getGivenName(), + user.getSurname(), + user.getMail(), + "GEANT_CO:GEANT Services:NREN Svc Mgmt:GEANT NRENs:members_GEANT NRENs")); + return self(); + } + } + + public GivenRouter router() { + return nest(GivenRouter.class); + } + + public static class GivenRouter extends NestedStage<GivenRouter, Given> { + + @ExpectedScenarioState + private Context context; + + @Autowired + @ExpectedScenarioState + private ConnectionFactory connectionFactory; + + @ExpectedScenarioState + private Session session; + + public GivenRouter publicly_accessible() { + context.withPublicRouter(); + return self(); + } + + public GivenRouter internal() { + context.withInternalRouter(); + return self(); + } + + @SneakyThrows + public GivenRouter ok() { + PipedOutputStream responseSource = new PipedOutputStream(); + PipedInputStream response = new PipedInputStream(responseSource); + + Mockito.when(session.getStdout()).thenReturn(response); + + Mockito.when(session.getExitStatus()) + .thenReturn(0); + + Mockito.doAnswer(invocation -> { + responseSource.write(invocation.getArgument(0, String.class).getBytes()); + responseSource.flush(); + responseSource.close(); + return null; + }).when(session).execCommand(any(String.class)); + + return self(); + } + } + + public GivenInventoryProvider inventory_provider() { + return nest(GivenInventoryProvider.class); + } + + public static class GivenInventoryProvider extends NestedStage<GivenInventoryProvider, Given> { + + @ExpectedScenarioState + private Context context; + + public GivenInventoryProvider returns_(int status) { + context.withInventoryProviderStub(get("/lg/routers/all") + .willReturn(WireMock.aResponse() + .withStatus(status)) + .build()); + return self(); + } + + public GivenInventoryProvider bad_body() { + StubMapping mapping = context.getInventoryProviderMapping(); + ResponseDefinition response = mapping.getResponse(); + mapping.setResponse(WireMock.aResponse().withStatus(response.getStatus()) + .withBody("{\"This is not valid response body\":true").build()); + return self(); + } + } + + @AfterStage + public void setupMocks() { + BaseIT.INVENTORY_PROVIDER.resetAll(); + BaseIT.INVENTORY_PROVIDER.addStubMapping(context.getInventoryProviderMapping()); + } +} diff --git a/src/test/java/org/geant/lgservice/integration/scenario/SubmitCommandRequestIT.java b/src/test/java/org/geant/lgservice/integration/scenario/SubmitCommandRequestIT.java new file mode 100644 index 0000000000000000000000000000000000000000..156fe3c887fd294ba63f20cd323a34395e6633dd --- /dev/null +++ b/src/test/java/org/geant/lgservice/integration/scenario/SubmitCommandRequestIT.java @@ -0,0 +1,161 @@ +package org.geant.lgservice.integration.scenario; + +import org.geant.lgservice.integration.BaseIT; +import org.junit.Test; + +public class SubmitCommandRequestIT extends BaseIT<Given, When, Then> { + + @Test + public void anonymous_user_sends_command_to_public_router(){ + given() + .a().user() + .that().is().anonymous().backup() + .and().a().router() + .that().is().publicly_accessible() + .and().is().ok(); + + when() + .a().request_to_the_router_is_sent(); + + then() + .the().command_is_executed() + .and().the().result_is_returned(); + } + + @Test + public void anonymous_user_sends_command_to_internal_router() { + given() + .a().user() + .that().is().anonymous().backup() + .and().a().router() + .that().is().internal(); + + when() + .a().request_to_the_router_is_sent(); + + then() + .the().response() + .is().an().error() + .with_a().status(401); + } + + @Test + public void logged_in_user_sends_command_to_public_router() { + given() + .a().user() + .that().is().logged_in() + .backup() + .and().a().router() + .that().is().publicly_accessible() + .and().is().ok(); + + when() + .a().request_to_the_router_is_sent(); + + then() + .the().command_is_executed() + .and().the().result_is_returned(); + } + + @Test + public void logged_in_user_sends_command_to_internal_router() { + given() + .a().user() + .that().is().logged_in() + .backup() + .and().a().router() + .that().is().internal() + .and().is().ok(); + + when() + .a().request_to_the_router_is_sent(); + + then() + .the().command_is_executed() + .and().the().result_is_returned(); + } + + @Test + public void logged_in_user_without_internal_access_sends_command_to_internal_router() { + given() + .a().user() + .that().is().logged_in() + .but().is().does_not_have_internal_access() + .backup() + .and().a().router() + .that().is().internal(); + + when() + .a().request_to_the_router_is_sent(); + + then() + .the().response() + .is().an().error() + .with_a().status(403); + } + + @Test + public void inventory_provider_returns_client_error() { + given() + .a().user() + .that().is().anonymous() + .backup() + .and().a().router() + .that().is().publicly_accessible() + .backup() + .and().inventory_provider() + .returns_(400); + + when() + .a().request_to_the_router_is_sent(); + + then() + .the().response() + .is().an().error() + .with().status(500); + } + + @Test + public void inventory_provider_returns_server_error() { + given() + .a().user() + .that().is().anonymous() + .backup() + .and().a().router() + .that().is().publicly_accessible() + .backup() + .and().inventory_provider() + .returns_(500); + + when() + .a().request_to_the_router_is_sent(); + + then() + .the().response() + .is().an().error() + .with().status(500); + } + + @Test + public void inventory_provider_returns_invalid_body() { + given() + .a().user() + .that().is().anonymous() + .backup() + .and().a().router() + .that().is().publicly_accessible() + .backup() + .and().inventory_provider() + .returns_(200) + .with().a().bad_body(); + + when() + .a().request_to_the_router_is_sent(); + + then() + .the().response() + .is().an().error() + .with().status(500); + } + +} diff --git a/src/test/java/org/geant/lgservice/integration/scenario/Then.java b/src/test/java/org/geant/lgservice/integration/scenario/Then.java new file mode 100644 index 0000000000000000000000000000000000000000..6af304fde81cdec81ed831c30f6fdfc6aab6cc02 --- /dev/null +++ b/src/test/java/org/geant/lgservice/integration/scenario/Then.java @@ -0,0 +1,112 @@ +package org.geant.lgservice.integration.scenario; + +import ch.ethz.ssh2.Session; +import com.alexlovett.jgivenextension.ExtendedStage; +import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import com.tngtech.jgiven.integration.spring.JGivenStage; +import lombok.SneakyThrows; +import org.geant.lgservice.integration.Context; +import org.mockito.ArgumentCaptor; + +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@JGivenStage +public class Then extends ExtendedStage<Then> { + + @ExpectedScenarioState + private Context context; + + @ExpectedScenarioState + private Session session; + + @SneakyThrows + public Then command_is_executed() { + ArgumentCaptor<String> command = ArgumentCaptor.forClass(String.class); + + verify(session, times(1)) + .execCommand(command.capture()); + assertThat(command.getValue()).isEqualTo(context.getCommand()); + return self(); + } + + public Then result_is_returned() { + Context.SubmitResponseBody submitResponseBody = context.getSubmitResponseBody(); + assertThat(submitResponseBody).isNotNull(); + + Context.SubmitResponseBody.CommandResult result = submitResponseBody.result(); + assertThat(result).isNotNull(); + assertThat(result.getRouterName()).isEqualTo(context.getRouterHost()); + assertThat(result.getResponse()).isEqualTo(context.getCommand()); + + return self(); + } + + public ThenResponse response() { + return nest(ThenResponse.class); + } + + public static class ThenResponse extends NestedStage<ThenResponse, Then> { + + @ExpectedScenarioState + private Context context; + + public ThenResponseError error() { + assertThat(context.getResponseStatus()).isNotEqualTo(200); + return nest(ThenResponseError.class); + } + + public ThenResponse ok() { + assertThat(context.getResponseStatus()).isEqualTo(200); + return self(); + } + + public static class ThenResponseError extends NestedStage<ThenResponseError, ThenResponse> { + + @ExpectedScenarioState + private Context context; + + public ThenResponseError status(int status) { + assertThat(context.getResponseStatus()).isEqualTo(status); + return self(); + } + + public ThenResponseError with_a() { + return self(); + } + } + + public ThenCommandResponse has_commands() { + assertThat(context.getCommandResponseBody()).isNotNull(); + return nest(ThenCommandResponse.class); + } + + public static class ThenCommandResponse extends NestedStage<ThenCommandResponse, ThenResponse> { + @ExpectedScenarioState + private Context context; + + public enum UserType { + PUBLIC, + EXTERNAL, + INTERNAL + } + + public ThenCommandResponse are_for_$_users(UserType userType) { + assertThat(context.getCommandResponseBody().stream().map(Context.CommandGroup::getName).collect(Collectors.toList())) + .contains(String.format("%s-group", userType.name().toLowerCase())); + return self(); + } + + public ThenCommandResponse are_not_for_$_users(UserType userType) { + assertThat(context.getCommandResponseBody().stream().map(Context.CommandGroup::getName).collect(Collectors.toList())) + .doesNotContain(String.format("%s-group", userType.name().toLowerCase())); + return self(); + } + } + + } + +} diff --git a/src/test/java/org/geant/lgservice/integration/scenario/When.java b/src/test/java/org/geant/lgservice/integration/scenario/When.java new file mode 100644 index 0000000000000000000000000000000000000000..7fa83d8f0de4284e5bad4beddfacb70d9c355647 --- /dev/null +++ b/src/test/java/org/geant/lgservice/integration/scenario/When.java @@ -0,0 +1,132 @@ +package org.geant.lgservice.integration.scenario; + +import com.alexlovett.jgivenextension.ExtendedStage; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import com.tngtech.jgiven.integration.spring.JGivenStage; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.geant.lgservice.integration.Context; +import org.geant.lgservice.integration.Context.SubmitResponseBody; +import org.geant.lgservice.security.LGUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Type; +import java.util.Collection; + +import static java.util.Collections.singletonList; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; + +@JGivenStage +@Slf4j +public class When extends ExtendedStage<When> { + + @ExpectedScenarioState + private Context context; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private MockMvc mvc; + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @SneakyThrows + public When request_to_the_router_is_sent() { + MockHttpServletResponse response = send(POST, "rest/submit", MAPPER.writeValueAsString(RequestBody.builder() + .command(context.getCommand()) + .routerHost(context.getRouterHost()) + .build())); + + context.withResponseStatus(response.getStatus()); + if (response.getStatus() == 200) { + String responseBody = response.getContentAsString(); + log.info("response body = {}", responseBody); + context.withSubmitResponseBody(MAPPER.readValue(responseBody, SubmitResponseBody.class)); + } + + return self(); + } + + @SneakyThrows + public When request_for_available_commands_is_sent() { + MockHttpServletResponse response = send(GET,"rest/commands/all", null); + if (response.getStatus() == 200) { + String responseBody = response.getContentAsString(); + log.info("response body = {}", responseBody); + context.withCommandResponseBody(MAPPER.readValue(responseBody, new TypeReference<Collection<Context.CommandGroup>>() {})); + } + return self(); + } + + @SneakyThrows + private MockHttpServletResponse send(HttpMethod method, String path, String body) { + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders + .request(method, UriComponentsBuilder.newInstance().scheme("http").path(path).build().toUri()) + .contentType(APPLICATION_JSON); + + if (StringUtils.isNotBlank(body)) { + requestBuilder.content(body); + } + + LGUser user = context.getUser(); + if (user != null) { + requestBuilder.with(user(user)); + } + + MockHttpServletResponse response = mvc.perform(requestBuilder).andReturn().getResponse(); + context.withResponseStatus(response.getStatus()); + return response; + } + + @Getter + public static class RequestBody { + + private final Collection<Router> selectedRouters; + private final Command selectedCommand; + + @Builder + private RequestBody(String routerHost, String command) { + this.selectedRouters = singletonList(Router.withHost(routerHost)); + this.selectedCommand = Command.withCommand(command); + } + + @RequiredArgsConstructor + @Getter + public static class Router { + + private final String name; + + public static Router withHost(String hostName) { + return new Router(hostName); + } + } + + @RequiredArgsConstructor + @Getter + public static class Command { + + private final String value; + + public static Command withCommand(String command) { + return new Command(command); + } + } + } +} diff --git a/src/test/java/org/geant/lgservice/services/LookingGlassServiceImplTest.java b/src/test/java/org/geant/lgservice/services/LookingGlassServiceImplTest.java deleted file mode 100644 index 31ed37f8968118af8a9da59251b9ed9e8ef10958..0000000000000000000000000000000000000000 --- a/src/test/java/org/geant/lgservice/services/LookingGlassServiceImplTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.geant.lgservice.services; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.SneakyThrows; -import org.geant.lgservice.ecmr.CommandExecutor; -import org.geant.lgservice.pojos.Command; -import org.geant.lgservice.pojos.CommandOutput; -import org.geant.lgservice.pojos.Router; -import org.junit.Before; -import org.junit.Test; - -import java.util.Collections; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.when; - -public class LookingGlassServiceImplTest { - - public static final String TEST_ROUTER_JSON = "{\"name\":\"router.geant.org\"}"; - private static final ObjectMapper MAPPER = new ObjectMapper(); - - private CommandExecutor commandExecutor = mock(CommandExecutor.class); - - private LookingGlassServiceImpl subject = new LookingGlassServiceImpl(null, commandExecutor); - - private Router router; - - @Before - @SneakyThrows - public void setup() { - reset(commandExecutor); - router = MAPPER.readValue(TEST_ROUTER_JSON, Router.class); - } - - @Test - public void testQuery() { - Command command = new Command(); - String arguments = "some args"; - - CommandOutput output = new CommandOutput(); - output.setRouterName(router.getName()); - - when(commandExecutor.execute(any(Router.class), any(Command.class), any(String.class), anyBoolean())).thenReturn(output); - - Map<String, CommandOutput> result = subject.submitQuery(Collections.singletonList(router), command, arguments, false); - - assertThat(result).hasSize(1); - assertThat(result).containsKey(router.getName()); - assertThat(result.get(router.getName())).isEqualTo(output); - } -} diff --git a/src/test/java/org/geant/lgservice/services/LookingGlassServiceTest.java b/src/test/java/org/geant/lgservice/services/LookingGlassServiceTest.java deleted file mode 100644 index 4669aad648d8c1b60982c7caa1a9544737bc45f5..0000000000000000000000000000000000000000 --- a/src/test/java/org/geant/lgservice/services/LookingGlassServiceTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.geant.lgservice.services; - -import static org.junit.Assert.assertEquals; - -import java.util.List; - -import org.geant.lgservice.pojos.Group; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -public class LookingGlassServiceTest { - - @Autowired - LookingGlassService fixture; - - @Test - public void test() { - List<Group> commandGroups = fixture.getAllCommands(null); - assertEquals("number of commands not correct", 1, commandGroups.size()); - assertEquals("group name not correct", "test-group", commandGroups.get(0).getName()); - assertEquals("command name not correct", "test-name", commandGroups.get(0).getCommands().get(0).getName()); - } - -} diff --git a/src/test/resources/__files/routers.json b/src/test/resources/__files/routers.json new file mode 100644 index 0000000000000000000000000000000000000000..c3e1d17320216e3e591da8824fdf8ecf79a5ffd7 --- /dev/null +++ b/src/test/resources/__files/routers.json @@ -0,0 +1,470 @@ +[ + { + "equipment name": "mx1.cbg.uk.geant.net", + "pop": { + "abbreviation": "lab", + "city": "Cambridge", + "country": "UK", + "country code": "UK", + "latitude": 0.0, + "longitude": 0.0, + "name": "DANTE Lab" + }, + "type": "INTERNAL" + }, + { + "equipment name": "mx2.tal.ee.geant.net", + "pop": { + "abbreviation": "tal", + "city": "Tallinn", + "country": "Estonia", + "country code": "EE", + "latitude": 59.434825, + "longitude": 24.71385, + "name": "Tallinn" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.lon.uk.geant.net", + "pop": { + "abbreviation": "lon", + "city": "London", + "country": "England", + "country code": "UK", + "latitude": 51.498652777778, + "longitude": -0.015805555555556, + "name": "London" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.pra.cz.geant.net", + "pop": { + "abbreviation": "pra", + "city": "Prague", + "country": "Czech Republic", + "country code": "CZ", + "latitude": 50.101847222222, + "longitude": 14.391738888889, + "name": "Prague" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.sof.bg.geant.net", + "pop": { + "abbreviation": "sof", + "city": "Sofia", + "country": "Bulgaria", + "country code": "BG", + "latitude": 42.675758333333, + "longitude": 23.370986111111, + "name": "Sofia" + }, + "type": "CORE" + }, + { + "equipment name": "mx2.rig.lv.geant.net", + "pop": { + "abbreviation": "rig", + "city": "Riga", + "country": "Latvia", + "country code": "LV", + "latitude": 56.948344444444, + "longitude": 24.118, + "name": "Riga" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.lon2.uk.geant.net", + "pop": { + "abbreviation": "lon2", + "city": "Slough", + "country": "UK", + "country code": "UK", + "latitude": 51.521986111111, + "longitude": 0.62331666666667, + "name": "London 2 (Slough)" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.ham.de.geant.net", + "pop": { + "abbreviation": "ham", + "city": "Hamburg", + "country": "Germany", + "country code": "DE", + "latitude": 53.550902777778, + "longitude": 10.046486111111, + "name": "Hamburg" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.poz.pl.geant.net", + "pop": { + "abbreviation": "poz", + "city": "Poznan", + "country": "Poland", + "country code": "PL", + "latitude": 52.411775, + "longitude": 16.917561111111, + "name": "Poznan" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.ath2.gr.geant.net", + "pop": { + "abbreviation": "ath2", + "city": "Attiki", + "country": "Greece", + "country code": "GR", + "latitude": 37.98, + "longitude": 23.73, + "name": "Athens 2" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.kau.lt.geant.net", + "pop": { + "abbreviation": "kau", + "city": "Kaunas", + "country": "Lithuania", + "country code": "LT", + "latitude": 54.940672222222, + "longitude": 24.018013888889, + "name": "Kaunas" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.bud.hu.geant.net", + "pop": { + "abbreviation": "bud", + "city": "Budapest", + "country": "Hungary", + "country code": "HU", + "latitude": 47.517902777778, + "longitude": 19.055436111111, + "name": "Budapest" + }, + "type": "CORE" + }, + { + "equipment name": "mx2.bru.be.geant.net", + "pop": { + "abbreviation": "bru", + "city": "Brussels", + "country": "Belgium", + "country code": "BE", + "latitude": 50.857002777778, + "longitude": 4.4115694444444, + "name": "Brussels" + }, + "type": "CORE" + }, + { + "equipment name": "mx2.bra.sk.geant.net", + "pop": { + "abbreviation": "bra", + "city": "Bratislava", + "country": "Slovakia", + "country code": "SK", + "latitude": 48.119027777778, + "longitude": 17.0957, + "name": "Bratislava" + }, + "type": "CORE" + }, + { + "equipment name": "mx2.kau.lt.geant.net", + "pop": { + "abbreviation": "kau", + "city": "Kaunas", + "country": "Lithuania", + "country code": "LT", + "latitude": 54.940672222222, + "longitude": 24.018013888889, + "name": "Kaunas" + }, + "type": "CORE" + }, + { + "equipment name": "mx2.cbg.uk", + "pop": { + "abbreviation": "oc", + "city": "CAMBRIDGE", + "country": "UK", + "country code": "UK", + "latitude": 0.0, + "longitude": 0.0, + "name": "Cambridge OC" + }, + "type": "INTERNAL" + }, + { + "equipment name": "mx1.buc.ro.geant.net", + "pop": { + "abbreviation": "buc", + "city": "Bucharest", + "country": "Romania", + "country code": "RO", + "latitude": 44.444741666667, + "longitude": 26.096422222222, + "name": "Bucharest" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.vie.at.geant.net", + "pop": { + "abbreviation": "vie", + "city": "Vienna", + "country": "Austria", + "country code": "AT", + "latitude": 48.268925, + "longitude": 16.410194444444, + "name": "Vienna" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.mil2.it.geant.net", + "pop": { + "abbreviation": "mil2", + "city": "Milan", + "country": "Italy", + "country code": "IT", + "latitude": 45.475522222222, + "longitude": 9.1033194444444, + "name": "Milan 2 Caldera" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.ams.nl.geant.net", + "pop": { + "abbreviation": "ams", + "city": "Amsterdam", + "country": "Netherlands", + "country code": "NL", + "latitude": 52.356363888889, + "longitude": 4.9529555555556, + "name": "Amsterdam" + }, + "type": "CORE" + }, + { + "equipment name": "sw2.am.office.geant.net", + "pop": { + "abbreviation": "am", + "city": "Amsterdam", + "country": "Netherlands", + "country code": "NL", + "latitude": 52.313305555556, + "longitude": 4.9491111111111, + "name": "Amsterdam GEANT Office" + }, + "type": "INTERNAL" + }, + { + "equipment name": "mx1.fra.de.geant.net", + "pop": { + "abbreviation": "fra", + "city": "Frankfurt", + "country": "Germany", + "country code": "DE", + "latitude": 50.120294444444, + "longitude": 8.7358444444444, + "name": "Frankfurt" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.lis.pt.geant.net", + "pop": { + "abbreviation": "lis2", + "city": "Lisbon", + "country": "Portugal", + "country code": "PT", + "latitude": 38.759097222222, + "longitude": -9.1421944444444, + "name": "Lisbon 2" + }, + "type": "CORE" + }, + { + "equipment name": "mx2.lju.si.geant.net", + "pop": { + "abbreviation": "lju", + "city": "Ljubljana", + "country": "Slovenia", + "country code": "SI", + "latitude": 46.042366666667, + "longitude": 14.488719444444, + "name": "Ljubljana" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.dub.ie.geant.net", + "pop": { + "abbreviation": "dub", + "city": "Dublin", + "country": "Ireland", + "country code": "IE", + "latitude": 53.291980555556, + "longitude": -6.4147333333333, + "name": "Dublin (City West)" + }, + "type": "CORE" + }, + { + "equipment name": "mx2.lis.pt.geant.net", + "pop": { + "abbreviation": "lis", + "city": "Lisbon", + "country": "Portugal", + "country code": "PT", + "latitude": 38.759097222222, + "longitude": -9.1421944444444, + "name": "Lisbon" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.mad.es.geant.net", + "pop": { + "abbreviation": "mad", + "city": "Alcobendas - Madrid", + "country": "Spain", + "country code": "ES", + "latitude": 40.536477777778, + "longitude": -3.6488027777778, + "name": "Madrid" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.tal.ee.geant.net", + "pop": { + "abbreviation": "tal", + "city": "Tallinn", + "country": "Estonia", + "country code": "EE", + "latitude": 59.434825, + "longitude": 24.71385, + "name": "Tallinn" + }, + "type": "CORE" + }, + { + "equipment name": "mx2.ath.gr.geant.net", + "pop": { + "abbreviation": "ath", + "city": "Athens", + "country": "Greece", + "country code": "GR", + "latitude": 37.973086111111, + "longitude": 23.745555555556, + "name": "Athens" + }, + "type": "CORE" + }, + { + "equipment name": "mx4 - Camb Lab", + "pop": { + "abbreviation": "lab", + "city": "Cambridge", + "country": "UK", + "country code": "UK", + "latitude": 0.0, + "longitude": 0.0, + "name": "DANTE Lab" + }, + "type": "INTERNAL" + }, + { + "equipment name": "mx1.par.fr.geant.net", + "pop": { + "abbreviation": "par", + "city": "Paris", + "country": "France", + "country code": "FR", + "latitude": 48.904630555556, + "longitude": 2.3713555555556, + "name": "Paris" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.dub2.ie.geant.net", + "pop": { + "abbreviation": "dub2", + "city": "Dublin 12", + "country": "Ireland", + "country code": "IE", + "latitude": 53.33425, + "longitude": -6.3655444444444, + "name": "Dublin 2 - Parkwest" + }, + "type": "CORE" + }, + { + "equipment name": "sw3.am.office.geant.net", + "pop": { + "abbreviation": "am", + "city": "Amsterdam", + "country": "Netherlands", + "country code": "NL", + "latitude": 52.313305555556, + "longitude": 4.9491111111111, + "name": "Amsterdam GEANT Office" + }, + "type": "INTERNAL" + }, + { + "equipment name": "mx2.zag.hr.geant.net", + "pop": { + "abbreviation": "zag", + "city": "Zagreb", + "country": "Crotia", + "country code": "HR", + "latitude": 45.792030555556, + "longitude": 15.969494444444, + "name": "Zagreb" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.mar.fr.geant.net", + "pop": { + "abbreviation": "mar", + "city": "Marseille", + "country": "France", + "country code": "FR", + "latitude": 43.311027777778, + "longitude": 5.3738888888889, + "name": "Marseille" + }, + "type": "CORE" + }, + { + "equipment name": "mx1.gen.ch.geant.net", + "pop": { + "abbreviation": "gen", + "city": "Geneva", + "country": "Switzerland", + "country code": "CH", + "latitude": 46.232194444444, + "longitude": 6.0457638888889, + "name": "Geneva" + }, + "type": "CORE" + } +] \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 5bddf85b14c909a998a8a58bd624eedd5ffafd07..3ceaa22a4b8c1e14e93d328803c1aaf7408735ab 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -10,24 +10,6 @@ configuration: security: saml2: metadataURL: "https://login.terena.org/wayf/saml2/idp/metadata" -server: - port: 8009 - ssl: - enabled: true -spring: - datasource: - driverClassName: com.mysql.jdbc.Driver - password: opsro - url: "jdbc:mysql://test-opsdb01.geant.net:3306/opsdb" - username: opsro - jpa: - generate-ddl: true - hibernate.ddl-auto: update - properties: - hibernate: - dialect: org.hibernate.dialect.MySQL5Dialect - show-sql: false - show-sql: false saml: metadata-url: https://login.terena.org/wayf/saml2/idp/metadata entity-name: https://test-lg.geant.net/saml diff --git a/src/test/resources/commands_external.xml b/src/test/resources/commands_external.xml new file mode 100644 index 0000000000000000000000000000000000000000..c955898fc7b6c9c0251af4d73723c329f836c7b1 --- /dev/null +++ b/src/test/resources/commands_external.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- List of Commands, here name element contains the command name that is + visible on UI and value contains actual command. --> +<commands> + <group name="public-group"> + <command> + <name>public-command</name> + <value>public-command</value> + <description>public-command</description> + <requiresParams>true</requiresParams> + </command> + </group> + <group name="external-group"> + <command> + <name>external-command</name> + <value>external-command</value> + <description>external-command</description> + <requiresParams>true</requiresParams> + </command> + </group> +</commands> \ No newline at end of file diff --git a/src/test/resources/commands_internal.xml b/src/test/resources/commands_internal.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1ef017e54a71a6ffa5e8766bc4abf51650fc32b --- /dev/null +++ b/src/test/resources/commands_internal.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- List of Commands, here name element contains the command name that is + visible on UI and value contains actual command. --> +<commands> + <group name="public-group"> + <command> + <name>public-command</name> + <value>public-command</value> + <description>public-command</description> + <requiresParams>true</requiresParams> + </command> + </group> + <group name="external-group"> + <command> + <name>external-command</name> + <value>external-command</value> + <description>external-command</description> + <requiresParams>true</requiresParams> + </command> + </group> + <group name="internal-group"> + <command> + <name>internal-command</name> + <value>internal-command</value> + <description>internal-command</description> + <requiresParams>true</requiresParams> + </command> + </group> +</commands> \ No newline at end of file diff --git a/src/test/resources/commands_public.xml b/src/test/resources/commands_public.xml index 8b785de5ab5590d7731b24c76472c263060f9888..70b1e69507f4ca895604c968a921ccb81ea9d1d9 100644 --- a/src/test/resources/commands_public.xml +++ b/src/test/resources/commands_public.xml @@ -1,13 +1,13 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- List of Commands, here name element contains the command name that is - visible on UI and value contains actual command. --> -<commands> - <group name="test-group"> - <command> - <name>test-name</name> - <value>test-value</value> - <description>test-description</description> - <requiresParams>true</requiresParams> - </command> - </group> +<?xml version="1.0" encoding="UTF-8"?> +<!-- List of Commands, here name element contains the command name that is + visible on UI and value contains actual command. --> +<commands> + <group name="public-group"> + <command> + <name>public-command</name> + <value>public-command</value> + <description>public-command</description> + <requiresParams>true</requiresParams> + </command> + </group> </commands> \ No newline at end of file