Conditions.java

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

import ch.colabproject.colab.api.controller.RequestManager;
import ch.colabproject.colab.api.controller.security.SecurityManager;
import ch.colabproject.colab.api.model.card.Card;
import ch.colabproject.colab.api.model.card.CardContent;
import ch.colabproject.colab.api.model.project.Project;
import ch.colabproject.colab.api.model.user.User;
import java.util.List;
import java.util.Objects;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility class to build conditions
 *
 * @author maxence
 */
public final class Conditions {

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

    /**
     * An always true condition
     */
    public static final Condition alwaysTrue = new AlwaysTrue();

    /**
     * A always false condition
     */
    public static final Condition alwaysFalse = new AlwaysFalse();

    /**
     * By default if the situation cannot happen
     */
    public static final Condition defaultForOrphan = alwaysTrue;

    /**
     * Is the current authenticated condition
     */
    public static final Condition authenticated = new IsAuthenticated();

    /**
     * Private constructor prevents instantiation
     */
    private Conditions() {
        throw new UnsupportedOperationException("This is a utility class");
    }

    /**
     * Abstract condition. To rule them all
     */
    public static abstract class Condition {

        /**
         * Evaluate the condition
         *
         * @param requestManager  the request Manager
         * @param securityManager the security manager
         *
         * @return evaluation result
         */
        public boolean eval(RequestManager requestManager, SecurityManager securityManager) {
            Boolean cachedResult = requestManager.getConditionResult(this);
            if (cachedResult != null) {
                logger.trace("Condition {} is cached and {}", this, cachedResult);
                return cachedResult;
            } else {
                boolean result = this.internalEval(requestManager, securityManager);
                logger.trace("Condition {} is not cached and {}", this, result);
                requestManager.registerConditionResult(this, result);
                return result;
            }
        }

        /**
         * Evaluate the condition
         *
         * @param requestManager  the request Manager
         * @param securityManager the security manager
         *
         * @return evaluation result
         */
        protected abstract boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager);
    }

    /**
     * Always true statement
     */
    private static class AlwaysTrue extends Condition {

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            return true;
        }

        @Override
        public String toString() {
            return "true";
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof AlwaysTrue;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 31 * hash + Objects.hashCode(true);
            return hash;
        }
    }

    /**
     * Always false statement
     */
    private static class AlwaysFalse extends Condition {

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            return false;
        }

        @Override
        public String toString() {
            return "false";
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof AlwaysFalse;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 31 * hash + Objects.hashCode(true);
            return hash;
        }

    }

    /**
     * AND condition
     */
    public static class And extends Condition {

        /** Sub conditions */
        private Condition[] conditions;

        /**
         * Build an AND statement
         *
         * @param conditions list of all conditions that should be true
         */
        public And(Condition... conditions) {
            this.conditions = conditions;
        }

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            for (Condition c : conditions) {
                if (!c.eval(requestManager, securityManager)) {
                    // not all conditions are true => false
                    return false;
                }
            }
            // no falsy condition found => true
            return true;
        }

        @Override
        public String toString() {
            return "And(" + List.of(conditions) + ')';
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 31 * hash + new HashCodeBuilder().append(this.conditions).toHashCode();
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final And other = (And) obj;
            if (!Objects.equals(this.conditions, other.conditions)) {
                return false;
            }
            return true;
        }
    }

    /**
     * OR condition
     */
    public static class Or extends Condition {

        /** Sub conditions */
        private Condition[] conditions;

        /**
         * Build an OR statement
         *
         * @param conditions list of all conditions that should be true
         */
        public Or(Condition... conditions) {
            this.conditions = conditions;
        }

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            for (Condition c : conditions) {
                if (c.eval(requestManager, securityManager)) {
                    // at least on sub condition is true => true
                    return true;
                }
            }
            // no true condition found => false
            return false;
        }

        @Override
        public String toString() {
            return "Or(" + List.of(conditions) + ')';
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 59 * hash + new HashCodeBuilder().append(this.conditions).toHashCode();
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Or other = (Or) obj;
            if (!new EqualsBuilder().append(this.conditions, other.conditions).isEquals()) {
                return false;
            } else {
                return true;
            }
        }
    }

    /**
     * NOT
     */
    public static class Not extends Condition {

        /** the condition to negate */
        private final Condition condition;

        /**
         * Build a NOT statement
         *
         * @param condition the condition to negate
         */
        public Not(Condition condition) {
            this.condition = condition;
        }

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            // just invert the given sub-conditions
            return !condition.eval(requestManager, securityManager);
        }

        @Override
        public String toString() {
            return "Not(" + condition + ')';
        }

        @Override
        public int hashCode() {
            int hash = 5;
            hash = 37 * hash + Objects.hashCode(this.condition);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Not other = (Not) obj;
            if (!Objects.equals(this.condition, other.condition)) {
                return false;
            }
            return true;
        }
    }

    /**
     * Is the current user the given one ?
     */
    public static class IsCurrentUserThisUser extends Condition {

        /** user to check against */
        private final User user;

        /**
         * Check who the current user is
         *
         * @param user user to check currentUser against
         */
        public IsCurrentUserThisUser(User user) {
            this.user = user;
        }

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            User currentUser = requestManager.getCurrentUser();
            return currentUser != null && currentUser.equals(user);
        }

        @Override
        public String toString() {
            return "IsUser(" + user + ")";
        }

        @Override
        public int hashCode() {
            int hash = 5;
            hash = 73 * hash + Objects.hashCode(this.user);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final IsCurrentUserThisUser other = (IsCurrentUserThisUser) obj;
            if (!Objects.equals(this.user, other.user)) {
                return false;
            }
            return true;
        }
    }

    /**
     * The current user must be member of the given project team
     */
    public static class IsCurrentUserMemberOfProject extends Condition {

        /** the project */
        private final Project project;

        /**
         * Create a "Is current user member of this project" statement
         *
         * @param project the project to check if the current user is member of
         */
        public IsCurrentUserMemberOfProject(Project project) {
            this.project = project;
        }

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            return securityManager.isCurrentUserMemberOfTheProjectTeam(project);
        }

        @Override
        public String toString() {
            return "IsMemberOf(" + project + ")";
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 37 * hash + Objects.hashCode(this.project);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final IsCurrentUserMemberOfProject other = (IsCurrentUserMemberOfProject) obj;
            if (!Objects.equals(this.project, other.project)) {
                return false;
            }
            return true;
        }
    }

    /**
     * The current user must be, at least, internal to given project team
     */
    public static class IsCurrentUserInternalToProject extends Condition {

        /** the project */
        private final Project project;

        /**
         * Create a "Is current user internal to this project" statement
         *
         * @param project the project to check if the current user is member of
         */
        public IsCurrentUserInternalToProject(Project project) {
            this.project = project;
        }

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            return securityManager.isCurrentUserInternalToProject(project);
        }

        @Override
        public String toString() {
            return "IsInternalTo(" + project + ")";
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 67 * hash + Objects.hashCode(this.project);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final IsCurrentUserInternalToProject other = (IsCurrentUserInternalToProject) obj;
            if (!Objects.equals(this.project, other.project)) {
                return false;
            }
            return true;
        }
    }

    /**
     * Are current and given users teammate ?
     */
    public static class IsCurrentUserTeamMateOfUser extends Condition {

        /** the other user */
        private final User user;

        /**
         * Create a are teammate statement
         *
         * @param user the user to check against
         */
        public IsCurrentUserTeamMateOfUser(User user) {
            this.user = user;
        }

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            User currentUser = requestManager.getCurrentUser();
            return currentUser != null && securityManager.areUserTeammate(currentUser, this.user);
        }

        @Override
        public String toString() {
            return "IsTeamMateOf(" + user + ")";
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 31 * hash + Objects.hashCode(this.user);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final IsCurrentUserTeamMateOfUser other = (IsCurrentUserTeamMateOfUser) obj;
            if (!Objects.equals(this.user, other.user)) {
                return false;
            }
            return true;
        }
    }

    /**
     * Do current and given user work on a common project ?
     */
    public static class DoCurrentUserWorkOnSameProjectThanUser extends Condition {

        /** the other user */
        private final User user;

        /**
         * Create a are teammate statement
         *
         * @param user the user to check against
         */
        public DoCurrentUserWorkOnSameProjectThanUser(User user) {
            this.user = user;
        }

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            User currentUser = requestManager.getCurrentUser();
            return currentUser != null
                && securityManager.doUsersHaveCommonProject(currentUser, this.user);
        }

        @Override
        public String toString() {
            return "DoCurrentUserWorkOnSameProjectThanUser(" + user + ")";
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 31 * hash + Objects.hashCode(this.user);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final DoCurrentUserWorkOnSameProjectThanUser other = (DoCurrentUserWorkOnSameProjectThanUser) obj;
            if (!Objects.equals(this.user, other.user)) {
                return false;
            }
            return true;
        }
    }

    /**
     * Is the current user authenticated ?
     */
    private static class IsAuthenticated extends Condition {

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            return requestManager.isAuthenticated();
        }

        @Override
        public String toString() {
            return "IsAuthenticated";
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof IsAuthenticated;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 83 * hash;
            return hash;
        }
    }

    /**
     * Has the current user write access to a card ?
     */
    public static class HasCardWriteRight extends Condition {

        /** The card * */
        private final Card card;

        /**
         * Create a has write access statement
         *
         * @param card the card
         */
        public HasCardWriteRight(Card card) {
            this.card = card;
        }

        /**
         * Create a has write access statement
         *
         * @param cardContent a card content
         */
        public HasCardWriteRight(CardContent cardContent) {
            this.card = cardContent.getCard();
        }

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            return securityManager.hasReadWriteAccess(card);
        }

        @Override
        public String toString() {
            return "HasCardWriteRight(" + card + ")";
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 47 * hash + Objects.hashCode(this.card);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final HasCardWriteRight other = (HasCardWriteRight) obj;
            if (!Objects.equals(this.card, other.card)) {
                return false;
            }
            return true;
        }
    }

    /**
     * Has the current user access to a card ?
     */
    public static class HasCardReadRight extends Condition {

        /** The card * */
        private final Card card;

        /**
         * Create a has read access statement
         *
         * @param card the card
         */
        public HasCardReadRight(Card card) {
            this.card = card;
        }

        /**
         * Create a has read access statement
         *
         * @param cardContent a card content
         */
        public HasCardReadRight(CardContent cardContent) {
            this.card = cardContent.getCard();
        }

        @Override
        protected boolean internalEval(RequestManager requestManager,
            SecurityManager securityManager) {
            return securityManager.hasReadAccess(card);
        }

        @Override
        public String toString() {
            return "HasCardReadRight(" + card + ")";
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 71 * hash + Objects.hashCode(this.card);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final HasCardReadRight other = (HasCardReadRight) obj;
            if (!Objects.equals(this.card, other.card)) {
                return false;
            }
            return true;
        }
    }

}