Skip to content

Commit

Permalink
EPMRPP-93599 implement create invitations endpoint (#2062)
Browse files Browse the repository at this point in the history
  • Loading branch information
grabsefx authored Sep 17, 2024
1 parent aa7986a commit 0cc6f5b
Show file tree
Hide file tree
Showing 17 changed files with 581 additions and 212 deletions.
23 changes: 18 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion project-properties.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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/**',
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UserOrgInfoWithProjects> 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<String, OrganizationDetails> organizationDetailsMap = HashMap.newHashMap(2);

var orgDetails = new OrganizationDetails(
organization.getId(),
organization.getName(),
orgUser.getOrganizationRole(),
new HashMap<>()
);
rpUser.getOrganizationDetails()
.put(organization.getId().toString(), orgDetails);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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')";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Object> meta = Maps.newHashMapWithExpectedSize(1);
meta.put(BID_TYPE, INTERNAL_BID_TYPE);
Expand Down
Loading

0 comments on commit 0cc6f5b

Please sign in to comment.