org.slc.sli.api.security.context.ContextValidator.java Source code

Java tutorial

Introduction

Here is the source code for org.slc.sli.api.security.context.ContextValidator.java

Source

/*
 * Copyright 2012-2013 inBloom, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.slc.sli.api.security.context;

import com.sun.jersey.spi.container.ContainerRequest;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.lang.math.NumberUtils;
import org.slc.sli.api.config.EntityDefinition;
import org.slc.sli.api.constants.ResourceNames;
import org.slc.sli.api.resources.generic.util.ResourceHelper;
import org.slc.sli.api.security.SLIPrincipal;
import org.slc.sli.api.security.context.resolver.EdOrgHelper;
import org.slc.sli.api.security.context.validator.IContextValidator;
import org.slc.sli.api.service.EntityNotFoundException;
import org.slc.sli.api.util.SecurityUtil;
import org.slc.sli.common.constants.EntityNames;
import org.slc.sli.domain.Entity;
import org.slc.sli.domain.NeutralCriteria;
import org.slc.sli.domain.NeutralQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;

import javax.ws.rs.core.PathSegment;
import java.util.*;

/**
 * ContextValidator
 * Determines if the principal has context to a resource.
 * Verifies the requested endpoint is accessible by the principal
 */
@Component
public class ContextValidator implements ApplicationContextAware {

    private static final Logger LOG = LoggerFactory.getLogger(ContextValidator.class);

    protected static final Set<String> GLOBAL_RESOURCES = new HashSet<String>(Arrays.asList(
            ResourceNames.ASSESSMENTS, ResourceNames.CALENDAR_DATES, ResourceNames.COMPETENCY_LEVEL_DESCRIPTORS,
            ResourceNames.COURSES, ResourceNames.COURSE_OFFERINGS, ResourceNames.EDUCATION_ORGANIZATIONS,
            ResourceNames.GRADUATION_PLANS, ResourceNames.GRADING_PERIODS, ResourceNames.LEARNINGOBJECTIVES,
            ResourceNames.LEARNINGSTANDARDS, ResourceNames.PROGRAMS, ResourceNames.SCHOOLS, ResourceNames.SECTIONS,
            ResourceNames.SESSIONS, ResourceNames.STUDENT_COMPETENCY_OBJECTIVES, ResourceNames.CUSTOM,
            "parentLearningObjectives", "childLearningObjectives", ResourceNames.CLASS_PERIODS,
            ResourceNames.BELL_SCHEDULES));

    private List<IContextValidator> validators;

    @Autowired
    private ResourceHelper resourceHelper;

    @Autowired
    private PagingRepositoryDelegate<Entity> repo;

    @Autowired
    private EntityOwnershipValidator ownership;

    @Autowired
    private StudentAccessValidator studentAccessValidator;

    @Autowired
    private EdOrgHelper edOrgHelper;

    @Autowired
    private ParentAccessValidator parentAccessValidator;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        validators = new ArrayList<IContextValidator>();
        validators.addAll(applicationContext.getBeansOfType(IContextValidator.class).values());
    }

    public void validateContextToUri(ContainerRequest request, SLIPrincipal principal) {
        validateUserHasContextToRequestedEntities(request, principal);
    }

    /**
     * white list student accessible URL. Can't do it in validateUserHasContextToRequestedEntity
     * because we must also block some url that only has 2 segment, i.e.
     * disciplineActions/disciplineIncidents
     *
     * @param request
     * @return if url is accessible to students principals
     */
    public boolean isUrlBlocked(ContainerRequest request) {
        List<PathSegment> segs = cleanEmptySegments(request.getPathSegments());

        if (isSystemCall(segs)) {
            // do not block system calls
            return false;
        }

        if (SecurityUtil.isStudent()) {
            return !studentAccessValidator.isAllowed(request);
        } else if (SecurityUtil.isParent()) {
            return !parentAccessValidator.isAllowed(request);
        }

        return false;
    }

    private boolean isSystemCall(List<PathSegment> pathSegments) {
        /**
         * assuming all resource endpoints are versioned
         */
        if (pathSegments == null || pathSegments.size() == 0) {
            // /api/rest/ root access?
            return false;
        }

        // all data model resources are versioned
        if (isVersionString(pathSegments.get(0).getPath())) {
            return false;
        }

        return true;
    }

    private boolean isVersionString(String path) {
        if (path != null && path.startsWith("v")) {
            return NumberUtils.isNumber(path.substring(1));
        }
        return false;
    }

    public List<PathSegment> cleanEmptySegments(List<PathSegment> pathSegments) {
        for (Iterator<PathSegment> i = pathSegments.iterator(); i.hasNext();) {
            if (i.next().getPath().isEmpty()) {
                i.remove();
            }
        }

        return pathSegments;
    }

    private void validateUserHasContextToRequestedEntities(ContainerRequest request, SLIPrincipal principal) {

        List<PathSegment> segs = request.getPathSegments();
        segs = cleanEmptySegments(segs);

        if (segs.size() < 3) {
            return;
        }

        /*
         * If the URI being requested is a GET full of global entities, we do
         * not need to attempt validation Global entities include: ASSESSMENT,
         * LEARNING_OBJECTIVE, LEARNING_STANDARD, COMPETENCY_LEVEL_DESCRIPTOR,
         * SESSION, COURSE_OFFERING, GRADING_PERIOD, COURSE,
         * EDUCATION_ORGANIZATION, SCHOOL, SECITON, PROGRAM, GRADUATION_PLAN,
         * STUDENT_COMPETENCY_OBJECTIVE, and CUSTOM (custom entity exists under
         * another entity, they should not prevent classification of a call
         * being global)
         */
        boolean isGlobal = true;
        for (PathSegment seg : segs) {
            // First segment is always API version, skip it
            // Third segment is always the ID, skip it
            if (seg.equals(segs.get(0)) || seg.equals(segs.get(2))) {
                continue;
            }
            // Check if the segment is not global, if so break
            if (!GLOBAL_RESOURCES.contains(seg.getPath())) {
                isGlobal = false;
                break;
            }
        }
        // Only skip validation if method is a get, updates may still require
        // validation
        if (isGlobal && request.getMethod().equals("GET")) {
            // The entity has global context, just return and don't call the
            // validators
            LOG.debug("Call to {} is of global context, skipping validation", request.getAbsolutePath().toString());
            return;
        }

        String rootEntity = segs.get(1).getPath();

        EntityDefinition def = resourceHelper.getEntityDefinition(rootEntity);
        if (def == null || def.skipContextValidation()) {
            return;
        }

        /*
         * e.g.
         * !isTransitive - /v1/staff/<ID>/disciplineActions
         * isTransitive - /v1/staff/<ID> OR /v1/staff/<ID>/custom
         */
        boolean isTransitive = segs.size() == 3
                || (segs.size() == 4 && segs.get(3).getPath().equals(ResourceNames.CUSTOM));

        validateContextToCallUri(segs);
        String idsString = segs.get(2).getPath();
        Set<String> ids = new HashSet<String>(Arrays.asList(idsString.split(",")));
        validateContextToEntities(def, ids, isTransitive);
    }

    /**
     * Validates the principal's context to call the given URI.
     *
     * @param segments
     *            List of Path Segments representing API call.
     */
    public void validateContextToCallUri(List<PathSegment> segments) {
        if (SecurityUtil.getSLIPrincipal().getEntity().getType().equals(EntityNames.TEACHER)
                && checkAccessOfStudentsThroughDisciplineIncidents(segments)) {
            throw new APIAccessDeniedException("Cannot access endpoint.");
        }
    }

    /**
     * Check for the path segments to match the following patterns:
     * /disciplineIncidents/{ids}/studentDisciplineIncidentAssociations,
     * /disciplineIncidents/{ids}/studentDisciplineIncidentAssociations/students
     *
     * @param segments
     *            List of Path Segments representing API call.
     * @return true if the URI exactly matches one of the enumerated patterns,
     *         false otherwise.
     */
    public boolean checkAccessOfStudentsThroughDisciplineIncidents(List<PathSegment> segments) {
        /*
         * Both /disciplineIncidents/{ids}/studentDisciplineIncidentAssociations and
         * /disciplineIncidents/{ids}/studentDisciplineIncidentAssociations/students
         * have the same pattern (first segment of discipline incident, third of
         * the association, so just check for those two conditions
         */
        if (segments.size() > 3) {
            return segments.get(1).getPath().equals(ResourceNames.DISCIPLINE_INCIDENTS)
                    && segments.get(3).getPath().equals(ResourceNames.STUDENT_DISCIPLINE_INCIDENT_ASSOCIATIONS);
        }
        return false; // Num segments is 3, just return
    }

    /**
    * Validates entities, based upon entity definition and entity ids to validate.
    *
    * @param def - Definition of entities to validate
    * @param ids - Collection of ids of entities to validate
    * @param isTransitive - Determines whether validation is through another entity type
    *
    * @throws APIAccessDeniedException - When entities cannot be accessed
    */
    public void validateContextToEntities(EntityDefinition def, Collection<String> ids, boolean isTransitive)
            throws APIAccessDeniedException {
        LOG.debug(">>>ContextValidator.validateContextToEntities()");
        LOG.debug("  def: " + ToStringBuilder.reflectionToString(def, ToStringStyle.DEFAULT_STYLE));
        LOG.debug("  ids: {}", (ids == null) ? "null" : ids.toArray().toString());

        LOG.debug("  isTransitive" + isTransitive);

        IContextValidator validator = findValidator(def.getType(), isTransitive);
        LOG.debug("  loaded validator: "
                + ToStringBuilder.reflectionToString(validator, ToStringStyle.DEFAULT_STYLE));

        if (validator != null) {
            NeutralQuery getIdsQuery = new NeutralQuery(
                    new NeutralCriteria("_id", "in", new ArrayList<String>(ids)));
            LOG.debug("  getIdsQuery: " + getIdsQuery.toString());

            Collection<Entity> entities = (Collection<Entity>) repo.findAll(def.getStoredCollectionName(),
                    getIdsQuery);
            LOG.debug("  entities: " + ToStringBuilder.reflectionToString(entities, ToStringStyle.DEFAULT_STYLE));

            Set<String> idsToValidate = getEntityIdsToValidate(def, entities, isTransitive, ids);
            LOG.debug("  idsToValidate: " + idsToValidate.toString());

            Set<String> validatedIds = getValidatedIds(def, idsToValidate, validator);
            LOG.debug("  validatedIds: " + validatedIds.toString());

            if (!validatedIds.containsAll(idsToValidate)) {
                LOG.debug("    ...APIAccessDeniedException");
                throw new APIAccessDeniedException("Cannot access entities", def.getType(), idsToValidate);
            }
        } else {
            throw new APIAccessDeniedException("No validator for " + def.getType() + ", transitive=" + isTransitive,
                    def.getType(), ids);
        }
    }

    /**
     * Validates entities, based upon entity definition and entity ids to validate.
     *
     * @param def - Definition of entities to validate
     * @param ids - Collection of ids of entities to validate
     * @param isTransitive - Determines whether validation is through another entity type
     *
     * @throws APIAccessDeniedException - When entities cannot be accessed
     */
    public Set<String> getValidIdsIncludeOrphans(EntityDefinition def, Set<String> ids, boolean isTransitive)
            throws APIAccessDeniedException {
        IContextValidator validator = findValidator(def.getType(), isTransitive);
        if (validator != null) {
            NeutralQuery getIdsQuery = new NeutralQuery(
                    new NeutralCriteria("_id", "in", new ArrayList<String>(ids)));
            Collection<Entity> entities = (Collection<Entity>) repo.findAll(def.getStoredCollectionName(),
                    getIdsQuery);
            Set<String> orphans = new HashSet<String>();
            for (Entity entity : entities) {
                if (isOrphanCreatedByUser(entity)) {
                    orphans.add(entity.getEntityId());
                }
            }
            Set<String> nonOrphanIds = new HashSet<String>();
            nonOrphanIds.addAll(ids);
            nonOrphanIds.removeAll(orphans);
            Set<String> validatedIds = validator.getValid(def.getType(), nonOrphanIds);

            validatedIds.addAll(orphans);
            return validatedIds;
        } else {
            throw new APIAccessDeniedException("No validator for " + def.getType() + ", transitive=" + isTransitive,
                    def.getType(), ids);
        }
    }

    /**
    * Returns a map of validated entity ids and their contexts, based upon entity definition and entities to validate.
    *
    * @param def - Definition of entities to validate
    * @param entities - Collection of entities to validate
    * @param isTransitive - Determines whether validation is through another entity type
    *
    * @return - Map of validated entity ids and their contexts, null if the validation is skipped
    *
    * @throws APIAccessDeniedException - When no entity validators can be found
    */
    public Map<String, SecurityUtil.UserContext> getValidatedEntityContexts(EntityDefinition def,
            Collection<Entity> entities, boolean isTransitive, boolean isRead) throws APIAccessDeniedException {

        if (skipValidation(def, isRead)) {
            return null;
        }

        Map<String, SecurityUtil.UserContext> entityContexts = new HashMap<String, SecurityUtil.UserContext>();

        List<IContextValidator> contextValidators = findContextualValidators(def.getType(), isTransitive);
        Collection<String> ids = getEntityIds(entities);
        if (!contextValidators.isEmpty()) {
            Set<String> idsToValidate = getEntityIdsToValidateForgiving(entities, isTransitive);
            for (IContextValidator validator : contextValidators) {
                // Add validated entity ids to the map.
                Set<String> validatedIds = getValidatedIds(def, idsToValidate, validator);
                for (String id : validatedIds) {
                    if (!entityContexts.containsKey(id)) {
                        entityContexts.put(id, validator.getContext());
                    } else if ((entityContexts.get(id).equals(SecurityUtil.UserContext.STAFF_CONTEXT))
                            && (validator.getContext().equals(SecurityUtil.UserContext.TEACHER_CONTEXT))
                            || (entityContexts.get(id).equals(SecurityUtil.UserContext.TEACHER_CONTEXT))
                                    && (validator.getContext().equals(SecurityUtil.UserContext.STAFF_CONTEXT))) {
                        entityContexts.put(id, SecurityUtil.UserContext.DUAL_CONTEXT);
                    }
                }
            }

            // Add accessible, non-validated entity ids (orphaned and owned/self) to the map.
            Set<String> accessibleNonValidatedIds = new HashSet<String>(ids);
            accessibleNonValidatedIds.removeAll(idsToValidate);
            for (String id : accessibleNonValidatedIds) {
                entityContexts.put(id, SecurityUtil.getUserContext());
            }
        } else {
            throw new APIAccessDeniedException("No validator for " + def.getType() + ", transitive=" + isTransitive,
                    def.getType(), ids);
        }

        return entityContexts;
    }

    /**
    * Returns a set of entity ids to validate, based upon entity type and list of entities to filter for validation.
    *
    * @param def - Definition of entities to filter
    * @param entities - Collection of entities to filter for validation
    * @param isTransitive - Determines whether validation is through another entity type
    * @param ids - Original set of entity ids to validate
    *
    * @return - Set of entity ids to validate
    *
    * @throws APIAccessDeniedException - When an entity cannot be accessed
    * @throws EntityNotFoundException - When an entity cannot be located
    */
    protected Set<String> getEntityIdsToValidate(EntityDefinition def, Collection<Entity> entities,
            boolean isTransitive, Collection<String> ids) throws APIAccessDeniedException, EntityNotFoundException {
        Set<String> entityIdsToValidate = new HashSet<String>();
        for (Entity ent : entities) {
            Collection<String> userEdOrgs = edOrgHelper.getDirectEdorgs(ent);
            if (isOrphanCreatedByUser(ent)) {
                LOG.debug("Entity is orphaned: id {} of type {}", ent.getEntityId(), ent.getType());
            } else if (SecurityUtil.getSLIPrincipal().getEntity() != null
                    && SecurityUtil.getSLIPrincipal().getEntity().getEntityId().equals(ent.getEntityId())) {
                LOG.debug("Entity is themselves: id {} of type {}", ent.getEntityId(), ent.getType());
            } else {
                if (ownership.canAccess(ent, isTransitive)) {
                    entityIdsToValidate.add(ent.getEntityId());
                } else {
                    throw new APIAccessDeniedException("Access to " + ent.getEntityId() + " is not authorized",
                            userEdOrgs);
                }
            }
        }

        // report an EntityNotFoundException on the id we find without a corresponding entity
        // so that we don't use the constructor for EntityNotFoundException incorrectly
        if (entities.size() != ids.size()) {
            for (String id : ids) {
                boolean foundentity = false;
                for (Entity ent : entities) {
                    if (ent.getEntityId().contains(id)) {
                        foundentity = true;
                        break;
                    }
                }
                if (!foundentity) {
                    LOG.debug("Invalid reference, an entity does not exist. collection: {} entities: {}",
                            def.getStoredCollectionName(), entities);
                    throw new EntityNotFoundException(id);
                }
            }
        }

        return entityIdsToValidate;
    }

    /**
     * Returns true if the entity is an orphan that is created by the user, false otherwise
     *
     * @param entity - Collection of entities to filter for validation
     *
     * @return
     */
    private boolean isOrphanCreatedByUser(Entity entity) {
        return SecurityUtil.principalId().equals(entity.getMetaData().get("createdBy"))
                && "true".equals(entity.getMetaData().get("isOrphaned"));
    }

    /**
     * This method forgivingly iterate through the input entities, returns a set of entity ids to validate,
     * based upon entity type and list of entities to filter for validation.
     *
     *
     * @param entities - Collection of entities to filter for validation
     * @param isTransitive - Determines whether validation is through another entity type
     *
     * @return - Set of entity ids to validate
     */
    protected Set<String> getEntityIdsToValidateForgiving(Collection<Entity> entities, boolean isTransitive) {
        Set<String> entityIdsToValidate = new HashSet<String>();
        for (Entity ent : entities) {
            if (isOrphanCreatedByUser(ent)) {
                LOG.debug("Entity is orphaned: id {} of type {}", ent.getEntityId(), ent.getType());
            } else if (SecurityUtil.getSLIPrincipal().getEntity() != null
                    && SecurityUtil.getSLIPrincipal().getEntity().getEntityId().equals(ent.getEntityId())) {
                LOG.debug("Entity is themselves: id {} of type {}", ent.getEntityId(), ent.getType());
            } else {
                try {
                    if (ownership.canAccess(ent, isTransitive)) {
                        entityIdsToValidate.add(ent.getEntityId());
                    }
                } catch (AccessDeniedException aex) {
                    LOG.error(aex.getMessage());
                }
            }
        }

        return entityIdsToValidate;
    }

    /**
    * Returns a set of validated entity ids based upon entity type and list of entity ids to validate.
    *
    * @param def - Definition of entities to validate
    * @param idsToValidate - Collection of entity ids to validate
    * @param validator - Validator to use
    *
    * @return - Set of validated entities
    */
    protected Set<String> getValidatedIds(EntityDefinition def, Set<String> idsToValidate,
            IContextValidator validator) {
        LOG.debug(">>>ContextValidator.getValidatedIds()");
        Set<String> validatedIds = new HashSet<String>();
        if (!idsToValidate.isEmpty()) {
            validatedIds = validator.validate(def.getType(), idsToValidate);
        }

        return validatedIds;
    }

    /**
    * Returns a set of entity ids from a set of entities.
    *
    * @param entities - Collection of entities to validate
    *
    * @return - Set of entity ids
    */
    protected Set<String> getEntityIds(Collection<Entity> entities) {
        Set<String> entityIds = new HashSet<String>();
        for (Entity ent : entities) {
            entityIds.add(ent.getEntityId());
        }

        return entityIds;
    }

    /**
     * Returns a contextual validator based upon entity type.
     *
    * @param toType - Type of entity to validate
    * @param isTransitive - Determines whether validation is through another entity type
     *
     * @return - Validator applicable to given type
     *
     * @throws IllegalStateException - ???
     */
    public IContextValidator findValidator(String toType, boolean isTransitive) throws IllegalStateException {

        IContextValidator found = null;
        for (IContextValidator validator : this.validators) {
            if (validator.canValidate(toType, isTransitive)) {
                LOG.info("Using {} to validate {}", new Object[] { validator.getClass().toString(), toType });
                found = validator;
                break;
            }
        }

        if (found == null) {
            LOG.warn("No {} validator to {}.", isTransitive ? "TRANSITIVE" : "NOT TRANSITIVE", toType);
        }

        return found;
    }

    public static boolean isTransitive(List<PathSegment> segs) {
        /*
        * e.g.
        * !isTransitive - /v1/staff/<ID>/disciplineActions
        * isTransitive - /v1/staff/<ID> OR /v1/staff/<ID>/custom
        */
        return segs.size() == 3 || (segs.size() == 4 && segs.get(3).getPath().equals(ResourceNames.CUSTOM));
    }

    /**
    * Returns a list of contextual validators based upon entity type.
    *
    * @param toType - Type of entity to validate
    * @param isTransitive - Determines whether validation is through another entity type
    *
    * @return - List of validators to use
    */
    public List<IContextValidator> findContextualValidators(String toType, boolean isTransitive) {

        List<IContextValidator> validators = new ArrayList<IContextValidator>();
        for (IContextValidator validator : this.validators) {
            if (validator.canValidate(toType, isTransitive)) {
                LOG.info("Using {} to validate {}", new Object[] { validator.getClass().toString(), toType });
                validators.add(validator);
            }
        }

        if (validators.isEmpty()) {
            LOG.warn("No {} validator to {}.", isTransitive ? "TRANSITIVE" : "NOT TRANSITIVE", toType);
        }

        return validators;
    }

    /**
     * Determines if the entity is global.
     *
     * @param type
     *            Type of entity to checked.
     * @return True if the entity is global, false otherwise.
     */
    public boolean isGlobalEntity(String type) {
        return EntityNames.isPublic(type);
    }

    private boolean skipValidation(EntityDefinition def, boolean isRead) {
        return def == null || def.skipContextValidation()
                || (GLOBAL_RESOURCES.contains(def.getResourceName()) && isRead);
    }

}