From 979e7b6dffdf2b954d22080a1e1ce5870976df64 Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 8 Nov 2022 11:32:49 +0100 Subject: [PATCH] AM-87 Update the certificate used by applications after a default cert renewal --- .../am/common/utils/ConstantKeys.java | 1 - .../gravitee/am/gateway/node/GatewayNode.java | 1 - .../management/service/tasks/TasksLoader.java | 83 +++++++++ .../service/tasks/TasksLoaderTest.java | 94 ++++++++++ .../standalone/node/ManagementNode.java | 9 +- .../src/main/resources/config/gravitee.yml | 5 + .../java/io/gravitee/am/model/SystemTask.java | 19 ++ .../management/api/SystemTaskRepository.java | 3 + .../api/JdbcSystemTaskRepository.java | 17 +- .../management/api/model/JdbcSystemTask.java | 9 + .../3.20.0-update-system-tasks-table.yml | 12 ++ .../src/main/resources/liquibase/master.yml | 2 + .../management/MongoSystemTaskRepository.java | 7 + .../internal/model/SystemTaskMongo.java | 24 +++ .../api/SystemTaskRepositoryTest.java | 57 +++++- .../io/gravitee/am/service/TaskManager.java | 109 +++++++++++ .../service/impl/CertificateServiceImpl.java | 25 ++- .../management/ManagementAuditBuilder.java | 5 + .../service/spring/ServiceConfiguration.java | 10 + .../am/service/tasks/AbstractTask.java | 60 ++++++ .../tasks/AssignSystemCertificate.java | 134 ++++++++++++++ .../AssignSystemCertificateDefinition.java | 94 ++++++++++ .../io/gravitee/am/service/tasks/Task.java | 51 +++++ .../am/service/tasks/TaskDefinition.java | 27 +++ .../gravitee/am/service/tasks/TaskType.java | 24 +++ .../am/service/CertificateServiceTest.java | 37 +++- .../gravitee/am/service/TaskManagerTest.java | 85 +++++++++ .../tasks/AssignSystemCertificateTest.java | 174 ++++++++++++++++++ 28 files changed, 1170 insertions(+), 8 deletions(-) create mode 100644 gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/tasks/TasksLoader.java create mode 100644 gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/tasks/TasksLoaderTest.java create mode 100644 gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_20_0/3.20.0-update-system-tasks-table.yml create mode 100644 gravitee-am-service/src/main/java/io/gravitee/am/service/TaskManager.java create mode 100644 gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AbstractTask.java create mode 100644 gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AssignSystemCertificate.java create mode 100644 gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AssignSystemCertificateDefinition.java create mode 100644 gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/Task.java create mode 100644 gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/TaskDefinition.java create mode 100644 gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/TaskType.java create mode 100644 gravitee-am-service/src/test/java/io/gravitee/am/service/TaskManagerTest.java create mode 100644 gravitee-am-service/src/test/java/io/gravitee/am/service/tasks/AssignSystemCertificateTest.java diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/utils/ConstantKeys.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/utils/ConstantKeys.java index 0448aaf522d..1cc8a9a134d 100644 --- a/gravitee-am-common/src/main/java/io/gravitee/am/common/utils/ConstantKeys.java +++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/utils/ConstantKeys.java @@ -20,7 +20,6 @@ * @author GraviteeSource Team */ public interface ConstantKeys { - // Common key. String CLIENT_CONTEXT_KEY = "client"; String USER_CONTEXT_KEY = "user"; diff --git a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/node/GatewayNode.java b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/node/GatewayNode.java index 139ea7ba037..a9118bab1c7 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/node/GatewayNode.java +++ b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/node/GatewayNode.java @@ -23,7 +23,6 @@ import io.gravitee.plugin.alert.AlertEventProducerManager; import org.springframework.beans.factory.annotation.Autowired; -import java.util.HashMap; import java.util.List; import java.util.Map; diff --git a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/tasks/TasksLoader.java b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/tasks/TasksLoader.java new file mode 100644 index 00000000000..d6f26e05779 --- /dev/null +++ b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/tasks/TasksLoader.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 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.am.management.service.tasks; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.gravitee.am.repository.management.api.CertificateRepository; +import io.gravitee.am.repository.management.api.SystemTaskRepository; +import io.gravitee.am.service.ApplicationService; +import io.gravitee.am.service.TaskManager; +import io.gravitee.am.service.tasks.AssignSystemCertificate; +import io.gravitee.am.service.tasks.AssignSystemCertificateDefinition; +import io.gravitee.am.service.tasks.TaskType; +import io.gravitee.common.component.LifecycleComponent; +import io.gravitee.common.service.AbstractService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +/** + * This class is used to read tasks from the SystemTask that may be scheduled. + * The goal is to ensure that scheduled task are executed if the Management instance + * restart before the task execution. + * + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +@Component +public class TasksLoader extends AbstractService implements LifecycleComponent { + + private final Logger logger = LoggerFactory.getLogger(TaskManager.class); + + @Autowired + private ObjectMapper mapper; + @Autowired + private TaskScheduler scheduler; + @Autowired + @Lazy + private SystemTaskRepository taskRepository; + @Autowired + @Lazy + private ApplicationService applicationService; + @Autowired + @Lazy + private CertificateRepository certificateRepository; + @Autowired + private TaskManager taskManager; + + @Override + protected void doStart() throws Exception { + super.doStart(); + this.logger.info("Load scheduled tasks"); + this.taskRepository.findByType(TaskType.SIMPLE.name()) + // currently only one kind of Simple tasks, so we simply filter on this value for safety + .filter(systemTask -> AssignSystemCertificate.class.getSimpleName().equals(systemTask.getKind())) + .map(systemTask -> { + final var assignSystemCert = new AssignSystemCertificate(systemTask.getId(), this.applicationService, this.certificateRepository, this.taskManager); + final var taskConfiguration = mapper.readValue(systemTask.getConfiguration(), AssignSystemCertificateDefinition.class); + assignSystemCert.setDefinition(taskConfiguration); + return assignSystemCert; + }) + .subscribe(task -> { + logger.debug("Reschedule {} task of type {} with definition {}", task.type(), task.kind(), task.getDefinition()); + task.registerScheduler(this.scheduler); + task.schedule(); + }); + } +} diff --git a/gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/tasks/TasksLoaderTest.java b/gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/tasks/TasksLoaderTest.java new file mode 100644 index 00000000000..0170cb4ea96 --- /dev/null +++ b/gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/tasks/TasksLoaderTest.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 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.am.management.service.tasks; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.gravitee.am.model.SystemTask; +import io.gravitee.am.repository.management.api.SystemTaskRepository; +import io.gravitee.am.service.tasks.AssignSystemCertificate; +import io.gravitee.am.service.tasks.TaskType; +import io.reactivex.Flowable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.scheduling.TaskScheduler; + +import javax.annotation.OverridingMethodsMustInvokeSuper; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.Random; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyChar; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static reactor.core.publisher.Mono.when; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +@RunWith(MockitoJUnitRunner.class) +public class TasksLoaderTest { + + @InjectMocks + private TasksLoader tasksLoader; + + @Mock + private SystemTaskRepository taskRepository; + + @Mock + private TaskScheduler scheduler; + + @Spy + private ObjectMapper mapper = new ObjectMapper(); + + @Test + public void shouldSchedule_SimpleTask() throws Exception { + var tasks = new ArrayList<>(); + final int numberOfSimpleTasks = new Random().nextInt(10); + for (int i = 0; i < numberOfSimpleTasks; ++i) { + var task = new SystemTask(); + task.setId("simple-task-"+i); + task.setType(TaskType.SIMPLE.name()); + task.setKind(AssignSystemCertificate.class.getSimpleName()); + task.setConfiguration("{\"delay\": 1 , \"unit\": \"MINUTES\"}"); + tasks.add(task); + } + + final int numberOfOtherTasks = new Random().nextInt(10); + for (int i = 0; i < numberOfOtherTasks; ++i) { + var task = new SystemTask(); + task.setId("other-task-"+i); + task.setType(TaskType.SIMPLE.name()); + task.setKind("other"); + tasks.add(task); + } + + doReturn(Flowable.fromIterable(tasks)).when(taskRepository).findByType(TaskType.SIMPLE.name()); + + tasksLoader.doStart(); + + verify(scheduler, times(numberOfSimpleTasks)).schedule(argThat(task -> task instanceof AssignSystemCertificate), any(Instant.class)); + } +} diff --git a/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-container/src/main/java/io/gravitee/am/management/standalone/node/ManagementNode.java b/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-container/src/main/java/io/gravitee/am/management/standalone/node/ManagementNode.java index 829858ac340..6475e472ac5 100644 --- a/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-container/src/main/java/io/gravitee/am/management/standalone/node/ManagementNode.java +++ b/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-container/src/main/java/io/gravitee/am/management/standalone/node/ManagementNode.java @@ -15,7 +15,13 @@ */ package io.gravitee.am.management.standalone.node; -import io.gravitee.am.management.service.*; +import io.gravitee.am.management.service.AlertTriggerManager; +import io.gravitee.am.management.service.AuditReporterManager; +import io.gravitee.am.management.service.CertificateManager; +import io.gravitee.am.management.service.EmailManager; +import io.gravitee.am.management.service.IdentityProviderManager; +import io.gravitee.am.management.service.InitializerService; +import io.gravitee.am.management.service.tasks.TasksLoader; import io.gravitee.common.component.LifecycleComponent; import io.gravitee.node.api.NodeMetadataResolver; import io.gravitee.node.jetty.node.JettyNode; @@ -70,6 +76,7 @@ public List> components() { components.add(AlertTriggerManager.class); components.add(AlertTriggerProviderManager.class); components.add(AlertEventProducerManager.class); + components.add(TasksLoader.class); return components; } diff --git a/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml b/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml index b63948898ef..8ef24ee2d13 100644 --- a/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml +++ b/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml @@ -231,6 +231,11 @@ domains: validity: 365 # Validity of the certificate algorithm: SHA256withRSA # Algorithm used to sign certificate name: cn=Gravitee.io # Certificate X.500 name +## Refresh section is used to define the delay between a system certificate renewal +## and the applications update to use this new certificate +# refresh: +# delay: 10 +# timeUnit: MINUTES # JWT used to generate signed token for management security mechanism (Bearer Token) and to verify emails jwt: diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/SystemTask.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/SystemTask.java index 191d4555ac1..a9c118f4430 100644 --- a/gravitee-am-model/src/main/java/io/gravitee/am/model/SystemTask.java +++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/SystemTask.java @@ -25,11 +25,14 @@ public class SystemTask { private String id; private String type; + private String kind; private String status; private String operationId; private Date createdAt; private Date updatedAt; + private String configuration; + public String getId() { return id; } @@ -46,6 +49,14 @@ public void setType(String type) { this.type = type; } + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + public String getStatus() { return status; } @@ -77,4 +88,12 @@ public String getOperationId() { public void setOperationId(String operationId) { this.operationId = operationId; } + + public String getConfiguration() { + return configuration; + } + + public void setConfiguration(String configuration) { + this.configuration = configuration; + } } diff --git a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/SystemTaskRepository.java b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/SystemTaskRepository.java index a3c6d0483bc..af5c927f1f6 100644 --- a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/SystemTaskRepository.java +++ b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/SystemTaskRepository.java @@ -17,6 +17,7 @@ import io.gravitee.am.model.SystemTask; import io.gravitee.am.repository.common.CrudRepository; +import io.reactivex.Flowable; import io.reactivex.Single; /** @@ -26,4 +27,6 @@ public interface SystemTaskRepository extends CrudRepository { Single updateIf(SystemTask item, String operationId); + + Flowable findByType(String type); } diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/management/api/JdbcSystemTaskRepository.java b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/management/api/JdbcSystemTaskRepository.java index a911627df59..bfc06cc12d1 100644 --- a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/management/api/JdbcSystemTaskRepository.java +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/management/api/JdbcSystemTaskRepository.java @@ -22,6 +22,7 @@ import io.gravitee.am.repository.jdbc.management.api.model.mapper.LocalDateConverter; import io.gravitee.am.repository.management.api.SystemTaskRepository; import io.reactivex.Completable; +import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.Single; import org.springframework.beans.factory.InitializingBean; @@ -49,6 +50,8 @@ public class JdbcSystemTaskRepository extends AbstractJdbcRepository implements public static final String COL_OPERATION_ID = "operation_id"; public static final String COL_CREATED_AT = "created_at"; public static final String COL_UPDATED_AT = "updated_at"; + public static final String COL_CONFIGURATION = "configuration"; + public static final String COL_KIND = "kind"; public static final String WHERE_SUFFIX = "_where"; private static final List columns = List.of( @@ -57,7 +60,9 @@ public class JdbcSystemTaskRepository extends AbstractJdbcRepository implements COL_STATUS, COL_OPERATION_ID, COL_CREATED_AT, - COL_UPDATED_AT + COL_UPDATED_AT, + COL_CONFIGURATION, + COL_KIND ); private String INSERT_STATEMENT; @@ -99,6 +104,8 @@ public Single create(SystemTask item) { insertSpec = addQuotedField(insertSpec, COL_OPERATION_ID, item.getOperationId(), String.class); insertSpec = addQuotedField(insertSpec, COL_CREATED_AT, dateConverter.convertTo(item.getCreatedAt(), null), LocalDateTime.class); insertSpec = addQuotedField(insertSpec, COL_UPDATED_AT, dateConverter.convertTo(item.getUpdatedAt(), null), LocalDateTime.class); + insertSpec = addQuotedField(insertSpec, COL_CONFIGURATION, item.getConfiguration(), String.class); + insertSpec = addQuotedField(insertSpec, COL_KIND, item.getKind(), String.class); Mono action = insertSpec.fetch().rowsUpdated(); return monoToSingle(action).flatMap((i) -> this.findById(item.getId()).toSingle()); @@ -121,6 +128,8 @@ public Single updateIf(SystemTask item, String operationId) { updateSpec = addQuotedField(updateSpec, COL_OPERATION_ID, item.getOperationId(), String.class); updateSpec = addQuotedField(updateSpec, COL_CREATED_AT, dateConverter.convertTo(item.getCreatedAt(), null), LocalDateTime.class); updateSpec = addQuotedField(updateSpec, COL_UPDATED_AT, dateConverter.convertTo(item.getUpdatedAt(), null), LocalDateTime.class); + updateSpec = addQuotedField(updateSpec, COL_CONFIGURATION, item.getConfiguration(), String.class); + updateSpec = addQuotedField(updateSpec, COL_KIND, item.getKind(), String.class); updateSpec = addQuotedField(updateSpec, COL_OPERATION_ID + WHERE_SUFFIX, operationId, String.class); Mono action = updateSpec.fetch().rowsUpdated(); @@ -134,4 +143,10 @@ public Completable delete(String id) { .matching(Query.query(where(COL_ID).is(id))).all(); return monoToCompletable(delete); } + + @Override + public Flowable findByType(String type) { + return fluxToFlowable(template.select(Query.query(where(COL_TYPE).is(type)), JdbcSystemTask.class)) + .map(this::toEntity); + } } diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/management/api/model/JdbcSystemTask.java b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/management/api/model/JdbcSystemTask.java index d86e2e98b09..24e14f17b3f 100644 --- a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/management/api/model/JdbcSystemTask.java +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/management/api/model/JdbcSystemTask.java @@ -38,6 +38,7 @@ public class JdbcSystemTask { @Column("updated_at") private LocalDateTime updatedAt; + private String configuration; public String getId() { return id; } @@ -85,4 +86,12 @@ public String getOperationId() { public void setOperationId(String operationId) { this.operationId = operationId; } + + public String getConfiguration() { + return configuration; + } + + public void setConfiguration(String configuration) { + this.configuration = configuration; + } } diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_20_0/3.20.0-update-system-tasks-table.yml b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_20_0/3.20.0-update-system-tasks-table.yml new file mode 100644 index 00000000000..8426518396c --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_20_0/3.20.0-update-system-tasks-table.yml @@ -0,0 +1,12 @@ +databaseChangeLog: + - changeSet: + id: 3.20.0-update-system-tasks-table + author: GraviteeSource Team + changes: + ## system task table + ##################### + - addColumn: + tableName: system_tasks + columns: + - column: { name: configuration, type: clob, constraints: { nullable: true } } + - column: { name: kind, type: varchar(128), constraints: { nullable: true } } diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/master.yml b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/master.yml index 7e8f5bfa1fb..19242f11665 100644 --- a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/master.yml +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/master.yml @@ -87,4 +87,6 @@ databaseChangeLog: - file: liquibase/changelogs/v3_20_0/3.20.0-update-certificates-table.yml - include: - file: liquibase/changelogs/v3_20_0/3.20.0-add-rate-limit-table.yml + - include: + - file: liquibase/changelogs/v3_20_0/3.20.0-update-system-tasks-table.yml diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoSystemTaskRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoSystemTaskRepository.java index f94da745885..54ec06433e2 100644 --- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoSystemTaskRepository.java +++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoSystemTaskRepository.java @@ -21,6 +21,7 @@ import io.gravitee.am.repository.management.api.SystemTaskRepository; import io.gravitee.am.repository.mongodb.management.internal.model.SystemTaskMongo; import io.reactivex.Completable; +import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.Single; @@ -38,6 +39,7 @@ @Component public class MongoSystemTaskRepository extends AbstractManagementMongoRepository implements SystemTaskRepository { protected static final String FIELD_OPERATION_ID = "operationId"; + protected static final String FIELD_TYPE = "type"; private MongoCollection systemTaskCollection; @PostConstruct @@ -77,4 +79,9 @@ public Single updateIf(SystemTask item, String operationId) { public Completable delete(String id) { return Completable.fromPublisher(systemTaskCollection.deleteOne(eq(FIELD_ID, id))); } + + @Override + public Flowable findByType(String type) { + return Flowable.fromPublisher(systemTaskCollection.find(eq(FIELD_TYPE, type))).map(SystemTaskMongo::convert); + } } diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/SystemTaskMongo.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/SystemTaskMongo.java index 53533189d93..d6557b13efd 100644 --- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/SystemTaskMongo.java +++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/SystemTaskMongo.java @@ -33,9 +33,12 @@ public class SystemTaskMongo extends Auditable { @BsonId private String id; private String type; + private String kind; private String status; private String operationId; + private String configuration; + public String getId() { return id; } @@ -52,6 +55,14 @@ public void setType(String type) { this.type = type; } + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + public String getStatus() { return status; } @@ -68,14 +79,24 @@ public void setOperationId(String operationId) { this.operationId = operationId; } + public String getConfiguration() { + return configuration; + } + + public void setConfiguration(String configuration) { + this.configuration = configuration; + } + public SystemTask convert() { SystemTask task = new SystemTask(); task.setId(getId()); task.setType(getType()); + task.setKind(getKind()); task.setStatus(getStatus()); task.setCreatedAt(getCreatedAt()); task.setUpdatedAt(getUpdatedAt()); task.setOperationId(getOperationId()); + task.setConfiguration(getConfiguration()); return task; } @@ -83,13 +104,16 @@ public static SystemTaskMongo convert(SystemTask task) { if (task == null) { return null; } + SystemTaskMongo taskMongo = new SystemTaskMongo(); taskMongo.setId(task.getId()); taskMongo.setType(task.getType()); + taskMongo.setKind(task.getKind()); taskMongo.setStatus(task.getStatus()); taskMongo.setCreatedAt(task.getCreatedAt()); taskMongo.setUpdatedAt(task.getUpdatedAt()); taskMongo.setOperationId(task.getOperationId()); + taskMongo.setConfiguration(task.getConfiguration()); return taskMongo; } diff --git a/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/management/api/SystemTaskRepositoryTest.java b/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/management/api/SystemTaskRepositoryTest.java index a8de07fc834..46751426dd5 100644 --- a/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/management/api/SystemTaskRepositoryTest.java +++ b/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/management/api/SystemTaskRepositoryTest.java @@ -19,10 +19,12 @@ import io.gravitee.am.repository.exceptions.TechnicalException; import io.gravitee.am.repository.management.AbstractManagementTest; import io.reactivex.observers.TestObserver; +import io.reactivex.subscribers.TestSubscriber; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import java.util.Date; +import java.util.Random; import java.util.UUID; /** @@ -48,6 +50,24 @@ public void testFindById() { assertEqualsTo(task, task.getOperationId(), testObserver); } + @Test + public void testFindById_WithConfig() { + // create task + SystemTask task = buildSystemTask(); + task.setConfiguration("value for config"); + SystemTask systemTaskCreated = taskRepository.create(task).blockingGet(); + + // fetch task + TestObserver testObserver = taskRepository.findById(systemTaskCreated.getId()).test(); + testObserver.awaitTerminalEvent(); + + testObserver.assertComplete(); + testObserver.assertNoErrors(); + assertEqualsTo(task, task.getOperationId(), testObserver); + testObserver.assertValue(s -> s.getConfiguration().equals(task.getConfiguration())); + + } + private void assertEqualsTo(SystemTask task, String expectedOpId, TestObserver testObserver) { testObserver.assertValue(s -> s.getId().equals(task.getId())); testObserver.assertValue(s -> s.getStatus().equals(task.getStatus())); @@ -56,10 +76,14 @@ private void assertEqualsTo(SystemTask task, String expectedOpId, TestObserver subscriber = taskRepository.findByType("type1").test(); + subscriber.awaitTerminalEvent(); + + subscriber.assertComplete(); + subscriber.assertNoErrors(); + subscriber.assertValueCount(nbOfType1); + + subscriber = taskRepository.findByType("type2").test(); + subscriber.awaitTerminalEvent(); + + subscriber.assertComplete(); + subscriber.assertNoErrors(); + subscriber.assertValueCount(nbOfType2); + } + @Test public void testDelete() { SystemTask task = buildSystemTask(); diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/TaskManager.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/TaskManager.java new file mode 100644 index 00000000000..f3c32e8f7cc --- /dev/null +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/TaskManager.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 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.am.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.gravitee.am.model.SystemTask; +import io.gravitee.am.model.SystemTaskStatus; +import io.gravitee.am.repository.management.api.SystemTaskRepository; +import io.gravitee.am.service.tasks.Task; +import io.gravitee.am.service.tasks.TaskDefinition; +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Single; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +import java.util.Date; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +@Component +public class TaskManager { + + private final Logger logger = LoggerFactory.getLogger(TaskManager.class); + + @Autowired + private ObjectMapper mapper; + @Autowired + @Lazy + private SystemTaskRepository taskRepository; + + @Autowired + private TaskScheduler scheduler; + + public void schedule(Task task) { + logger.debug("schedule {} task of type {}", task.type(), task.kind()); + + try { + final var systemTask = new SystemTask(); + systemTask.setId(task.getId()); + systemTask.setCreatedAt(new Date()); + systemTask.setUpdatedAt(systemTask.getCreatedAt()); + systemTask.setType(task.type().name()); + systemTask.setKind(task.kind()); + systemTask.setStatus(SystemTaskStatus.INITIALIZED.name()); + systemTask.setOperationId(task.kind() + "-" + systemTask.getId()); + systemTask.setConfiguration(mapper.writeValueAsString(task.getDefinition())); + + // persist the task to be able to reload it + // if the management restart before the task execution + this.taskRepository.create(systemTask) + .subscribe( + createdTask -> logger.debug("Task {} of type {} persisted", createdTask.getId(), task.kind()), + error -> logger.warn("Task of type {} can't be persisted", task.kind(), error) + ); + + task.registerScheduler(this.scheduler); + task.schedule(); + logger.debug("{} task of type {} with id {} scheduled: {}", task.type(), task.kind(), systemTask.getId(), task.getDefinition()); + + } catch (JsonProcessingException e) { + if (logger.isDebugEnabled()) { + logger.debug("Unable to schedule the task {} with definition {} due to: ", task.getId(), task.getDefinition(), e); + } else { + logger.error("Unable to schedule the task {} with definition {} due to : {}", task.getId(), task.getDefinition(), e.getMessage()); + } + } + } + + public Single isActiveTask(String taskId) { + return this.taskRepository.findById(taskId) + .isEmpty() + // as isEmpty return Single if the task doesn't exist, + // we have to apply a negation be able to return true if Maybe contains something + .map(value -> !value); + } + + public Completable remove(String taskId) { + return this.taskRepository.delete(taskId); + } + + public Completable markAsError(String taskId) { + return this.taskRepository.findById(taskId) + .flatMapSingle(task -> { + task.setStatus(SystemTaskStatus.FAILURE.name()); + return this.taskRepository.updateIf(task, task.getOperationId()); + }).ignoreElement(); + } +} diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/CertificateServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/CertificateServiceImpl.java index 1a6e33fdc84..d7e9dd78567 100644 --- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/CertificateServiceImpl.java +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/CertificateServiceImpl.java @@ -36,6 +36,7 @@ import io.gravitee.am.service.CertificatePluginService; import io.gravitee.am.service.CertificateService; import io.gravitee.am.service.EventService; +import io.gravitee.am.service.TaskManager; import io.gravitee.am.service.exception.AbstractManagementException; import io.gravitee.am.service.exception.CertificateNotFoundException; import io.gravitee.am.service.exception.CertificatePluginSchemaNotFoundException; @@ -45,6 +46,8 @@ import io.gravitee.am.service.model.UpdateCertificate; import io.gravitee.am.service.reporter.builder.AuditBuilder; import io.gravitee.am.service.reporter.builder.management.CertificateAuditBuilder; +import io.gravitee.am.service.tasks.AssignSystemCertificate; +import io.gravitee.am.service.tasks.AssignSystemCertificateDefinition; import io.gravitee.am.service.utils.CertificateTimeComparator; import io.reactivex.Completable; import io.reactivex.Flowable; @@ -63,6 +66,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.core.env.Environment; @@ -82,6 +86,7 @@ import java.util.Collections; import java.util.Date; import java.util.Optional; +import java.util.concurrent.TimeUnit; /** * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) @@ -90,6 +95,7 @@ @Component @Primary public class CertificateServiceImpl implements CertificateService { + public static final String DEFAULT_CERTIFICATE_PLUGIN = "pkcs12-am-certificate"; public static final String ECDSA = "ECDSA"; public static final String DEFAULT_CERT_CN_NAME = "cn=Gravitee.io"; @@ -127,7 +133,14 @@ public class CertificateServiceImpl implements CertificateService { @Autowired private Environment environment; - public static final String DEFAULT_CERTIFICATE_PLUGIN = "pkcs12-am-certificate"; + @Autowired + private TaskManager taskManager; + + @Value("${domains.certificates.default.refresh.delay:10}") + private int delay; + + @Value("${domains.certificates.default.refresh.timeUnit:MINUTES}") + private String timeUnit; @Override public Maybe findById(String id) { @@ -462,7 +475,15 @@ public Single rotate(String domain, User principal) { rotatedCertificate.setName("Default " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(now)); rotatedCertificate.setType(DEFAULT_CERTIFICATE_PLUGIN); rotatedCertificate.setConfiguration(generateCertificateConfiguration(domain, deprecatedCert.getConfiguration(), now)); - return create(domain, rotatedCertificate, true); + return create(domain, rotatedCertificate, true).map(newCertificate -> { + final var task = new AssignSystemCertificate(applicationService, certificateRepository, taskManager); + final var definition = new AssignSystemCertificateDefinition(domain, newCertificate.getId(), deprecatedCert.getId()); + definition.setDelay(delay); + definition.setUnit(TimeUnit.valueOf(timeUnit.toUpperCase())); + task.setDefinition(definition); + taskManager.schedule(task); + return newCertificate; + }); } else { // If no certificate has been found, we still create a default certificate return create(domain); diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/reporter/builder/management/ManagementAuditBuilder.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/reporter/builder/management/ManagementAuditBuilder.java index 93a78dac6dc..b1f7717f5ea 100644 --- a/gravitee-am-service/src/main/java/io/gravitee/am/service/reporter/builder/management/ManagementAuditBuilder.java +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/reporter/builder/management/ManagementAuditBuilder.java @@ -56,6 +56,11 @@ public T principal(User principal) { return (T) this; } + public T systemPrincipal() { + setActor(SYSTEM, SYSTEM, SYSTEM, SYSTEM, ReferenceType.PLATFORM, Platform.DEFAULT); + return (T) this; + } + private String getDisplayName(User user) { final String displayName = // display name diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/spring/ServiceConfiguration.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/spring/ServiceConfiguration.java index ca876b93414..82076a0a9fd 100644 --- a/gravitee-am-service/src/main/java/io/gravitee/am/service/spring/ServiceConfiguration.java +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/spring/ServiceConfiguration.java @@ -16,9 +16,12 @@ package io.gravitee.am.service.spring; import io.gravitee.am.service.spring.email.EmailConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; /** * @author David BRASSELY (david.brassely at graviteesource.com) @@ -28,4 +31,11 @@ @ComponentScan("io.gravitee.am.service") @Import({EmailConfiguration.class}) public class ServiceConfiguration { + + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("task-mng-"); + return scheduler; + } } diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AbstractTask.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AbstractTask.java new file mode 100644 index 00000000000..7c0b593a03c --- /dev/null +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AbstractTask.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 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.am.service.tasks; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.TaskScheduler; + +import java.time.Instant; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public abstract class AbstractTask implements Task { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @JsonIgnore + private TaskScheduler scheduler; + + private String id; + + protected AbstractTask(String id) { + this.id = id; + } + + @Override + public void registerScheduler(TaskScheduler scheduler) { + this.scheduler = scheduler; + } + + public final void schedule() { + if (this.scheduler != null) { + var def = this.getDefinition(); + this.scheduler.schedule(this, Instant.now().plus(def.getDelay(), def.getUnit().toChronoUnit())); + } else { + logger.warn("Trying to schedule a {} task before the registration of the TaskScheduler", this.kind()); + } + } + + @Override + public String getId() { + return id; + } +} diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AssignSystemCertificate.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AssignSystemCertificate.java new file mode 100644 index 00000000000..eed36cd16fe --- /dev/null +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AssignSystemCertificate.java @@ -0,0 +1,134 @@ +/** + * Copyright (C) 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.am.service.tasks; + +import io.gravitee.am.model.Application; +import io.gravitee.am.repository.management.api.CertificateRepository; +import io.gravitee.am.service.ApplicationService; +import io.gravitee.am.service.TaskManager; +import io.reactivex.Maybe; +import io.reactivex.Single; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class AssignSystemCertificate extends AbstractTask { + + private final Logger logger = LoggerFactory.getLogger(AssignSystemCertificate.class); + + private final ApplicationService applicationService; + + private final CertificateRepository certificateRepository; + + private final TaskManager taskManager; + + private AssignSystemCertificateDefinition configuration; + + public AssignSystemCertificate(ApplicationService applicationService, CertificateRepository certificateRepository, TaskManager taskManager) { + this(UUID.randomUUID().toString(), applicationService, certificateRepository, taskManager); + } + + public AssignSystemCertificate(String taskId, ApplicationService applicationService, CertificateRepository certificateRepository, TaskManager taskManager) { + super(taskId); + this.applicationService = applicationService; + this.certificateRepository = certificateRepository; + this.taskManager = taskManager; + } + + @Override + public AssignSystemCertificateDefinition getDefinition() { + return this.configuration; + } + + public void setDefinition(AssignSystemCertificateDefinition definition) { + this.configuration = definition; + } + + @Override + public TaskType type() { + return TaskType.SIMPLE; + } + + @Override + public boolean rescheduledOnError() { + return true; + } + + @Override + public void run() { + final var domainId = this.configuration.getDomainId(); + final var renewedCertificate = this.configuration.getRenewedCertificate(); + final var deprecatedCertificate = this.configuration.getDeprecatedCertificate(); + this.logger.debug("Start assign system certificate for domain {}. (deprecated certificate: {} / new certificate: {})", + domainId, deprecatedCertificate, renewedCertificate); + + this.taskManager.isActiveTask(getId()) + .flatMap(needProcessing -> { + if (needProcessing) { + return this.certificateRepository.findById(renewedCertificate) + .map(Optional::ofNullable) + .switchIfEmpty(Maybe.just(Optional.empty())) + .flatMapSingle(cert -> { + if (cert.isPresent()) { + return this.applicationService.findByDomain(domainId) + .flattenAsFlowable(apps -> apps) + .filter(app -> deprecatedCertificate.equals(app.getCertificate())) + .flatMapSingle(app -> { + Application toUpdateApp = new Application(app); + toUpdateApp.setCertificate(renewedCertificate); + return this.applicationService.update(toUpdateApp); + }) + .count(); + } else { + logger.warn("System certificate {} doesn't exist, unable to assigne it to applications of domain {}", renewedCertificate, domainId); + return Single.just(0L); + } + }); + } else { + return Single.just(-1L); + } + } + ) + .subscribe(apps -> { + if (apps >= 0) { + this.logger.info("System certificate {} assigned to {} applications of domain {}", renewedCertificate, apps, domainId); + this.taskManager.remove(this.getId()) + .doOnError(error -> logger.warn("Unable to delete task {}", this.getId(), error)) + .subscribe(); + } else { + this.logger.debug("Task already executed to assign system certificate for domain {}. (deprecated certificate: {} / new certificate: {})", + domainId, deprecatedCertificate, renewedCertificate); + } + }, + error -> { + logger.warn("System certificate {} can't be assigned to applications of domain {}: {}", renewedCertificate, domainId, error.getMessage()); + if (rescheduledOnError()) { + logger.info("Reschedule task {} to assign system certificate {}", getId(), renewedCertificate); + this.schedule(); + } else { + this.taskManager.markAsError(this.getId()) + .doOnError(e -> logger.warn("Unable to register error status for task {}", this.getId(), e)) + .subscribe(); + } + }); + } +} diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AssignSystemCertificateDefinition.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AssignSystemCertificateDefinition.java new file mode 100644 index 00000000000..9a1ed84eb9f --- /dev/null +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/AssignSystemCertificateDefinition.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 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.am.service.tasks; + +import java.util.concurrent.TimeUnit; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class AssignSystemCertificateDefinition implements TaskDefinition { + private String domainId; + + private String renewedCertificate; + + private String deprecatedCertificate; + + private long delay; + + private TimeUnit unit; + + public AssignSystemCertificateDefinition() { + } + + public AssignSystemCertificateDefinition(String domainId, String renewedCertificate, String deprecatedCertificate) { + this.domainId = domainId; + this.renewedCertificate = renewedCertificate; + this.deprecatedCertificate = deprecatedCertificate; + } + + public String getDomainId() { + return domainId; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public String getRenewedCertificate() { + return renewedCertificate; + } + + public void setRenewedCertificate(String renewedCertificate) { + this.renewedCertificate = renewedCertificate; + } + + public String getDeprecatedCertificate() { + return deprecatedCertificate; + } + + public void setDeprecatedCertificate(String deprecatedCertificate) { + this.deprecatedCertificate = deprecatedCertificate; + } + + public long getDelay() { + return delay; + } + + public void setDelay(long delay) { + this.delay = delay; + } + + public TimeUnit getUnit() { + return unit; + } + + public void setUnit(TimeUnit unit) { + this.unit = unit; + } + + @Override + public String toString() { + return "AssignSystemCertificateDefinition{" + + "domainId='" + domainId + '\'' + + ", renewedCertificate='" + renewedCertificate + '\'' + + ", deprecatedCertificate='" + deprecatedCertificate + '\'' + + ", delay=" + delay + + ", unit=" + unit + + '}'; + } +} diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/Task.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/Task.java new file mode 100644 index 00000000000..16de04ed815 --- /dev/null +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/Task.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 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.am.service.tasks; + +import org.springframework.scheduling.TaskScheduler; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public interface Task extends Runnable { + + String getId(); + + TaskType type(); + + Def getDefinition(); + default String kind() { + return this.getClass().getSimpleName(); + } + + /** + * @return true if the task have to be scheduled again on execution error + */ + boolean rescheduledOnError(); + + /** + * + * @param scheduler scheduler on which the task will be scheduled + */ + void registerScheduler(TaskScheduler scheduler); + + /** + * schedule the task using the TaskDefinition + */ + void schedule(); + +} \ No newline at end of file diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/TaskDefinition.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/TaskDefinition.java new file mode 100644 index 00000000000..5bb934c32f9 --- /dev/null +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/TaskDefinition.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 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.am.service.tasks; + +import java.util.concurrent.TimeUnit; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public interface TaskDefinition { + long getDelay(); + TimeUnit getUnit(); +} diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/TaskType.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/TaskType.java new file mode 100644 index 00000000000..5f0ad9ee0c2 --- /dev/null +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/tasks/TaskType.java @@ -0,0 +1,24 @@ +/** + * Copyright (C) 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.am.service.tasks; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public enum TaskType { + SIMPLE; // simple scheduled task persisted to be evaluated when service is restarting before the task execution +} diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/CertificateServiceTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/CertificateServiceTest.java index ee0f016001b..178c93e5e05 100644 --- a/gravitee-am-service/src/test/java/io/gravitee/am/service/CertificateServiceTest.java +++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/CertificateServiceTest.java @@ -27,12 +27,16 @@ import io.gravitee.am.repository.management.api.CertificateRepository; import io.gravitee.am.service.exception.TechnicalManagementException; import io.gravitee.am.service.impl.CertificateServiceImpl; +import io.gravitee.am.service.tasks.AssignSystemCertificate; +import io.gravitee.am.service.tasks.AssignSystemCertificateDefinition; +import io.gravitee.am.service.tasks.TaskType; import io.reactivex.Completable; import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.Single; import io.reactivex.observers.TestObserver; import io.reactivex.subscribers.TestSubscriber; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,6 +47,7 @@ import org.mockito.internal.util.io.IOUtil; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.core.env.Environment; +import org.springframework.test.util.ReflectionTestUtils; import java.io.IOException; import java.io.InputStream; @@ -50,6 +55,7 @@ import java.time.ZoneOffset; import java.util.Collections; import java.util.Date; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static io.gravitee.am.service.impl.CertificateServiceImpl.DEFAULT_CERTIFICATE_PLUGIN; @@ -99,6 +105,9 @@ public class CertificateServiceTest { @Mock private CertificatePluginService certificatePluginService; + @Mock + private TaskManager taskManager; + private final static String DOMAIN = "domain1"; @BeforeClass @@ -108,6 +117,12 @@ public static void readCertificateSchemaDefinition() throws Exception { certificateConfigurationWithOptions = loadResource("certificate-configuration-with-options.json"); } + @Before + public void initCertificateServiceValues() { + ReflectionTestUtils.setField(certificateService, "delay", 1); + ReflectionTestUtils.setField(certificateService, "timeUnit", "Minutes"); + } + private static String loadResource(String name) throws IOException { try (InputStream input = CertificateServiceTest.class.getClassLoader().getResourceAsStream(name)) { return IOUtil.readLines(input).stream().collect(Collectors.joining()); @@ -310,6 +325,7 @@ public void shouldRotate_defaultCertificate_Rsa() throws Exception { final Certificate certLatest = new Certificate(); certLatest.setSystem(true); + certLatest.setId("latest-cert-id"); certLatest.setDomain(DOMAIN); certLatest.setName("Cert-3"); certLatest.setConfiguration(certificateConfigurationWithOptions); @@ -330,7 +346,10 @@ public void shouldRotate_defaultCertificate_Rsa() throws Exception { initializeCertificatSettings(2048, "SHA256withRSA"); - when(certificateRepository.create(any())).thenReturn(Single.just(new Certificate())); + final Certificate renewedCert = new Certificate(); + renewedCert.setId("renewed-cert-id"); + when(certificateRepository.create(any())).thenReturn(Single.just(renewedCert)); + when(certificateRepository.update(any())).thenReturn(Single.just(certLatest)); when(eventService.create(any(Event.class))).thenReturn(Single.just(new Event())); CertificateSchema schema = new CertificateSchema(); @@ -347,6 +366,21 @@ public void shouldRotate_defaultCertificate_Rsa() throws Exception { && cert.getConfiguration().contains("[\"sig\"]") && cert.getConfiguration().contains("RS256") )); + verify(taskManager).schedule(argThat(task -> { + boolean result = task.kind().equals(AssignSystemCertificate.class.getSimpleName()); + result &= task.type().equals(TaskType.SIMPLE); + var definition = task.getDefinition(); + result &= definition.getDelay() == 1; + result &= definition.getUnit().equals(TimeUnit.MINUTES); + if (definition instanceof AssignSystemCertificateDefinition) { + result &= ((AssignSystemCertificateDefinition) definition).getDomainId().equals(DOMAIN); + result &= ((AssignSystemCertificateDefinition) definition).getDeprecatedCertificate().equals(certLatest.getId()); + result &= ((AssignSystemCertificateDefinition) definition).getRenewedCertificate() != null; + } else { + result = false; + } + return result; + })); } @Test @@ -383,5 +417,6 @@ public void shouldRotate_defaultCertificate_Rsa_firstDefault() throws Exception && !cert.getConfiguration().contains("[\"sig\"]") && !cert.getConfiguration().contains("RS256") )); + verify(taskManager, never()).schedule(any()); } } diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/TaskManagerTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/TaskManagerTest.java new file mode 100644 index 00000000000..98b672bbb94 --- /dev/null +++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/TaskManagerTest.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 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.am.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.gravitee.am.model.SystemTask; +import io.gravitee.am.repository.management.api.CertificateRepository; +import io.gravitee.am.repository.management.api.SystemTaskRepository; +import io.gravitee.am.service.tasks.AssignSystemCertificate; +import io.gravitee.am.service.tasks.AssignSystemCertificateDefinition; +import io.reactivex.Single; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.scheduling.TaskScheduler; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +@RunWith(MockitoJUnitRunner.class) +public class TaskManagerTest { + + @InjectMocks + private TaskManager taskManager; + + @Spy + private ObjectMapper mapper = new ObjectMapper(); + + @Mock + private SystemTaskRepository taskRepository; + + @Mock + private TaskScheduler scheduler; + + @Mock + private ApplicationService applicationService; + + @Mock + private CertificateRepository certificateRepository; + + @Test + public void shouldSchedule_task(){ + + final var task = new AssignSystemCertificate(applicationService, certificateRepository, taskManager); + final var definition = new AssignSystemCertificateDefinition("domainid", "nexcertid", "deprecatedcertid"); + definition.setDelay(1); + definition.setUnit(TimeUnit.HOURS); + task.setDefinition(definition); + + when(taskRepository.create(any())).thenReturn(Single.just(new SystemTask())); + + taskManager.schedule(task); + + verify(taskRepository).create(argThat(sysTask -> sysTask.getKind().equals(task.kind()) && + sysTask.getId().equals(task.getId()) && + sysTask.getType().equals(task.type().name()) )); + + verify(scheduler).schedule(argThat(scheduledTask -> scheduledTask instanceof AssignSystemCertificate), any(Instant.class)); + } +} diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/tasks/AssignSystemCertificateTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/tasks/AssignSystemCertificateTest.java new file mode 100644 index 00000000000..910eec03580 --- /dev/null +++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/tasks/AssignSystemCertificateTest.java @@ -0,0 +1,174 @@ +/** + * Copyright (C) 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.am.service.tasks; + +import io.gravitee.am.model.Application; +import io.gravitee.am.model.Certificate; +import io.gravitee.am.repository.management.api.CertificateRepository; +import io.gravitee.am.service.ApplicationService; +import io.gravitee.am.service.TaskManager; +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Single; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.scheduling.TaskScheduler; + +import java.time.Instant; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +@RunWith(MockitoJUnitRunner.class) +public class AssignSystemCertificateTest { + + @Mock + private ApplicationService applicationService; + + @Mock + private CertificateRepository certificateRepository; + + @Mock + private TaskManager taskManager; + + @Test + public void shouldNoProcessTask_MissingFromDB() { + when(taskManager.isActiveTask(anyString())).thenReturn(Single.just(false)); + + final var task = new AssignSystemCertificate(applicationService, certificateRepository, taskManager); + final var definition = new AssignSystemCertificateDefinition(); + definition.setUnit(TimeUnit.SECONDS); + definition.setDelay(10); + definition.setDeprecatedCertificate(UUID.randomUUID().toString()); + definition.setRenewedCertificate(UUID.randomUUID().toString()); + definition.setDomainId(UUID.randomUUID().toString()); + task.setDefinition(definition); + + task.run(); + + verify(taskManager).isActiveTask(eq(task.getId())); + verify(applicationService, never()).findByDomain(eq(definition.getDomainId())); + verify(applicationService, never()).update(any()); + verify(taskManager, never()).remove(any()); + } + + @Test + public void shouldNoProcessTask_ErrorOnApplicationSearch() { + when(taskManager.isActiveTask(anyString())).thenReturn(Single.just(true)); + when(applicationService.findByDomain(anyString())).thenReturn(Single.error(new Exception())); + when(certificateRepository.findById(anyString())).thenReturn(Maybe.just(new Certificate())); + + final var task = new AssignSystemCertificate(applicationService, certificateRepository, taskManager); + final var scheduler = mock(TaskScheduler.class); + task.registerScheduler(scheduler); + final var definition = new AssignSystemCertificateDefinition(); + definition.setUnit(TimeUnit.SECONDS); + definition.setDelay(10); + definition.setDeprecatedCertificate(UUID.randomUUID().toString()); + definition.setRenewedCertificate(UUID.randomUUID().toString()); + definition.setDomainId(UUID.randomUUID().toString()); + task.setDefinition(definition); + + task.run(); + + verify(taskManager).isActiveTask(eq(task.getId())); + verify(applicationService, never()).update(any()); + verify(taskManager, never()).remove(any()); + verify(scheduler).schedule(any(), any(Instant.class)); + } + + @Test + public void shouldProcessTask() { + final var task = new AssignSystemCertificate(applicationService, certificateRepository, taskManager); + final var scheduler = mock(TaskScheduler.class); + task.registerScheduler(scheduler); + final var definition = new AssignSystemCertificateDefinition(); + definition.setUnit(TimeUnit.SECONDS); + definition.setDelay(10); + definition.setDeprecatedCertificate(UUID.randomUUID().toString()); + definition.setRenewedCertificate(UUID.randomUUID().toString()); + definition.setDomainId(UUID.randomUUID().toString()); + task.setDefinition(definition); + + when(taskManager.isActiveTask(anyString())).thenReturn(Single.just(true)); + when(certificateRepository.findById(anyString())).thenReturn(Maybe.just(new Certificate())); + + var appToUpdate = new Application(); + appToUpdate.setId("appToUpdate"); + appToUpdate.setCertificate(definition.getDeprecatedCertificate()); + + var appToIgnore = new Application(); + appToIgnore.setId("appToIgnore"); + appToIgnore.setCertificate(UUID.randomUUID().toString()); + + var appToIgnoreNoCert = new Application(); + appToIgnoreNoCert.setId("appToIgnoreNoCert"); + appToIgnoreNoCert.setCertificate(null); + + when(applicationService.findByDomain(anyString())).thenReturn(Single.just(Set.of(appToUpdate, appToIgnore, appToIgnoreNoCert))); + when(applicationService.update(any())).thenReturn(Single.just(appToUpdate)); + when(taskManager.remove(anyString())).thenReturn(Completable.complete()); + + task.run(); + + verify(taskManager).isActiveTask(eq(task.getId())); + verify(applicationService).update(argThat(appli -> appli.getCertificate().equals(definition.getRenewedCertificate()) && + appli.getId().equals(appToUpdate.getId()))); + verify(applicationService, never()).update(argThat(appli -> !appli.getId().equals(appToUpdate.getId()))); + verify(taskManager).remove(anyString()); + verify(scheduler, never()).schedule(any(), any(Instant.class)); + } + + @Test + public void shouldNotProcessTask_UnknownCertificate() { + final var task = new AssignSystemCertificate(applicationService, certificateRepository, taskManager); + final var scheduler = mock(TaskScheduler.class); + task.registerScheduler(scheduler); + final var definition = new AssignSystemCertificateDefinition(); + definition.setUnit(TimeUnit.SECONDS); + definition.setDelay(10); + definition.setDeprecatedCertificate(UUID.randomUUID().toString()); + definition.setRenewedCertificate(UUID.randomUUID().toString()); + definition.setDomainId(UUID.randomUUID().toString()); + task.setDefinition(definition); + + when(taskManager.isActiveTask(anyString())).thenReturn(Single.just(true)); + when(certificateRepository.findById(anyString())).thenReturn(Maybe.empty()); + when(taskManager.remove(anyString())).thenReturn(Completable.complete()); + + task.run(); + + verify(taskManager).isActiveTask(eq(task.getId())); + verify(applicationService, never()).update(any()); + verify(taskManager).remove(anyString()); + verify(scheduler, never()).schedule(any(), any(Instant.class)); + } +}