/*
* 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));
}
}
}