MenuService.java

package access.menu;

import access.config.Config;
import access.config.Feature;
import access.model.Authority;
import access.model.Organization;
import access.model.OrganizationMembership;
import access.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Set;

/**
 * Single source of truth for the visibility of the menu items in the SURF Access shell. Both the Access client and
 * (through the Invite server) the Invite client render their menu from the result.
 */
@Service
@EnableConfigurationProperties(Config.class)
public class MenuService {

    public static final String HOME = "home";
    public static final String USERS = "users";
    public static final String IDP = "idp";
    public static final String YOUR_APPS = "yourApps";
    public static final String CATALOGUE = "catalogue";
    public static final String POLICIES = "policies";
    public static final String ACCESSIBLE_APPS = "accessibleApps";
    public static final String INVITE = "invite";
    public static final String SRAM = "sram";
    public static final String SERVICE_DESK = "serviceDesk";
    public static final String FEEDBACK = "feedback";
    public static final String STATISTICS = "statistics";

    private final Config config;

    @Autowired
    public MenuService(Config config) {
        this.config = config;
    }

    public MenuResponse menu(User user, String organizationId) {
        List<OrganizationMembership> memberships = user.getOrganizationMemberships().stream()
            .sorted(Comparator.comparing(OrganizationMembership::getId, Comparator.nullsLast(Comparator.naturalOrder())))
            .toList();
        List<MenuResponse.OrganizationSummary> organizations = memberships.stream()
            .map(OrganizationMembership::getOrganization)
            .map(MenuService::summary)
            .toList();
        Optional<Organization> current = currentOrganization(memberships, organizationId);
        List<String> menuItems = filterDisabledFeatures(menuItemsForUser(user, current.orElse(null)));
        return new MenuResponse(
            menuItems,
            organizations,
            current.map(MenuService::summary).orElse(null),
            new MenuResponse.UserSummary(user.getName(), user.isSuperUser()));
    }

    /**
     * The requested organization (id or manageIdentifier) when the user is a member, otherwise the first membership.
     */
    private Optional<Organization> currentOrganization(List<OrganizationMembership> memberships, String organizationId) {
        List<Organization> organizations = memberships.stream().map(OrganizationMembership::getOrganization).toList();
        if (StringUtils.hasText(organizationId)) {
            Optional<Organization> requested = organizations.stream()
                .filter(o -> String.valueOf(o.getId()).equals(organizationId) || organizationId.equals(o.getManageIdentifier()))
                .findFirst();
            if (requested.isPresent()) {
                return requested;
            }
        }
        return organizations.stream().findFirst();
    }

    private List<String> menuItemsForUser(User user, Organization currentOrganization) {
        //Every user has access to the home, catalogue and help menu items
        List<String> items = new ArrayList<>(List.of(HOME, CATALOGUE, SERVICE_DESK));
        if (!config.isFeedbackWidgetEnabled()) {
            items.add(FEEDBACK);
        }
        if (currentOrganization == null) {
            return items;
        }
        //Only the memberships of the current organization matter, the user can have other authorities elsewhere
        Set<Authority> authorities = user.getOrganizationMemberships().stream()
            .filter(m -> currentOrganization.getId().equals(m.getOrganization().getId()))
            .map(OrganizationMembership::getAuthority)
            .collect(java.util.stream.Collectors.toSet());
        boolean onlyGuest = !authorities.isEmpty() && authorities.stream().allMatch(a -> a == Authority.GUEST);
        if (onlyGuest) {
            items.add(YOUR_APPS);
            return items;
        }
        boolean isMember = authorities.contains(Authority.MEMBER);
        boolean isAdmin = authorities.contains(Authority.ADMIN);
        if (isMember || isAdmin) {
            items.addAll(List.of(IDP, USERS, YOUR_APPS));
        }
        boolean isInstitution = StringUtils.hasText(currentOrganization.getManageIdentifier());
        if (isInstitution) {
            items.addAll(List.of(ACCESSIBLE_APPS, INVITE, SRAM));
        }
        if ((isAdmin || user.isSuperUser()) && isInstitution) {
            items.addAll(List.of(STATISTICS, POLICIES));
        }
        return items;
    }

    private List<String> filterDisabledFeatures(List<String> items) {
        Set<String> disabled = config.getFeatures() == null ? Set.of() : config.getFeatures().stream()
            .filter(feature -> !feature.enabled())
            .map(Feature::name)
            .map(Enum::name)
            .collect(java.util.stream.Collectors.toSet());
        return items.stream().filter(item -> !disabled.contains(item)).toList();
    }

    private static MenuResponse.OrganizationSummary summary(Organization organization) {
        return new MenuResponse.OrganizationSummary(organization.getId(), organization.getName(), organization.getManageIdentifier());
    }
}