Package org.jboss.seam.faces.security

Source Code of org.jboss.seam.faces.security.SecurityPhaseListener

/*
* JBoss, Home of Professional Open Source
* Copyright 2011, Red Hat, Inc., and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* 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.jboss.seam.faces.security;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import javax.enterprise.event.Event;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.BeanManager;
import javax.faces.application.NavigationHandler;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.inject.Inject;

import org.jboss.seam.faces.event.PhaseIdType;
import org.jboss.seam.faces.event.PostLoginEvent;
import org.jboss.seam.faces.event.PreLoginEvent;
import org.jboss.seam.faces.event.PreNavigateEvent;
import org.jboss.seam.faces.event.qualifier.After;
import org.jboss.seam.faces.event.qualifier.ApplyRequestValues;
import org.jboss.seam.faces.event.qualifier.Before;
import org.jboss.seam.faces.event.qualifier.InvokeApplication;
import org.jboss.seam.faces.event.qualifier.ProcessValidations;
import org.jboss.seam.faces.event.qualifier.RenderResponse;
import org.jboss.seam.faces.event.qualifier.RestoreView;
import org.jboss.seam.faces.event.qualifier.UpdateModelValues;
import org.jboss.seam.faces.view.config.ViewConfigStore;
import org.jboss.solder.logging.Logger;
import org.jboss.seam.security.Identity;
import org.jboss.seam.security.annotations.SecurityBindingType;
import org.jboss.seam.security.events.AuthorizationCheckEvent;
import org.jboss.seam.security.events.NotAuthorizedEvent;
import org.jboss.solder.core.Requires;
import org.jboss.solder.reflection.AnnotationInspector;

/**
* Use the annotations stored in the ViewConfigStore to restrict view access.
* Authorization is delegated to Seam Security through by firing a AuthorizationCheckEvent.
*
* @author <a href="mailto:bleathem@gmail.com">Brian Leathem</a>
*/
@Requires("org.jboss.seam.security.SecurityExtension")
public class SecurityPhaseListener {

    private transient final Logger log = Logger.getLogger(SecurityPhaseListener.class);

    @Inject
    private ViewConfigStore viewConfigStore;
    @Inject
    private Event<AuthorizationCheckEvent> authorizationCheckEvent;
    @Inject
    private Event<PreLoginEvent> preLoginEvent;
    @Inject
    private Event<PostLoginEvent> postLoginEvent;
    @Inject
    private Event<NotAuthorizedEvent> notAuthorizedEventEvent;
    @Inject
    private BeanManager beanManager;
    @Inject
    private Identity identity;

    /**
     * Enforce any security annotations applicable to the RestoreView phase
     *
     * @param event
     */
    public void observeRestoreView(@Observes @After @RestoreView PhaseEvent event) {
        log.debug("After Restore View event");
        performObservation(event, PhaseIdType.RESTORE_VIEW);
    }

    /**
     * Enforce any security annotations applicable to the ApplyRequestValues phase
     *
     * @param event
     */
    public void observeApplyRequestValues(@Observes @Before @ApplyRequestValues PhaseEvent event) {
        log.debug("After Apply Request Values event");
        performObservation(event, PhaseIdType.APPLY_REQUEST_VALUES);
    }

    /**
     * Enforce any security annotations applicable to the ProcessValidations phase
     *
     * @param event
     */
    public void observeProcessValidations(@Observes @Before @ProcessValidations PhaseEvent event) {
        log.debug("After Process Validations event");
        performObservation(event, PhaseIdType.PROCESS_VALIDATIONS);
    }

    /**
     * Enforce any security annotations applicable to the UpdateModelValues phase
     *
     * @param event
     */
    public void observeUpdateModelValues(@Observes @Before @UpdateModelValues PhaseEvent event) {
        log.debug("After Update Model Values event");
        performObservation(event, PhaseIdType.UPDATE_MODEL_VALUES);
    }

    /**
     * Enforce any security annotations applicable to the InvokeApplication phase
     *
     * @param event
     */
    public void observeInvokeApplication(@Observes @Before @InvokeApplication PhaseEvent event) {
        log.debug("Before Render Response event");
        performObservation(event, PhaseIdType.INVOKE_APPLICATION);
    }

    /**
     * Enforce any security annotations applicable to the RenderResponse phase
     *
     * @param event
     */
    public void observeRenderResponse(@Observes @Before @RenderResponse PhaseEvent event) {
        log.debug("Before Render Response event");
        performObservation(event, PhaseIdType.RENDER_RESPONSE);
    }

    /**
     * Inspect the annotations in the ViewConfigStore, enforcing any restrictions applicable to this phase
     *
     * @param event
     * @param phaseIdType
     */
    private void performObservation(PhaseEvent event, PhaseIdType phaseIdType) {
        UIViewRoot viewRoot = (UIViewRoot) event.getFacesContext().getViewRoot();
        List<? extends Annotation> restrictionsForPhase = getRestrictionsForPhase(phaseIdType, viewRoot.getViewId());
        if (restrictionsForPhase != null) {
            log.debugf("Enforcing on phase %s", phaseIdType);
            enforce(event.getFacesContext(), viewRoot, restrictionsForPhase);
        }
    }

    /**
     * Retrieve all annotations from the ViewConfigStore for a given a JSF phase, and a view id,
     * and where the annotation is qualified by @SecurityBindingType
     *
     * @param currentPhase
     * @param viewId
     * @return list of restrictions applicable to this viewId and PhaseTypeId
     */
    public List<? extends Annotation> getRestrictionsForPhase(PhaseIdType currentPhase, String viewId) {
        List<? extends Annotation> allSecurityAnnotations = viewConfigStore.getAllQualifierData(viewId, SecurityBindingType.class);
        List<Annotation> applicableSecurityAnnotations = null;
        for (Annotation annotation : allSecurityAnnotations) {
            PhaseIdType[] defaultPhases = getDefaultPhases(viewId);
            if (isAnnotationApplicableToPhase(annotation, currentPhase, defaultPhases)) {
                if (applicableSecurityAnnotations == null) { // avoid spawning arrays at all phases of the lifecycle
                    applicableSecurityAnnotations = new ArrayList<Annotation>();
                }
                applicableSecurityAnnotations.add(annotation);
            }
        }
        return applicableSecurityAnnotations;
    }

    /**
     * Inspect an annotation to see if it specifies a view in which it should be.  Fall back on default view otherwise.
     *
     * @param annotation
     * @param currentPhase
     * @param defaultPhases
     * @return true if the annotation is applicable to this view and phase, false otherwise
     */
    public boolean isAnnotationApplicableToPhase(Annotation annotation, PhaseIdType currentPhase, PhaseIdType[] defaultPhases) {
        Method restrictAtViewMethod = getRestrictAtViewMethod(annotation);
        PhaseIdType[] phasedIds = null;
        if (restrictAtViewMethod != null) {
            log.warnf("Annotation %s is using the restrictAtViewMethod. Use a @RestrictAtPhase qualifier on the annotation instead.");
            phasedIds = getRestrictedPhaseIds(restrictAtViewMethod, annotation);
        }
        RestrictAtPhase restrictAtPhaseQualifier = AnnotationInspector.getAnnotation(annotation.annotationType(), RestrictAtPhase.class, beanManager);
        if (restrictAtPhaseQualifier != null) {
            log.debug("Using Phases found in @RestrictAtView qualifier on the annotation.");
            phasedIds = restrictAtPhaseQualifier.value();
        }
        if (phasedIds == null) {
            log.debug("Falling back on default phase ids");
            phasedIds = defaultPhases;
        }
        if (Arrays.binarySearch(phasedIds, currentPhase) >= 0) {
            return true;
        }
        return false;
    }

    /**
     * Get the default phases at which restrictions should be applied, by looking for a @RestrictAtPhase on a matching
     *
     * @param viewId
     * @return default phases for a view
     * @ViewPattern, falling back on global defaults if none are found
     */
    public PhaseIdType[] getDefaultPhases(String viewId) {
        PhaseIdType[] defaultPhases = null;
        RestrictAtPhase restrictAtPhase = viewConfigStore.getAnnotationData(viewId, RestrictAtPhase.class);
        if (restrictAtPhase != null) {
            defaultPhases = restrictAtPhase.value();
        }
        if (defaultPhases == null) {
            defaultPhases = RestrictAtPhaseDefault.DEFAULT_PHASES;
        }
        return defaultPhases;
    }

    /**
     * Utility method to extract the "restrictAtPhase" method from an annotation
     *
     * @param annotation
     * @return restrictAtViewMethod if found, null otherwise
     */
    public Method getRestrictAtViewMethod(Annotation annotation) {
        Method restrictAtViewMethod;
        try {
            restrictAtViewMethod = annotation.annotationType().getDeclaredMethod("restrictAtPhase");
        } catch (NoSuchMethodException ex) {
            restrictAtViewMethod = null;
        } catch (SecurityException ex) {
            throw new IllegalArgumentException("restrictAtView method must be accessible", ex);
        }
        return restrictAtViewMethod;
    }

    /**
     * Retrieve the default PhaseIdTypes defined by the restrictAtViewMethod in the annotation
     *
     * @param restrictAtViewMethod
     * @param annotation
     * @return PhaseIdTypes from the restrictAtViewMethod, null if empty
     */
    public PhaseIdType[] getRestrictedPhaseIds(Method restrictAtViewMethod, Annotation annotation) {
        PhaseIdType[] phaseIds;
        try {
            phaseIds = (PhaseIdType[]) restrictAtViewMethod.invoke(annotation);
        } catch (IllegalAccessException ex) {
            throw new IllegalArgumentException("restrictAtView method must be accessible", ex);
        } catch (InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
        return phaseIds;
    }

    /**
     * Enforce the list of applicable annotations, by firing an AuthorizationCheckEvent.  The event is then inspected to
     * determine if access is allowed.  Faces navigation is then re-routed to the @LoginView if the user is not logged in,
     * otherwise to the @AccessDenied view.
     *
     * @param context
     * @param viewRoot
     * @param annotations
     */
    private void enforce(FacesContext context, UIViewRoot viewRoot, List<? extends Annotation> annotations) {
        if (annotations == null || annotations.isEmpty()) {
            log.debug("Annotations is null/empty");
            return;
        }
        AuthorizationCheckEvent event = new AuthorizationCheckEvent(annotations);
        authorizationCheckEvent.fire(event);
        if (!event.isPassed()) {
            if (!identity.isLoggedIn()) {
                log.debug("Access denied - not logged in");
                redirectToLoginPage(context, viewRoot);
                return;
            } else {
                log.debug("Access denied - not authorized");
                notAuthorizedEventEvent.fire(new NotAuthorizedEvent());
                redirectToAccessDeniedView(context, viewRoot);
                return;
            }
        } else {
            log.debug("Access granted");
        }
    }

    /**
     * Perform the navigation to the @LoginView.  If not @LoginView is defined, return a 401 response.
     * The original view id requested by the user is stored in the session map, for use after a successful login.
     *
     * @param context
     * @param viewRoot
     */
    private void redirectToLoginPage(FacesContext context, UIViewRoot viewRoot) {
        Map<String, Object> sessionMap = context.getExternalContext().getSessionMap();
        preLoginEvent.fire(new PreLoginEvent(context, sessionMap));
        LoginView loginView = viewConfigStore.getAnnotationData(viewRoot.getViewId(), LoginView.class);
        if (loginView == null || loginView.value() == null || loginView.value().isEmpty()) {
            log.debug("Returning 401 response (login required)");
            context.getExternalContext().setResponseStatus(401);
            context.responseComplete();
            return;
        }
        String loginViewId = loginView.value();
        log.debugf("Redirecting to configured LoginView %s", loginViewId);
        NavigationHandler navHandler = context.getApplication().getNavigationHandler();
        navHandler.handleNavigation(context, "", loginViewId);
        context.renderResponse();
    }

    /**
     * Perform the navigation to the @AccessDeniedView.  If not @AccessDeniedView is defined, return a 401 response
     *
     * @param context
     * @param viewRoot
     */
    private void redirectToAccessDeniedView(FacesContext context, UIViewRoot viewRoot) {
        // If a user has already done a redirect and rendered the response (possibly in an observer) we cannot do this output
        final PhaseId currentPhase = context.getCurrentPhaseId();
        if (!context.getResponseComplete() && !PhaseId.RENDER_RESPONSE.equals(currentPhase)) {
            AccessDeniedView accessDeniedView = viewConfigStore.getAnnotationData(viewRoot.getViewId(), AccessDeniedView.class);
            if (accessDeniedView == null || accessDeniedView.value() == null || accessDeniedView.value().isEmpty()) {
                log.warn("No AccessDeniedView is configured, returning 401 response (access denied). Please configure an AccessDeniedView in the ViewConfig.");
                context.getExternalContext().setResponseStatus(401);
                context.responseComplete();
                return;
            }
            String accessDeniedViewId = accessDeniedView.value();
            log.debugf("Redirecting to configured AccessDenied %s", accessDeniedViewId);
            NavigationHandler navHandler = context.getApplication().getNavigationHandler();
            navHandler.handleNavigation(context, "", accessDeniedViewId);
            context.renderResponse();
        }
    }

    /**
     * Monitor PreNavigationEvents, looking for a successful navigation from the Seam Security login button.  When such a
     * navigation is encountered, redirect to the the viewId captured before the login redirect was triggered.
     *
     * @param event
     */
    public void observePreNavigateEvent(@Observes PreNavigateEvent event) {
        log.debugf("PreNavigateEvent observed %s, %s", event.getOutcome(), event.getFromAction());
        if (Identity.RESPONSE_LOGIN_SUCCESS.equals(event.getOutcome())
                && "#{identity.login}".equals(event.getFromAction())) {
            FacesContext context = event.getContext();
            Map<String, Object> sessionMap = context.getExternalContext().getSessionMap();
            postLoginEvent.fire(new PostLoginEvent(context, sessionMap));
        }
    }
}
TOP

Related Classes of org.jboss.seam.faces.security.SecurityPhaseListener

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.