CustomAclServiceImpl.java

/*
 * Copyright 2018 Global Crop Diversity Trust
 *
 * 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.genesys.blocks.security.service.impl;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.EntityManager;

import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.blocks.security.model.AclAwareModel;
import org.genesys.blocks.security.model.AclClass;
import org.genesys.blocks.security.model.AclEntry;
import org.genesys.blocks.security.model.AclObjectIdentity;
import org.genesys.blocks.security.model.AclSid;
import org.genesys.blocks.security.persistence.AclClassPersistence;
import org.genesys.blocks.security.persistence.AclEntryPersistence;
import org.genesys.blocks.security.persistence.AclObjectIdentityPersistence;
import org.genesys.blocks.security.persistence.AclSidPersistence;
import org.genesys.blocks.security.serialization.Permissions;
import org.genesys.blocks.security.serialization.SidPermissions;
import org.genesys.blocks.security.service.CustomAclService;
import org.genesys.blocks.util.ClassAclOid;
import org.hibernate.proxy.HibernateProxyHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.model.Permission;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import lombok.extern.slf4j.Slf4j;

/**
 * The Class CustomAclServiceImpl.
 */
@Service
@Transactional
@Slf4j
public class CustomAclServiceImpl implements CustomAclService {

	/**
	 * The name of the cache holding SID id-to-name mappings.
	 */
	public static final String CACHE_SID_NAMES = "aclSidNames";

	/** The base permissions. */
	private static Permission[] basePermissions;

	/** The acl object identity persistence. */
	@Autowired
	private AclObjectIdentityPersistence aclObjectIdentityPersistence;

	/** The acl class persistence. */
	@Autowired
	private AclClassPersistence aclClassPersistence;

	/** The acl entry persistence. */
	@Autowired
	private AclEntryPersistence aclEntryPersistence;

	/** The cache manager. */
	@Autowired(required = false)
	private CacheManager cacheManager;

	/** The acl sid persistence. */
	@Autowired
	private AclSidPersistence aclSidPersistence;

	/** The entity manager. */
	@Autowired
	private EntityManager entityManager;

	static {
		basePermissions = new Permission[] { BasePermission.CREATE, BasePermission.READ, BasePermission.WRITE, BasePermission.DELETE, BasePermission.ADMINISTRATION };
	}

	@Override
	@Transactional(readOnly = true)
	public AclSid getSid(Long id) {
		return aclSidPersistence.findById(id).orElse(null);
	}


	@Override
	@Transactional(readOnly = true)
	@Cacheable(cacheNames = { CACHE_SID_NAMES }, key = "#id", unless = "#result == null")
	public String getSidName(long id) {
		AclSid sid = aclSidPersistence.findById(id).orElse(null);
		return sid == null ? null : sid.getSid();
	}
	
	@Override
	@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
	@Cacheable(cacheNames = { CACHE_SID_NAMES }, key = "#sid", unless = "#result == null")
	public Long getSidId(String sid) {
		return aclSidPersistence.getSidId(sid);
	}
	
	@Override
	public AclSid getAuthoritySid(String authority) {
		return aclSidPersistence.findBySidAndPrincipal(authority, false);
	}

	@Override
	@Transactional(propagation = Propagation.REQUIRED)
	public AclSid ensureAuthoritySid(String authority) {
		return ensureSidForAuthority(authority);
	}
	
	@Override
	@Transactional(propagation = Propagation.REQUIRED)
	public AclSid removeAuthoritySid(String authority) {
		AclSid authoritySid = aclSidPersistence.findBySidAndPrincipal(authority, false);

		if (authoritySid == null) {
			log.warn("ACL SID for authority {} does not exist", authority);
			return null;
		}

		// Remove ACL entries
		removePermissionsFor(authoritySid);

		aclSidPersistence.delete(authoritySid);

		return authoritySid;
	}
	
	@Override
	@Transactional(propagation = Propagation.REQUIRED)
	public AclObjectIdentity createOrUpdatePermissions(final AclAwareModel target, AclSid ownerSid) {
		if (target == null || (target.getId() <= 0l && !(target instanceof ClassAclOid<?>))) {
			log.warn("No target specified for ACL permissions, bailing out!");
			return null;
		}

		String className = target.getClass().getName();
		if (target instanceof ClassAclOid<?>) {
			className = ((ClassAclOid<?>) target).getClassName();
		}

		final AclClass aclClass = ensureAclClass(className);

		// save object identity
		AclObjectIdentity objectIdentity = aclObjectIdentityPersistence.findByObjectIdAndClassname(target.getId(), aclClass.getAclClass());

		if (objectIdentity == null) {
			objectIdentity = new AclObjectIdentity();

			if (ownerSid == null) {
				log.warn("No SID in security context, not assigning creator permissions");
			} else if (ownerSid.isNew()) {
				log.warn("Owner SID not persisted, not assigning creator permissions");
			} else {
				objectIdentity.setOwnerSid(ownerSid);
			}

			log.debug("Inserting owner ACL entries for owner={} class={} id={}", ownerSid, target.getClass().getName(), target.getId());

			objectIdentity.setObjectIdIdentity(target.getId());
			objectIdentity.setAclClass(aclClass);

			AclObjectIdentity parentObject = target.aclParentObjectIdentity(); 
			if (parentObject == null && target.aclParentObject() != null) {
				// get OID of parent business entity
				parentObject = getObjectIdentity(target.aclParentObject());
			}
			if (parentObject != null) {
				objectIdentity.setParentObject(parentObject);
			}
			objectIdentity.setEntriesInheriting(true);

			objectIdentity = aclObjectIdentityPersistence.save(objectIdentity);
			if (objectIdentity.getOwnerSid() != null) {
				// Grant permissions to owner
				final Permissions permissions = new Permissions().grantAll();
				addPermissions(objectIdentity, objectIdentity.getOwnerSid(), permissions);
			}

		} else {
			// update permissions
			log.debug("Updating ACL parent object for class={} id={}", target.getClass().getName(), target.getId());

			if (objectIdentity.getOwnerSid() == null) {
				if (ownerSid != null && ! ownerSid.isNew()) {
					objectIdentity.setOwnerSid(ownerSid);
					
					// Grant permissions to owner
					final Permissions permissions = new Permissions().grantAll();
					addPermissions(objectIdentity, objectIdentity.getOwnerSid(), permissions);
				} else {
					log.debug("Owner SID not persisted or is null.");
				}
			}
			
			AclObjectIdentity parentObject = target.aclParentObjectIdentity(); 
			if (parentObject == null && target.aclParentObject() != null) {
				// get OID of parent business entity
				parentObject = getObjectIdentity(target.aclParentObject());
			}

			if (parentObject != null) {
				log.trace("Updating ACL parent to {}", parentObject);
				objectIdentity.setParentObject(parentObject);
				objectIdentity.setEntriesInheriting(true);
			} else {
				log.trace("Clearing ACL parent");
				objectIdentity.setParentObject(null);
				// objectIdentity.setEntriesInheriting(false);
			}

			objectIdentity = aclObjectIdentityPersistence.save(objectIdentity);
		}

		clearAclCache();
		return objectIdentity;
	}

	@Override
	@Transactional(propagation = Propagation.REQUIRED)
	public AclObjectIdentity createOrUpdatePermissions(final AclAwareModel target) {
		if (target == null || (target.getId() <= 0l && !(target instanceof ClassAclOid<?>))) {
			log.warn("No target specified for ACL permissions, bailing out!");
			return null;
		}

		final AclSid ownerSid = SecurityContextUtil.getCurrentUser();
		return createOrUpdatePermissions(target, ownerSid);
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public AclObjectIdentity updateInheriting(final long objectIdIdentity, final boolean entriesInheriting) {
		final AclObjectIdentity objectIdentity = aclObjectIdentityPersistence.findById(objectIdIdentity).orElse(null);
		if (objectIdentity == null) {
			log.warn("ACL object identity not found by id={}", objectIdIdentity);
			return null;
		}

		if (objectIdentity.isEntriesInheriting() == entriesInheriting) {
			return objectIdentity;
		} else {
			try {
				log.info("Updating inheriting status for OID={} to {}", objectIdentity, entriesInheriting);
				objectIdentity.setEntriesInheriting(entriesInheriting);
				return aclObjectIdentityPersistence.save(objectIdentity);
			} finally {
				clearAclCache();
			}
		}
	}
	
	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public AclObjectIdentity setAclParent(AclAwareModel target, AclAwareModel parent) {
		final AclObjectIdentity objectIdentity = getObjectIdentity(target);
		final AclObjectIdentity parentIdentity = parent == null ? null : getObjectIdentity(parent);

		return updateAclParentObject(objectIdentity, parentIdentity);
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public AclObjectIdentity updateParentObject(final long objectIdIdentity, final long parentObjectId) {
		final AclObjectIdentity objectIdentity = aclObjectIdentityPersistence.findById(objectIdIdentity).orElse(null);
		if (objectIdentity == null) {
			log.warn("ACL object identity not found by id={}", objectIdIdentity);
			return null;
		}

		final AclObjectIdentity parentIdentity = aclObjectIdentityPersistence.findById(parentObjectId).orElse(null);
		if (parentIdentity == null) {
			log.warn("ACL object identity not found by id={}", objectIdIdentity);
			return null;
		}

		return updateAclParentObject(objectIdentity, parentIdentity);
	}


	private AclObjectIdentity updateAclParentObject(final AclObjectIdentity objectIdentity, final AclObjectIdentity parentObject) {
		try {
			log.trace("Updating ACL parent to {}", parentObject);
			objectIdentity.setParentObject(parentObject);
			objectIdentity.setEntriesInheriting(parentObject != null);
			return aclObjectIdentityPersistence.save(objectIdentity);
		} finally {
			clearAclCache();
		}
	}

	/**
	 * Remove ACL data for AclAwareModel: deletes {@link AclObjectIdentity} and
	 * associated {@link AclEntry} list. If target happens to be {@link AclSid},
	 * permissions granted to the SID are removed.
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_UNCOMMITTED)
	public void removeAclAwareModel(final AclAwareModel target) {
		log.debug("Deleting ACL data for {}", target);
		
		if (target instanceof AclSid) {
			log.info("Deleting permissions for {}", target);
			removePermissionsFor((AclSid) target);
		}

		final AclObjectIdentity aclObjectIdentity = getObjectIdentity(target);
		if (aclObjectIdentity != null) {
			log.debug("OID {}#{} of {}", aclObjectIdentity.getAclClass().getAclClass(), aclObjectIdentity.getObjectIdIdentity(), target);
			for (AclObjectIdentity child : aclObjectIdentityPersistence.findByParentObject(aclObjectIdentity)) {
				log.debug("Has child {}#{}", child.getAclClass().getAclClass(), child.getObjectIdIdentity());
			}
			
			log.info("Deleting ACL data of {}", target);
			final List<AclEntry> aclEntries = aclEntryPersistence.findByObjectIdentity(aclObjectIdentity);
			if (aclEntries != null) {
				aclEntryPersistence.deleteAll(aclEntries);
			}
			aclObjectIdentityPersistence.delete(aclObjectIdentity);
		}

		clearAclCache();
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.blocks.security.service.CustomAclService#removePermissions(org.
	 * genesys.blocks.security.model.AclAwareModel)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED)
	public void removePermissionsFor(final AclSid sid) {
		int count = aclEntryPersistence.deleteForSid(sid);
		log.debug("Deleting {} permision entries granted to {}", count, sid);
		if (count > 0) {
			clearAclCache();
		}
	}

	/**
	 * Adds the permissions.
	 *
	 * @param objectIdentity the object identity
	 * @param sid the sid
	 * @param permissions the permissions
	 * @return the list
	 */
	private AclObjectIdentity addPermissions(final AclObjectIdentity objectIdentity, final AclSid sid, final Permissions permissions) {
		if (objectIdentity == null) {
			throw new NullPointerException("AclObjectIdentity must be provided, was null.");
		}
		if (sid == null) {
			throw new NullPointerException("AclSid must be provided, was null.");
		}
		if (permissions == null) {
			throw new NullPointerException("Permissions must be provided, was null.");
		}

		try {
			List<AclEntry> aclEntries = new ArrayList<>(10);
			long nextAceOrder = getAceOrder(objectIdentity.getId());
			// create Acl Entry
			for (final Permission permission : basePermissions) {
				final int mask = permission.getMask();
				final AclEntry aclEntry = new AclEntry();
				aclEntry.setAclObjectIdentity(objectIdentity);
				aclEntry.setAclSid(sid);
				aclEntry.setAceOrder(nextAceOrder++);
				aclEntry.setGranting(permissions.isGranting(mask));
				aclEntry.setAuditSuccess(true);
				aclEntry.setAuditFailure(true);
				// set full access for own organization
				aclEntry.setMask(mask);
				aclEntries.add(aclEntry);
			}
			// save ACL
			aclEntryPersistence.saveAll(aclEntries);
			return getObjectIdentity(objectIdentity.getId());
		} finally {
			clearAclCache();
		}
	}
	
	private void clearAclCache() {
		if (cacheManager!=null) {
			final Cache aclCache = cacheManager.getCache("aclCache");
			if (aclCache != null)
				aclCache.clear();
		}
	}

	/**
	 * Generates next ace_order value (to avoid DuplicateIndex exception :
	 * acl_object_identity + ace_order is unique index).
	 *
	 * @param aclObjectEntityId - id of acl_object_identity table
	 * @return - ace_order value
	 */
	private Long getAceOrder(final long aclObjectEntityId) {
		final Long maxAceOrder = aclEntryPersistence.getMaxAceOrderForObjectEntity(aclObjectEntityId);
		return maxAceOrder != null ? maxAceOrder + 1 : 1;
	}

	/**
	 * Ensure acl class.
	 *
	 * @param className the class name
	 * @return the acl class
	 */
	private AclClass ensureAclClass(final String className) {
		AclClass aclClass = aclClassPersistence.findByAclClass(className);

		if (aclClass == null) {
			log.debug("Registering missing AclClass '{}'", className);
			aclClass = new AclClass();
			aclClass.setAclClass(className);
			return aclClassPersistence.save(aclClass);
		}

		return aclClass;
	}
	
	@Override
	@Transactional(readOnly = true)
	public AclObjectIdentityExt loadObjectIdentityExt(AclObjectIdentity objectIdentity) {
		if (objectIdentity != null) {
			objectIdentity = getObjectIdentity(objectIdentity.getId());
			AclObjectIdentityExt _aclObjectIdentity = new AclObjectIdentityExt(objectIdentity);
			
			// lazy load SIDs for AclEntries
			if (objectIdentity.getAclEntries() != null) {
				objectIdentity.getAclEntries().forEach(entry -> entry.getAclSid().getId());
			}

			List<AclEntry> inheritedEntries = inherited(objectIdentity.getParentObject(), new ArrayList<>(), new HashSet<>());
			_aclObjectIdentity.inherited.addAll(inheritedEntries);
			// lazy load for JSON
			_aclObjectIdentity.inherited.forEach(entry -> entry.getAclSid().getId());
			return _aclObjectIdentity;
		}

		return null;
	}
	
	private List<AclEntry> inherited(AclObjectIdentity objectIdentity, List<AclEntry> aclEntries, Set<AclObjectIdentity> handled) {
		if (objectIdentity == null || handled.contains(objectIdentity)) {
			return aclEntries;
		}

		// lazy load SIDs for AclEntries
		objectIdentity.getAclEntries().forEach(entry -> entry.getAclSid().getId());
		
		aclEntries.addAll(objectIdentity.getAclEntries());
		handled.add(objectIdentity);

		if (objectIdentity.getParentObject() != null) {
			inherited(objectIdentity.getParentObject(), aclEntries, handled);
		}

		return aclEntries;
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.blocks.security.service.CustomAclService#getObjectIdentity(java.
	 * lang.String, long)
	 */
	@Override
	@Transactional(readOnly = true)
	@PostAuthorize("returnObject==null or hasRole('ADMINISTRATOR') or hasPermission(returnObject.objectIdIdentity, returnObject.aclClass.aclClass, 'READ')")
	public AclObjectIdentity getObjectIdentity(final long id) {
		return aclObjectIdentityPersistence.findById(id).orElse(null);
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.blocks.security.service.CustomAclService#getObjectIdentity(java.
	 * lang.String, long)
	 */
	@Override
	@Transactional(readOnly = true)
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#id, #className, 'ADMINISTRATION')")
	public AclObjectIdentity getObjectIdentity(final long id, final String className) {
		return aclObjectIdentityPersistence.findByObjectIdAndClassname(id, className);
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.blocks.security.service.CustomAclService#getObjectIdentity(org.
	 * genesys.blocks.security.model.AclAwareModel)
	 */
	@Override
	@Transactional(readOnly = true)
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#entity, 'ADMINISTRATION')")
	public AclObjectIdentity getObjectIdentity(final AclAwareModel entity) {
		if (entity == null) {
			log.trace("getObjectIdentity: Entity is null");
			return null;
		}
		String className = HibernateProxyHelper.getClassWithoutInitializingProxy(entity).getName();
		
		if (entity instanceof ClassAclOid<?>) {
			className = ((ClassAclOid<?>) entity).getClassName();
		}

		final AclObjectIdentity oid = aclObjectIdentityPersistence.findByObjectIdAndClassname(entity.getId(), className);
		if (oid == null) {
			log.warn("ACL object identity not found for class={} id={}", className, entity.getId());
		}
		return oid;
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.blocks.security.service.CustomAclService#getAvailablePermissions(
	 * java.lang.String)
	 */
	@Override
	@Transactional(readOnly = true)
	public Permission[] getAvailablePermissions(final String className) {
		// Do not remove parameter. We may change available permissions based on
		// parameter type!
		return basePermissions;
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.blocks.security.service.CustomAclService#getPermissions(long,
	 * java.lang.String)
	 */
	@Transactional(readOnly = true)
	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#id, #className, 'ADMINISTRATION')")
	public List<SidPermissions> getPermissions(final long id, final String className) {
		final List<AclEntry> aclEntries = getAclEntries(getObjectIdentity(id, className));
		return SidPermissions.fromEntries(aclEntries);
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.blocks.security.service.CustomAclService#getPermissions(org.
	 * genesys.blocks.security.model.AclAwareModel)
	 */
	@Transactional(readOnly = true)
	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#entity, 'ADMINISTRATION')")
	public List<SidPermissions> getPermissions(final AclAwareModel entity) {
		return getPermissions(entity.getId(), entity.getClass().getName());
	}

	@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_UNCOMMITTED)
	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#entity, 'ADMINISTRATION')")
	public AclObjectIdentity setPermissions(final AclAwareModel entity, final AclSid sid, final Permissions permissions) {
		if (entity == null) {
			throw new NullPointerException("AclAwareModel must be provided, was null.");
		}
		if (sid == null) {
			throw new NullPointerException("AclSid must be provided, was null.");
		}
		if (permissions == null) {
			throw new NullPointerException("Permissions must be provided, was null.");
		}

		final AclObjectIdentity objectIdentity = ensureObjectIdentity(entity);
		return setPermissions(objectIdentity, sid, permissions);
	}

	private AclObjectIdentity ensureObjectIdentity(final AclAwareModel entity) {
		String className = entity.getClass().getName();
		if (entity instanceof ClassAclOid<?>) {
			className = ((ClassAclOid<?>) entity).getClassName();
		}

		final AclObjectIdentity objectIdentity = ensureObjectIdentity(entity.getId(), className);
		return objectIdentity;
	}

	@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_UNCOMMITTED)
	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#objectIdentity.objectIdIdentity, #objectIdentity.aclClass.aclClass, 'ADMINISTRATION')")
	public AclObjectIdentity setPermissions(AclObjectIdentity objectIdentity, AclSid sid, final Permissions permissions) {
		if (objectIdentity == null) {
			throw new NullPointerException("AclObjectIdentity must be provided, was null.");
		}
		assert objectIdentity.getId() != null;
		if (sid == null) {
			throw new NullPointerException("AclSid must be provided, was null.");
		}
		if (permissions == null) {
			throw new NullPointerException("Permissions must be provided, was null.");
		}

		try {

			final List<AclEntry> aclEntries = aclEntryPersistence.findBySidAndObjectIdentity(sid, objectIdentity);

			if (aclEntries.isEmpty()) {
				// add
				return addPermissions(objectIdentity, sid, permissions);
			} else {
				// update
				for (final AclEntry aclEntry : aclEntries) {
					aclEntry.setGranting(permissions.isGranting(aclEntry.getMask()));
				}
				log.info("Saving {}", aclEntries);
				aclEntryPersistence.saveAll(aclEntries);
				return getObjectIdentity(objectIdentity.getId());
			}

			// // This is the original code, one that cleared permissions when none were granted
			// // It didn't take into account inherited permissions.
			// if (permissions.isOneGranting()) {
			// need to update or add permissions
			// ...
			// } else {
			// // delete existing
			// final List<AclEntry> aclEntries =
			// aclEntryPersistence.findBySidAndObjectIdentity(sid, objectIdentity);
			// if (!aclEntries.isEmpty()) {
			// log.info("Deleting " + aclEntries);
			// aclEntryPersistence.delete(aclEntries);
			// entityManager.flush();
			// }
			// return getObjectIdentity(objectIdentity.getId());
			// }
		} finally {
			clearAclCache();
		}
	}

	/* (non-Javadoc)
	 * @see org.genesys.blocks.security.service.CustomAclService#removePermissions(org.genesys.blocks.security.model.AclObjectIdentity, org.genesys.blocks.security.model.AclSid)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_UNCOMMITTED)
	public AclObjectIdentity removePermissions(AclObjectIdentity objectIdentity, AclSid sid) {
		if (objectIdentity == null) {
			throw new NullPointerException("AclObjectIdentity must be provided, was null.");
		}
		if (sid == null) {
			throw new NullPointerException("AclSid must be provided, was null.");
		}
		
		try {
			final List<AclEntry> aclEntries = aclEntryPersistence.findBySidAndObjectIdentity(sid, objectIdentity);
			// delete ACL entries for sid
			log.debug("Deleting {} AclEntries for {}", aclEntries.size(), sid);
			aclEntryPersistence.deleteAll(aclEntries);

			return getObjectIdentity(objectIdentity.getId());
		} finally {
			clearAclCache();
		}
	}

	
	/*
	 * (non-Javadoc)
	 * @see org.genesys.blocks.security.service.CustomAclService#getAclEntries(org.
	 * genesys.blocks.security.model.AclObjectIdentity)
	 */
	@Override
	@Transactional(readOnly = true)
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#objectIdentity.objectIdIdentity, #objectIdentity.aclClass.aclClass, 'ADMINISTRATION')")
	public List<AclEntry> getAclEntries(final AclObjectIdentity objectIdentity) {
		return aclEntryPersistence.findByObjectIdentity(objectIdentity);
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.blocks.security.service.CustomAclService#getAclEntries(org.
	 * genesys.blocks.security.model.AclAwareModel)
	 */
	@Override
	@Transactional(readOnly = true)
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#entity, 'ADMINISTRATION')")
	public List<AclEntry> getAclEntries(final AclAwareModel entity) {
		return aclEntryPersistence.findByObjectIdentity(getObjectIdentity(entity));
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.blocks.security.service.CustomAclService#getSids(long,
	 * java.lang.String)
	 */
	@Override
	@Transactional(readOnly = true)
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#id, #className, 'ADMINISTRATION')")
	public List<AclSid> getSids(final long id, final String className) {
		return aclEntryPersistence.getSids(getObjectIdentity(id, className));
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.blocks.security.service.CustomAclService#getSids(org.genesys.
	 * blocks.security.model.AclAwareModel)
	 */
	@Override
	@Transactional(readOnly = true)
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#entity, 'ADMINISTRATION')")
	public List<AclSid> getSids(final AclAwareModel entity) {
		return aclEntryPersistence.getSids(getObjectIdentity(entity));
	}

	/**
	 * Ensure ACL SID entry for the specified authority name (role)
	 */
	private AclSid ensureSidForAuthority(String authority) {
		AclSid roleSid = aclSidPersistence.findBySidAndPrincipal(authority, false);

		if (roleSid == null) {
			log.warn("Creating AclSid for role '{}'", authority);
			roleSid = new AclSid();
			roleSid.setPrincipal(false);
			roleSid.setSid(authority);
			return aclSidPersistence.save(roleSid);
		}

		return roleSid;
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.blocks.security.service.CustomAclService#listAuthoritySids()
	 */
	@Override
	@Transactional(readOnly = true)
	public List<AclSid> listAuthoritySids() {
		return aclSidPersistence.listAuthoritySids();
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.blocks.security.service.CustomAclService#ensureObjectIdentity(
	 * java.lang.String, long)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_UNCOMMITTED)
	public AclObjectIdentity ensureObjectIdentity(final long objectIdIdentity, final String className) {
		AclObjectIdentity aoi = aclObjectIdentityPersistence.findByObjectIdAndClassname(objectIdIdentity, className);
		if (aoi == null) {
			aoi = new AclObjectIdentity();
			aoi.setObjectIdIdentity(objectIdIdentity);
			aoi.setAclClass(ensureAclClass(className));
			final AclSid ownerSid = SecurityContextUtil.getCurrentUser();
			aoi.setOwnerSid(ownerSid);
			aoi = aclObjectIdentityPersistence.save(aoi);
		}
		return aoi;
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.blocks.security.service.CustomAclService#listIdentitiesForSid(
	 * java.lang.Class, org.springframework.security.core.userdetails.UserDetails,
	 * org.springframework.security.acls.model.Permission)
	 */
	@Override
	@Transactional(readOnly = true)
	public List<Long> listObjectIdentityIdsForSid(final Class<? extends AclAwareModel> clazz, final AclSid sid, final Permission permission) {
		return aclEntryPersistence.findObjectIdentitiesForSidAndAclClassAndMask(sid, clazz.getName(), permission.getMask());
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.blocks.security.service.CustomAclService#makePubliclyReadable(org
	 * .genesys.blocks.security.model.AclAwareModel, boolean)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_UNCOMMITTED)
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#entity, 'ADMINISTRATION')")
	public void makePubliclyReadable(AclAwareModel entity, boolean publiclyReadable) {
		AclSid roleEveryone = ensureAuthoritySid("ROLE_EVERYONE");

		if (!publiclyReadable) {
			final AclObjectIdentity objectIdentity = ensureObjectIdentity(entity);
			removePermissions(objectIdentity, roleEveryone);

		} else {

			Permissions readPermissions = new Permissions().grantNone();
			readPermissions.read = publiclyReadable;
			setPermissions(entity, roleEveryone, readPermissions);
		}
	}

	@Override
	@Transactional
	public void cleanupAcl() {
		List<AclObjectIdentity> OIDs = aclObjectIdentityPersistence.findAll();
		log.warn("Cleaning ACL for {} OIDs", OIDs.size());
		for (AclObjectIdentity OID : OIDs) {
			try {
				Class<?> clazz = Class.forName(OID.getAclClass().getAclClass());
				Object entity = entityManager.find(clazz, OID.getObjectIdIdentity());
				if (entity == null) {
					log.info("{} with OID={} no longer exists, clearing ACL", clazz.getName(), OID.getObjectIdIdentity());
					final List<AclEntry> aclEntries = aclEntryPersistence.findByObjectIdentity(OID);
					if (aclEntries != null) {
						aclEntryPersistence.deleteAll(aclEntries);
					}
					aclObjectIdentityPersistence.resetChildrenOfOID(OID);
					aclObjectIdentityPersistence.delete(OID);
				}
			} catch (ClassNotFoundException e) {
				log.info("{} for OID={} no longer exists, clearing ACL", OID.getAclClass().getAclClass(), OID.getObjectIdIdentity());
				final List<AclEntry> aclEntries = aclEntryPersistence.findByObjectIdentity(OID);
				if (aclEntries != null) {
					aclEntryPersistence.deleteAll(aclEntries);
				}
				aclObjectIdentityPersistence.resetChildrenOfOID(OID);
				aclObjectIdentityPersistence.delete(OID);
			}
		}
		log.warn("Done cleaning ACL for {} OIDs", OIDs.size());

		List<AclClass> aclClasses = aclClassPersistence.findAll();
		log.warn("Cleaning ACL for {} ACL classes", aclClasses.size());
		for (AclClass aclClass : aclClasses) {
			try {
				Class.forName(aclClass.getAclClass());
			} catch (ClassNotFoundException e) {
				log.info("{} no longer exists, clearing ACL", aclClass.getAclClass());
				aclClassPersistence.delete(aclClass);
			}
		}
		log.warn("Done cleaning ACL for {} ACL classes", aclClasses.size());
	}

}