OrganizationController.java
package access.api;
import access.config.Config;
import access.exception.InvalidInputException;
import access.exception.NotFoundException;
import access.exception.UserRestrictionException;
import access.jira.JiraClient;
import access.jira.JiraIssue;
import access.manage.Manage;
import access.manage.ManageData;
import access.model.*;
import access.repository.OrganizationMembershipRepository;
import access.repository.OrganizationRepository;
import access.repository.UserRepository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
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.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME;
import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME;
import static access.api.Results.deleteResult;
@RestController
@RequestMapping(value = {"/api/v1/organizations"}, produces = MediaType.APPLICATION_JSON_VALUE)
@Transactional
@EnableConfigurationProperties(Config.class)
@SecurityRequirement(name = OPEN_ID_SCHEME_NAME, scopes = {"openid"})
@SecurityRequirement(name = API_TOKENS_SCHEME_NAME)
public class OrganizationController implements UserAccessRights {
private static final Log LOG = LogFactory.getLog(OrganizationController.class);
private final OrganizationRepository organizationRepository;
private final OrganizationMembershipRepository organizationMembershipRepository;
private final Config config;
private final Manage manage;
private final UserRepository userRepository;
private final ObjectMapper objectMapper ;
private final JiraClient jiraClient;
@Autowired
public OrganizationController(OrganizationRepository organizationRepository,
OrganizationMembershipRepository organizationMembershipRepository,
Manage manage,
UserRepository userRepository,
Config config,
ObjectMapper objectMapper,
JiraClient jiraClient) {
this.organizationRepository = organizationRepository;
this.organizationMembershipRepository = organizationMembershipRepository;
this.manage = manage;
this.config = config;
this.userRepository = userRepository;
this.objectMapper = objectMapper;
this.jiraClient = jiraClient;
}
@GetMapping("/applications/{id}")
@SuppressWarnings("unchecked")
public ResponseEntity<Map<String, Object>> findOrganizationDetailById(User user,
@PathVariable Long id) {
LOG.debug("/find Organization by " + user.getEmail());
User userFromDB = reinitializeUser(user, userRepository);
Organization organization = organizationRepository.findApplicationsDetailsOrganizationById(id)
.orElseThrow(() -> new NotFoundException("Organisation not found"));
OrganizationMembership organizationMembership= userFromDB.getOrganizationMemberships()
.stream()
.filter(membership -> membership.getOrganization().getId().equals(organization.getId()))
.findFirst()
//little hack to avoid multiple logic branches
.or(() -> userFromDB.isSuperUser() ? Optional.of(new OrganizationMembership(Authority.ADMIN)) : Optional.empty())
.orElseThrow(() -> new UserRestrictionException(
String.format("User %s is not a member of organization %s", user.getEmail(), id)));
organization.getApplications().forEach(application -> {
//We only fetch change-requests for applications with production status
application.getConnections().stream()
.filter(conn -> conn.changeRequestRequired())
.forEach(connection ->
connection.convertChangeRequests(manage.getChangeRequests(connection)));
});
Map<String, Object> organizationMap = objectMapper.convertValue(organization, new TypeReference<>() {
});
if (organizationMembership.getAuthority().equals(Authority.GUEST)) {
//we filter out applications where there is no app membership for the GUEST user
List<Long> applicationIdentifiers = organizationMembership.getApplicationMemberships().stream()
.map(applicationMembership -> applicationMembership.getApplication().getId())
.toList();
List<Map<String, Object>> applications = (List<Map<String, Object>>) organizationMap.getOrDefault("applications", List.of());
applications.removeIf(application -> !applicationIdentifiers.contains((Long)application.get("id")));
}
List<Map<String, Object>> applications = (List<Map<String, Object>>) organizationMap.getOrDefault("applications", List.of());
applications.forEach(application -> ManageData.removeSecrets(application));
return ResponseEntity.ok(organizationMap);
}
@GetMapping("/details/{id}")
public ResponseEntity<Organization> findUserManagementDetails(User user,
@PathVariable("id") Long id) {
LOG.debug("/find Organization user-management by " + user.getEmail());
User userFromDB = reinitializeUser(user, userRepository);
Organization organization = organizationRepository.findUserManagementOrganizationById(id)
.orElseThrow(() -> new NotFoundException("Organisation not found"));
confirmOrganizationMembership(userFromDB, organization, Authority.MEMBER);
return ResponseEntity.ok(organization);
}
@GetMapping("/mine/{id}")
public ResponseEntity<Organization> findMyOrganization(User user,
@PathVariable("id") Long id) {
LOG.debug("/find Organization mine by " + user.getEmail());
User userFromDB = reinitializeUser(user, userRepository);
Organization organization = organizationRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Organisation not found"));
confirmOrganizationMembership(userFromDB, organization, Authority.GUEST);
if (StringUtils.hasText(organization.getManageIdentifier())) {
Map<String, Object> provider = manage.providerByManageIdentifier(EntityType.saml20_idp, organization.getManageIdentifier());
if (organization.mergeMetaData(provider, false)) {
organizationRepository.save(organization);
}
}
return ResponseEntity.ok(organization);
}
@GetMapping("/search")
public ResponseEntity<Page<Map<String, Object>>> search(@Parameter(hidden = true) User user,
@RequestParam(value = "query", required = false, defaultValue = "") String query,
@RequestParam(value = "pageNumber", required = false, defaultValue = "0") int pageNumber,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") int pageSize,
@RequestParam(value = "sort", required = false, defaultValue = "name") String sort,
@RequestParam(value = "sortDirection", required = false, defaultValue = "ASC") String sortDirection) {
LOG.debug(String.format("/search/paginated for user %s", user.getEduPersonPrincipalName()));
//Only used in the Systems tab and exclusively for superusers
confirmSuperUser(user);
Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.fromString(sortDirection), sort));
Page<Map<String, Object>> usersPage = StringUtils.hasText(query) ? organizationRepository.searchByPageWithKeyword(FullSearchQueryParser.parse(query), pageable) :
organizationRepository.searchByPage(pageable);
return ResponseEntity.ok(usersPage);
}
@GetMapping("/search-landing")
public ResponseEntity<List<Map<String, Object>>> search(@Parameter(hidden = true) User user,
@RequestParam(value = "query") String query) {
LOG.debug(String.format("/landing-search for user %s", user.getEduPersonPrincipalName()));
//We can enforce any authorization rules here, as this is performed on the landing page
List<Map<String, Object>> organizations = organizationRepository.searchWithKeyword(FullSearchQueryParser.parse(query));
return ResponseEntity.ok(organizations);
}
@GetMapping("/all")
public ResponseEntity<List<Map<String, Object>>> allLight(@Parameter(hidden = true) User user) {
LOG.debug(String.format("/all organization for user %s", user.getEduPersonPrincipalName()));
confirmSuperUser(user);
List<Map<String, Object>> organizations = organizationRepository.findAllLight();
return ResponseEntity.ok(organizations);
}
@GetMapping("/users/{id}")
public ResponseEntity<Organization> usersOfOrganization(User user, @PathVariable("id") Long id) {
LOG.debug("/find Organization light by " + user.getEmail());
Organization organization = organizationRepository.findUsersOfOrganizationById(id)
.orElseThrow(() -> new NotFoundException("Organisation not found"));
User userFromDB = reinitializeUser(user, userRepository);
confirmOrganizationMembership(userFromDB, organization, Authority.GUEST);
return ResponseEntity.ok(organization);
}
@GetMapping("/light/{id}")
public ResponseEntity<Map<String, String>> light(@PathVariable Long id) {
LOG.debug("/light");
Organization organization = organizationRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Organisation not found"));
return ResponseEntity.ok(Map.of("name", organization.getName()));
}
@GetMapping("/invitation/{id}")
public ResponseEntity<Organization> name(User user, @PathVariable Long id) {
LOG.debug("/name");
Organization organization = organizationRepository.findApplicationsOrganizationById(id)
.orElseThrow(() -> new NotFoundException("Organisation not found"));
user = reinitializeUser(user, userRepository);
confirmOrganizationMembership(user, organization, Authority.MEMBER);
return ResponseEntity.ok(organization);
}
@GetMapping("/status/pending")
public ResponseEntity<List<Organization>> pendingApproval(User user) {
LOG.debug("/pendingApproval Organizations by " + user.getEmail());
confirmSuperUser(user);
List<Organization> organizations = organizationRepository.findByStatus(OrganizationStatus.PENDING_APPROVAL);
return ResponseEntity.ok(organizations);
}
@PostMapping({"", "/"})
public ResponseEntity<Organization> create(User user, @RequestBody @Validated Organization organization) {
String name = organization.getName();
Organization newOrganization = createOrganization(user, name);
String orgName = newOrganization.getName();
LOG.info(String.format("Creating new Organisation %s for %s", name, user.getEmail()));
// Now create a Jira ticket
String summary = String.format("User %s created a new Organisation %s in Access.",
user.getName(),
orgName);
String jiraKey = jiraClient.create(new JiraIssue(
orgName,
null,// There is no identity provider for approving organizations
String.format("%s The new organisation is pending approval. Visit to evaluate:%s%s",
summary,
System.lineSeparator(),
String.format("%s/system/organizationPendingApproval", config.getClientUrl())),
summary,
EntityType.oidc10_rp,
user.getEmail()
));
LOG.info("Created Jira issue for new Organization: " + jiraKey);
newOrganization.setTicketKey(jiraKey);
Organization savedOrganization = organizationRepository.save(newOrganization);
// User becomes admin
OrganizationMembership organizationMembership = new OrganizationMembership(user, savedOrganization, Authority.ADMIN);
organizationMembershipRepository.save(organizationMembership);
return ResponseEntity.status(HttpStatus.CREATED).body(savedOrganization);
}
@PutMapping({"", "/"})
public ResponseEntity<Organization> update(User user, @RequestBody @Validated OrganizationForm organizationForm) {
Organization organization = findOrganizationById(organizationForm.getId());
//IdP can't change the name of the IdP in Manage
if (StringUtils.hasText(organization.getManageIdentifier())) {
throw new InvalidInputException("Can not update name, Organization has manage identifier. " + organization.getName());
}
User userFromDB = reinitializeUser(user, userRepository);
confirmOrganizationMembership(userFromDB, organization, Authority.ADMIN);
organization.setName(organizationForm.getName());
Organization savedOrganization = organizationRepository.save(organization);
return ResponseEntity.status(HttpStatus.CREATED).body(savedOrganization);
}
@PutMapping("/status/{organizationId}/{status}")
public ResponseEntity<Organization> approve(User user, @PathVariable("organizationId") Long organizationId,
@PathVariable("status") OrganizationStatus status) {
confirmSuperUser(user);
Organization organization = findOrganizationById(organizationId);
organization.setStatus(status);
Organization savedOrganization = organizationRepository.save(organization);
return ResponseEntity.status(HttpStatus.CREATED).body(savedOrganization);
}
@PutMapping("/metadata/{organizationId}")
public ResponseEntity<Organization> updateMetaData(User user,
@PathVariable("organizationId") Long organizationId,
@RequestBody Map<String, Object> metaData) {
Organization organization = findOrganizationById(organizationId);
if (!StringUtils.hasText(organization.getManageIdentifier())) {
throw new InvalidInputException("Can not update metadata. Organization has no manage identifier: " + organization.getName());
}
User userFromDB = reinitializeUser(user, userRepository);
confirmOrganizationMembership(userFromDB, organization, Authority.ADMIN);
organization.setMetaData(metaData);
manage.saveIdentityProvider(organization);
return ResponseEntity.status(HttpStatus.CREATED).body(organization);
}
private Organization findOrganizationById(Long organizationId) {
return organizationRepository.findById(organizationId)
.orElseThrow(() -> new NotFoundException("Organization not found"));
}
@DeleteMapping({"", "/{organizationId}"})
public ResponseEntity<Map<String, Object>> delete(User user, @PathVariable("organizationId") Long organizationId) {
LOG.debug("/delete organization by " + user.getEmail());
Organization organization = findOrganizationById(organizationId);
user = this.reinitializeUser(user, userRepository);
confirmOrganizationMembership(user, organization, Authority.ADMIN);
organization.getApplications().stream()
.map(Application::getConnections)
.flatMap(Collection::stream)
.filter(connection -> StringUtils.hasText(connection.getManageIdentifier()))
.forEach(connection -> manage.deleteProvider(connection));
organizationRepository.deleteOrganizationById(organizationId);
return deleteResult();
}
@GetMapping("/admin-email/{organizationId}")
public ResponseEntity<Map<String, Object>> adminEmail(User user, @PathVariable Long organizationId) {
LOG.debug("/admin-email by: " + user.getEmail());
Map<String, Object> admin = userRepository.findAdminByOrganizationAndMember(organizationId, user.getId());
return ResponseEntity.ok(admin);
}
@GetMapping("/identity-providers-allowed-connections/{organizationId}")
public ResponseEntity<List<Map<String, Object>>> identityProvidersByAllowedConnections(User user,
@PathVariable Long organizationId) {
LOG.debug("/identityProvidersByAllowedConnections by: "+user.getEmail());
Organization organization = organizationRepository.findApplicationsOrganizationById(organizationId)
.orElseThrow(() -> new NotFoundException("Organisation not found"));
user = reinitializeUser(user ,userRepository);
confirmOrganizationMembership(user, organization, Authority.ADMIN);
List<Connection> connections = organization.getApplications().stream()
.map(Application::getConnections)
.flatMap(Collection::stream)
.toList();
List<Map<String, Object>> identityProviders = manage.identityProvidersByAllowedConnections(connections);
return ResponseEntity.ok(identityProviders);
}
private Organization createOrganization(User user, String name) {
String orgSchacHomeOrganization = getOrgSchacHomeOrganization(user, name);
return new Organization(name, orgSchacHomeOrganization);
}
private String getOrgSchacHomeOrganization(User user, String name) {
String schacHomeOrganization = user.getSchacHomeOrganization().toLowerCase();
String orgSchacHomeOrganization;
if (config.getEduIdSchacHomeOrganization().equals(schacHomeOrganization)) {
String normalizedName = name
.replaceAll("[^a-zA-Z_ ]", "")
.trim()
.replaceAll(" ", "_")
.toLowerCase();
orgSchacHomeOrganization = String.format("%s.%s", normalizedName, config.getEduIdSchacHomeOrganization());
} else {
orgSchacHomeOrganization = schacHomeOrganization;
}
return orgSchacHomeOrganization;
}
}