From 0cc6f5bc0890c317e4214acb92d2ac6d2acbb12a Mon Sep 17 00:00:00 2001 From: Siarhei Hrabko <45555481+grabsefx@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:59:24 +0300 Subject: [PATCH] EPMRPP-93599 implement create invitations endpoint (#2062) --- build.gradle | 23 ++- project-properties.gradle | 3 +- reportportal-common-api | 2 +- .../permissions/InvitationPermission.java | 152 +++++++++++++++ .../auth/permissions/Permissions.java | 2 + .../core/user/CreateUserHandler.java | 11 -- .../core/user/UserInvitationHandler.java | 37 ++++ .../core/user/impl/CreateUserHandlerImpl.java | 69 ------- .../user/impl/UserInvitationHandlerImpl.java | 176 ++++++++++++++++++ .../ws/controller/InvitationController.java | 61 ++++++ .../ws/controller/UserController.java | 13 -- .../converters/UserCreationBidConverter.java | 48 ----- src/main/resources/openapi/config.json | 6 +- .../user/impl/CreateUserHandlerImplTest.java | 21 ++- .../controller/InvitationControllerTest.java | 114 ++++++++++++ .../ws/controller/UserControllerTest.java | 3 + .../UserCreationBidConverterTest.java | 52 ------ 17 files changed, 581 insertions(+), 212 deletions(-) create mode 100644 src/main/java/com/epam/ta/reportportal/auth/permissions/InvitationPermission.java create mode 100644 src/main/java/com/epam/ta/reportportal/core/user/UserInvitationHandler.java create mode 100644 src/main/java/com/epam/ta/reportportal/core/user/impl/UserInvitationHandlerImpl.java create mode 100644 src/main/java/com/epam/ta/reportportal/ws/controller/InvitationController.java delete mode 100644 src/main/java/com/epam/ta/reportportal/ws/converter/converters/UserCreationBidConverter.java create mode 100644 src/test/java/com/epam/ta/reportportal/ws/controller/InvitationControllerTest.java delete mode 100644 src/test/java/com/epam/ta/reportportal/ws/converter/converters/UserCreationBidConverterTest.java diff --git a/build.gradle b/build.gradle index d60cee8ea2..86ea69e57a 100644 --- a/build.gradle +++ b/build.gradle @@ -69,11 +69,11 @@ dependencies { implementation 'com.epam.reportportal:commons' implementation 'com.epam.reportportal:plugin-api:5.11.1' } else { - implementation 'com.github.reportportal:commons-dao:69da66d' - implementation 'com.github.reportportal:commons:7f23a5f' + implementation 'com.github.reportportal:commons-dao:d4f1c47' + implementation 'com.github.reportportal:commons:59af813d4' implementation 'com.github.reportportal:plugin-api:d1c0f0e' } - swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli:3.0.57' + swaggerCodegen 'io.swagger.codegen.v3:swagger-codegen-cli:3.0.61' implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -210,7 +210,20 @@ swaggerSources { components = ['apis'] jvmArgs = ['--add-opens=java.base/java.util=ALL-UNNAMED'] outputDir = file("$buildDir/generated") - wipeOutputDir = true + wipeOutputDir = false + + } + } + + users { + inputFile = file("$rootDir/reportportal-common-api/api/openapi/apis/users.yaml") + code { + language = 'spring' + configFile = file("$rootDir/src/main/resources/openapi/config.json") + components = ['apis'] + jvmArgs = ['--add-opens=java.base/java.util=ALL-UNNAMED'] + outputDir = file("$buildDir/generated") + wipeOutputDir = false } } } @@ -241,7 +254,7 @@ test { } swaggerSources.organizations.code.dependsOn updateApiSubmodule -compileJava.dependsOn swaggerSources.organizations.code +compileJava.dependsOn swaggerSources.organizations.code, swaggerSources.users.code publish.dependsOn build publish.mustRunAfter build checkCommitNeeded.dependsOn removeScripts diff --git a/project-properties.gradle b/project-properties.gradle index 79144d7110..f11a7126a4 100755 --- a/project-properties.gradle +++ b/project-properties.gradle @@ -15,7 +15,7 @@ project.ext { isDebugMode = System.getProperty("DEBUG", "false") == "true" releaseMode = project.hasProperty("releaseMode") scriptsUrl = commonScriptsUrl + (releaseMode ? '5.11.0' : 'develop') - migrationsUrl = migrationsScriptsUrl + (releaseMode ? '5.11.0' : 'feature/orgs') + migrationsUrl = migrationsScriptsUrl + (releaseMode ? '5.11.0' : 'EPMRPP-93599-bid') //TODO refactor with archive download testScriptsSrc = [ (migrationsUrl + '/migrations/0_extensions.up.sql') : 'V001__extensions.sql', @@ -76,6 +76,7 @@ project.ext { (migrationsUrl + '/migrations/201_drop_table_onboarding.up.sql') : 'V201__drop_table_onboarding.sql', (migrationsUrl + '/migrations/202_update_project_table.up.sql') : 'V202__update_project_table.up.sql', (migrationsUrl + '/migrations/203_user_table_extend.up.sql') : 'V203__user_table_extend.up.sql', + (migrationsUrl + '/migrations/204_bid_extend_metadata.up.sql') : 'V204_bid_extend_metadata.up.sql', ] excludeTests = ['**/entity/**', '**/aop/**', diff --git a/reportportal-common-api b/reportportal-common-api index bd462f609d..8c672b638b 160000 --- a/reportportal-common-api +++ b/reportportal-common-api @@ -1 +1 @@ -Subproject commit bd462f609d7f823b3eea574df614cce36eff454d +Subproject commit 8c672b638bec1ee0db41d77e6266daef58beae3c diff --git a/src/main/java/com/epam/ta/reportportal/auth/permissions/InvitationPermission.java b/src/main/java/com/epam/ta/reportportal/auth/permissions/InvitationPermission.java new file mode 100644 index 0000000000..2771dae7fd --- /dev/null +++ b/src/main/java/com/epam/ta/reportportal/auth/permissions/InvitationPermission.java @@ -0,0 +1,152 @@ +/* + * Copyright 2024 EPAM Systems + * + * 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 com.epam.ta.reportportal.auth.permissions; + +import static com.epam.reportportal.rules.commons.validation.BusinessRule.expect; +import static com.epam.reportportal.rules.commons.validation.Suppliers.formattedSupplier; +import static com.epam.reportportal.rules.exception.ErrorType.ACCESS_DENIED; +import static com.epam.ta.reportportal.commons.Predicates.isNull; +import static com.epam.ta.reportportal.commons.Predicates.not; +import static com.epam.ta.reportportal.entity.organization.OrganizationRole.MANAGER; +import static com.epam.ta.reportportal.entity.organization.OrganizationRole.MEMBER; +import static com.epam.ta.reportportal.entity.project.ProjectUtils.findUserConfigByLogin; +import static java.util.function.Predicate.isEqual; + +import com.epam.reportportal.api.model.CreateInvitationRequest; +import com.epam.reportportal.api.model.UserOrgInfoWithProjects; +import com.epam.reportportal.api.model.UserProjectInfo; +import com.epam.reportportal.rules.commons.validation.BusinessRule; +import com.epam.reportportal.rules.exception.ErrorType; +import com.epam.reportportal.rules.exception.ReportPortalException; +import com.epam.ta.reportportal.commons.ReportPortalUser; +import com.epam.ta.reportportal.commons.ReportPortalUser.OrganizationDetails; +import com.epam.ta.reportportal.dao.ProjectRepository; +import com.epam.ta.reportportal.dao.ProjectUserRepository; +import com.epam.ta.reportportal.dao.organization.OrganizationRepositoryCustom; +import com.epam.ta.reportportal.dao.organization.OrganizationUserRepository; +import com.epam.ta.reportportal.entity.organization.Organization; +import com.epam.ta.reportportal.entity.project.Project; +import com.epam.ta.reportportal.entity.user.OrganizationUser; +import com.epam.ta.reportportal.entity.user.ProjectUser; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Component; + +@Component("invitationPermission") +@LookupPermission({"invitationAllowed"}) +public class InvitationPermission implements Permission { + + private final OrganizationUserRepository organizationUserRepository; + + private final OrganizationRepositoryCustom organizationRepositoryCustom; + private final ProjectRepository projectRepository; + private final ProjectUserRepository projectUserRepository; + + @Autowired + InvitationPermission(OrganizationUserRepository organizationUserRepository, + OrganizationRepositoryCustom organizationRepositoryCustom, + ProjectRepository projectRepository, ProjectUserRepository projectUserRepository) { + this.organizationUserRepository = organizationUserRepository; + this.organizationRepositoryCustom = organizationRepositoryCustom; + this.projectRepository = projectRepository; + this.projectUserRepository = projectUserRepository; + } + + @Override + public boolean isAllowed(Authentication authentication, Object invitationRequest) { + if (!authentication.isAuthenticated()) { + return false; + } + OAuth2Authentication oauth = (OAuth2Authentication) authentication; + ReportPortalUser rpUser = (ReportPortalUser) oauth.getUserAuthentication().getPrincipal(); + BusinessRule.expect(rpUser, Objects::nonNull).verify(ErrorType.ACCESS_DENIED); + + List orgs = + ((CreateInvitationRequest) invitationRequest).getOrganizations(); + + expect(CollectionUtils.isEmpty(orgs), isEqual(false)) + .verify(ACCESS_DENIED, "Only administrators allowed to invite users on instance level"); + + rpUser.setOrganizationDetails(new HashMap<>()); + orgs.forEach(orgInfo -> checkOrganizationAccess(rpUser, orgInfo)); + + return true; + + } + + private void checkOrganizationAccess(ReportPortalUser rpUser, UserOrgInfoWithProjects orgInfo) { + + var org = organizationRepositoryCustom.findById(orgInfo.getId()) + .orElseThrow( + () -> new ReportPortalException(ErrorType.ORGANIZATION_NOT_FOUND, orgInfo.getId())); + + var orgUser = organizationUserRepository + .findByUserIdAndOrganization_Id(rpUser.getUserId(), org.getId()) + .orElseThrow(() -> new ReportPortalException(ErrorType.ACCESS_DENIED, orgInfo.getId())); + + fillOrganizationDetails(rpUser, org, orgUser); + + if (CollectionUtils.isEmpty(orgInfo.getProjects())) { + expect(orgUser.getOrganizationRole().sameOrHigherThan(MANAGER), isEqual(true)) + .verify(ACCESS_DENIED, + formattedSupplier("You are not manager of organization '{}'", org.getName()) + ); + } + + orgInfo.getProjects() + .forEach(assigningPrj -> checkProjectAccess(rpUser, orgUser, assigningPrj)); + + } + + private void checkProjectAccess(ReportPortalUser rpUser, OrganizationUser orgUser, + UserProjectInfo assigningPrj) { + + Project prj = projectRepository.findById(assigningPrj.getId()) + .orElseThrow( + () -> new ReportPortalException(ErrorType.ORGANIZATION_NOT_FOUND, + assigningPrj.getId())); + if (orgUser.getOrganizationRole().equals(MEMBER)) { + ProjectUser projectUser = findUserConfigByLogin(prj, rpUser.getUsername()); + expect(projectUser, not(isNull())).verify(ACCESS_DENIED, + formattedSupplier("'{}' is not your project", prj.getName()) + ); + } + + } + + + private void fillOrganizationDetails(ReportPortalUser rpUser, Organization organization, + OrganizationUser orgUser) { + final Map organizationDetailsMap = HashMap.newHashMap(2); + + var orgDetails = new OrganizationDetails( + organization.getId(), + organization.getName(), + orgUser.getOrganizationRole(), + new HashMap<>() + ); + rpUser.getOrganizationDetails() + .put(organization.getId().toString(), orgDetails); + } + +} diff --git a/src/main/java/com/epam/ta/reportportal/auth/permissions/Permissions.java b/src/main/java/com/epam/ta/reportportal/auth/permissions/Permissions.java index 7dd73ab666..c1717ff79d 100644 --- a/src/main/java/com/epam/ta/reportportal/auth/permissions/Permissions.java +++ b/src/main/java/com/epam/ta/reportportal/auth/permissions/Permissions.java @@ -44,4 +44,6 @@ private Permissions() { public static final String ALLOWED_TO_VIEW_PROJECT = "hasPermission(#projectKey.toLowerCase(), 'allowedToViewProject')" + "||" + IS_ADMIN; + public static final String INVITATION_ALLOWED = IS_ADMIN + "||" + + "hasPermission(#invitationRequest, 'invitationAllowed')"; } diff --git a/src/main/java/com/epam/ta/reportportal/core/user/CreateUserHandler.java b/src/main/java/com/epam/ta/reportportal/core/user/CreateUserHandler.java index d4142416ad..cb7079dd0f 100644 --- a/src/main/java/com/epam/ta/reportportal/core/user/CreateUserHandler.java +++ b/src/main/java/com/epam/ta/reportportal/core/user/CreateUserHandler.java @@ -18,8 +18,6 @@ import com.epam.ta.reportportal.commons.ReportPortalUser; import com.epam.ta.reportportal.model.YesNoRS; -import com.epam.ta.reportportal.model.user.CreateUserBidRS; -import com.epam.ta.reportportal.model.user.CreateUserRQ; import com.epam.ta.reportportal.model.user.CreateUserRQConfirm; import com.epam.ta.reportportal.model.user.CreateUserRQFull; import com.epam.ta.reportportal.model.user.CreateUserRS; @@ -53,15 +51,6 @@ public interface CreateUserHandler { */ CreateUserRS createUser(CreateUserRQConfirm request, String uuid); - /** - * Create user bid (send invitation) - * - * @param request Create Request - * @param username Username/User that creates the request - * @param userRegURL User registration url - * @return Operation result - */ - CreateUserBidRS createUserBid(CreateUserRQ request, ReportPortalUser username, String userRegURL); /** * Create restore password bid diff --git a/src/main/java/com/epam/ta/reportportal/core/user/UserInvitationHandler.java b/src/main/java/com/epam/ta/reportportal/core/user/UserInvitationHandler.java new file mode 100644 index 0000000000..0462948385 --- /dev/null +++ b/src/main/java/com/epam/ta/reportportal/core/user/UserInvitationHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 EPAM Systems + * + * 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 com.epam.ta.reportportal.core.user; + +import com.epam.reportportal.api.model.CreateInvitationRequest; +import com.epam.reportportal.api.model.Invitation; +import com.epam.ta.reportportal.commons.ReportPortalUser; + + +public interface UserInvitationHandler { + + /** + * Create user bid (send invitation) + * + * @param request Create Request + * @param username Username/User that creates the request + * @param userRegURL User registration url + * @return Operation result + */ + Invitation createUserInvitation(CreateInvitationRequest request, ReportPortalUser username, + String userRegURL); + +} diff --git a/src/main/java/com/epam/ta/reportportal/core/user/impl/CreateUserHandlerImpl.java b/src/main/java/com/epam/ta/reportportal/core/user/impl/CreateUserHandlerImpl.java index f1c641acf8..1947659712 100644 --- a/src/main/java/com/epam/ta/reportportal/core/user/impl/CreateUserHandlerImpl.java +++ b/src/main/java/com/epam/ta/reportportal/core/user/impl/CreateUserHandlerImpl.java @@ -74,7 +74,6 @@ import com.epam.ta.reportportal.util.email.MailServiceFactory; import com.epam.ta.reportportal.ws.converter.builders.UserBuilder; import com.epam.ta.reportportal.ws.converter.converters.RestorePasswordBidConverter; -import com.epam.ta.reportportal.ws.converter.converters.UserCreationBidConverter; import com.epam.reportportal.rules.exception.ErrorType; import com.epam.ta.reportportal.ws.reporting.OperationCompletionRS; import com.google.common.collect.Maps; @@ -299,78 +298,10 @@ private CreateUserRQFull convertToCreateRequest(CreateUserRQConfirm request, createUserRQFull.setEmail(request.getEmail()); createUserRQFull.setFullName(request.getFullName()); createUserRQFull.setPassword(request.getPassword()); - createUserRQFull.setDefaultProject(bid.getProjectName()); createUserRQFull.setAccountRole(UserRole.USER.name()); - createUserRQFull.setProjectRole(bid.getRole()); return createUserRQFull; } - @Override - public CreateUserBidRS createUserBid(CreateUserRQ request, ReportPortalUser loggedInUser, - String emailURL) { - - final Project defaultProject = getProjectHandler.get(normalizeId(request.getDefaultProject())); - - expect(userRepository.existsById(loggedInUser.getUserId()), BooleanUtils::isTrue).verify( - USER_NOT_FOUND, - loggedInUser.getUsername() - ); - - Integration integration = - getIntegrationHandler.getEnabledByProjectIdOrGlobalAndIntegrationGroup( - defaultProject.getId(), - IntegrationGroupEnum.NOTIFICATION - ) - .orElseThrow(() -> new ReportPortalException(EMAIL_CONFIGURATION_IS_INCORRECT, - "Please configure email server in ReportPortal settings." - )); - - final String normalizedEmail = normalizeEmail(request.getEmail()); - request.setEmail(normalizedEmail); - - if (loggedInUser.getUserRole() != UserRole.ADMINISTRATOR) { - ProjectUser projectUser = findUserConfigByLogin(defaultProject, loggedInUser.getUsername()); - expect(projectUser, not(isNull())).verify(ACCESS_DENIED, - formattedSupplier("'{}' is not your project", defaultProject.getName()) - ); - // TODO FIX ROLE CHECK - expect(projectUser.getProjectRole(), Predicate.isEqual(ProjectRole.EDITOR)).verify( - ACCESS_DENIED); - } - - request.setRole(forName(request.getRole()).orElseThrow( - () -> new ReportPortalException(ROLE_NOT_FOUND, request.getRole())).name()); - - UserCreationBid bid = UserCreationBidConverter.TO_USER.apply(request, defaultProject); - bid.setMetadata(getUserCreationBidMetadata()); - bid.setInvitingUser(userRepository.getById(loggedInUser.getUserId())); - try { - userCreationBidRepository.save(bid); - } catch (Exception e) { - throw new ReportPortalException("Error while user creation bid registering.", e); - } - - StringBuilder emailLink = - new StringBuilder(emailURL).append("/ui/#registration?uuid=").append(bid.getUuid()); - emailExecutorService.execute(() -> emailServiceFactory.getEmailService(integration, false) - .sendCreateUserConfirmationEmail("User registration confirmation", - new String[] {bid.getEmail()}, emailLink.toString())); - - eventPublisher.publishEvent( - new CreateInvitationLinkEvent(loggedInUser.getUserId(), loggedInUser.getUsername(), - defaultProject.getId())); - - CreateUserBidRS response = new CreateUserBidRS(); - String msg = "Bid for user creation with email '" + request.getEmail() - + "' is successfully registered. Confirmation info will be send on provided email. " - + "Expiration: 1 day."; - - response.setMessage(msg); - response.setBid(bid.getUuid()); - response.setBackLink(emailLink.toString()); - return response; - } - private Metadata getUserCreationBidMetadata() { final Map meta = Maps.newHashMapWithExpectedSize(1); meta.put(BID_TYPE, INTERNAL_BID_TYPE); diff --git a/src/main/java/com/epam/ta/reportportal/core/user/impl/UserInvitationHandlerImpl.java b/src/main/java/com/epam/ta/reportportal/core/user/impl/UserInvitationHandlerImpl.java new file mode 100644 index 0000000000..0e4dfe48a9 --- /dev/null +++ b/src/main/java/com/epam/ta/reportportal/core/user/impl/UserInvitationHandlerImpl.java @@ -0,0 +1,176 @@ +/* + * Copyright 2024 EPAM Systems + * + * 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 com.epam.ta.reportportal.core.user.impl; + +import static com.epam.reportportal.api.model.Invitation.StatusEnum.PENDING; +import static com.epam.reportportal.rules.commons.validation.BusinessRule.expect; +import static com.epam.reportportal.rules.commons.validation.Suppliers.formattedSupplier; +import static com.epam.reportportal.rules.exception.ErrorType.BAD_REQUEST_ERROR; +import static com.epam.reportportal.rules.exception.ErrorType.USER_ALREADY_EXISTS; +import static com.epam.ta.reportportal.commons.Predicates.equalTo; + +import com.epam.reportportal.api.model.CreateInvitationRequest; +import com.epam.reportportal.api.model.Invitation; +import com.epam.reportportal.api.model.UserOrgInfoWithProjects; +import com.epam.reportportal.rules.exception.ReportPortalException; +import com.epam.ta.reportportal.commons.ReportPortalUser; +import com.epam.ta.reportportal.core.integration.GetIntegrationHandler; +import com.epam.ta.reportportal.core.user.UserInvitationHandler; +import com.epam.ta.reportportal.dao.UserCreationBidRepository; +import com.epam.ta.reportportal.dao.UserRepository; +import com.epam.ta.reportportal.entity.Metadata; +import com.epam.ta.reportportal.entity.user.User; +import com.epam.ta.reportportal.entity.user.UserCreationBid; +import com.epam.ta.reportportal.util.UserUtils; +import com.epam.ta.reportportal.util.email.MailServiceFactory; +import com.google.common.collect.Maps; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import lombok.extern.log4j.Log4j2; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +public class UserInvitationHandlerImpl implements UserInvitationHandler { + + public static final String BID_TYPE = "type"; + public static final String INTERNAL_BID_TYPE = "internal"; + + private final UserCreationBidRepository userCreationBidRepository; + private final ThreadPoolTaskExecutor emailExecutorService; + private final MailServiceFactory emailServiceFactory; + private final UserRepository userRepository; + private final GetIntegrationHandler getIntegrationHandler; + private final ApplicationEventPublisher eventPublisher; + + public UserInvitationHandlerImpl(UserCreationBidRepository userCreationBidRepository, + ThreadPoolTaskExecutor emailExecutorService, MailServiceFactory emailServiceFactory, + UserRepository userRepository, GetIntegrationHandler getIntegrationHandler, + ApplicationEventPublisher eventPublisher) { + this.userCreationBidRepository = userCreationBidRepository; + this.emailExecutorService = emailExecutorService; + this.emailServiceFactory = emailServiceFactory; + this.userRepository = userRepository; + this.getIntegrationHandler = getIntegrationHandler; + this.eventPublisher = eventPublisher; + } + + public Invitation createUserInvitation(CreateInvitationRequest request, ReportPortalUser rpUser, + String baseUrl) { + log.debug("User '{}' is trying to create invitation for user '{}'", + rpUser.getUsername(), + request.getEmail()); + + validateInvitationRequest(request); + + var now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + + /* TODO: waiting for requirements + Integration integration = getIntegrationHandler + .getEnabledByProjectIdOrGlobalAndIntegrationGroup(defaultProject.getId(), + IntegrationGroupEnum.NOTIFICATION) + .orElseThrow(() -> new ReportPortalException(EMAIL_CONFIGURATION_IS_INCORRECT, + "Please configure email server in ReportPortal settings." + )); + */ + + UserCreationBid userBid = new UserCreationBid(); + var user = userRepository.getById(rpUser.getUserId()); + userBid.setUuid(UUID.randomUUID().toString()); + userBid.setEmail(request.getEmail().trim()); + userBid.setInvitingUser(user); + userBid.setMetadata(getUserCreationBidMetadata(request.getOrganizations())); + + try { + userCreationBidRepository.save(userBid); + } catch (Exception e) { + throw new ReportPortalException("Error while user creation bid registering.", e); + } + + StringBuilder emailLink = new StringBuilder(baseUrl) + .append("/ui/#registration?uuid=") + .append(userBid.getUuid()); + + var response = new Invitation(); + response.setCreatedAt(now); + response.setExpiresAt(now.plus(1, ChronoUnit.DAYS)); + response.setId(UUID.fromString(userBid.getUuid())); + response.setLink(emailLink.toString()); + response.setStatus(PENDING); + + /* + emailExecutorService.execute(() -> emailServiceFactory.getEmailService(integration, false) + .sendCreateUserConfirmationEmail("User registration confirmation", + new String[]{bid.getEmail()}, emailLink.toString())); + eventPublisher.publishEvent( + new CreateInvitationLinkEvent(rpUser.getUserId(), rpUser.getUsername(), + defaultProject.getId())); + */ + + return response; + } + + private void validateInvitationRequest(CreateInvitationRequest request) { + expect(UserUtils.isEmailValid(request.getEmail().trim()), equalTo(true)) + .verify(BAD_REQUEST_ERROR, formattedSupplier("email='{}'", request.getEmail())); + + Optional emailUser = userRepository.findByEmail(request.getEmail().trim()); + + expect(emailUser.isPresent(), equalTo(Boolean.FALSE)).verify(USER_ALREADY_EXISTS, + formattedSupplier("email='{}'", request.getEmail())); + } + + private Metadata getUserCreationBidMetadata(List organizations) { + final Map meta = Maps.newHashMapWithExpectedSize(1); + meta.put(BID_TYPE, INTERNAL_BID_TYPE); + meta.put("organizations", getOrganizationsMetadata(organizations)); + meta.put("projects", getProjectsMetadata(organizations)); + + return new Metadata(meta); + } + + private List> getProjectsMetadata( + List organizations) { + return organizations.stream() + .flatMap(org -> org.getProjects().stream()) + .map(project -> { + Map obj = new HashMap<>(); + obj.put("id", project.getId()); + obj.put("role", project.getProjectRole().name()); + return obj; + }).toList(); + } + + + private List> getOrganizationsMetadata( + List organizations) { + return organizations.stream() + .map(org -> { + Map obj = new HashMap<>(); + obj.put("id", org.getId()); + obj.put("role", org.getOrgRole().name()); + return obj; + }).toList(); + } +} diff --git a/src/main/java/com/epam/ta/reportportal/ws/controller/InvitationController.java b/src/main/java/com/epam/ta/reportportal/ws/controller/InvitationController.java new file mode 100644 index 0000000000..68b636472e --- /dev/null +++ b/src/main/java/com/epam/ta/reportportal/ws/controller/InvitationController.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024 EPAM Systems + * + * 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 com.epam.ta.reportportal.ws.controller; + +import static com.epam.ta.reportportal.auth.permissions.Permissions.INVITATION_ALLOWED; +import static com.epam.ta.reportportal.core.launch.util.LinkGenerator.composeBaseUrl; +import static org.springframework.http.HttpStatus.OK; + +import com.epam.reportportal.api.InvitationApi; +import com.epam.reportportal.api.model.CreateInvitationRequest; +import com.epam.reportportal.api.model.Invitation; +import com.epam.ta.reportportal.core.user.UserInvitationHandler; +import javax.servlet.http.HttpServletRequest; +import javax.transaction.Transactional; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class InvitationController extends BaseController implements InvitationApi { + + private final UserInvitationHandler userInvitationHandler; + private final HttpServletRequest httpServletRequest; + + public InvitationController(UserInvitationHandler userInvitationHandler, + HttpServletRequest httpServletRequest) { + this.userInvitationHandler = userInvitationHandler; + this.httpServletRequest = httpServletRequest; + } + + @Transactional + @Override + @PreAuthorize(INVITATION_ALLOWED) + public ResponseEntity postInvitations(CreateInvitationRequest invitationRequest) { + var rpUser = getLoggedUser(); + + // TODO: remove invitationRequest.getOrganizations duplicates? + + var response = userInvitationHandler.createUserInvitation(invitationRequest, rpUser, + composeBaseUrl(httpServletRequest)); + + return ResponseEntity + .status(OK) + .body(response); + } + +} diff --git a/src/main/java/com/epam/ta/reportportal/ws/controller/UserController.java b/src/main/java/com/epam/ta/reportportal/ws/controller/UserController.java index 7242adf0e6..780e797b5d 100644 --- a/src/main/java/com/epam/ta/reportportal/ws/controller/UserController.java +++ b/src/main/java/com/epam/ta/reportportal/ws/controller/UserController.java @@ -45,8 +45,6 @@ import com.epam.ta.reportportal.model.ModelViews; import com.epam.ta.reportportal.model.YesNoRS; import com.epam.ta.reportportal.model.user.ChangePasswordRQ; -import com.epam.ta.reportportal.model.user.CreateUserBidRS; -import com.epam.ta.reportportal.model.user.CreateUserRQ; import com.epam.ta.reportportal.model.user.CreateUserRQConfirm; import com.epam.ta.reportportal.model.user.CreateUserRQFull; import com.epam.ta.reportportal.model.user.CreateUserRS; @@ -129,17 +127,6 @@ public CreateUserRS createUserByAdmin(@RequestBody @Validated CreateUserRQFull r return createUserMessageHandler.createUserByAdmin(rq, currentUser, composeBaseUrl(request)); } - @Transactional - @PostMapping(value = "/bid") - @ResponseStatus(CREATED) - @PreAuthorize("(hasPermission(#createUserRQ.getDefaultProject(), 'allowedToEditProject')) || hasRole('ADMINISTRATOR')") - @Operation(summary = "Register invitation for user who will be created") - public CreateUserBidRS createUserBid(@RequestBody @Validated CreateUserRQ createUserRQ, - @AuthenticationPrincipal ReportPortalUser currentUser, HttpServletRequest request) { - return createUserMessageHandler.createUserBid(createUserRQ, currentUser, - composeBaseUrl(request) - ); - } @PostMapping(value = "/registration") @ResponseStatus(CREATED) diff --git a/src/main/java/com/epam/ta/reportportal/ws/converter/converters/UserCreationBidConverter.java b/src/main/java/com/epam/ta/reportportal/ws/converter/converters/UserCreationBidConverter.java deleted file mode 100644 index b217522e81..0000000000 --- a/src/main/java/com/epam/ta/reportportal/ws/converter/converters/UserCreationBidConverter.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2019 EPAM Systems - * - * 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 com.epam.ta.reportportal.ws.converter.converters; - -import com.epam.ta.reportportal.commons.EntityUtils; -import com.epam.ta.reportportal.entity.project.Project; -import com.epam.ta.reportportal.entity.user.UserCreationBid; -import com.epam.ta.reportportal.model.user.CreateUserRQ; -import com.google.common.base.Preconditions; -import java.util.UUID; -import java.util.function.BiFunction; - -/** - * Converts internal DB model to DTO - * - * @author Pavel Bortnik - */ -public final class UserCreationBidConverter { - - private UserCreationBidConverter() { - //static only - } - - public static final BiFunction TO_USER = - (request, project) -> { - Preconditions.checkNotNull(request); - UserCreationBid user = new UserCreationBid(); - user.setUuid(UUID.randomUUID().toString()); - user.setEmail(EntityUtils.normalizeId(request.getEmail().trim())); - user.setProjectName(project.getName()); - user.setRole(request.getRole()); - return user; - }; -} diff --git a/src/main/resources/openapi/config.json b/src/main/resources/openapi/config.json index c348ca80e7..81bfc747f0 100644 --- a/src/main/resources/openapi/config.json +++ b/src/main/resources/openapi/config.json @@ -6,12 +6,12 @@ "OffsetDateTime": "Instant" }, "importMappings": { - "java.time.OffsetDateTime": "java.time.Instant", - "ReportPortalUser": "com.epam.ta.reportportal.commons.ReportPortalUser" + "java.time.OffsetDateTime": "java.time.Instant" }, "interfaceOnly": true, "useTags": true, "java21": true, "library": "spring-boot", - "hideGenerationTimestamp": true + "hideGenerationTimestamp": true, + "skipOverwrite": true } diff --git a/src/test/java/com/epam/ta/reportportal/core/user/impl/CreateUserHandlerImplTest.java b/src/test/java/com/epam/ta/reportportal/core/user/impl/CreateUserHandlerImplTest.java index f044408b5a..87ffcf14ae 100644 --- a/src/test/java/com/epam/ta/reportportal/core/user/impl/CreateUserHandlerImplTest.java +++ b/src/test/java/com/epam/ta/reportportal/core/user/impl/CreateUserHandlerImplTest.java @@ -51,6 +51,7 @@ import com.epam.ta.reportportal.model.user.CreateUserRQFull; import com.epam.reportportal.rules.exception.ErrorType; import java.util.Optional; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -64,6 +65,7 @@ * @author Ihar Kahadouski */ @ExtendWith(MockitoExtension.class) +@Disabled("To be deleted") class CreateUserHandlerImplTest { @Mock @@ -222,6 +224,7 @@ void createByAdminWithExistedEmailUppercase() { } @Test + @Disabled("To be deleted") void createUserBid() { final ReportPortalUser rpUser = getRpUser("admin", UserRole.ADMINISTRATOR, OrganizationRole.MANAGER, ProjectRole.VIEWER, 1L); @@ -246,7 +249,7 @@ void createUserBid() { request.setEmail(email); request.setRole(role.name()); - handler.createUserBid(request, rpUser, "emailUrl"); + //handler.createUserBid(request, rpUser, "emailUrl"); final ArgumentCaptor bidCaptor = ArgumentCaptor.forClass(UserCreationBid.class); @@ -254,9 +257,9 @@ void createUserBid() { final UserCreationBid bid = bidCaptor.getValue(); - assertEquals(projectName, bid.getProjectName()); + //assertEquals(projectName, bid.getProjectName()); assertEquals(email, bid.getEmail()); - assertEquals(role.name(), bid.getRole()); + //assertEquals(role.name(), bid.getRole()); assertNotNull(bid.getMetadata()); assertEquals(INTERNAL_BID_TYPE, String.valueOf(bid.getMetadata().getMetadata().get(BID_TYPE))); @@ -272,12 +275,12 @@ void CreateUserBidOnNotExistedProject() { CreateUserRQ request = new CreateUserRQ(); request.setDefaultProject("not_exists"); - final ReportPortalException exception = assertThrows(ReportPortalException.class, + /* final ReportPortalException exception = assertThrows(ReportPortalException.class, () -> handler.createUserBid(request, rpUser, "emailUrl") ); assertEquals("Project 'not_exists' not found. Did you use correct project name?", exception.getMessage() - ); + );*/ } @Test @@ -297,7 +300,7 @@ void createUserWithoutBid() { @Test void createAlreadyExistedUser() { final UserCreationBid creationBid = new UserCreationBid(); - creationBid.setProjectName("project"); + // creationBid.setProjectName("project"); when(userCreationBidRepository.findByUuidAndType("uuid", INTERNAL_BID_TYPE)).thenReturn( Optional.of(creationBid)); when(userRepository.findByLogin("test")).thenReturn(Optional.of(new User())); @@ -314,7 +317,7 @@ void createAlreadyExistedUser() { @Test public void createUserWithIncorrectLogin() { final UserCreationBid creationBid = new UserCreationBid(); - creationBid.setProjectName("project"); + // creationBid.setProjectName("project"); when(userCreationBidRepository.findByUuidAndType("uuid", INTERNAL_BID_TYPE)).thenReturn( Optional.of(creationBid)); when(userRepository.findByLogin("##$%/")).thenReturn(Optional.empty()); @@ -331,7 +334,7 @@ public void createUserWithIncorrectLogin() { @Test void createUserWithIncorrectEmail() { final UserCreationBid bid = new UserCreationBid(); - bid.setProjectName(TEST_PROJECT_KEY); + // bid.setProjectName(TEST_PROJECT_KEY); when(userCreationBidRepository.findByUuidAndType("uuid", INTERNAL_BID_TYPE)).thenReturn( Optional.of(bid)); when(userRepository.findByLogin("test")).thenReturn(Optional.empty()); @@ -350,7 +353,7 @@ void createUserWithIncorrectEmail() { @Test void createUserWithExistedEmail() { final UserCreationBid bid = new UserCreationBid(); - bid.setProjectName(TEST_PROJECT_KEY); + // bid.setProjectName(TEST_PROJECT_KEY); when(userCreationBidRepository.findByUuidAndType("uuid", INTERNAL_BID_TYPE)).thenReturn( Optional.of(bid)); when(userRepository.findByLogin("test")).thenReturn(Optional.empty()); diff --git a/src/test/java/com/epam/ta/reportportal/ws/controller/InvitationControllerTest.java b/src/test/java/com/epam/ta/reportportal/ws/controller/InvitationControllerTest.java new file mode 100644 index 0000000000..1c1353f192 --- /dev/null +++ b/src/test/java/com/epam/ta/reportportal/ws/controller/InvitationControllerTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2024 EPAM Systems + * + * 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 com.epam.ta.reportportal.ws.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.epam.reportportal.api.model.CreateInvitationRequest; +import com.epam.reportportal.api.model.Invitation; +import com.epam.reportportal.api.model.Invitation.StatusEnum; +import com.epam.reportportal.api.model.UserOrgInfo.OrgRoleEnum; +import com.epam.reportportal.api.model.UserOrgInfoWithProjects; +import com.epam.reportportal.api.model.UserProjectInfo; +import com.epam.reportportal.api.model.UserProjectInfo.ProjectRoleEnum; +import com.epam.ta.reportportal.ws.BaseMvcTest; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +class InvitationControllerTest extends BaseMvcTest { + + private static final String INVITATIONS_ENDPOINT = "/invitations"; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void createInvitationByAdmin() throws Exception { + List organizations = new ArrayList<>(); + List projects = new ArrayList<>(); + UserOrgInfoWithProjects orgInfo = new UserOrgInfoWithProjects(); + UserProjectInfo projectInfo = new UserProjectInfo() + .id(1L) + .projectRole(ProjectRoleEnum.VIEWER); + + projects.add(projectInfo); + + orgInfo.setId(1L); + orgInfo.setOrgRole(OrgRoleEnum.MANAGER); + orgInfo.setProjects(projects); + + organizations.add(orgInfo); + + var rq = new CreateInvitationRequest(); + + rq.setEmail("invitation@example.com"); + rq.setOrganizations(organizations); + + var result = mockMvc.perform(MockMvcRequestBuilders.post(INVITATIONS_ENDPOINT) + .content(objectMapper.writeValueAsBytes(rq)) + .contentType(APPLICATION_JSON) + .with(token(oAuthHelper.getSuperadminToken()))) + .andExpect(status().isOk()) + .andReturn() + .getResponse().getContentAsString(); + + var invitation = objectMapper.readValue(result, Invitation.class); + + assertNotNull(invitation); + + assertEquals(StatusEnum.PENDING, invitation.getStatus()); + + } + + + @Test + void createInvitationNotEnoughPermissions() throws Exception { + List organizations = new ArrayList<>(); + List projects = new ArrayList<>(); + UserOrgInfoWithProjects orgInfo = new UserOrgInfoWithProjects(); + UserProjectInfo projectInfo = new UserProjectInfo() + .id(1L) + .projectRole(ProjectRoleEnum.VIEWER); + + projects.add(projectInfo); + + orgInfo.setId(1L); + orgInfo.setOrgRole(OrgRoleEnum.MANAGER); + orgInfo.setProjects(projects); + + organizations.add(orgInfo); + + var rq = new CreateInvitationRequest(); + + rq.setEmail("invitation@example.com"); + rq.setOrganizations(organizations); + + mockMvc.perform(MockMvcRequestBuilders.post(INVITATIONS_ENDPOINT) + .content(objectMapper.writeValueAsBytes(rq)) + .contentType(APPLICATION_JSON) + .with(token(oAuthHelper.getDefaultToken()))) + .andExpect(status().isForbidden()); + + } +} diff --git a/src/test/java/com/epam/ta/reportportal/ws/controller/UserControllerTest.java b/src/test/java/com/epam/ta/reportportal/ws/controller/UserControllerTest.java index a6b3ce1b8f..8e884b52ee 100644 --- a/src/test/java/com/epam/ta/reportportal/ws/controller/UserControllerTest.java +++ b/src/test/java/com/epam/ta/reportportal/ws/controller/UserControllerTest.java @@ -58,6 +58,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; @@ -130,6 +131,7 @@ void createUserByAdminPositive() throws Exception { } @Test + @Disabled("to be deleted") void createUserBidPositive() throws Exception { CreateUserRQ rq = new CreateUserRQ(); rq.setDefaultProject("default_personal"); @@ -155,6 +157,7 @@ void createUserBidPositive() throws Exception { } @Test + @Disabled("to be deleted") void createUserPositive() throws Exception { CreateUserRQConfirm rq = new CreateUserRQConfirm(); rq.setLogin("testLogin"); diff --git a/src/test/java/com/epam/ta/reportportal/ws/converter/converters/UserCreationBidConverterTest.java b/src/test/java/com/epam/ta/reportportal/ws/converter/converters/UserCreationBidConverterTest.java deleted file mode 100644 index c6dd790d72..0000000000 --- a/src/test/java/com/epam/ta/reportportal/ws/converter/converters/UserCreationBidConverterTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2019 EPAM Systems - * - * 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 com.epam.ta.reportportal.ws.converter.converters; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import com.epam.ta.reportportal.entity.project.Project; -import com.epam.ta.reportportal.entity.user.UserCreationBid; -import com.epam.ta.reportportal.model.user.CreateUserRQ; -import java.time.Instant; -import org.junit.jupiter.api.Test; - -/** - * @author Ihar Kahadouski - */ -class UserCreationBidConverterTest { - - @Test - void toUser() { - CreateUserRQ request = new CreateUserRQ(); - final String email = "email@example.com"; - request.setEmail(email); - final String role = "role"; - request.setRole(role); - final Project project = new Project(); - project.setName("projectName"); - final Instant creationDate = Instant.now(); - project.setCreationDate(creationDate); - - final UserCreationBid bid = UserCreationBidConverter.TO_USER.apply(request, project); - - assertNotNull(bid.getUuid()); - assertEquals(bid.getEmail(), email); - assertEquals(bid.getRole(), role); - assertEquals(bid.getProjectName(), project.getName()); - } -}