diff --git a/gravitee-apim-repository/gravitee-apim-repository-api/src/main/java/io/gravitee/repository/management/model/Integration.java b/gravitee-apim-repository/gravitee-apim-repository-api/src/main/java/io/gravitee/repository/management/model/Integration.java index e69b1f30efe..838cd59eedf 100644 --- a/gravitee-apim-repository/gravitee-apim-repository-api/src/main/java/io/gravitee/repository/management/model/Integration.java +++ b/gravitee-apim-repository/gravitee-apim-repository-api/src/main/java/io/gravitee/repository/management/model/Integration.java @@ -27,7 +27,7 @@ */ @NoArgsConstructor @AllArgsConstructor -@Builder +@Builder(toBuilder = true) @Data public class Integration { diff --git a/gravitee-apim-repository/gravitee-apim-repository-api/src/main/java/io/gravitee/repository/management/model/Token.java b/gravitee-apim-repository/gravitee-apim-repository-api/src/main/java/io/gravitee/repository/management/model/Token.java index 5b90c06ec48..6876814d2a6 100644 --- a/gravitee-apim-repository/gravitee-apim-repository-api/src/main/java/io/gravitee/repository/management/model/Token.java +++ b/gravitee-apim-repository/gravitee-apim-repository-api/src/main/java/io/gravitee/repository/management/model/Token.java @@ -17,11 +17,17 @@ import java.util.Date; import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; /** * @author Azize ELAMRANI (azize.elamrani at graviteesource.com) * @author GraviteeSource Team */ +@Builder(toBuilder = true) +@AllArgsConstructor +@NoArgsConstructor public class Token { public enum AuditEvent implements Audit.AuditEvent { diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/pom.xml b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/pom.xml new file mode 100644 index 00000000000..58c57722a35 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/pom.xml @@ -0,0 +1,86 @@ + + + + 4.0.0 + + io.gravitee.apim.rest.api + gravitee-apim-rest-api + ${revision}${sha1}${changelist} + + + gravitee-apim-rest-api-integration-controller + jar + + Gravitee.io APIM - Management API - Integration Controller + Controller interacting with Integration Agents + + + + + io.gravitee.exchange + gravitee-exchange-api + ${gravitee-exchange.version} + + + io.gravitee.exchange + gravitee-exchange-controller-core + ${gravitee-exchange.version} + + + io.gravitee.exchange + gravitee-exchange-controller-websocket + ${gravitee-exchange.version} + + + io.gravitee.integration + gravitee-integration-api + ${gravitee-integration-api.version} + + + io.gravitee.apim.rest.api + gravitee-apim-rest-api-service + ${project.version} + provided + + + + + io.reactivex.rxjava3 + rxjava + provided + + + org.slf4j + slf4j-api + provided + + + + + io.gravitee.apim.rest.api + gravitee-apim-rest-api-service + ${project.version} + test-jar + test + + + + diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/command/IntegrationCommandContext.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/command/IntegrationCommandContext.java new file mode 100644 index 00000000000..e89268a2462 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/command/IntegrationCommandContext.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.integration.controller.command; + +import io.gravitee.exchange.api.controller.ControllerCommandContext; +import java.util.Set; + +public record IntegrationCommandContext(boolean valid) implements ControllerCommandContext { + @Override + public boolean isValid() { + return valid; + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/command/IntegrationControllerCommandHandlerFactory.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/command/IntegrationControllerCommandHandlerFactory.java new file mode 100644 index 00000000000..1e68277776b --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/command/IntegrationControllerCommandHandlerFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.integration.controller.command; + +import io.gravitee.apim.core.integration.crud_service.IntegrationCrudService; +import io.gravitee.exchange.api.command.Command; +import io.gravitee.exchange.api.command.CommandAdapter; +import io.gravitee.exchange.api.command.CommandHandler; +import io.gravitee.exchange.api.command.Reply; +import io.gravitee.exchange.api.command.ReplyAdapter; +import io.gravitee.exchange.api.controller.ControllerCommandContext; +import io.gravitee.exchange.api.controller.ControllerCommandHandlersFactory; +import io.gravitee.exchange.api.websocket.protocol.ProtocolVersion; +import io.gravitee.integration.controller.command.hello.HelloCommandHandler; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class IntegrationControllerCommandHandlerFactory implements ControllerCommandHandlersFactory { + + private final IntegrationCrudService integrationCrudService; + + @Override + public List, ? extends Reply>> buildCommandHandlers( + final ControllerCommandContext controllerCommandContext + ) { + return List.of(new HelloCommandHandler(integrationCrudService)); + } + + @Override + public List, ? extends Command, ? extends Reply>> buildCommandAdapters( + ControllerCommandContext controllerCommandContext, + ProtocolVersion protocolVersion + ) { + return List.of(); + } + + @Override + public List, ? extends Reply>> buildReplyAdapters( + ControllerCommandContext controllerCommandContext, + ProtocolVersion protocolVersion + ) { + return List.of(); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/command/hello/HelloCommandHandler.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/command/hello/HelloCommandHandler.java new file mode 100644 index 00000000000..5612cac4918 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/command/hello/HelloCommandHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.integration.controller.command.hello; + +import io.gravitee.apim.core.integration.crud_service.IntegrationCrudService; +import io.gravitee.exchange.api.command.CommandHandler; +import io.gravitee.exchange.api.command.hello.HelloReply; +import io.gravitee.exchange.api.command.hello.HelloReplyPayload; +import io.gravitee.integration.api.command.IntegrationCommandType; +import io.gravitee.integration.api.command.hello.HelloCommand; +import io.gravitee.integration.api.command.hello.HelloCommandPayload; +import io.reactivex.rxjava3.core.Single; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RequiredArgsConstructor +@Slf4j +public class HelloCommandHandler implements CommandHandler { + + private final IntegrationCrudService integrationCrudService; + + @Override + public String supportType() { + return IntegrationCommandType.HELLO.name(); + } + + @Override + public Single handle(HelloCommand command) { + return Single + .fromCallable(() -> { + HelloCommandPayload payload = command.getPayload(); + + return integrationCrudService + .findById(payload.getTargetId()) + .map(integration -> { + if (integration.getProvider().equals(payload.getProvider())) { + return new HelloReply(command.getId(), HelloReplyPayload.builder().targetId(integration.getId()).build()); + } + return new HelloReply( + command.getId(), + String.format( + "Integration [id=%s] does not match. Expected provider [provider=%s]", + integration.getId(), + integration.getProvider() + ) + ); + }) + .orElse(new HelloReply(command.getId(), String.format("Integration [id=%s] not found", payload.getTargetId()))); + }) + .doOnError(throwable -> + log.error("Unable to process hello command payload for target [{}]", command.getPayload().getTargetId(), throwable) + ) + .onErrorReturn(throwable -> new HelloReply(command.getId(), throwable.getMessage())); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/spring/IntegrationControllerConfiguration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/spring/IntegrationControllerConfiguration.java new file mode 100644 index 00000000000..c9d1141d769 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/spring/IntegrationControllerConfiguration.java @@ -0,0 +1,110 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.integration.controller.spring; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.gravitee.apim.core.integration.crud_service.IntegrationCrudService; +import io.gravitee.apim.core.user.crud_service.UserCrudService; +import io.gravitee.exchange.api.configuration.IdentifyConfiguration; +import io.gravitee.exchange.api.controller.ControllerCommandHandlersFactory; +import io.gravitee.exchange.api.controller.ExchangeController; +import io.gravitee.exchange.api.websocket.command.ExchangeSerDe; +import io.gravitee.exchange.controller.websocket.WebSocketExchangeController; +import io.gravitee.exchange.controller.websocket.auth.WebSocketControllerAuthentication; +import io.gravitee.integration.api.websocket.command.IntegrationExchangeSerDe; +import io.gravitee.integration.controller.command.IntegrationControllerCommandHandlerFactory; +import io.gravitee.integration.controller.websocket.auth.IntegrationWebsocketControllerAuthentication; +import io.gravitee.node.api.cache.CacheManager; +import io.gravitee.node.api.certificate.KeyStoreLoaderFactoryRegistry; +import io.gravitee.node.api.certificate.KeyStoreLoaderOptions; +import io.gravitee.node.api.certificate.TrustStoreLoaderOptions; +import io.gravitee.node.api.cluster.ClusterManager; +import io.gravitee.rest.api.service.TokenService; +import io.vertx.rxjava3.core.Vertx; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; + +@Configuration +public class IntegrationControllerConfiguration { + + @Bean("integrationWebsocketControllerAuthentication") + public IntegrationWebsocketControllerAuthentication integrationWebsocketControllerAuthentication( + final TokenService tokenService, + final UserCrudService userCrudService + ) { + return new IntegrationWebsocketControllerAuthentication(tokenService, userCrudService); + } + + @Bean("integrationExchangeSerDe") + public IntegrationExchangeSerDe integrationExchangeSerDe() { + return new IntegrationExchangeSerDe(objectMapper()); + } + + @Bean("integrationIdentifyConfiguration") + public IdentifyConfiguration integrationPrefixConfiguration(final Environment environment) { + return new IdentifyConfiguration(environment, "integration"); + } + + @Bean("integrationControllerCommandHandlerFactory") + public IntegrationControllerCommandHandlerFactory integrationControllerCommandHandlerFactory( + final IntegrationCrudService integrationCrudService + ) { + return new IntegrationControllerCommandHandlerFactory(integrationCrudService); + } + + @Bean("integrationExchangeController") + public ExchangeController integrationExchangeController( + final @Lazy ClusterManager clusterManager, + final @Lazy CacheManager cacheManager, + final Vertx vertx, + final KeyStoreLoaderFactoryRegistry keyStoreLoaderFactoryRegistry, + final KeyStoreLoaderFactoryRegistry trustStoreLoaderFactoryRegistry, + final @Qualifier("integrationIdentifyConfiguration") IdentifyConfiguration identifyConfiguration, + final @Qualifier( + "integrationWebsocketControllerAuthentication" + ) WebSocketControllerAuthentication integrationWebsocketControllerAuthentication, + final @Qualifier( + "integrationControllerCommandHandlerFactory" + ) ControllerCommandHandlersFactory integrationControllerCommandHandlerFactory, + final @Qualifier("integrationExchangeSerDe") ExchangeSerDe integrationExchangeSerDe + ) { + return new WebSocketExchangeController( + identifyConfiguration, + clusterManager, + cacheManager, + vertx, + keyStoreLoaderFactoryRegistry, + trustStoreLoaderFactoryRegistry, + integrationWebsocketControllerAuthentication, + integrationControllerCommandHandlerFactory, + integrationExchangeSerDe + ); + } + + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper; + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/websocket/auth/IntegrationWebsocketControllerAuthentication.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/websocket/auth/IntegrationWebsocketControllerAuthentication.java new file mode 100644 index 00000000000..1c8f471958d --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/main/java/io/gravitee/integration/controller/websocket/auth/IntegrationWebsocketControllerAuthentication.java @@ -0,0 +1,62 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.integration.controller.websocket.auth; + +import io.gravitee.apim.core.user.crud_service.UserCrudService; +import io.gravitee.exchange.controller.websocket.auth.WebSocketControllerAuthentication; +import io.gravitee.integration.controller.command.IntegrationCommandContext; +import io.gravitee.repository.management.model.Token; +import io.gravitee.rest.api.service.TokenService; +import io.gravitee.rest.api.service.exceptions.UserNotFoundException; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.vertx.rxjava3.core.http.HttpServerRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author Guillaume LAMIRAND (guillaume.lamirand at graviteesource.com) + * @author GraviteeSource Team + */ +@Slf4j +@Service("integrationWebsocketControllerAuthentication") +@RequiredArgsConstructor +public class IntegrationWebsocketControllerAuthentication implements WebSocketControllerAuthentication { + + public static final String AUTHORIZATION_HEADER = HttpHeaderNames.AUTHORIZATION.toString(); + public static final String AUTHORIZATION_HEADER_BEARER = "bearer"; + private final TokenService tokenService; + private final UserCrudService userCrudService; + + @Override + public IntegrationCommandContext authenticate(final HttpServerRequest httpServerRequest) { + String header = httpServerRequest.headers().get(AUTHORIZATION_HEADER); + if (header != null) { + final String tokenValue = header.substring(AUTHORIZATION_HEADER_BEARER.length()).trim(); + try { + final Token token = tokenService.findByToken(tokenValue); + return userCrudService + .findBaseUserById(token.getReferenceId()) + .map(user -> new IntegrationCommandContext(true)) + .orElseThrow(() -> new UserNotFoundException(token.getReferenceId())); + } catch (Exception e) { + log.warn("Unable to authenticate incoming websocket controller request"); + } + } + log.warn("No authentication header in the incoming websocket controller request"); + return new IntegrationCommandContext(false); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/test/java/io/gravitee/integration/controller/command/hello/HelloCommandHandlerTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/test/java/io/gravitee/integration/controller/command/hello/HelloCommandHandlerTest.java new file mode 100644 index 00000000000..bb828feee63 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/test/java/io/gravitee/integration/controller/command/hello/HelloCommandHandlerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.integration.controller.command.hello; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; + +import fixtures.core.model.IntegrationFixture; +import inmemory.IntegrationCrudServiceInMemory; +import io.gravitee.apim.core.exception.TechnicalDomainException; +import io.gravitee.apim.core.integration.model.Integration; +import io.gravitee.exchange.api.command.CommandStatus; +import io.gravitee.exchange.api.command.hello.HelloReplyPayload; +import io.gravitee.integration.api.command.hello.HelloCommand; +import io.gravitee.integration.api.command.hello.HelloCommandPayload; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class HelloCommandHandlerTest { + + private static final String COMMAND_ID = "command-id"; + private static final String INTEGRATION_ID = "my-integration-id"; + private static final String INTEGRATION_PROVIDER = "amazon"; + + private static final HelloCommand COMMAND = new HelloCommand( + COMMAND_ID, + HelloCommandPayload.builder().targetId(INTEGRATION_ID).provider(INTEGRATION_PROVIDER).build() + ); + + IntegrationCrudServiceInMemory integrationCrudServiceInMemory = new IntegrationCrudServiceInMemory(); + HelloCommandHandler commandHandler; + + @BeforeEach + void setUp() { + commandHandler = new HelloCommandHandler(integrationCrudServiceInMemory); + } + + @AfterEach + void tearDown() { + integrationCrudServiceInMemory.reset(); + } + + @Test + void should_reply_succeeded_when_integration_exists() { + var integration = givenIntegration( + IntegrationFixture.anIntegration().toBuilder().id(INTEGRATION_ID).provider(INTEGRATION_PROVIDER).build() + ); + + commandHandler + .handle(COMMAND) + .test() + .awaitDone(10, TimeUnit.SECONDS) + .assertValue(reply -> { + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(reply.getCommandStatus()).isEqualTo(CommandStatus.SUCCEEDED); + soft.assertThat(reply.getCommandId()).isEqualTo(COMMAND_ID); + soft.assertThat(reply.getPayload()).isEqualTo(HelloReplyPayload.builder().targetId(integration.getId()).build()); + }); + + return true; + }) + .assertNoErrors(); + } + + @Test + void should_reply_error_when_integration_does_not_exist() { + commandHandler + .handle(COMMAND) + .test() + .awaitDone(10, TimeUnit.SECONDS) + .assertValue(reply -> { + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(reply.getCommandStatus()).isEqualTo(CommandStatus.ERROR); + soft.assertThat(reply.getCommandId()).isEqualTo(COMMAND_ID); + soft.assertThat(reply.getErrorDetails()).isEqualTo("Integration [id=my-integration-id] not found"); + }); + + return true; + }) + .assertNoErrors(); + } + + @Test + void should_reply_error_when_integration_exist_but_provider_mismatch() { + givenIntegration(IntegrationFixture.anIntegration().toBuilder().id("my-integration-id").provider("other").build()); + + commandHandler + .handle(COMMAND) + .test() + .awaitDone(10, TimeUnit.SECONDS) + .assertValue(reply -> { + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(reply.getCommandStatus()).isEqualTo(CommandStatus.ERROR); + soft.assertThat(reply.getCommandId()).isEqualTo(COMMAND_ID); + soft + .assertThat(reply.getErrorDetails()) + .isEqualTo("Integration [id=my-integration-id] does not match. Expected provider [provider=other]"); + }); + + return true; + }) + .assertNoErrors(); + } + + @Test + void should_reply_error_when_exception_occurs() { + var spied = Mockito.spy(integrationCrudServiceInMemory); + lenient().when(spied.findById(any())).thenThrow(new TechnicalDomainException("error")); + commandHandler = new HelloCommandHandler(spied); + + commandHandler + .handle(COMMAND) + .test() + .awaitDone(10, TimeUnit.SECONDS) + .assertValue(reply -> { + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(reply.getCommandStatus()).isEqualTo(CommandStatus.ERROR); + soft.assertThat(reply.getCommandId()).isEqualTo(COMMAND_ID); + soft.assertThat(reply.getErrorDetails()).isEqualTo("error"); + }); + + return true; + }) + .assertNoErrors(); + } + + private Integration givenIntegration(Integration integration) { + integrationCrudServiceInMemory.initWith(List.of(integration)); + return integration; + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/test/java/io/gravitee/integration/controller/websocket/auth/IntegrationWebsocketControllerAuthenticationTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/test/java/io/gravitee/integration/controller/websocket/auth/IntegrationWebsocketControllerAuthenticationTest.java new file mode 100644 index 00000000000..d2875df3e51 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-integration-controller/src/test/java/io/gravitee/integration/controller/websocket/auth/IntegrationWebsocketControllerAuthenticationTest.java @@ -0,0 +1,117 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.integration.controller.websocket.auth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; + +import inmemory.UserCrudServiceInMemory; +import io.gravitee.apim.core.user.model.BaseUserEntity; +import io.gravitee.integration.controller.command.IntegrationCommandContext; +import io.gravitee.repository.management.model.Token; +import io.gravitee.rest.api.service.TokenService; +import io.gravitee.rest.api.service.exceptions.TokenNotFoundException; +import io.vertx.rxjava3.core.MultiMap; +import io.vertx.rxjava3.core.http.HttpHeaders; +import io.vertx.rxjava3.core.http.HttpServerRequest; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class IntegrationWebsocketControllerAuthenticationTest { + + private static final String TOKEN_VALUE = "my-token-value"; + + @Mock + TokenService tokenService; + + @Mock + HttpServerRequest request; + + UserCrudServiceInMemory userCrudServiceInMemory = new UserCrudServiceInMemory(); + + IntegrationWebsocketControllerAuthentication authentication; + + @BeforeEach + void setUp() { + authentication = new IntegrationWebsocketControllerAuthentication(tokenService, userCrudServiceInMemory); + + MultiMap requestHeaders = HttpHeaders.headers(); + requestHeaders.add(IntegrationWebsocketControllerAuthentication.AUTHORIZATION_HEADER, "bearer " + TOKEN_VALUE); + lenient().when(request.headers()).thenReturn(requestHeaders); + } + + @AfterEach + void tearDown() { + userCrudServiceInMemory.reset(); + } + + @Test + void should_return_a_valid_IntegrationCommandContext_when_authentication_succeed() { + var token = givenToken(Token.builder().token(TOKEN_VALUE).referenceId("user-id").build()); + givenUser(BaseUserEntity.builder().id(token.getReferenceId()).build()); + + var result = authentication.authenticate(request); + + assertThat(result).isEqualTo(new IntegrationCommandContext(true)); + } + + @Test + void should_return_an_invalid_IntegrationCommandContext_when_no_token_found() { + givenNoToken(); + + var result = authentication.authenticate(request); + + assertThat(result).isEqualTo(new IntegrationCommandContext(false)); + } + + @Test + void should_return_an_invalid_IntegrationCommandContext_when_no_user_found() { + givenToken(Token.builder().token(TOKEN_VALUE).referenceId("user-id").build()); + + var result = authentication.authenticate(request); + + assertThat(result).isEqualTo(new IntegrationCommandContext(false)); + } + + @Test + void should_return_an_invalid_IntegrationCommandContext_when_no_authorization_headers() { + lenient().when(request.headers()).thenReturn(HttpHeaders.headers()); + + var result = authentication.authenticate(request); + + assertThat(result).isEqualTo(new IntegrationCommandContext(false)); + } + + private Token givenToken(Token token) { + lenient().when(tokenService.findByToken(token.getToken())).thenReturn(token); + return token; + } + + private void givenNoToken() { + lenient().when(tokenService.findByToken(any())).thenThrow(new TokenNotFoundException("token")); + } + + private void givenUser(BaseUserEntity user) { + userCrudServiceInMemory.initWith(List.of(user)); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/crud_service/IntegrationCrudService.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/crud_service/IntegrationCrudService.java index 04070feb702..fdff9446ed2 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/crud_service/IntegrationCrudService.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/integration/crud_service/IntegrationCrudService.java @@ -16,6 +16,7 @@ package io.gravitee.apim.core.integration.crud_service; import io.gravitee.apim.core.integration.model.Integration; +import java.util.Optional; /** * @author Remi Baptiste (remi.baptiste at graviteesource.com) @@ -23,4 +24,6 @@ */ public interface IntegrationCrudService { Integration create(Integration integration); + + Optional findById(String id); } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/crud_service/integration/IntegrationCrudServiceImpl.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/crud_service/integration/IntegrationCrudServiceImpl.java index 7e3d26e8214..87e3bec21ca 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/crud_service/integration/IntegrationCrudServiceImpl.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/infra/crud_service/integration/IntegrationCrudServiceImpl.java @@ -15,7 +15,6 @@ */ package io.gravitee.apim.infra.crud_service.integration; -import io.gravitee.apim.core.exception.TechnicalDomainException; import io.gravitee.apim.core.integration.crud_service.IntegrationCrudService; import io.gravitee.apim.core.integration.model.Integration; import io.gravitee.apim.infra.adapter.IntegrationAdapter; @@ -23,6 +22,7 @@ import io.gravitee.repository.management.api.IntegrationRepository; import io.gravitee.rest.api.service.exceptions.TechnicalManagementException; import io.gravitee.rest.api.service.impl.AbstractService; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -50,4 +50,13 @@ public Integration create(Integration integration) { throw new TechnicalManagementException("Error when creating Integration: " + integration.getName(), e); } } + + @Override + public Optional findById(String id) { + try { + return integrationRepository.findById(id).map(IntegrationAdapter.INSTANCE::toEntity); + } catch (TechnicalException e) { + throw new TechnicalManagementException("An error occurs while trying to find the integration: " + id, e); + } + } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/DefaultOrganizationAdminRoleInitializer.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/DefaultOrganizationAdminRoleInitializer.java index 4c5c36ab21b..bb5e4702413 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/DefaultOrganizationAdminRoleInitializer.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/DefaultOrganizationAdminRoleInitializer.java @@ -15,6 +15,7 @@ */ package io.gravitee.rest.api.service.impl.upgrade.initializer; +import io.gravitee.rest.api.model.permissions.EnvironmentPermission; import io.gravitee.rest.api.model.permissions.OrganizationPermission; import io.gravitee.rest.api.model.permissions.RoleScope; import io.gravitee.rest.api.model.permissions.SystemRole; @@ -45,6 +46,14 @@ protected void initializeOrganization(ExecutionContext executionContext) { OrganizationPermission.values(), executionContext.getOrganizationId() ); + + roleService.createOrUpdateSystemRole( + executionContext, + SystemRole.ADMIN, + RoleScope.ENVIRONMENT, + EnvironmentPermission.values(), + executionContext.getOrganizationId() + ); } @Override diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/InitializerOrder.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/InitializerOrder.java index 292cd2ee089..bb6d9623169 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/InitializerOrder.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/InitializerOrder.java @@ -37,4 +37,5 @@ private InitializerOrder() {} public static final int IDENTITY_PROVIDER_ACTIVATION_INITIALIZER = 400; public static final int IDENTITY_PROVIDER_INITIALIZER = 350; public static final int SEARCH_INDEX_INITIALIZER = 250; + public static final int INTEGRATION_CONTROLLER_INITIALIZER = 400; } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/IntegrationControllerInitializer.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/IntegrationControllerInitializer.java new file mode 100644 index 00000000000..1a580671753 --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/impl/upgrade/initializer/IntegrationControllerInitializer.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.rest.api.service.impl.upgrade.initializer; + +import io.gravitee.exchange.api.controller.ExchangeController; +import io.gravitee.node.api.initializer.Initializer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class IntegrationControllerInitializer implements Initializer { + + @Autowired + @Qualifier("integrationExchangeController") + private ExchangeController integrationExchangeController; + + @Override + public boolean initialize() { + try { + // TODO check license before starting controller + integrationExchangeController.start(); + log.info("Integrations started."); + } catch (Exception e) { + log.error("Fail to start Integration Controller", e); + throw new RuntimeException(e); + } + return true; + } + + @Override + public int getOrder() { + return InitializerOrder.INTEGRATION_CONTROLLER_INITIALIZER; + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/IntegrationFixture.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/IntegrationFixture.java index 3c626f42553..146e200d067 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/IntegrationFixture.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/core/model/IntegrationFixture.java @@ -19,7 +19,6 @@ import io.gravitee.rest.api.service.common.UuidString; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.UUID; import java.util.function.Supplier; public class IntegrationFixture { diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/repository/IntegrationFixture.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/repository/IntegrationFixture.java new file mode 100644 index 00000000000..161f3f0ed8d --- /dev/null +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/fixtures/repository/IntegrationFixture.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fixtures.repository; + +import io.gravitee.repository.management.model.Integration; +import java.time.Instant; +import java.util.Date; +import java.util.function.Supplier; + +public class IntegrationFixture { + + private IntegrationFixture() {} + + public static final Supplier BASE = () -> + Integration + .builder() + .id("integration-id") + .name("An integration") + .description("A description") + .provider("amazon") + .environmentId("environment-id") + .createdAt(Date.from(Instant.parse("2020-02-03T20:22:02.00Z"))) + .updatedAt(Date.from(Instant.parse("2020-02-04T20:22:02.00Z"))); + + public static Integration anIntegration() { + return BASE.get().build(); + } +} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/IntegrationCrudServiceInMemory.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/IntegrationCrudServiceInMemory.java index ff6d77b6423..96ccb29b5d3 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/IntegrationCrudServiceInMemory.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/inmemory/IntegrationCrudServiceInMemory.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; public class IntegrationCrudServiceInMemory implements IntegrationCrudService, InMemoryAlternative { @@ -31,6 +32,11 @@ public Integration create(Integration integration) { return integration; } + @Override + public Optional findById(String id) { + return storage.stream().filter(item -> item.getId().equals(id)).findFirst(); + } + @Override public void initWith(List items) { storage.clear(); diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/crud_service/integration/IntegrationCrudServiceImplTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/crud_service/integration/IntegrationCrudServiceImplTest.java index 4f7abcc4c52..b8893006a93 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/crud_service/integration/IntegrationCrudServiceImplTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/infra/crud_service/integration/IntegrationCrudServiceImplTest.java @@ -26,8 +26,12 @@ import io.gravitee.repository.exceptions.TechnicalException; import io.gravitee.repository.management.api.IntegrationRepository; import io.gravitee.rest.api.service.exceptions.TechnicalManagementException; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Optional; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; public class IntegrationCrudServiceImplTest { @@ -42,32 +46,97 @@ void setUp() { service = new IntegrationCrudServiceImpl(integrationRepository); } - @Test - @SneakyThrows - void should_create_integration() { - //Given - Integration integration = IntegrationFixture.anIntegration(); - when(integrationRepository.create(any())).thenAnswer(invocation -> invocation.getArgument(0)); + @Nested + class Create { - //When - Integration createdIntegration = service.create(integration); + @Test + @SneakyThrows + void should_create_integration() { + //Given + Integration integration = IntegrationFixture.anIntegration(); + when(integrationRepository.create(any())).thenAnswer(invocation -> invocation.getArgument(0)); - //Then - assertThat(createdIntegration).isEqualTo(integration); + //When + Integration createdIntegration = service.create(integration); + + //Then + assertThat(createdIntegration).isEqualTo(integration); + } + + @Test + void should_throw_when_technical_exception_occurs() throws TechnicalException { + // Given + var integration = IntegrationFixture.anIntegration(); + when(integrationRepository.create(any())).thenThrow(TechnicalException.class); + + // When + Throwable throwable = catchThrowable(() -> service.create(integration)); + + // Then + assertThat(throwable) + .isInstanceOf(TechnicalManagementException.class) + .hasMessage("Error when creating Integration: Test integration"); + } } - @Test - void should_throw_when_technical_exception_occurs() throws TechnicalException { - // Given - var integration = IntegrationFixture.anIntegration(); - when(integrationRepository.create(any())).thenThrow(TechnicalException.class); + @Nested + class FindById { + + @Test + @SneakyThrows + void should_return_the_found_integration() { + //Given + when(integrationRepository.findById(any())) + .thenAnswer(invocation -> + Optional.of(fixtures.repository.IntegrationFixture.anIntegration().toBuilder().id(invocation.getArgument(0)).build()) + ); + + //When + var result = service.findById("my-id"); + + //Then + assertThat(result) + .contains( + IntegrationFixture + .anIntegration() + .toBuilder() + .id("my-id") + .name("An integration") + .description("A description") + .provider("amazon") + .environmentId("environment-id") + .createdAt(Instant.parse("2020-02-03T20:22:02.00Z").atZone(ZoneId.systemDefault())) + .updatedAt(Instant.parse("2020-02-04T20:22:02.00Z").atZone(ZoneId.systemDefault())) + .build() + ); + } + + @Test + @SneakyThrows + void should_return_empty_when_not_found() { + //Given + when(integrationRepository.findById(any())).thenAnswer(invocation -> Optional.empty()); + + //When + var result = service.findById("my-id"); + + //Then + assertThat(result).isEmpty(); + } + + @Test + void should_throw_when_technical_exception_occurs() throws TechnicalException { + // Given + var integration = IntegrationFixture.anIntegration(); + when(integrationRepository.findById(any())).thenThrow(TechnicalException.class); - // When - Throwable throwable = catchThrowable(() -> service.create(integration)); + // When + Throwable throwable = catchThrowable(() -> service.findById("my-id")); - // Then - assertThat(throwable) - .isInstanceOf(TechnicalManagementException.class) - .hasMessage("Error when creating Integration: Test integration"); + // Then + assertThat(throwable) + .isInstanceOf(TechnicalManagementException.class) + .hasMessage("An error occurs while trying to find the integration: my-id"); + } } } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-standalone/gravitee-apim-rest-api-standalone-container/pom.xml b/gravitee-apim-rest-api/gravitee-apim-rest-api-standalone/gravitee-apim-rest-api-standalone-container/pom.xml index 2952e307e55..3582a8d5b71 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-standalone/gravitee-apim-rest-api-standalone-container/pom.xml +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-standalone/gravitee-apim-rest-api-standalone-container/pom.xml @@ -60,6 +60,12 @@ ${project.version} + + io.gravitee.apim.rest.api + gravitee-apim-rest-api-integration-controller + ${project.version} + + io.gravitee.apim.rest.api.management gravitee-apim-rest-api-management-rest diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-standalone/gravitee-apim-rest-api-standalone-container/src/main/java/io/gravitee/rest/api/standalone/spring/StandaloneConfiguration.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-standalone/gravitee-apim-rest-api-standalone-container/src/main/java/io/gravitee/rest/api/standalone/spring/StandaloneConfiguration.java index 35c40c1e105..28e2191402e 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-standalone/gravitee-apim-rest-api-standalone-container/src/main/java/io/gravitee/rest/api/standalone/spring/StandaloneConfiguration.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-standalone/gravitee-apim-rest-api-standalone-container/src/main/java/io/gravitee/rest/api/standalone/spring/StandaloneConfiguration.java @@ -15,6 +15,7 @@ */ package io.gravitee.rest.api.standalone.spring; +import io.gravitee.integration.controller.spring.IntegrationControllerConfiguration; import io.gravitee.node.api.Node; import io.gravitee.node.api.NodeMetadataResolver; import io.gravitee.node.container.NodeFactory; @@ -40,6 +41,7 @@ { RestManagementConfiguration.class, io.gravitee.rest.api.management.v2.rest.spring.RestManagementConfiguration.class, + IntegrationControllerConfiguration.class, RestPortalConfiguration.class, } ) diff --git a/gravitee-apim-rest-api/pom.xml b/gravitee-apim-rest-api/pom.xml index a166599c2be..cf9a438c682 100644 --- a/gravitee-apim-rest-api/pom.xml +++ b/gravitee-apim-rest-api/pom.xml @@ -39,6 +39,7 @@ gravitee-apim-rest-api-fetcher gravitee-apim-rest-api-model gravitee-apim-rest-api-idp + gravitee-apim-rest-api-integration-controller gravitee-apim-rest-api-repository gravitee-apim-rest-api-rest gravitee-apim-rest-api-service diff --git a/pom.xml b/pom.xml index af5fadea93e..385d8f305d9 100644 --- a/pom.xml +++ b/pom.xml @@ -62,9 +62,11 @@ 3.0.2 4.0.0 1.1.4 + 1.0.0-alpha.7 3.1.0 1.4.0 3.5.0 + 1.0.0-alpha.3 5.10.0 1.4.3 3.1.0