/*
 * Copyright 2002-2016 the original author or authors.
 *
 * 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.springframework.security.acls;

import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.acls.domain.DefaultPermissionFactory;
import org.springframework.security.acls.domain.ObjectIdentityRetrievalStrategyImpl;
import org.springframework.security.acls.domain.PermissionFactory;
import org.springframework.security.acls.domain.SidRetrievalStrategyImpl;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.acls.model.AclService;
import org.springframework.security.acls.model.NotFoundException;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.acls.model.ObjectIdentityGenerator;
import org.springframework.security.acls.model.ObjectIdentityRetrievalStrategy;
import org.springframework.security.acls.model.Permission;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.acls.model.SidRetrievalStrategy;
import org.springframework.security.core.Authentication;

/**
 * Used by Spring Security's expression-based access control implementation to evaluate
 * permissions for a particular object using the ACL module. Similar in behaviour to
 * {@link org.springframework.security.acls.AclEntryVoter AclEntryVoter}.
 *
 * @author Luke Taylor
 * @since 3.0
 */
public class AclPermissionEvaluator implements PermissionEvaluator {

	private final Log logger = LogFactory.getLog(getClass());

	private final AclService aclService;
	private ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy = new ObjectIdentityRetrievalStrategyImpl();
	private ObjectIdentityGenerator objectIdentityGenerator = new ObjectIdentityRetrievalStrategyImpl();
	private SidRetrievalStrategy sidRetrievalStrategy = new SidRetrievalStrategyImpl();
	private PermissionFactory permissionFactory = new DefaultPermissionFactory();

	public AclPermissionEvaluator(AclService aclService) {
		this.aclService = aclService;
	}

	/**
	 * Determines whether the user has the given permission(s) on the domain object using
	 * the ACL configuration. If the domain object is null, returns false (this can always
	 * be overridden using a null check in the expression itself).
	 */
	public boolean hasPermission(Authentication authentication, Object domainObject,
			Object permission) {
		if (domainObject == null) {
			return false;
		}

		ObjectIdentity objectIdentity = objectIdentityRetrievalStrategy
				.getObjectIdentity(domainObject);

		return checkPermission(authentication, objectIdentity, permission);
	}

	public boolean hasPermission(Authentication authentication, Serializable targetId,
			String targetType, Object permission) {
		ObjectIdentity objectIdentity = objectIdentityGenerator.createObjectIdentity(
				targetId, targetType);

		return checkPermission(authentication, objectIdentity, permission);
	}

	private boolean checkPermission(Authentication authentication, ObjectIdentity oid,
			Object permission) {
		// Obtain the SIDs applicable to the principal
		List<Sid> sids = sidRetrievalStrategy.getSids(authentication);
		List<Permission> requiredPermission = resolvePermission(permission);

		final boolean debug = logger.isDebugEnabled();

		if (debug) {
			logger.debug("Checking permission '" + permission + "' for object '" + oid
					+ "'");
		}

		try {
			// Lookup only ACLs for SIDs we're interested in
			Acl acl = aclService.readAclById(oid, sids);

			if (acl.isGranted(requiredPermission, sids, false)) {
				if (debug) {
					logger.debug("Access is granted");
				}

				return true;
			}

			if (debug) {
				logger.debug("Returning false - ACLs returned, but insufficient permissions for this principal");
			}

		}
		catch (NotFoundException nfe) {
			if (debug) {
				logger.debug("Returning false - no ACLs apply for this principal");
			}
		}

		return false;

	}

	List<Permission> resolvePermission(Object permission) {
		if (permission instanceof Integer) {
			return Arrays.asList(permissionFactory.buildFromMask(((Integer) permission)
					.intValue()));
		}

		if (permission instanceof Permission) {
			return Arrays.asList((Permission) permission);
		}

		if (permission instanceof Permission[]) {
			return Arrays.asList((Permission[]) permission);
		}

		if (permission instanceof String) {
			String permString = (String) permission;
			Permission p;

			try {
				p = permissionFactory.buildFromName(permString);
			}
			catch (IllegalArgumentException notfound) {
				p = permissionFactory.buildFromName(permString.toUpperCase(Locale.ENGLISH));
			}

			if (p != null) {
				return Arrays.asList(p);
			}

		}
		throw new IllegalArgumentException("Unsupported permission: " + permission);
	}

	public void setObjectIdentityRetrievalStrategy(
			ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy) {
		this.objectIdentityRetrievalStrategy = objectIdentityRetrievalStrategy;
	}

	public void setObjectIdentityGenerator(ObjectIdentityGenerator objectIdentityGenerator) {
		this.objectIdentityGenerator = objectIdentityGenerator;
	}

	public void setSidRetrievalStrategy(SidRetrievalStrategy sidRetrievalStrategy) {
		this.sidRetrievalStrategy = sidRetrievalStrategy;
	}

	public void setPermissionFactory(PermissionFactory permissionFactory) {
		this.permissionFactory = permissionFactory;
	}
}
