InvitationController.java
package access.api;
import access.config.HashGenerator;
import access.exception.NotAllowedException;
import access.exception.NotFoundException;
import access.mail.MailBox;
import access.model.AcceptInvitation;
import access.model.Application;
import access.model.ApplicationMembership;
import access.model.Authority;
import access.model.Invitation;
import access.model.InvitationForm;
import access.model.Organization;
import access.model.OrganizationMembership;
import access.model.User;
import access.repository.ApplicationRepository;
import access.repository.InvitationRepository;
import access.repository.OrganizationMembershipRepository;
import access.repository.OrganizationRepository;
import access.repository.UserRepository;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME;
import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME;
import static access.api.Results.*;
@RestController
@RequestMapping(value = {"/api/v1/invitations"}, produces = MediaType.APPLICATION_JSON_VALUE)
@Transactional
@SecurityRequirement(name = OPEN_ID_SCHEME_NAME, scopes = {"openid"})
@SecurityRequirement(name = API_TOKENS_SCHEME_NAME)
public class InvitationController implements UserAccessRights {
private static final Log LOG = LogFactory.getLog(InvitationController.class);
private final InvitationRepository invitationRepository;
private final OrganizationRepository organizationRepository;
private final ApplicationRepository applicationRepository;
private final MailBox mailBox;
private final UserRepository userRepository;
private final OrganizationMembershipRepository organizationMembershipRepository;
public InvitationController(InvitationRepository invitationRepository,
OrganizationRepository organizationRepository,
ApplicationRepository applicationRepository,
MailBox mailBox, UserRepository userRepository, OrganizationMembershipRepository organizationMembershipRepository) {
this.invitationRepository = invitationRepository;
this.organizationRepository = organizationRepository;
this.applicationRepository = applicationRepository;
this.mailBox = mailBox;
this.userRepository = userRepository;
this.organizationMembershipRepository = organizationMembershipRepository;
}
@GetMapping({"/all/{organizationId}"})
public ResponseEntity<List<Invitation>> byOrganization(User user, @PathVariable("organizationId") Long organizationId) {
LOG.debug("/by organization by " + user.getEmail());
Organization organization = organizationRepository.findById(organizationId)
.orElseThrow(() -> new NotFoundException("Organization not found"));
confirmOrganizationMembership(user, organization, Authority.ADMIN);
List<Invitation> invitations = invitationRepository.findByOrganization(organization);
return ResponseEntity.status(HttpStatus.CREATED).body(invitations);
}
@GetMapping({"/hash"})
public ResponseEntity<Invitation> byHash(User user, @RequestParam(value = "hash") String hash) {
LOG.debug("/by hash by " + user.getEmail());
Invitation invitation = invitationRepository.findDetailsByHash(hash).orElseThrow(() -> new NotFoundException("Invitation not found"));
return ResponseEntity.ok(invitation);
}
@PostMapping({"", "/"})
public ResponseEntity<Map<String, Object>> create(User user, @RequestBody InvitationForm invitationForm) {
LOG.debug("/create invitation by " + user.getEmail());
Long organizationID = invitationForm.getOrganizationId();
Organization organization = organizationRepository.findById(organizationID)
.orElseThrow(() -> new NotFoundException("Organization not found"));
boolean isGuestInvitation = invitationForm.getIntendedAuthority().equals(Authority.GUEST);
User userFromDB = reinitializeUser(user, userRepository);
confirmOrganizationMembership(userFromDB, organization, isGuestInvitation ? Authority.MEMBER : Authority.ADMIN);
Set<Application> applications = invitationForm.getApplicationIdentifiers().stream()
.map(applicationId -> this.applicationRepository.findById(applicationId)
.orElseThrow(() -> new NotFoundException("Application not found")))
.collect(Collectors.toSet());
if (!applications.stream().allMatch(application -> application.getOrganization().getId().equals(organizationID))) {
throw new NotAllowedException("Not allowed to add applications outside the organization");
}
applications.forEach(application -> confirmApplicationWriteAccess(userFromDB, application, Authority.MEMBER));
invitationForm.getInvites().forEach(invitee -> {
Invitation invitation = new Invitation(
invitationForm.getLanguage(),
HashGenerator.generateRandomHash(),
invitee,
invitationForm.getMessage(),
invitationForm.getIntendedAuthority(),
organization,
user,
applications);
invitation = invitationRepository.save(invitation);
mailBox.sendInviteMail(invitation);
});
return createResult();
}
@PutMapping({"/accept"})
public ResponseEntity<Map<String, Object>> accept(User user, @Validated @RequestBody AcceptInvitation acceptInvitation) {
LOG.debug("/accept invitation by " + user.getEmail());
Invitation invitation = invitationRepository.findByIdAndHash(acceptInvitation.invitationId(), acceptInvitation.hash())
.orElseThrow(() -> new NotFoundException("Invitation not found"));
invitation.accept();
user = reinitializeUser(user, userRepository);
invitationRepository.save(invitation);
Organization organization = invitation.getOrganization();
//Internal users are already provisioned in their organization
Optional<OrganizationMembership> organizationMembershipOptional = user.getOrganizationMemberships().stream()
.filter(organizationMembership -> organizationMembership.getOrganization().getId().equals(organization.getId()))
.findFirst();
if (organizationMembershipOptional.isEmpty()) {
//Now create organization_membership and - if any - applicationMemberships
OrganizationMembership organizationMembership = new OrganizationMembership(user, organization, invitation.getIntendedAuthority());
List<ApplicationMembership> applicationMemberships = invitation.getApplications().stream()
.map(application -> new ApplicationMembership(application, organizationMembership))
.toList();
applicationMemberships.forEach(organizationMembership::addApplicationMembership);
organization.addOrganizationMembership(organizationMembership);
organizationRepository.save(organization);
} else {
//Add missing applicationMemberships
OrganizationMembership organizationMembership = organizationMembershipOptional.get();
List<Long> applicationIdentifiers = organizationMembership.getApplicationMemberships().stream()
.map(applicationMembership -> applicationMembership.getApplication().getId())
.toList();
List<ApplicationMembership> applicationMemberships = invitation.getApplications().stream()
.filter(application -> !applicationIdentifiers.contains(application.getId()))
.map(application -> new ApplicationMembership(application, organizationMembership))
.toList();
applicationMemberships.forEach(organizationMembership::addApplicationMembership);
organizationMembershipRepository.save(organizationMembership);
}
return createResult();
}
@DeleteMapping({"/{invitationId}"})
public ResponseEntity<Map<String, Object>> deleteInvitation(User user, @PathVariable("invitationId") Long invitationId) {
LOG.debug("/delete invitation by " + user.getEmail());
Invitation invitation = invitationRepository.findById(invitationId)
.orElseThrow(() -> new NotFoundException("Invitation not found"));
Organization organization = invitation.getOrganization();
Authority requiredAuthority = invitation.getIntendedAuthority().equals(Authority.ADMIN) ? Authority.ADMIN : Authority.MEMBER;
confirmOrganizationMembership(user, organization, requiredAuthority);
invitationRepository.delete(invitation);
return deleteResult();
}
@DeleteMapping({"/delete/all/{organizationId}"})
public ResponseEntity<Map<String, Object>> deleteAll(User user, @PathVariable("organizationId") Long organizationId) {
LOG.debug("/delete all invitation by " + user.getEmail());
Organization organization = organizationRepository.findById(organizationId)
.orElseThrow(() -> new NotFoundException("Organization not found"));
confirmOrganizationMembership(user, organization, Authority.ADMIN);
Set<Invitation> invitations = organization.getInvitations();
organization.getInvitations().clear();
invitationRepository.deleteAll(invitations);
return deleteResult();
}
@PutMapping({"/resend/{invitationId}"})
public ResponseEntity<Map<String, Object>> resendInvitation(User user, @PathVariable("invitationId") Long invitationId) {
LOG.debug("/resend invitation by " + user.getEmail());
Invitation invitation = invitationRepository.findById(invitationId)
.orElseThrow(() -> new NotFoundException("Invitation not found"));
Organization organization = invitation.getOrganization();
Authority requiredAuthority = invitation.getIntendedAuthority().equals(Authority.ADMIN) ? Authority.ADMIN : Authority.MEMBER;
confirmOrganizationMembership(user, organization, requiredAuthority);
invitation.setExpiryDate(Instant.now().plus(30, ChronoUnit.DAYS));
invitationRepository.save(invitation);
mailBox.sendInviteMail(invitation);
return okResult();
}
}