IdentityProviderController.java
package access.api;
import access.exception.NotAllowedException;
import access.exception.NotFoundException;
import access.jira.JiraClient;
import access.jira.JiraIssue;
import access.mail.MailBox;
import access.manage.ChangeRequest;
import access.manage.DashBoardConnectionOption;
import access.manage.Manage;
import access.manage.PathUpdateType;
import access.manage.RequestType;
import access.model.Authority;
import access.model.ConnectionRequest;
import access.model.DisconnectionRequest;
import access.model.EntityType;
import access.model.Organization;
import access.model.OrganizationMembership;
import access.model.User;
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.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
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.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME;
import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME;
import static access.manage.Manage.INSTITUTION_GUID;
import static access.manage.ManageData.*;
@RestController
@RequestMapping(value = {"/api/v1/idp"}, produces = MediaType.APPLICATION_JSON_VALUE)
@Transactional
@SecurityRequirement(name = OPEN_ID_SCHEME_NAME, scopes = {"openid"})
@SecurityRequirement(name = API_TOKENS_SCHEME_NAME)
@SuppressWarnings("unchecked")
public class IdentityProviderController implements UserAccessRights {
private static final Log LOG = LogFactory.getLog(IdentityProviderController.class);
private final UserRepository userRepository;
private final OrganizationRepository organizationRepository;
private final Manage manage;
private final JiraClient jiraClient;
private final MailBox mailBox;
public IdentityProviderController(UserRepository userRepository,
OrganizationRepository organizationRepository,
Manage manage,
JiraClient jiraClient,
MailBox mailBox) {
this.userRepository = userRepository;
this.organizationRepository = organizationRepository;
this.manage = manage;
this.jiraClient = jiraClient;
this.mailBox = mailBox;
}
@PutMapping({"/connect"})
public ResponseEntity<Map<String, Object>> connect(User user, @RequestBody @Validated ConnectionRequest connectionRequest) {
String email = user.getEmail();
LOG.debug("/connect SP to IdP connection for " + email);
user = reinitializeUser(user, userRepository);
String idpManageIdentifier = connectionRequest.getIdpManageIdentifier();
Organization organization = organizationRepository.findByManageIdentifier(idpManageIdentifier)
.orElseThrow(() -> new NotFoundException("Organization with manageIdentifier not found: " + idpManageIdentifier));
Map<String, Object> serviceProvider = manage.providerByManageIdentifier(connectionRequest.getEntityType(),
connectionRequest.getApplicationManageIdentifier());
boolean memberRequest = !user.isSuperUser();
if (memberRequest) {
OrganizationMembership organizationMembership = getOrganizationMembership(user, organization, Authority.GUEST)
.orElseThrow(() -> new NotAllowedException(
String.format("User %s is not a member of organization %s", email, organization.getName())));
memberRequest = !organizationMembership.getAuthority().equals(Authority.ADMIN);
}
if (memberRequest) {
//The only action is to email the institution admin of the organization, with a deep link to App
List<User> admins = organization.getOrganizationMemberships().stream()
.filter(membership -> membership.getAuthority().equals(Authority.ADMIN))
.map(membership -> membership.getUser())
.toList();
if (admins.isEmpty()) {
//Edge case, send the mail to the superusers instead
admins = userRepository.findBySuperUser(true);
}
String deeplink = String.format("/application-detail/%s/%s",
serviceProvider.get("type"),
serviceProvider.get("id"));
//Avoid UnsupportedException for immutable collections
admins = new ArrayList<>(admins);
admins.add(user);
mailBox.sendConnectionRequest(user, admins, organization, getProviderName(serviceProvider),
connectionRequest.getMessage(), deeplink);
return Results.createResult();
}
Map<String, Object> identityProvider = manage.providerByManageIdentifier(EntityType.saml20_idp, idpManageIdentifier);
//See https://github.com/OpenConext/OpenConext-access/wiki/Service-Connect-Flow
//Now check if the connection can be made automatically
Map<String, Object> spMetaDataFields = getMetaDataFields(getData(serviceProvider));
DashBoardConnectionOption connectOption = DashBoardConnectionOption
.fromValue((String) spMetaDataFields.getOrDefault("coin:dashboard_connect_option", "connect_with_interaction"));
String idpInstitutionGUID = (String) getMetaDataFields(getData(identityProvider)).get(INSTITUTION_GUID);
boolean idpAndSpShareInstitution = spMetaDataFields.getOrDefault(INSTITUTION_GUID, "nope")
.equals(idpInstitutionGUID);
boolean connectWithoutInteraction = idpAndSpShareInstitution || !connectOption.equals(DashBoardConnectionOption.connectWithInteraction);
if (connectWithoutInteraction) {
manage.connectWithoutInteraction(identityProvider, serviceProvider, user);
if (connectOption.equals(DashBoardConnectionOption.connectWithoutInteractionWithEmail)) {
List<String> recipients = contactPersons(serviceProvider);
if (!CollectionUtils.isEmpty(recipients)) {
mailBox.sendNewConnectionCreated(
user,
recipients,
getProviderName(identityProvider),
getProviderName(serviceProvider),
(String) getData(serviceProvider).get("entityid"));
}
}
return Results.createResult();
}
String changeRequestURL = manage.changeRequestURLConnectionRequest(EntityType.saml20_idp, idpManageIdentifier);
String identityProviderEntityID = getEntityID(identityProvider);
String serviceProviderEntityID = getEntityID(serviceProvider);
String lineSeparator = System.lineSeparator();
String summary = String.format("Connection request requested by %s for %s.",
user.getName(), getProviderName(identityProvider));
String jiraKey = jiraClient.create(new JiraIssue(
serviceProviderEntityID,
identityProviderEntityID,
String.format("%s%sA change request in manage has been created to merge this user request. See:%s%s",
summary,
lineSeparator,
lineSeparator,
changeRequestURL),
summary,
EntityType.valueOf((String) serviceProvider.get("type")),
email
));
Map<String, Object> auditData = Map.of("user", email,
"notes", String.format("Connection request requested by %s from %s for %s. See Jira %s",
user.getName(),
identityProviderEntityID,
serviceProviderEntityID,
jiraKey));
ChangeRequest changeRequest = new ChangeRequest(
idpManageIdentifier,
EntityType.saml20_idp,
Map.of("allowedEntities", Map.of("name", serviceProviderEntityID)),
true,
PathUpdateType.ADDITION,
RequestType.LinkRequest);
changeRequest.setTicketKey(jiraKey);
changeRequest.setAuditData(auditData);
manage.createChangeRequest(changeRequest);
return ResponseEntity.status(HttpStatus.CREATED).body(
Map.of("status", HttpStatus.CREATED.value(), "jiraKey", jiraKey));
}
@PutMapping({"/disconnect"})
public ResponseEntity<Map<String, Object>> disconnect(User user, @RequestBody @Validated DisconnectionRequest disconnectionRequest) {
LOG.debug("/disconnect SP to IdP request by " + user.getEmail());
user = reinitializeUser(user, userRepository);
String idpManageIdentifier = disconnectionRequest.getIdpManageIdentifier();
Organization organization = organizationRepository.findByManageIdentifier(idpManageIdentifier)
.orElseThrow(() -> new NotFoundException("Organization with manageIdentifier not found: " + idpManageIdentifier));
Map<String, Object> serviceProvider = manage.providerByManageIdentifier(disconnectionRequest.getEntityType(),
disconnectionRequest.getApplicationManageIdentifier());
confirmOrganizationMembership(user, organization, Authority.ADMIN);
Map<String, Object> identityProvider = manage.providerByManageIdentifier(EntityType.saml20_idp, idpManageIdentifier);
String changeRequestURL = manage.changeRequestURLConnectionRequest(EntityType.saml20_idp, idpManageIdentifier);
String identityProviderEntityID = getEntityID(identityProvider);
String serviceProviderEntityID = getEntityID(serviceProvider);
String lineSeparator = System.lineSeparator();
String summary = String.format("Disconnection request requested by %s for %s.",
user.getName(), getProviderName(identityProvider));
String jiraKey = jiraClient.create(new JiraIssue(
serviceProviderEntityID,
identityProviderEntityID,
String.format("%s%sA change request in manage has been created to merge this user request. See:%s%s",
summary,
lineSeparator,
lineSeparator,
changeRequestURL),
summary,
EntityType.valueOf((String) serviceProvider.get("type")),
user.getEmail()
));
Map<String,Object> auditData = Map.of("user", user.getEmail(),
"notes", String.format("Disconnection request requested by %s from %s for %s. See Jira %s",
user.getName(),
identityProviderEntityID,
serviceProviderEntityID,
jiraKey));
ChangeRequest changeRequest = new ChangeRequest(
idpManageIdentifier,
EntityType.saml20_idp,
Map.of("allowedEntities", Map.of("name", serviceProviderEntityID)),
true,
PathUpdateType.REMOVAL,
RequestType.UnlinkRequest);
changeRequest.setTicketKey(jiraKey);
changeRequest.setAuditData(auditData);
manage.createChangeRequest(changeRequest);
return ResponseEntity.status(HttpStatus.CREATED).body(
Map.of("status", HttpStatus.CREATED.value(), "jiraKey", jiraKey));
}
@PutMapping({"/cancel-connection-request"})
public ResponseEntity<Map<String, Object>> cancelConnectionRequest(User user, @RequestBody @Validated ConnectionRequest connectionRequest) {
LOG.debug("/cancelConnectionRequest SP to IdP request by " + user.getEmail());
doCancelRequest(user, connectionRequest, PathUpdateType.ADDITION, RequestType.LinkRequest);
return Results.okResult();
}
@PutMapping({"/cancel-disconnection-request"})
public ResponseEntity<Map<String, Object>> cancelDisconnectionRequest(User user, @RequestBody @Validated DisconnectionRequest disconnectionRequest) {
LOG.debug("/cancelDisconnectionRequest SP to IdP request by " + user.getEmail());
doCancelRequest(user, disconnectionRequest, PathUpdateType.REMOVAL, RequestType.UnlinkRequest);
return Results.okResult();
}
private void doCancelRequest(User user, ConnectionRequest connectionRequest, PathUpdateType addition, RequestType linkRequest) {
user = reinitializeUser(user, userRepository);
String idpManageIdentifier = connectionRequest.getIdpManageIdentifier();
Organization organization = organizationRepository.findByManageIdentifier(idpManageIdentifier)
.orElseThrow(() -> new NotFoundException("Organization with manageIdentifier not found: " + idpManageIdentifier));
Map<String, Object> serviceProvider = manage.providerByManageIdentifier(connectionRequest.getEntityType(),
connectionRequest.getApplicationManageIdentifier());
confirmOrganizationMembership(user, organization, Authority.ADMIN);
Map<String, Object> identityProvider = manage.providerByManageIdentifier(EntityType.saml20_idp, idpManageIdentifier);
List<Map<String, Object>> changeRequests = manage.getChangeRequestsIdentityProvider(identityProvider);
String serviceProviderEntityID = getEntityID(serviceProvider);
List<Map<String, Object>> openChangeRequests = changeRequests.stream()
.filter(changeRequest ->
EntityType.saml20_idp.name().equals(changeRequest.get("type")) &&
addition.name().equalsIgnoreCase((String) changeRequest.get("pathUpdateType")) &&
linkRequest.name().equalsIgnoreCase((String) changeRequest.get("requestType")) &&
serviceProviderEntityID.equals(((Map<String, Map<String, String>>)
changeRequest.getOrDefault("pathUpdates", Map.of()))
.getOrDefault("allowedEntities", Map.of()).get("name")))
.toList();
//First delete all manage change request - this is most likely to succeed
openChangeRequests.forEach(changeRequest -> manage.rejectChangeRequest(new ChangeRequest(changeRequest)));
//Then update all Jira comments, this API is not so stable
String comment = "Ticket can be closed by request of the requestor";
openChangeRequests.forEach(changeRequest -> jiraClient.comment((String) changeRequest.get("ticketKey"), comment));
}
}