/*
 * Copyright 2007 Open Source Applications Foundation
 * 
 * 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.unitedinternet.cosmo.dav.acegisecurity;

import java.util.Collection;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.unitedinternet.cosmo.acegisecurity.providers.ticket.TicketAuthenticationToken;
import org.unitedinternet.cosmo.acegisecurity.userdetails.CosmoUserDetails;
import org.unitedinternet.cosmo.dav.CaldavMethodType;
import org.unitedinternet.cosmo.dav.ExtendedDavConstants;
import org.unitedinternet.cosmo.dav.acl.AclEvaluator;
import org.unitedinternet.cosmo.dav.acl.DavPrivilege;
import org.unitedinternet.cosmo.dav.acl.TicketAclEvaluator;
import org.unitedinternet.cosmo.dav.acl.UserAclEvaluator;
import org.unitedinternet.cosmo.model.Item;
import org.unitedinternet.cosmo.model.Ticket;
import org.unitedinternet.cosmo.model.User;
import org.unitedinternet.cosmo.service.UserService;
import org.unitedinternet.cosmo.util.UriTemplate;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

/**
 * <p>
 * Makes access control decisions for users and user
 * resources.  Allow service layer to handle authorization
 * for all other resources.
 * </p>
 */
public class DavAccessDecisionManager
        implements AccessDecisionManager, ExtendedDavConstants {
    private static final Log LOG = LogFactory.getLog(DavAccessDecisionManager.class);

    private UserService userService;

    // DavAccessDecisionManager methods

    /**
     * <p>
     * </p>
     *
     * @throws InsufficientAuthenticationException
     *          if
     *          <code>Authentication</code> is not a
     *          {@link UsernamePasswordAuthenticationToken} or a
     *          {@link TicketAuthenticationToken}.
     */
    @Override
    public void decide(Authentication authentication, Object object,
                       Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        AclEvaluator evaluator = null;
        if (authentication instanceof UsernamePasswordAuthenticationToken) {
            CosmoUserDetails details = (CosmoUserDetails)
                    authentication.getPrincipal();
            evaluator = new UserAclEvaluator(details.getUser());
        } else if (authentication instanceof PreAuthenticatedAuthenticationToken) {
            User user = userService.getUser((String)authentication.getPrincipal());
            evaluator = new UserAclEvaluator(user);
        } else if (authentication instanceof TicketAuthenticationToken) {
            Ticket ticket = (Ticket) authentication.getPrincipal();
            evaluator = new TicketAclEvaluator(ticket);
        } else {
            LOG.error("Unrecognized authentication token");
            throw new InsufficientAuthenticationException("Unrecognized authentication token");
        }

        HttpServletRequest request =
                ((FilterInvocation) object).getHttpRequest();

        String path = request.getPathInfo();
        if (path == null) {
            path = "/";
        }
        // remove trailing slash that denotes a collection
        if (!path.equals("/") && path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }

        try {
            match(path, request.getMethod(), evaluator);
        } catch (AclEvaluationException e) {
            throw new DavAccessDeniedException(request.getRequestURI(),
                    e.getPrivilege());
        }
    }

    /**
     * Always returns true, as this manager does not support any
     * config attributes.
     */
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    /**
     * Returns true if the secure object is a
     * {@link FilterInvocation}.
     */
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }

    // our methods

    protected void match(String path,
                         String method,
                         AclEvaluator evaluator)
            throws AclEvaluationException {
        if (LOG.isDebugEnabled()) {
            //Fix Log Forging - fortify
            //Writing unvalidated user input to log files can allow an attacker
            //to forge log entries or inject malicious content into the logs.
            LOG.debug("matching resource " + path
                    + " with method " + method);
        }

        UriTemplate.Match match = null;

        match = TEMPLATE_USERS.match(false, path);
        if (match != null) {
            evaluateUserPrincipalCollection(match, method, evaluator);
            return;
        }

        match = TEMPLATE_USER.match(false, path);
        if (match != null) {
            evaluateUserPrincipal(match, method, evaluator);
            return;
        }
    }

    protected void evaluateUserPrincipalCollection(UriTemplate.Match match,
                                                   String method,
                                                   AclEvaluator evaluator)
            throws AclEvaluationException {
        if (evaluator instanceof TicketAclEvaluator) {
            throw new IllegalStateException("A ticket may not be used to access the user principal collection");
        }
        if (method.equals("PROPFIND")) {
            if (LOG.isDebugEnabled()) {
                //Fix Log Forging - fortify
                //Writing unvalidated user input to log files can allow an attacker to forge log entries
                //or inject malicious content into the logs.
                LOG.debug("Allowing method " + method + " so provider can evaluate check access itself");
            }
            return;
        }

        UserAclEvaluator uae = (UserAclEvaluator) evaluator;
        DavPrivilege privilege = CaldavMethodType.isReadMethod(method) ?
                DavPrivilege.READ : DavPrivilege.WRITE;
        if (!uae.evaluateUserPrincipalCollection(privilege)) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Principal does not have privilege " + privilege + "; denying access");
            }
            throw new AclEvaluationException(null, privilege);
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Principal has privilege " + privilege + "; allowing access");
        }
    }

    protected void evaluateUserPrincipal(UriTemplate.Match match,
                                         String method,
                                         AclEvaluator evaluator)
            throws AclEvaluationException {
        if (evaluator instanceof TicketAclEvaluator) {
            throw new IllegalStateException("A ticket may not be used to access the user principal collection");
        }

        String username = match.get("username");
        User user = getUserService().getUser(username);
        if (user == null) {
            if (LOG.isDebugEnabled()) {
                //Fix Log Forging - fortify
                //Writing unvalidated user input to log files can allow an attacker to forge log entries or
                //inject malicious content into the logs.
                LOG.debug("User " + username + " not found; allowing for 404");
            }
            return;
        }

        if (method.equals("PROPFIND")) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Allowing method " + method + " so provider can evaluate check access itself");
            }
            return;
        }

        UserAclEvaluator uae = (UserAclEvaluator) evaluator;
        DavPrivilege privilege = CaldavMethodType.isReadMethod(method) ?
                DavPrivilege.READ : DavPrivilege.WRITE;
        if (!uae.evaluateUserPrincipal(user, privilege)) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Principal does not have privilege " + privilege + "; denying access");
            }
            throw new AclEvaluationException(null, privilege);
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Principal has privilege " + privilege + "; allowing access");
        }
    }

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public UserService getUserService() {
        return userService;
    }

    public static class AclEvaluationException extends Exception {
        private Item item;
        private transient DavPrivilege privilege;

        public AclEvaluationException(Item item,
                                      DavPrivilege privilege) {
            this.item = item;
            this.privilege = privilege;
        }

        public Item getItem() {
            return item;
        }

        public DavPrivilege getPrivilege() {
            return privilege;
        }
    }

}
