SecurityManager.java

/*
 * The coLAB project
 * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
 *
 * Licensed under the MIT License
 */
package ch.colabproject.colab.api.controller.security;

import ch.colabproject.colab.api.controller.RequestManager;
import ch.colabproject.colab.api.controller.card.CardTypeManager;
import ch.colabproject.colab.api.controller.project.ProjectManager;
import ch.colabproject.colab.api.controller.team.TeamManager;
import ch.colabproject.colab.api.model.WithPermission;
import ch.colabproject.colab.api.model.card.Card;
import ch.colabproject.colab.api.model.project.Project;
import ch.colabproject.colab.api.model.team.TeamMember;
import ch.colabproject.colab.api.model.team.acl.Assignment;
import ch.colabproject.colab.api.model.team.acl.HierarchicalPosition;
import ch.colabproject.colab.api.model.user.User;
import ch.colabproject.colab.api.security.permissions.Conditions.Condition;
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
import java.util.List;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * To check access rights.
 *
 * @author maxence
 */
@Stateless
@LocalBean
public class SecurityManager {

    /** logger */
    private static final Logger logger = LoggerFactory.getLogger(SecurityManager.class);

    /**
     * As check are done against current authenticated user, we need access to the requestManager:
     */
    @Inject
    private RequestManager requestManager;

    /**
     * To check membership-ness
     */
    @Inject
    private TeamManager teamManager;

    /**
     * Card type specific logic
     */
    @Inject
    private CardTypeManager cardTypeManager;

    /**
     * Project specific logic
     */
    @Inject
    private ProjectManager projectManager;

    /**
     * Get the current user if it exists.
     *
     * @return the current user
     *
     * @throws HttpErrorMessage authRequired if currentUser is not authenticated
     */
    public User assertAndGetCurrentUser() {
        User user = requestManager.getCurrentUser();
        if (user != null) {
            return user;
        } else {
            throw HttpErrorMessage.authenticationRequired();
        }
    }

    /**
     * Mark the current request as {@link RequestManager#setInSecurityTx(boolean) in-security-tx}
     * and run the given action.
     *
     * @param action action to run
     */
    private void runInSecurityTx(Runnable action) {
        requestManager.setInSecurityTx(true);
        action.run();
        requestManager.setInSecurityTx(false);
    }

    /**
     * Assert the given condition is true
     *
     * @param condition the condition to check
     * @param message   message to log in case the assertion failed
     *
     * @throws HttpErrorMessage
     *                          <ul>
     *                          <li>with authenticationRequired if assertion fails and current user
     *                          is not authenticated;
     *                          <li>with forbidden if the authenticated user does not have enough
     *                          permission
     *                          </ul>
     */
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void assertConditionTx(Condition condition, String message) {
        runInSecurityTx(() -> this.assertCondition(condition, message, null));
    }

    /**
     * Assert the given condition is true. If the current user is an admin, this assertion will
     * never fail
     *
     * @param condition the condition to evaluate
     * @param message   message to display if the assertion failed
     * @param o         related object to log, may be null
     *
     * @throws HttpErrorMessage
     *                          <ul>
     *                          <li>with authenticationRequired if assertion fails and current user
     *                          is not authenticated;
     *                          <li>with forbidden if the authenticated user does not have enough
     *                          permission
     *                          </ul>
     */
    private void assertCondition(Condition condition, String message, WithPermission o) {
        if (!requestManager.isAdmin()) {
            requestManager.sudo(() -> {
                if (!condition.eval(requestManager, this)) {
                    if (logger.isErrorEnabled()) {
                        if (o != null) {
                            logger.error("{} Permission denied: {} ({}) currentUser: {}",
                                message, o, condition, requestManager.getCurrentUser());
                        } else {
                            logger.error("{} Permission denied: ({}) currentUser: {}",
                                message, condition, requestManager.getCurrentUser());
                        }
                    }
                    if (requestManager.isAuthenticated()) {
                        throw HttpErrorMessage.forbidden();
                    } else {
                        throw HttpErrorMessage.authenticationRequired();
                    }
                }
            });
        }
    }

    /**
     * Assert the currentUser has right to create the given object
     *
     * @param o object the user want to create
     */
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void assertCreatePermissionTx(WithPermission o) {
        runInSecurityTx(() -> this.assertCreatePermission(o));
    }

    /**
     * Assert the currentUser has right to create the given object
     *
     * @param o object the user want to create
     */
    private void assertCreatePermission(WithPermission o) {
        this.assertCondition(o.getCreateCondition(), "Create", o);
    }

    /**
     * Assert the currentUser has right to read the given object
     *
     * @param o object the user want to read
     */
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void assertReadPermissionTx(WithPermission o) {
        runInSecurityTx(() -> this.assertReadPermission(o));
    }

    /**
     * Assert the currentUser has right to read the given object
     *
     * @param o object the user want to read
     */
    private void assertReadPermission(WithPermission o) {
        this.assertCondition(o.getReadCondition(), "Read", o);
    }

    /**
     * Assert the currentUser has right to update the given object
     *
     * @param o object the user want to update
     */
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void assertUpdatePermissionTx(WithPermission o) {
        runInSecurityTx(() -> this.assertUpdatePermission(o));
    }

    /**
     * Assert the currentUser has right to update the given object
     *
     * @param o object the user want to update
     */
    private void assertUpdatePermission(WithPermission o) {
        this.assertCondition(o.getUpdateCondition(), "Update", o);
    }

    /**
     * Assert the currentUser has right to update the given object
     *
     * @param o object the user want to delete
     */
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void assertDeletePermissionTx(WithPermission o) {
        runInSecurityTx(() -> this.assertDeletePermission(o));
    }

    /**
     * Assert the currentUser has right to update the given object
     *
     * @param o object the user want to delete
     */
    private void assertDeletePermission(WithPermission o) {
        this.assertCondition(o.getDeleteCondition(), "Delete", o);
    }

    /**
     * Are two user team mate?
     *
     * @param a a user
     * @param b another user
     *
     * @return true if both user are both member of the same team
     */
    public boolean areUserTeammate(User a, User b) {
        return teamManager.areUserTeammate(a, b);
    }

    /**
     * Do the two users have common project ?
     *
     * @param a one user
     * @param b another user
     *
     * @return true if both users are related to the same project
     */
    public boolean doUsersHaveCommonProject(User a, User b) {
        return projectManager.doUsersHaveCommonProject(a, b);
    }

    /**
     * Has the current user read/write access to the given card
     *
     * @param card the card
     *
     * @return true if current user can write the card
     */
    public boolean hasReadWriteAccess(Card card) {
        User currentUser = requestManager.getCurrentUser();

        if (card == null || currentUser == null) {
            return false;
        }

        TeamMember member = teamManager.findMemberByProjectAndUser(card.getProject(), currentUser);

        if (member == null) {
            return false;
        }
        return canWrite(card, member);
    }

    /**
     * Determines if the given team member has write permissions on the given card.
     *
     * @param card   the card
     * @param member the member
     *
     * @return True if the member can write on the card, false otherwise
     */
    private boolean canWrite(Card card, TeamMember member) {
        if (card == null || member == null) {
            return false;
        }

        // 1. owner
        // has full power on his project
        if (member.getPosition() == HierarchicalPosition.OWNER) {
            return true;
        }

        // 2. assignment
        // gives write access
        Assignment byMember = card.getAssignmentByMember(member);
        if (byMember != null) {
            return true;
        }

        // 3. team member position
        return member.getPosition().canWrite();
    }

    /**
     * Has the current user the right to read the given card ?
     *
     * @param card the card to read
     *
     * @return true if current user can read the card
     */
    public boolean hasReadAccess(Card card) {

        User currentUser = requestManager.getCurrentUser();
        if (card == null || currentUser == null) {
            return false;
        }
        TeamMember member = teamManager.findMemberByProjectAndUser(card.getProject(), currentUser);
        return member != null;
    }

    /**
     * Is the current user member of the team of the given project?
     *
     * @param project the project
     *
     * @return true if the user if member of the project team
     */
    public boolean isCurrentUserMemberOfTheProjectTeam(Project project) {
        User currentUser = requestManager.getCurrentUser();
        if (project == null || currentUser == null) {
            return false;
        }
        TeamMember member = teamManager.findMemberByProjectAndUser(project, currentUser);
        return member != null;
    }

    /**
     * Is the current user the project owner ?
     *
     * @param project the project
     *
     * @return true if the current user is owner of the project
     */
    public boolean isCurrentUserOwnerOfTheProject(Project project) {
        User currentUser = requestManager.getCurrentUser();
        if (project == null || currentUser == null) {
            return false;
        }
        TeamMember member = teamManager.findMemberByProjectAndUser(project, currentUser);
        return member != null && member.getPosition() == HierarchicalPosition.OWNER;
    }

    /**
     * Is the current user internal to the project team?
     *
     * @param project the project
     *
     * @return true if the current user is internal to the project
     */
    public boolean isCurrentUserInternalToProject(Project project) {
        User currentUser = requestManager.getCurrentUser();
        if (project == null || currentUser == null) {
            return false;
        }
        TeamMember member = teamManager.findMemberByProjectAndUser(project, currentUser);
        return member != null && member.getPosition() != HierarchicalPosition.GUEST;
    }

    /**
     * Has the current user the right to read the card type (/ reference) ?
     * <p>
     * A user can read
     * <ul>
     * <li>any global published card type</li>
     * <li>any card type (/ reference) defined in a project he is member of</li>
     * <li>and all the chain of targets of those card types references</li>
     * </ul>
     *
     * @param cardTypeOrRefId the id of the card type or reference
     *
     * @return true if the current user can read the card type or reference
     */
    public boolean isCardTypeOrRefReadableByCurrentUser(Long cardTypeOrRefId) {
        User currentUser = requestManager.getCurrentUser();
        if (cardTypeOrRefId == null || currentUser == null) {
            return false;
        }

        List<Long> globalPublished = cardTypeManager.findGlobalPublishedCardTypeIds();
        if (globalPublished.contains(cardTypeOrRefId)) {
            return true;
        }

        List<Long> accessibleFromProjects = cardTypeManager
            .findCurrentUserReadableProjectsCardTypesIds();
        if (accessibleFromProjects.contains(cardTypeOrRefId)) {
            return true;
        }

        return false;
    }

    /**
     * Has the current user the right to read the project ?
     * <p>
     * A user can read any project he is a member of, has instance maker for or any project which
     * contains a card type or reference the current user has a read access.
     *
     * @param projectId the id of the project
     *
     * @return True if the current user can read the project
     */
    public boolean isProjectReadableByCurrentUser(Long projectId) {
        User currentUser = requestManager.getCurrentUser();

        if (projectId == null || currentUser == null) {
            return false;
        }

        Project currentProject = projectManager.assertAndGetProject(projectId);

        if (currentProject.isGlobalProject()) {
            return true;
        }

        List<Long> projectsWhereMemberOf = projectManager
            .findIdsOfProjectsCurrentUserIsMemberOf();
        if (projectsWhereMemberOf.contains(projectId)) {
            return true;
        }

        List<Long> projectsWhereInstanceMakerFor = projectManager
            .findIdsOfProjectsCurrentUserIsInstanceMakerFor();
        if (projectsWhereInstanceMakerFor.contains(projectId)) {
            return true;
        }

        List<Long> accessibleByCardTypes = projectManager
            .findIdsOfProjectsReadableThroughCardTypes();
        if (accessibleByCardTypes.contains(projectId)) {
            return true;
        }

        return false;
    }

    /**
     * Has the current user the right to read the project copy params ?
     * <p>
     * A user can read any project he is a member of or has instance maker for.
     *
     * @param projectId the id of the project
     *
     * @return True if the current user can read the project
     */
    public boolean isCopyParamReadableByCurrentUser(Long projectId) {
        User currentUser = requestManager.getCurrentUser();

        if (projectId == null || currentUser == null) {
            return false;
        }

        List<Long> projectsWhereMemberOf = projectManager
            .findIdsOfProjectsCurrentUserIsMemberOf();
        if (projectsWhereMemberOf.contains(projectId)) {
            return true;
        }

        List<Long> projectsWhereInstanceMakerFor = projectManager
            .findIdsOfProjectsCurrentUserIsInstanceMakerFor();
        if (projectsWhereInstanceMakerFor.contains(projectId)) {
            return true;
        }

        return false;
    }
}