diff --git a/src/main/java/net/geant/nmaas/orchestration/jobs/DomainCreationJob.java b/src/main/java/net/geant/nmaas/orchestration/jobs/DomainCreationJob.java index 3d5d751dc1c2fdf6e29f30d5c7f278305e43e37a..aaf0fd167f24d7bc422ba0cef7bf4724457e79db 100644 --- a/src/main/java/net/geant/nmaas/orchestration/jobs/DomainCreationJob.java +++ b/src/main/java/net/geant/nmaas/orchestration/jobs/DomainCreationJob.java @@ -45,9 +45,7 @@ public class DomainCreationJob extends WebhookJob { } Domain domain = domainService.findDomain(domainId).orElseThrow(() -> new MissingElementException(String.format("Domain with id: %d cannot be found", domainId))); - DomainView view = modelMapper.map(domain, DomainView.class); - - callWebhook(webhook, view); + callWebhook(webhook, modelMapper.map(domain, DomainView.class)); } catch (GeneralSecurityException e) { log.error("Failed to decrypt webhook with id {}", webhookId); throw new JobExecutionException("Failed webhook decryption"); diff --git a/src/main/java/net/geant/nmaas/orchestration/jobs/UserDomainAssignmentJob.java b/src/main/java/net/geant/nmaas/orchestration/jobs/UserDomainAssignmentJob.java new file mode 100644 index 0000000000000000000000000000000000000000..eaf449f99de8e6843d72af3dd4e1226775692e4d --- /dev/null +++ b/src/main/java/net/geant/nmaas/orchestration/jobs/UserDomainAssignmentJob.java @@ -0,0 +1,72 @@ +package net.geant.nmaas.orchestration.jobs; + +import lombok.extern.slf4j.Slf4j; +import net.geant.nmaas.orchestration.exceptions.WebServiceCommunicationException; +import net.geant.nmaas.portal.api.domain.DomainView; +import net.geant.nmaas.portal.api.domain.UserDomainAssignmentWebhookDto; +import net.geant.nmaas.portal.api.domain.UserView; +import net.geant.nmaas.portal.api.domain.WebhookEventDto; +import net.geant.nmaas.portal.api.exceptions.MissingElementException; +import net.geant.nmaas.portal.persistent.entity.Domain; +import net.geant.nmaas.portal.persistent.entity.Role; +import net.geant.nmaas.portal.persistent.entity.User; +import net.geant.nmaas.portal.persistent.entity.WebhookEventType; +import net.geant.nmaas.portal.service.DomainService; +import net.geant.nmaas.portal.service.UserService; +import net.geant.nmaas.portal.service.WebhookEventService; +import org.modelmapper.ModelMapper; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.security.GeneralSecurityException; + +@Slf4j +@Component +public class UserDomainAssignmentJob extends WebhookJob { + + private final DomainService domainService; + private final UserService userService; + + @Autowired + public UserDomainAssignmentJob(RestClient restClient, WebhookEventService webhookEventService, ModelMapper modelMapper, DomainService domainService, UserService userService) { + super(restClient, webhookEventService, modelMapper); + this.domainService = domainService; + this.userService = userService; + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + JobDataMap dataMap = context.getJobDetail().getJobDataMap(); + Long webhookId = dataMap.getLong("webhookId"); + Long domainId = dataMap.getLong("domainId"); + Long userId = dataMap.getLong("userId"); + Role role = Role.valueOf(dataMap.getString("role")); + String action = dataMap.getString("action"); + + try { + WebhookEventDto webhook = webhookEventService.getById(webhookId); + if (!WebhookEventType.USER_ASSIGNMENT.equals(webhook.getEventType())) { + log.warn("Webhook's event type with id {} has been updated. UserDomainAssignmentJob is abandoned", webhookId); + return; + } + Domain domain = domainService.findDomain(domainId).orElseThrow(() -> new MissingElementException(String.format("Domain with id: %d cannot be found", domainId))); + User user = userService.findById(userId).orElseThrow(() -> new MissingElementException(String.format("User with id: %d cannot be found", userId))); + + UserDomainAssignmentWebhookDto dto = new UserDomainAssignmentWebhookDto(modelMapper.map(user, UserView.class), modelMapper.map(domain, DomainView.class), role, action); + callWebhook(webhook, dto); + } catch (GeneralSecurityException e) { + log.error("Failed to decrypt webhook with id {}", webhookId); + throw new JobExecutionException("Failed webhook decryption"); + } catch (MissingElementException e) { + log.warn(e.getMessage() + " UserDomainAssignmentJob is abandoned"); + } catch (WebServiceCommunicationException e) { + log.error("Failed to communicate with external system for the webhoook of assignment of the user with id {} in the domain with id {}", userId, domainId); + throw new JobExecutionException("Failed communication with external system"); + } + } +} + diff --git a/src/main/java/net/geant/nmaas/portal/api/domain/UserDomainAssignmentWebhookDto.java b/src/main/java/net/geant/nmaas/portal/api/domain/UserDomainAssignmentWebhookDto.java new file mode 100644 index 0000000000000000000000000000000000000000..a414eda477cfa75da8908c9077b0a90b9a9a77cd --- /dev/null +++ b/src/main/java/net/geant/nmaas/portal/api/domain/UserDomainAssignmentWebhookDto.java @@ -0,0 +1,17 @@ +package net.geant.nmaas.portal.api.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import net.geant.nmaas.portal.persistent.entity.Role; + +@AllArgsConstructor +@Getter +@Setter +public class UserDomainAssignmentWebhookDto { + + private UserView user; + private DomainView domain; + private Role role; + private String action; +} diff --git a/src/main/java/net/geant/nmaas/portal/api/user/UsersController.java b/src/main/java/net/geant/nmaas/portal/api/user/UsersController.java index f2a6103f6e605ee2732e5afebaa42348d8e0a919..d7b8e2ccdb1783331a8d6144a1822662e477c137 100644 --- a/src/main/java/net/geant/nmaas/portal/api/user/UsersController.java +++ b/src/main/java/net/geant/nmaas/portal/api/user/UsersController.java @@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j; import net.geant.nmaas.notifications.MailAttributes; import net.geant.nmaas.notifications.NotificationEvent; import net.geant.nmaas.notifications.templates.MailType; +import net.geant.nmaas.orchestration.jobs.UserDomainAssignmentJob; import net.geant.nmaas.portal.api.domain.PasswordChange; import net.geant.nmaas.portal.api.domain.PasswordReset; import net.geant.nmaas.portal.api.domain.UserBase; @@ -22,11 +23,14 @@ import net.geant.nmaas.portal.persistent.entity.Domain; import net.geant.nmaas.portal.persistent.entity.Role; import net.geant.nmaas.portal.persistent.entity.User; import net.geant.nmaas.portal.persistent.entity.UserRole; +import net.geant.nmaas.portal.persistent.entity.WebhookEventType; +import net.geant.nmaas.portal.persistent.repositories.WebhookEventRepository; import net.geant.nmaas.portal.persistent.results.UserLoginDate; import net.geant.nmaas.portal.service.ApplicationInstanceService; import net.geant.nmaas.portal.service.DomainService; import net.geant.nmaas.portal.service.UserLoginRegisterService; import net.geant.nmaas.portal.service.UserService; +import net.geant.nmaas.scheduling.ScheduleManager; import net.geant.nmaas.utils.captcha.ValidateCaptcha; import org.apache.commons.lang3.StringUtils; import org.modelmapper.ModelMapper; @@ -52,6 +56,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; +import java.time.LocalDateTime; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; @@ -100,6 +105,8 @@ public class UsersController { private final UserLoginRegisterService userLoginService; private final ApplicationInstanceService instanceService; + private final WebhookEventRepository webhookEventRepository; + private final ScheduleManager scheduleManager; @Autowired public UsersController(UserService userService, @@ -109,7 +116,9 @@ public class UsersController { JWTTokenService jwtTokenService, ApplicationEventPublisher eventPublisher, UserLoginRegisterService userLoginService, - ApplicationInstanceService instanceService) { + ApplicationInstanceService instanceService, + WebhookEventRepository webhookEventRepository, + ScheduleManager scheduleManager) { this.userService = userService; this.domainService = domainService; this.modelMapper = modelMapper; @@ -118,6 +127,8 @@ public class UsersController { this.eventPublisher = eventPublisher; this.userLoginService = userLoginService; this.instanceService = instanceService; + this.webhookEventRepository = webhookEventRepository; + this.scheduleManager = scheduleManager; } @GetMapping("/users") @@ -508,6 +519,9 @@ public class UsersController { try { domainService.addMemberRole(domain.getId(), user.getId(), role); + if (!domain.equals(globalDomain)){ + webhookEventRepository.findIdByEventType(WebhookEventType.USER_ASSIGNMENT).forEach(id -> scheduleManager.createOneTimeJob(UserDomainAssignmentJob.class, "UserDomainAssignmentJobCreate_" + id + "_user" + user.getId()+ "_domain" + domain.getId()+"_" + LocalDateTime.now(), Map.of("webhookId", id, "domainId", domain.getId(), "userId", user.getId(), "role", role.name(), "action", "create"))); + } final User adminUser = userService.findByUsername(principal.getName()).orElseThrow(() -> new ObjectNotFoundException(USER_NOT_FOUND_ERROR_MESSAGE)); final String adminRoles = getRoleAsString(adminUser.getRoles()); @@ -541,6 +555,10 @@ public class UsersController { domainService.removeMemberRole(domain.getId(), user.getId(), role); final User adminUser = userService.findByUsername(principal.getName()).orElseThrow(() -> new ObjectNotFoundException(USER_NOT_FOUND_ERROR_MESSAGE)); final String adminRoles = getRoleAsString(adminUser.getRoles()); + final Domain globalDomain = domainService.getGlobalDomain().orElse(null); + if (!domain.equals(globalDomain)){ + webhookEventRepository.findIdByEventType(WebhookEventType.USER_ASSIGNMENT).forEach(id -> scheduleManager.createOneTimeJob(UserDomainAssignmentJob.class, "UserDomainAssignmentJobDelete_" + id + "_user" + user.getId()+ "_domain" + domain.getId()+"_" + LocalDateTime.now(), Map.of("webhookId", id, "domainId", domain.getId(), "userId", user.getId(), "role", role.name(), "action", "delete"))); + } log.info(String.format("User [%s] with role [%s] removed role [%s] of user name [%s] in domain [%d].", principal.getName(), diff --git a/src/test/java/net/geant/nmaas/portal/api/market/UsersControllerTest.java b/src/test/java/net/geant/nmaas/portal/api/market/UsersControllerTest.java index 4b70031a859db6b376533e8fc2289470f652365d..08ee1aab9e26f09fd7e0f61c35a7c3a9b75b66c8 100644 --- a/src/test/java/net/geant/nmaas/portal/api/market/UsersControllerTest.java +++ b/src/test/java/net/geant/nmaas/portal/api/market/UsersControllerTest.java @@ -1,6 +1,8 @@ package net.geant.nmaas.portal.api.market; import com.google.common.collect.ImmutableSet; +import net.geant.nmaas.orchestration.jobs.DomainCreationJob; +import net.geant.nmaas.orchestration.jobs.UserDomainAssignmentJob; import net.geant.nmaas.portal.api.domain.PasswordChange; import net.geant.nmaas.portal.api.domain.UserRequest; import net.geant.nmaas.portal.api.domain.UserRoleView; @@ -14,13 +16,23 @@ import net.geant.nmaas.portal.exceptions.ObjectNotFoundException; import net.geant.nmaas.portal.persistent.entity.Domain; import net.geant.nmaas.portal.persistent.entity.Role; import net.geant.nmaas.portal.persistent.entity.User; +import net.geant.nmaas.portal.persistent.entity.WebhookEvent; +import net.geant.nmaas.portal.persistent.entity.WebhookEventType; +import net.geant.nmaas.portal.persistent.repositories.WebhookEventRepository; import net.geant.nmaas.portal.service.ApplicationInstanceService; import net.geant.nmaas.portal.service.DomainService; import net.geant.nmaas.portal.service.UserLoginRegisterService; import net.geant.nmaas.portal.service.UserService; +import net.geant.nmaas.scheduling.ScheduleManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.internal.verification.VerificationModeFactory; import org.modelmapper.ModelMapper; +import org.quartz.JobListener; +import org.quartz.ListenerManager; +import org.quartz.Matcher; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.crypto.password.PasswordEncoder; @@ -30,15 +42,19 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; public class UsersControllerTest { @@ -69,9 +85,15 @@ public class UsersControllerTest { private final ApplicationInstanceService instanceService = mock(ApplicationInstanceService.class); + WebhookEventRepository webhookEventRepository = mock(WebhookEventRepository.class); + Scheduler scheduler = mock(Scheduler.class); + ListenerManager listenerManager = mock(ListenerManager.class); + ScheduleManager scheduleManager; + @BeforeEach public void setup(){ - usersController = new UsersController(userService, domainService, modelMapper, passwordEncoder, jwtTokenService, eventPublisher, userLoginService, instanceService); + scheduleManager = new ScheduleManager(scheduler); + usersController = new UsersController(userService, domainService, modelMapper, passwordEncoder, jwtTokenService, eventPublisher, userLoginService, instanceService, webhookEventRepository, scheduleManager); User tester = new User("tester", true, "test123", DOMAIN, Role.ROLE_USER); tester.setId(1L); User admin = new User("testadmin", true, "testadmin123", DOMAIN, Role.ROLE_SYSTEM_ADMIN); @@ -329,12 +351,31 @@ public class UsersControllerTest { } @Test - public void shouldAddUserRoleToCustomDomain(){ + public void shouldAddUserRoleToCustomDomain() throws SchedulerException { UserRoleView userRole = new UserRoleView(); userRole.setDomainId(DOMAIN.getId()); userRole.setRole(Role.ROLE_USER); + // Setup webhook event + WebhookEvent webhookEvent = new WebhookEvent(1L, "webhook", "https://example.com/webhook", WebhookEventType.USER_ASSIGNMENT, null, null); + when(webhookEventRepository.findIdByEventType(WebhookEventType.USER_ASSIGNMENT)) + .thenReturn(Stream.of(1L)); + when(webhookEventRepository.findById(1L)) + .thenReturn(Optional.of(webhookEvent)); + when(scheduler.getListenerManager()).thenReturn(listenerManager); + doNothing().when(listenerManager).addJobListener(any(JobListener.class), any(Matcher.class)); + usersController.addUserRole(DOMAIN.getId(), userList.get(0).getId(), userRole, principal); verify(domainService, times(1)).addMemberRole(DOMAIN.getId(), userList.get(0).getId(), userRole.getRole()); + // Verify webhook job was scheduled + verify(scheduler, VerificationModeFactory.times(1)).scheduleJob( + argThat(jobDetail -> + jobDetail.getKey().getName().startsWith("UserDomainAssignmentJobCreate_1_user"+userList.get(0).getId()+"_domain2") && + jobDetail.getJobClass().equals(UserDomainAssignmentJob.class) + ), + argThat(trigger -> + trigger.getKey().getName().startsWith("UserDomainAssignmentJobCreate_1_user"+userList.get(0).getId()+"_domain2") + ) + ); } @Test @@ -345,6 +386,8 @@ public class UsersControllerTest { when(domainService.findDomain(GLOBAL_DOMAIN.getId())).thenReturn(Optional.of(GLOBAL_DOMAIN)); usersController.addUserRole(GLOBAL_DOMAIN.getId(), userList.get(0).getId(), userRole, principal); verify(domainService, times(1)).addMemberRole(GLOBAL_DOMAIN.getId(), userList.get(0).getId(), userRole.getRole()); + + verifyNoInteractions(scheduler); } @Test @@ -396,10 +439,29 @@ public class UsersControllerTest { } @Test - public void shouldRemoveUserRole(){ + public void shouldRemoveUserRole() throws SchedulerException { String userRole = "ROLE_SYSTEM_ADMIN"; + // Setup webhook event + WebhookEvent webhookEvent = new WebhookEvent(1L, "webhook", "https://example.com/webhook", WebhookEventType.USER_ASSIGNMENT, null, null); + when(webhookEventRepository.findIdByEventType(WebhookEventType.USER_ASSIGNMENT)) + .thenReturn(Stream.of(1L)); + when(webhookEventRepository.findById(1L)) + .thenReturn(Optional.of(webhookEvent)); + when(scheduler.getListenerManager()).thenReturn(listenerManager); + doNothing().when(listenerManager).addJobListener(any(JobListener.class), any(Matcher.class)); + usersController.removeUserRole(DOMAIN.getId(), userList.get(0).getId(), userRole, principal); verify(domainService, times(1)).removeMemberRole(DOMAIN.getId(), userList.get(0).getId(), Role.ROLE_SYSTEM_ADMIN); + // Verify webhook job was scheduled + verify(scheduler, VerificationModeFactory.times(1)).scheduleJob( + argThat(jobDetail -> + jobDetail.getKey().getName().startsWith("UserDomainAssignmentJobDelete_1_user"+userList.get(0).getId()+"_domain2") && + jobDetail.getJobClass().equals(UserDomainAssignmentJob.class) + ), + argThat(trigger -> + trigger.getKey().getName().startsWith("UserDomainAssignmentJobDelete_1_user"+userList.get(0).getId()+"_domain2") + ) + ); } @Test