Java tutorial
/* * 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.service; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; import org.slc.sli.api.config.BasicDefinitionStore; import org.slc.sli.api.config.EntityDefinition; import org.slc.sli.api.constants.PathConstants; import org.slc.sli.api.representation.EntityBody; import org.slc.sli.api.security.CallingApplicationInfoProvider; import org.slc.sli.api.security.SLIPrincipal; import org.slc.sli.api.security.context.APIAccessDeniedException; import org.slc.sli.api.security.context.ContextValidator; import org.slc.sli.api.security.roles.EntityRightsFilter; import org.slc.sli.api.security.roles.RightAccessValidator; import org.slc.sli.api.security.schema.SchemaDataProvider; import org.slc.sli.api.util.SecurityUtil; import org.slc.sli.api.util.SecurityUtil.UserContext; import org.slc.sli.common.constants.EntityNames; import org.slc.sli.common.constants.ParameterConstants; import org.slc.sli.domain.*; import org.slc.sli.domain.enums.Right; import org.slc.sli.validation.EntityValidationException; import org.slc.sli.validation.ValidationError; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Scope; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.*; /** * Implementation of EntityService that can be used for most entities. * <p/> * <p/> * It is very important this bean prototype scope, since one service is needed per * entity/association. */ @Scope("prototype") @Component("basicService") public class BasicService implements EntityService, AccessibilityCheck { private static final Logger LOG = LoggerFactory.getLogger(BasicService.class); protected static final int MAX_RESULT_SIZE = 0; protected static final String CUSTOM_ENTITY_COLLECTION = "custom_entities"; protected static final String CUSTOM_ENTITY_CLIENT_ID = "clientId"; protected static final String CUSTOM_ENTITY_ENTITY_ID = "entityId"; protected static final List<String> STUDENT_SELF = Arrays.asList(EntityNames.STUDENT, EntityNames.STUDENT_PROGRAM_ASSOCIATION, EntityNames.STUDENT_COHORT_ASSOCIATION, EntityNames.STUDENT_SECTION_ASSOCIATION, EntityNames.PARENT, EntityNames.STUDENT_PARENT_ASSOCIATION); protected String collectionName; protected List<Treatment> treatments; protected EntityDefinition defn; protected Repository<Entity> repo; @Autowired protected ContextValidator contextValidator; @Autowired protected SchemaDataProvider provider; @Autowired protected CallingApplicationInfoProvider clientInfo; @Autowired protected BasicDefinitionStore definitionStore; @Autowired protected CustomEntityValidator customEntityValidator; @Autowired protected RightAccessValidator rightAccessValidator; @Autowired protected EntityRightsFilter entityRightsFilter; // The size limit of GET returns for dual contexts/differing user rights across edOrgs. @Value("${sli.api.service.countLimit:10000}") protected long countLimit; // The size limit of GET returns for dual contexts/differing user rights across edOrgs. @Value("${sli.api.service.batchSize:10000}") protected int batchSize; protected static ThreadLocal<ContextualRolesListingCache> rolesListingCacheTL = new ThreadLocal<ContextualRolesListingCache>() { @Override protected ContextualRolesListingCache initialValue() { return new ContextualRolesListingCache(); } }; public BasicService(String collectionName, List<Treatment> treatments, Repository<Entity> repo) { this.collectionName = collectionName; this.treatments = treatments; this.repo = repo; if (repo == null) { throw new IllegalArgumentException("Please provide repo"); } } @Override public long count(NeutralQuery neutralQuery) { boolean isSelf = isSelf(neutralQuery); checkAccess(true, isSelf, null); checkFieldAccess(neutralQuery, isSelf); return getRepo().count(collectionName, neutralQuery); } @Override public long countBasedOnContextualRoles(NeutralQuery neutralQuery) { boolean isSelf = isSelf(neutralQuery); Collection<GrantedAuthority> auths = SecurityUtil.getSLIPrincipal().getAllContextRights(isSelf); rightAccessValidator.checkAccess(true, null, defn.getType(), auths); rightAccessValidator.checkFieldAccess(neutralQuery, defn.getType(), auths); long count = 0; if (userHasMultipleContextsOrDifferingRights() && (!EntityNames.isPublic(defn.getType()))) { count = getAccessibleEntitiesCount(collectionName); } else { count = getRepo().count(collectionName, neutralQuery); } return count; } @Override public List<String> create(List<EntityBody> content) { List<String> entityIds = new ArrayList<String>(); for (EntityBody entityBody : content) { entityIds.add(create(entityBody)); } if (entityIds.size() != content.size()) { for (String id : entityIds) { delete(id); } } return entityIds; } @Override public List<String> createBasedOnContextualRoles(List<EntityBody> content) { List<String> entityIds = new ArrayList<String>(); for (EntityBody entityBody : content) { entityIds.add(createBasedOnContextualRoles(entityBody)); } if (entityIds.size() != content.size()) { for (String id : entityIds) { delete(id); } } return entityIds; } /** * Retrieves an entity from the data store with certain fields added/removed. * * @param neutralQuery all parameters to be included in query * @return the body of the entity */ @Override public Iterable<String> listIds(final NeutralQuery neutralQuery) { LOG.debug(">>>BasicService.listIds()"); LOG.debug(" neutralQuery: " + ToStringBuilder.reflectionToString(neutralQuery, ToStringStyle.MULTI_LINE_STYLE)); checkAccess(true, false, null); checkFieldAccess(neutralQuery, false); NeutralQuery nq = neutralQuery; injectSecurity(nq); Iterable<Entity> entities = repo.findAll(collectionName, nq); List<String> results = new ArrayList<String>(); for (Entity entity : entities) { results.add(entity.getEntityId()); } return results; } @Override public String create(EntityBody content) { LOG.debug(">>>BasicService.create()"); checkAccess(false, false, content); checkReferences(null, content); List<String> entityIds = new ArrayList<String>(); sanitizeEntityBody(content); // ideally we should validate everything first before actually persisting Entity entity = getRepo().create(defn.getType(), content, createMetadata(), collectionName); if (entity != null) { entityIds.add(entity.getEntityId()); } return entity.getEntityId(); } @Override public String createBasedOnContextualRoles(EntityBody content) { LOG.debug(">>>BasicService.createBasedOnContextualRoles()"); Entity entity = new MongoEntity(defn.getType(), null, content, createMetadata()); Collection<GrantedAuthority> auths = rightAccessValidator.getContextualAuthorities(false, entity, SecurityUtil.getUserContext(), false); rightAccessValidator.checkAccess(false, false, entity, entity.getType(), auths); checkReferences(null, content); List<String> entityIds = new ArrayList<String>(); sanitizeEntityBody(content); // Ideally, we should validate everything first before actually persisting! Entity created = getRepo().create(defn.getDbType(), content, createMetadata(), collectionName); if (created != null) { entityIds.add(created.getEntityId()); } return created.getEntityId(); } /** * Determines if the user has multiple contexts, or differing sets of rights per role. * * @return Whether or not the user has multiple contexts, or differing sets of rights per role */ protected boolean userHasMultipleContextsOrDifferingRights() { if (SecurityUtil.getUserContext() == UserContext.DUAL_CONTEXT) { return true; } SLIPrincipal principal = SecurityUtil.getSLIPrincipal(); if (principal.getEdOrgRights().size() > 1) { Iterator<Collection<GrantedAuthority>> edOrgRightsIter = principal.getEdOrgRights().values().iterator(); Collection<GrantedAuthority> firstRightsSet = edOrgRightsIter.next(); while (edOrgRightsIter.hasNext()) { Collection<GrantedAuthority> nextRightsSet = edOrgRightsIter.next(); if ((!firstRightsSet.containsAll(nextRightsSet)) || (!nextRightsSet.containsAll(firstRightsSet))) { return true; } } } return false; } /** * Validates that user roles allow access to fields * * @param isRead whether operation is "read" or "write" * @param isSelf whether operation is being done in "self" context * @param content item under inspection */ protected void checkAccess(boolean isRead, boolean isSelf, EntityBody content) { LOG.trace(">>>BasicService.checkAccess()"); Collection<GrantedAuthority> auths = getAuths(isSelf); rightAccessValidator.checkAccess(isRead, content, defn.getType(), auths); } protected void checkAccess(boolean isRead, String entityId, EntityBody content) { LOG.trace(">>>BasicService.checkAccess()"); rightAccessValidator.checkSecurity(isRead, entityId, defn.getType(), collectionName, getRepo()); try { checkAccess(isRead, isSelf(entityId), content); } catch (APIAccessDeniedException e) { // we only know the target entity here so rethrow with that info so it can be used in the security event Set<String> entityIds = new HashSet<String>(); entityIds.add(entityId); e.setEntityType(defn.getType()); e.setEntityIds(entityIds); throw e; } } /* * Check routine for interface * @see org.slc.sli.domain.AccessibilityCheck#accessibilityCheck(java.lang.String) */ @Override public boolean accessibilityCheck(String id) { try { checkAccess(false, id, null); } catch (APIAccessDeniedException e) { return false; } return true; } // This will be replaced by a call to: // getRepo().safeDelete(collectionName, id, true, false, 0, this); // I.e., cascade=true, dryrun=false, max=0=unlimited, access check = this service // // Future "enhanced" versions of delete exposed to the API can call safeDelete() // with different combinations of parameters @Override public void delete(String id) { LOG.debug(">>>BasicService.delete()"); checkAccess(false, id, null); if (!getRepo().delete(collectionName, id)) { LOG.info("Could not find {}", id); throw new EntityNotFoundException(id); } deleteAttachedCustomEntities(id); } @Override public void deleteBasedOnContextualRoles(String id) { NeutralQuery query = new NeutralQuery(); query.addCriteria(new NeutralCriteria("_id", "=", id)); boolean isSelf = isSelf(query); Entity entity = repo.findOne(collectionName, query); if (entity == null) { LOG.info("Could not find {}", id); throw new EntityNotFoundException(id); } Collection<GrantedAuthority> auths = getEntityContextAuthorities(entity, isSelf, false); rightAccessValidator.checkAccess(false, id, null, defn.getType(), collectionName, getRepo(), auths); if (!getRepo().delete(collectionName, id)) { LOG.info("Could not find {}", id); throw new EntityNotFoundException(id); } deleteAttachedCustomEntities(id); } @Override public boolean update(String id, EntityBody content, boolean applySecurityContext) { LOG.debug("Updating {} in {} with {}", new Object[] { id, collectionName, SecurityUtil.getSLIPrincipal() }); NeutralQuery query = new NeutralQuery(); query.addCriteria(new NeutralCriteria("_id", "=", id)); Entity entity = getRepo().findOne(collectionName, query); if (entity == null) { LOG.info("Could not find {}", id); throw new EntityNotFoundException(id); } if (applySecurityContext) { boolean isSelf = isSelf(id); Collection<GrantedAuthority> auths = getEntityContextAuthorities(entity, isSelf, false); rightAccessValidator.checkAccess(false, id, content, defn.getType(), collectionName, getRepo(), auths); } else { checkAccess(false, id, content); } sanitizeEntityBody(content); boolean success = false; if (entity.getBody().equals(content)) { LOG.info("No change detected to {}", id); return false; } checkReferences(id, content); LOG.info("new body is {}", content); entity.getBody().clear(); entity.getBody().putAll(content); success = repo.update(collectionName, entity, FullSuperDoc.isFullSuperdoc(entity)); return success; } @Override public boolean patch(String id, EntityBody content, boolean applySecurityContext) { LOG.debug("Patching {} in {}", id, collectionName); NeutralQuery query = new NeutralQuery(); query.addCriteria(new NeutralCriteria("_id", "=", id)); Entity entity = repo.findOne(collectionName, query); if (entity == null) { LOG.info("Could not find {}", id); throw new EntityNotFoundException(id); } if (applySecurityContext) { boolean isSelf = isSelf(query); Collection<GrantedAuthority> auths = getEntityContextAuthorities(entity, isSelf, false); rightAccessValidator.checkAccess(false, id, content, defn.getType(), collectionName, getRepo(), auths); } else { checkAccess(false, id, content); } sanitizeEntityBody(content); LOG.info("patch value(s): ", content); //run this check after sanitization if (content.isEmpty()) { //in this case there are no fields included for the PATCH request. This is a problem // because the update is built without an update operator expression which mongodb // interprets as a request to replace the entire document LOG.info("Entity body was empty on PATCH request for {}", id); return false; } // don't check references until things are combined checkReferences(id, content); repo.patch(defn.getType(), collectionName, id, content); return true; } @Override public EntityBody get(String id) { LOG.debug(">>>BasicService.get(id)"); return get(id, new NeutralQuery()); } @Override public EntityBody get(String id, NeutralQuery neutralQuery) { LOG.debug(">>>BasicService.get(id, neutralQuery)"); Entity entity = getEntity(id, neutralQuery); if (entity == null) { throw new EntityNotFoundException(id); } return makeEntityBody(entity); } protected Entity getEntity(String id, final NeutralQuery neutralQuery) { LOG.debug(">>>BasicService.getEntity(id, neutralQuery)"); checkAccess(true, id, null); checkFieldAccess(neutralQuery, isSelf(id)); NeutralQuery nq = neutralQuery; if (nq == null) { nq = new NeutralQuery(); } nq.addCriteria(new NeutralCriteria("_id", "=", id)); Entity entity = repo.findOne(collectionName, nq); return entity; } protected Iterable<EntityBody> noEntitiesFound(NeutralQuery neutralQuery) { // this.addDefaultQueryParams(neutralQuery, collectionName); if (!repo.findAll(collectionName, neutralQuery).iterator().hasNext()) { return new ArrayList<EntityBody>(); } else { throw new APIAccessDeniedException("Access to resource denied."); } } protected Iterable<EntityBody> noEntitiesFound(Boolean noDataInDB) { if (noDataInDB) { return new ArrayList<EntityBody>(); } else { throw new APIAccessDeniedException("Access to resource denied."); } } @Override public Iterable<EntityBody> get(Iterable<String> ids) { LOG.debug(">>>BasicService.get(Iterable id)"); NeutralQuery neutralQuery = new NeutralQuery(); neutralQuery.setOffset(0); neutralQuery.setLimit(MAX_RESULT_SIZE); return get(ids, neutralQuery); } @Override public Iterable<EntityBody> get(Iterable<String> ids, final NeutralQuery neutralQuery) { LOG.debug(">>>BasicService.get(Iterable id, neutralQuery)"); if (!ids.iterator().hasNext()) { return Collections.emptyList(); } checkAccess(true, false, null); checkFieldAccess(neutralQuery, false); List<String> idList = new ArrayList<String>(); for (String id : ids) { idList.add(id); } if (!idList.isEmpty()) { NeutralQuery nq = neutralQuery; if (nq == null) { nq = new NeutralQuery(); nq.setOffset(0); nq.setLimit(MAX_RESULT_SIZE); } // add the ids requested nq.addCriteria(new NeutralCriteria("_id", "in", idList)); injectSecurity(nq); Iterable<Entity> entities = repo.findAll(collectionName, nq); List<EntityBody> results = new ArrayList<EntityBody>(); for (Entity e : entities) { results.add(makeEntityBody(e)); } return results; } return Collections.emptyList(); } @Override public Iterable<EntityBody> list(NeutralQuery neutralQuery) { LOG.debug(">>>BasicService.list(neutralQuery)"); listSecurityCheck(neutralQuery); return listImplementationAfterSecurityChecks(neutralQuery); } protected void listSecurityCheck(NeutralQuery neutralQuery) { LOG.debug(">>>BasicService.list(neutralQuery)"); boolean isSelf = isSelf(neutralQuery); checkAccess(true, isSelf, null); checkFieldAccess(neutralQuery, isSelf); injectSecurity(neutralQuery); } protected Iterable<EntityBody> listImplementationAfterSecurityChecks(NeutralQuery neutralQuery) { Collection<Entity> entities = (Collection<Entity>) repo.findAll(collectionName, neutralQuery); setAccessibleEntitiesCount(collectionName, entities.size()); List<EntityBody> results = new ArrayList<EntityBody>(); for (Entity entity : entities) { results.add(makeEntityBody(entity)); } if (results.isEmpty()) { return noEntitiesFound(neutralQuery); } return results; } @Override public Iterable<EntityBody> listBasedOnContextualRoles(NeutralQuery neutralQuery) { boolean isSelf = isSelf(neutralQuery); boolean noDataInDB = true; Map<String, UserContext> entityContexts = null; injectSecurity(neutralQuery); boolean findSpecial = userHasMultipleContextsOrDifferingRights() && (!EntityNames.isPublic(defn.getType())); Collection<Entity> entities = new HashSet<Entity>(); if (findSpecial) { entities = getResponseEntities(neutralQuery, isSelf); entityContexts = getEntityContexts(); } else { entities = (Collection<Entity>) repo.findAll(collectionName, neutralQuery); if (SecurityUtil.getUserContext() == UserContext.DUAL_CONTEXT) { entityContexts = getEntityContextMap(entities, true); } } //entities wihout bodies are considered deleted Collection<Entity> bodylessEntities = new HashSet<Entity>(); for (Entity ent : entities) { if (ent.getBody() == null || ent.getBody().size() == 0) { bodylessEntities.add(ent); } } entities.removeAll(bodylessEntities); noDataInDB = entities.isEmpty(); List<EntityBody> results = new ArrayList<EntityBody>(); for (Entity entity : entities) { UserContext context = getEntityContext(entity.getEntityId(), entityContexts); try { Collection<GrantedAuthority> auths = null; if (!findSpecial) { auths = rightAccessValidator.getContextualAuthorities(isSelf, entity, context, true); rightAccessValidator.checkAccess(true, isSelf, entity, defn.getType(), auths); rightAccessValidator.checkFieldAccess(neutralQuery, entity, defn.getType(), auths); } else { auths = getEntityAuthorities(entity.getEntityId()); } results.add(entityRightsFilter.makeEntityBody(entity, treatments, defn, isSelf, auths, context)); } catch (AccessDeniedException aex) { if (entities.size() == 1) { throw aex; } else { LOG.error(aex.getMessage()); } } } if (results.isEmpty()) { validateQuery(neutralQuery, isSelf); return noEntitiesFound(noDataInDB); } return results; } protected Collection<Entity> getResponseEntities(NeutralQuery neutralQuery, boolean isSelf) throws AccessDeniedException { LOG.debug(">>>BasicService.getResponseEntities()"); Collection<Entity> responseEntities = new ArrayList<Entity>(); Map<String, UserContext> entityContexts = new HashMap<String, UserContext>(); final int limit = neutralQuery.getLimit(); final int offset = neutralQuery.getOffset(); // Iterate through all queried entities. For each one that passes access checks, increment the count. // Additionally, collect the accessible entities requested for this call. Stop at the hard count limit. final int responseLimit = ((0 < limit) && (limit < getCountLimit())) ? limit : (int) getCountLimit(); long accessibleCount = 0; int currentOffset = 0; int totalCount = 0; Collection<Entity> batchedEntities; do { batchedEntities = getBatchedEntities(neutralQuery, getBatchSize(), currentOffset); totalCount += batchedEntities.size(); Map<String, UserContext> batchedEntityContexts = new HashMap<String, UserContext>(); if (SecurityUtil.getUserContext() == UserContext.DUAL_CONTEXT) { batchedEntityContexts = getEntityContextMap(batchedEntities, true); } Iterator<Entity> batchedEntitiesIt = batchedEntities.iterator(); while ((accessibleCount < getCountLimit()) && batchedEntitiesIt.hasNext()) { Entity entity = batchedEntitiesIt.next(); try { validateEntity(entity, isSelf, neutralQuery, batchedEntityContexts); accessibleCount++; if ((accessibleCount > offset) && (responseEntities.size() < responseLimit)) { responseEntities.add(entity); entityContexts.put(entity.getEntityId(), getEntityContext(entity.getEntityId(), batchedEntityContexts)); } } catch (AccessDeniedException aex) { if (totalCount == 1) { throw aex; } else if ((accessibleCount > offset) && (responseEntities.size() < responseLimit)) { LOG.error(aex.getMessage()); } } } currentOffset += getBatchSize(); } while ((accessibleCount < getCountLimit()) && (batchedEntities.size() == getBatchSize())); neutralQuery.setLimit(limit); neutralQuery.setOffset(offset); if ((totalCount > 0) && (accessibleCount == 0)) { validateQuery(neutralQuery, isSelf); throw new APIAccessDeniedException("Access to resource denied."); } setEntityContexts(entityContexts); // Store the total accessible entity count, for later retrieval by countBasedOnContextualRoles. setAccessibleEntitiesCount(collectionName, accessibleCount); return responseEntities; } protected Collection<Entity> getBatchedEntities(NeutralQuery neutralQuery, int limit, int offset) { LOG.debug(">>>BasicService.getBatchedEntities()"); neutralQuery.setOffset(offset); neutralQuery.setLimit(limit); Collection<Entity> batchedEntities = (Collection<Entity>) getRepo().findAll(collectionName, neutralQuery); return batchedEntities; } protected void validateEntity(Entity entity, boolean isSelf, NeutralQuery neutralQuery, Map<String, UserContext> entityContexts) { UserContext context = getEntityContext(entity.getEntityId(), entityContexts); Collection<GrantedAuthority> auths = rightAccessValidator.getContextualAuthorities(isSelf, entity, context, true); rightAccessValidator.checkAccess(true, isSelf, entity, defn.getType(), auths); rightAccessValidator.checkFieldAccess(neutralQuery, entity, defn.getType(), auths); setEntityAuthorities(entity.getEntityId(), auths); } protected UserContext getEntityContext(String entityId, Map<String, UserContext> entityContext) { UserContext context = SecurityUtil.getUserContext(); if ((SecurityUtil.getUserContext() == UserContext.DUAL_CONTEXT) && (entityContext != null)) { if (entityContext.containsKey(entityId)) { context = entityContext.get(entityId); } else { context = UserContext.NO_CONTEXT; } } return context; } protected void validateQuery(NeutralQuery neutralQuery, boolean self) { LOG.debug(">>>BasicService.validateQuery()"); NeutralQuery newQuery = new NeutralQuery(neutralQuery); boolean removableCriteriaExists = false; for (NeutralCriteria cr : neutralQuery.getCriteria()) { if (cr.isRemovable()) { newQuery.removeCriteria(cr); removableCriteriaExists = true; } } if (removableCriteriaExists) { Collection<Entity> noSearchEntities = (Collection<Entity>) repo.findAll(collectionName, newQuery); for (Entity en : noSearchEntities) { Collection<GrantedAuthority> auths = getEntityContextAuthorities(en, self, true); rightAccessValidator.checkAccess(true, self, en, defn.getType(), auths); rightAccessValidator.checkFieldAccess(neutralQuery, en, defn.getType(), auths); } } } @Override public boolean exists(String id) { checkAccess(true, isSelf(id), null); boolean exists = false; NeutralQuery query = new NeutralQuery(); query.addCriteria(new NeutralCriteria("_id", "=", id)); injectSecurity(query); Iterable<Entity> entities = repo.findAll(collectionName, query); if (entities != null && entities.iterator().hasNext()) { exists = true; } return exists; } /** * Returns the custom entity associated with a security event and the application associated with the * user's session. * * @param id * @return */ @Override public EntityBody getCustom(String id) { getCustomSecurityCheck(id); String clientId = null; try { clientId = getClientId(id); } catch (APIAccessDeniedException e) { // set custom entity data for security event targetEdOrgList APIAccessDeniedException wrapperE = new APIAccessDeniedException( "Custom entity HTTP GET request denied.", e); Set<String> entityIds = new HashSet<String>(); entityIds.add(id); wrapperE.setEntityType(defn.getType()); wrapperE.setEntityIds(entityIds); throw wrapperE; } LOG.debug("Reading custom entity: entity={}, entityId={}, clientId={}", new Object[] { getEntityDefinition().getType(), id, clientId }); Entity customEntity = getCustomEntity(id, clientId); if (customEntity != null) { EntityBody clonedBody = new EntityBody(customEntity.getBody()); return clonedBody; } else { return null; } } /** * Does security checks other than retrieving the client id for the application which the * current user is using. * * @param id of the entity for which to fetch the custom document */ protected void getCustomSecurityCheck(String id) { if (SecurityUtil.isStaffUser()) { Entity entity = getEntity(id); Collection<GrantedAuthority> auths = getEntityContextAuthorities(entity, isSelf(id), true); rightAccessValidator.checkAccess(true, id, null, defn.getType(), collectionName, getRepo(), auths); } else { checkAccess(true, id, null); } } /** * Deletes the custom entity associated with a security event and the application associated with the * user's session. * * @param id * @return */ @Override public void deleteCustom(String id) { deleteCustomSecurityCheck(id); String clientId = null; try { clientId = getClientId(id); } catch (APIAccessDeniedException e) { // set custom entity data for security event targetEdOrgList APIAccessDeniedException wrapperE = new APIAccessDeniedException("Custom entity delete denied.", e); Set<String> entityIds = new HashSet<String>(); entityIds.add(id); wrapperE.setEntityType(defn.getType()); wrapperE.setEntityIds(entityIds); throw wrapperE; } Entity customEntity = getCustomEntity(id, clientId); if (customEntity == null) { throw new EntityNotFoundException(id); } boolean deleted = getRepo().delete(CUSTOM_ENTITY_COLLECTION, customEntity.getEntityId()); LOG.debug("Deleting custom entity: entity={}, entityId={}, clientId={}, deleted?={}", new Object[] { getEntityDefinition().getType(), id, clientId, String.valueOf(deleted) }); } /* * Does security checks other than retrieving the client id for the application which the * current user is using. * * @param id of the entity for which to fetch the custom document */ protected void deleteCustomSecurityCheck(String id) { if (SecurityUtil.isStaffUser()) { Entity entity = getEntity(id); Collection<GrantedAuthority> auths = getEntityContextAuthorities(entity, isSelf(id), false); rightAccessValidator.checkAccess(false, id, null, defn.getType(), collectionName, getRepo(), auths); } else { checkAccess(false, id, null); } } /** * Creates or updates the custom entity associated with a security event and the application associated with the * user's session. * * @param id * @return */ @Override public void createOrUpdateCustom(String id, EntityBody customEntity) throws EntityValidationException { createOrUpdateCustomSecurityCheck(id, customEntity); String clientId = null; try { clientId = getClientId(id); } catch (APIAccessDeniedException e) { // set custom entity data for security event targetEdOrgList APIAccessDeniedException wrapperE = new APIAccessDeniedException("Custom entity write denied.", e); Set<String> entityIds = new HashSet<String>(); entityIds.add(id); wrapperE.setEntityType(defn.getType()); wrapperE.setEntityIds(entityIds); throw wrapperE; } Entity entity = getCustomEntity(id, clientId); if (entity != null && entity.getBody().equals(customEntity)) { LOG.debug("No change detected to custom entity, ignoring update: entity={}, entityId={}, clientId={}", new Object[] { getEntityDefinition().getType(), id, clientId }); return; } // Verify field names contain no blacklisted components. List<ValidationError> errorList = customEntityValidator.validate(customEntity); if (!errorList.isEmpty()) { LOG.debug("Blacklist validation failed for custom entity {}", id); throw new EntityValidationException(id, PathConstants.CUSTOM_ENTITIES, errorList); } EntityBody clonedEntity = new EntityBody(customEntity); if (entity != null) { LOG.debug("Overwriting existing custom entity: entity={}, entityId={}, clientId={}", new Object[] { getEntityDefinition().getType(), id, clientId }); entity.getBody().clear(); entity.getBody().putAll(clonedEntity); // custom entity is not superdoc getRepo().update(CUSTOM_ENTITY_COLLECTION, entity, false); } else { LOG.debug("Creating new custom entity: entity={}, entityId={}, clientId={}", new Object[] { getEntityDefinition().getType(), id, clientId }); EntityBody metaData = new EntityBody(); SLIPrincipal principal = (SLIPrincipal) SecurityContextHolder.getContext().getAuthentication() .getPrincipal(); metaData.put(CUSTOM_ENTITY_CLIENT_ID, clientId); metaData.put(CUSTOM_ENTITY_ENTITY_ID, id); metaData.put("tenantId", principal.getTenantId()); getRepo().create(CUSTOM_ENTITY_COLLECTION, clonedEntity, metaData, CUSTOM_ENTITY_COLLECTION); } } /* * Does security checks other than retrieving the client id for the application which the * current user is using. * * @param id of the entity for which to fetch the custom document */ protected void createOrUpdateCustomSecurityCheck(String id, EntityBody customEntity) { if (SecurityUtil.isStaffUser()) { Entity parentEntity = getEntity(id); Collection<GrantedAuthority> auths = getEntityContextAuthorities(parentEntity, isSelf(id), false); rightAccessValidator.checkAccess(false, id, customEntity, defn.getType(), collectionName, getRepo(), auths); } else { checkAccess(false, id, customEntity); } } protected void checkReferences(String entityId, EntityBody eb) { /* TODO: MAKE BETTER * Note that this is a workaround to allow students to validate * only their own student ID when checking references, else they'd never * be able to POST or PUT anything */ if (SecurityUtil.isStudent()) { String entityType = defn.getType(); if (entityType.equals(EntityNames.STUDENT)) { // Validate id is yourself if (!SecurityUtil.getSLIPrincipal().getEntity().getEntityId().equals(entityId)) { throw new APIAccessDeniedException("Cannot update student not yourself", entityType, entityId); } } else if (entityType.equals(EntityNames.STUDENT_ASSESSMENT)) { String studentId = (String) eb.get(ParameterConstants.STUDENT_ID); // Validate student ID is yourself if (studentId != null && !SecurityUtil.getSLIPrincipal().getEntity().getEntityId().equals(studentId)) { throw new APIAccessDeniedException("Cannot update student assessments that are not your own", EntityNames.STUDENT, studentId); } } else if (entityType.equals(EntityNames.STUDENT_GRADEBOOK_ENTRY) || entityType.equals(EntityNames.GRADE)) { String studentId = (String) eb.get(ParameterConstants.STUDENT_ID); String ssaId = (String) eb.get(ParameterConstants.STUDENT_SECTION_ASSOCIATION_ID); // Validate student ID is yourself if (studentId != null && !SecurityUtil.getSLIPrincipal().getEntity().getEntityId().equals(studentId)) { throw new APIAccessDeniedException("Cannot update " + entityType + " that are not your own", EntityNames.STUDENT, studentId); } // Validate SSA ids are accessible via non-transitive SSA validator if (ssaId != null) { EntityDefinition def = definitionStore .lookupByEntityType(EntityNames.STUDENT_SECTION_ASSOCIATION); contextValidator.validateContextToEntities(def, Arrays.asList(ssaId), false); } } else { // At the time of this comment, students can only write to student, studentAssessment, studentGradebookEntry, or grade throw new IllegalArgumentException("Students cannot write entities of type " + entityType); } // If you get this far, its all good return; } else if (SecurityUtil.isParent()) { String entityType = defn.getType(); if (entityType.equals(EntityNames.PARENT)) { // Validate id is yourself if (!SecurityUtil.getSLIPrincipal().getEntity().getEntityId().equals(entityId)) { throw new APIAccessDeniedException("Cannot update parent not yourself", entityType, entityId); } } else if (entityType.equals(EntityNames.STUDENT)) { Set<String> ownStudents = SecurityUtil.getSLIPrincipal().getOwnedStudentIds(); if (!ownStudents.contains(entityId)) { throw new APIAccessDeniedException("Cannot update student that are not your own", EntityNames.STUDENT, entityId); } } else { // At the time of this comment, parents can only write to student and parent throw new IllegalArgumentException("Parents cannot write entities of type " + entityType); } // If you get this far, its all good return; } // else if staff/teacher, do legacy for (Map.Entry<String, Object> entry : eb.entrySet()) { String fieldName = entry.getKey(); Object value = entry.getValue(); String entityType = provider.getReferencingEntity(defn.getType(), fieldName); if (value == null || entityType == null) { continue; } LOG.debug("Field {} is referencing {}", fieldName, entityType); @SuppressWarnings("unchecked") List<String> ids = value instanceof List ? (List<String>) value : Arrays.asList((String) value); EntityDefinition def = definitionStore.lookupByEntityType(entityType); if (def == null) { LOG.debug("Invalid reference field: {} does not have an entity definition registered", fieldName); ValidationError error = new ValidationError(ValidationError.ErrorType.INVALID_FIELD_NAME, fieldName, value, null); throw new EntityValidationException(null, null, Arrays.asList(error)); } try { contextValidator.validateContextToEntities(def, ids, true); } catch (APIAccessDeniedException e) { LOG.debug("Invalid Reference: {} in {} is not accessible by user", value, def.getStoredCollectionName()); throw new APIAccessDeniedException("Invalid reference. No association to referenced entity.", e); } catch (EntityNotFoundException e) { LOG.debug("Invalid Reference: {} in {} does not exist", value, def.getStoredCollectionName()); throw (APIAccessDeniedException) new APIAccessDeniedException( "Invalid reference. No association to referenced entity.", defn.getType(), entityId) .initCause(e); } } } protected String getClientId(String id) { String clientId = null; try { clientId = clientInfo.getClientId(); } catch (APIAccessDeniedException e) { // set custom entity data for security event targetEdOrgList APIAccessDeniedException wrapperE = new APIAccessDeniedException("Custom entity get denied.", e); Set<String> entityIds = new HashSet<String>(); entityIds.add(id); wrapperE.setEntityType(defn.getType()); wrapperE.setEntityIds(entityIds); } if (clientId == null) { Set<String> entityIds = new HashSet<String>(); entityIds.add(id); throw new APIAccessDeniedException("No Application Id", defn.getType(), entityIds); } return clientId; } /** * given an entity, make the entity body to expose * * @param entity * @return */ protected EntityBody makeEntityBody(Entity entity) { Collection<GrantedAuthority> selfAuths = getAuths(isSelf(entity.getEntityId())); Collection<GrantedAuthority> nonSelfAuths = getAuths(false); EntityBody toReturn = entityRightsFilter.makeEntityBody(entity, treatments, defn, nonSelfAuths, selfAuths); return toReturn; } /** * given an entity body that was exposed, return the version with the treatments reversed * * @param content * @return */ protected EntityBody sanitizeEntityBody(EntityBody content) { for (Treatment treatment : treatments) { treatment.toStored(content, defn); } return content; } protected void deleteAttachedCustomEntities(String sourceId) { NeutralQuery query = new NeutralQuery(); query.addCriteria(new NeutralCriteria("metaData." + CUSTOM_ENTITY_ENTITY_ID, "=", sourceId, false)); Iterable<String> ids = getRepo().findAllIds(CUSTOM_ENTITY_COLLECTION, query); for (String id : ids) { getRepo().delete(CUSTOM_ENTITY_COLLECTION, id); } } protected boolean isSelf(NeutralQuery query) { //This checks if they're querying for a self entity. It's overly convoluted because going to //resourcename/<ID> calls this method instead of calling get(String id) List<NeutralCriteria> allTheCriteria = query.getCriteria(); for (NeutralQuery orQuery : query.getOrQueries()) { if (!isSelf(orQuery)) { return false; } } for (NeutralCriteria criteria : allTheCriteria) { if (criteria.getOperator().equals(NeutralCriteria.CRITERIA_IN) && criteria.getValue() instanceof List) { // key IN [{self id}] List<?> value = (List<?>) criteria.getValue(); if (value.size() == 1 && isSelf(value.get(0).toString())) { return true; } } else if (criteria.getOperator().equals(NeutralCriteria.OPERATOR_EQUAL) && criteria.getValue() instanceof String) { if (isSelf((String) criteria.getValue())) { return true; } } } return false; } protected boolean isSelf(String entityId) { SLIPrincipal principal = SecurityUtil.getSLIPrincipal(); String selfId = principal.getEntity().getEntityId(); Collection<String> studentIds = principal.getOwnedStudentIds(); String type = defn.getType(); if (selfId != null) { if (selfId.equals(entityId) || studentIds.contains(entityId)) { return true; } else if (EntityNames.STAFF_ED_ORG_ASSOCIATION.equals(type)) { Entity entity = repo.findById(defn.getStoredCollectionName(), entityId); if (entity != null) { Map<String, Object> body = entity.getBody(); return selfId.equals(body.get(ParameterConstants.STAFF_REFERENCE)); } } else if (EntityNames.TEACHER_SCHOOL_ASSOCIATION.equals(type)) { Entity entity = repo.findById(defn.getStoredCollectionName(), entityId); if (entity != null) { Map<String, Object> body = entity.getBody(); return selfId.equals(body.get(ParameterConstants.TEACHER_ID)); } } else if (SecurityUtil.isStudentOrParent() && STUDENT_SELF.contains(type)) { Entity entity = repo.findById(defn.getStoredCollectionName(), entityId); if (entity != null) { Set<String> owned = principal.getOwnedStudentIds(); return owned.contains(entity.getBody().get(ParameterConstants.STUDENT_ID)) || owned.contains(entityId); } } } return false; } protected Collection<GrantedAuthority> getAuths(boolean isSelf) { Collection<GrantedAuthority> result = new HashSet<GrantedAuthority>(); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); result.addAll(auth.getAuthorities()); if (isSelf) { SLIPrincipal principal = SecurityUtil.getSLIPrincipal(); result.addAll(principal.getSelfRights()); } return result; } /** * Checks query params for access restrictions * * @param query The query to check */ protected void checkFieldAccess(NeutralQuery query, boolean isSelf) { if (query != null) { // get the authorities Collection<GrantedAuthority> auths = getAuths(isSelf); rightAccessValidator.checkFieldAccess(query, defn.getType(), auths); } } /** * Determines if there is a union of all needed rights with the user's collection of granted * authorities. * * @param authorities User's collection of granted authorities. * @param neededRights Set of rights needed for accessing a given field. * @return True if the user can access the field, false otherwise. */ protected boolean union(Collection<GrantedAuthority> authorities, Set<Right> neededRights) { boolean union = true; for (Right neededRight : neededRights) { if (!authorities.contains(neededRight)) { union = false; break; } } return union; } /** * Creates the metaData HashMap to be added to the entity created in mongo. * * @return Map containing important metadata for the created entity. */ protected Map<String, Object> createMetadata() { Map<String, Object> metadata = new HashMap<String, Object>(); SLIPrincipal principal = (SLIPrincipal) SecurityContextHolder.getContext().getAuthentication() .getPrincipal(); String createdBy = principal.getEntity().getEntityId(); if (createdBy != null && createdBy.equals(SLIPrincipal.NULL_ENTITY_ID)) { createdBy = principal.getExternalId(); } metadata.put("createdBy", createdBy); metadata.put("isOrphaned", "true"); metadata.put("tenantId", principal.getTenantId()); return metadata; } protected Entity getEntity(String id) { NeutralQuery entityQuery = new NeutralQuery(); entityQuery.addCriteria(new NeutralCriteria("_id", "=", id)); Entity entity = repo.findOne(collectionName, entityQuery); if (entity == null) { LOG.info("Could not find {}", id); throw new EntityNotFoundException(id); } return entity; } protected Entity getCustomEntity(String id, String clientId) { NeutralQuery query = new NeutralQuery(); query.addCriteria(new NeutralCriteria("metaData." + CUSTOM_ENTITY_CLIENT_ID, "=", clientId, false)); query.addCriteria(new NeutralCriteria("metaData." + CUSTOM_ENTITY_ENTITY_ID, "=", id, false)); return getRepo().findOne(CUSTOM_ENTITY_COLLECTION, query); } /** * Set the entity definition for this service. There is a circular dependency between * BasicService and * EntityDefinition, so they both can't have it be a constructor arg. */ public void setDefn(EntityDefinition defn) { this.defn = defn; } @Override public EntityDefinition getEntityDefinition() { return defn; } protected String getCollectionName() { return collectionName; } protected List<Treatment> getTreatments() { return treatments; } protected Repository<Entity> getRepo() { return repo; } public long getCountLimit() { return countLimit; } public int getBatchSize() { return batchSize; } protected void setClientInfo(CallingApplicationInfoProvider clientInfo) { this.clientInfo = clientInfo; } @Override public CalculatedData<String> getCalculatedValues(String id) { Entity entity = getEntity(id, new NeutralQuery()); return entity.getCalculatedValues(); } @Override public CalculatedData<Map<String, Integer>> getAggregates(String id) { Entity entity = getEntity(id, new NeutralQuery()); return entity.getAggregates(); } @Override public boolean collectionExists(String collection) { return getRepo().collectionExists(collection); } protected void injectSecurity(NeutralQuery nq) { SLIPrincipal prince = SecurityUtil.getSLIPrincipal(); List<NeutralQuery> obligations = prince.getObligation(this.collectionName); for (NeutralQuery obligation : obligations) { nq.addOrQuery(obligation); } } protected Map<String, UserContext> getEntityContextMap(Collection<Entity> entities, boolean isRead) { return contextValidator.getValidatedEntityContexts(defn, entities, SecurityUtil.isTransitive(), isRead); } protected Collection<GrantedAuthority> getEntityContextAuthorities(Entity entity, boolean isSelf, boolean isRead) { UserContext context = SecurityUtil.getUserContext(); if (context == UserContext.DUAL_CONTEXT) { Map<String, UserContext> entityContext = getEntityContextMap(Arrays.asList(entity), isRead); if (entityContext != null) { context = entityContext.get(entity.getEntityId()); } } return rightAccessValidator.getContextualAuthorities(isSelf, entity, context, isRead); } protected long getAccessibleEntitiesCount(String collectionName) { return rolesListingCacheTL.get().getAccessibleEntitiesCount(collectionName); } protected void setAccessibleEntitiesCount(String collectionName, long count) { rolesListingCacheTL.get().setAccessibleEntitiesCount(collectionName, count); } protected Map<String, UserContext> getEntityContexts() { return rolesListingCacheTL.get().getEntityContexts(); } protected void setEntityContexts(Map<String, UserContext> entityContexts) { rolesListingCacheTL.get().setEntityContexts(entityContexts); } protected Collection<GrantedAuthority> getEntityAuthorities(String entityId) { return rolesListingCacheTL.get().getEntityAuthorities(entityId); } protected void setEntityAuthorities(String entityId, Collection<GrantedAuthority> authorities) { rolesListingCacheTL.get().setEntityAuthorities(entityId, authorities); } }