/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.server.authz;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.authz.Permission;
import org.rhq.enterprise.server.auth.SubjectManagerLocal;
import org.rhq.enterprise.server.util.LookupUtil;

/**
 * An EJB3 interceptor that checks to ensure a given {@link Subject} has all of the global permissions that are
 * specified via the {@link RequiredPermissions} annotation on the method to be invoked. If the method being invoked is
 * not annotated with {@link RequiredPermissions} or it has an empty list of permissions, this interceptor passes the
 * security check immediately. Otherwise, the method must have a {@link Subject} as its first parameter - that
 * {@link Subject} will be checked to see if it has all the permissions required. If it does not, or if there is no
 * {@link Subject} as the method's first parameter, this interceptor throws an exception and does not allow the method
 * to be invoked.
 *
 * @author John Mazzitelli
 */
public class RequiredPermissionsInterceptor {
    private static Log LOG = LogFactory.getLog(RequiredPermissionsInterceptor.class);

    /**
     * Checks to ensure the method can be invoked.
     *
     * @param  invocation_context the invocation context
     *
     * @return the results of the invocation
     *
     * @throws Exception           if an error occurred further down the interceptor stack
     * @throws PermissionException if the security check fails
     */
    @AroundInvoke
    public Object checkRequiredPermissions(InvocationContext invocation_context) throws Exception {
        try {
            Map<Permission, String> perms_errors_list = new HashMap<Permission, String>();
            Method method = invocation_context.getMethod();
            RequiredPermissions perms_anno = method.getAnnotation(RequiredPermissions.class);
            RequiredPermission perm_anno = method.getAnnotation(RequiredPermission.class);

            // process the list of permissions, if specified
            if (((perms_anno != null) && (perms_anno.value().length > 0))) {
                for (RequiredPermission rq : perms_anno.value()) {
                    perms_errors_list.put(rq.value(), rq.error());
                }
            }

            // process the individual permission, if specified
            if ((perm_anno != null) && (perm_anno.value() != null)) {
                perms_errors_list.put(perm_anno.value(), perm_anno.error());
            }

            // get the subject, if there is one as the first parameter to the method invocation
            Subject subject = null;
            Object[] params = invocation_context.getParameters();
            if ((params != null) && (params.length > 0) && (params[0] instanceof Subject)) {
                subject = (Subject) params[0];
            }

            // Make sure someone is not spoofing another user - ensure the associated session ID is valid.
            // This means that anytime we pass Subject as the first parameter, we are assuming it needs
            // its session validated.  If there is ever a case where we pass Subject as the first parameter
            // to an EJB and we do NOT want to validate its session, you need to annotate that EJB
            // method with @ExcludeDefaultInterceptors so we don't call this interceptor.
            if (subject != null) {
                if (subject.getSessionId() != null) {
                    SubjectManagerLocal subject_manager = LookupUtil.getSubjectManager();

                    // isValidSessionId will also update the session's last-access-time
                    if (!subject_manager.isValidSessionId(subject.getSessionId(), subject.getName(), subject.getId())) {
                        // if this happens, it is possible someone is trying to spoof an authenticated user!
                        throw buildPermissionException("The session ID for user [" + subject.getName()
                            + "] is invalid!", invocation_context);
                    }
                } else {
                    throw buildPermissionException("The subject [" + subject.getName() + "] did not have a session",
                        invocation_context);
                }
            }

            // if the method is not annotated or it has no permissions that are required for it to be invoked,
            // don't do anything; otherwise, we need to check the permissions
            if (perms_errors_list.size() > 0) {
                // the method to be invoked has one or more required permissions;
                // therefore, the method must have a Subject as its first argument value
                if (subject == null) {
                    throw buildPermissionException("Method requires permissions but does not have a subject parameter",
                        invocation_context);
                }

                // look these up now - we don't use @EJB because I don't want the container wasting time
                // injecting EJBs if I don't need them for those methods not annotated with @RequiredPermissions
                AuthorizationManagerLocal authorization_manager = LookupUtil.getAuthorizationManager();

                Set<Permission> required_permissions = perms_errors_list.keySet();
                Set<Permission> subject_permissions = authorization_manager.getExplicitGlobalPermissions(subject);

                for (Permission required_permission : required_permissions) {
                    if (!Permission.Target.GLOBAL.equals(required_permission.getTarget())) {
                        throw buildPermissionException("@RequiredPermissions must be Permission.Target.GLOBAL: ["
                            + required_permission + "]", invocation_context);
                    }

                    if (!subject_permissions.contains(required_permission)) {
                        String perm_error = perms_errors_list.get(required_permission);
                        String full_error = "Subject [" + subject.getName() + "] is not authorized for ["
                            + required_permission + "]";

                        if ((perm_error != null) && (perm_error.length() > 0)) {
                            full_error = perm_error + ": " + full_error;
                        }

                        throw buildPermissionException(full_error, invocation_context);
                    }
                }
            }
        } catch (PermissionException pe) {
            LOG.debug("Interceptor detected a permission exception", pe);
            throw pe;
        } catch (Exception e) {
            Exception ex = buildPermissionException("Failed to check required permissions to invoke: ",
                invocation_context, e);
            LOG.debug("Permission Exception", ex);
            throw ex;
        }

        // we are authorized for all the required permissions - let the invocation continue
        return invocation_context.proceed();
    }

    private PermissionException buildPermissionException(String message, InvocationContext context) {
        return buildPermissionException(message, context, null);
    }

    private PermissionException buildPermissionException(String message, InvocationContext context, Exception e) {
        return new PermissionException(message + ": " + getInvocationString(context), e);
    }

    /**
     * Returns a string representation of the given invocation context so it can be displayed in messages.
     *
     * @param  invocation
     *
     * @return string containing information about the invocation
     */
    private String getInvocationString(InvocationContext invocation) {
        StringBuffer buf = new StringBuffer("invocation: ");
        buf.append("method=" + invocation.getMethod().toGenericString());
        buf.append(",context-data=" + invocation.getContextData());
        return buf.toString();
    }
}