package org.jboss.seam.security; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.event.Observes; import javax.enterprise.inject.spi.AfterBeanDiscovery; import javax.enterprise.inject.spi.Annotated; import javax.enterprise.inject.spi.AnnotatedMethod; import javax.enterprise.inject.spi.AnnotatedType; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.Extension; import javax.enterprise.inject.spi.InjectionPoint; import javax.enterprise.inject.spi.ProcessAnnotatedType; import javax.enterprise.inject.spi.ProcessSessionBean; import javax.enterprise.inject.spi.SessionBeanType; import javax.enterprise.util.Nonbinding; import javax.interceptor.InvocationContext; import org.apache.deltaspike.core.util.metadata.builder.AnnotatedTypeBuilder; import org.apache.deltaspike.core.util.metadata.builder.InjectableMethod; import org.apache.deltaspike.core.util.metadata.builder.ParameterValueRedefiner; import org.jboss.seam.security.annotations.Secures; import org.jboss.seam.security.annotations.SecurityBindingType; import org.jboss.seam.security.annotations.SecurityParameterBinding; //import org.jboss.solder.reflection.annotated.AnnotatedTypeBuilder; //import org.jboss.solder.reflection.annotated.InjectableMethod; //import org.jboss.solder.reflection.annotated.ParameterValueRedefiner; /** * Extension for typesafe security annotations * * @author Shane Bryzak * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a> */ public class SecurityExtension implements Extension { private BeanManager beanManager; /** * Responsible for supplying requested method invocation values to the security binding method. */ public class SecurityParameterValueRedefiner implements ParameterValueRedefiner { private CreationalContext<?> creationalContext; private InvocationContext invocation; public SecurityParameterValueRedefiner(CreationalContext<?> creationalContext, InvocationContext invocation) { this.invocation = invocation; this.creationalContext = creationalContext; } @Override public Object redefineParameterValue(ParameterValue value) { Object result = value.getDefaultValue(creationalContext); InjectionPoint injectionPoint = value.getInjectionPoint(); if (injectionPoint != null) { Annotated securingParameterAnnotatedType = injectionPoint.getAnnotated(); Set<Annotation> securingParameterAnnotations = securingParameterAnnotatedType.getAnnotations(); Set<Annotation> requiredBindingAnnotations = new HashSet<Annotation>(); for (Annotation annotation : securingParameterAnnotations) { if (annotation.annotationType().isAnnotationPresent(SecurityParameterBinding.class)) { requiredBindingAnnotations.add(annotation); } } if (!requiredBindingAnnotations.isEmpty()) { Annotation[][] businessMethodParameterAnnotations = invocation.getMethod().getParameterAnnotations(); for (int i = 0; i < businessMethodParameterAnnotations.length; i++) { List<Annotation> businessParameterAnnotations = Arrays.asList(businessMethodParameterAnnotations[i]); for (Annotation annotation : requiredBindingAnnotations) { if (businessParameterAnnotations.contains(annotation)) { return invocation.getParameters()[i]; } } } } } return result; } } class Authorizer { private Annotation binding; private Map<Method, Object> memberValues = new HashMap<Method, Object>(); private AnnotatedMethod<?> implementationMethod; private Bean<?> targetBean; private InjectableMethod<?> injectableMethod; public Authorizer(Annotation binding, AnnotatedMethod<?> implementationMethod) { this.binding = binding; this.implementationMethod = implementationMethod; try { for (Method m : binding.annotationType().getDeclaredMethods()) { if (m.isAnnotationPresent(Nonbinding.class)) continue; memberValues.put(m, m.invoke(binding)); } } catch (InvocationTargetException ex) { throw new SecurityDefinitionException("Error reading security binding members", ex); } catch (IllegalAccessException ex) { throw new SecurityDefinitionException("Error reading security binding members", ex); } } public void authorize() { if (targetBean == null) { lookupTargetBean(); } CreationalContext<?> cc = beanManager.createCreationalContext(targetBean); Object reference = beanManager.getReference(targetBean, implementationMethod.getJavaMember().getDeclaringClass(), cc); Object result = injectableMethod.invoke(reference, cc, null); if (result.equals(Boolean.FALSE)) { throw new AuthorizationException("Authorization check failed"); } } public void authorize(InvocationContext context) { if (targetBean == null) { lookupTargetBean(); } CreationalContext<?> cc = beanManager.createCreationalContext(targetBean); Object reference = beanManager.getReference(targetBean, implementationMethod.getJavaMember().getDeclaringClass(), cc); Object result = injectableMethod.invoke(reference, cc, new SecurityParameterValueRedefiner(cc, context)); if (result.equals(Boolean.FALSE)) { throw new AuthorizationException("Authorization check failed"); } } @SuppressWarnings({ "unchecked", "rawtypes" }) private synchronized void lookupTargetBean() { if (targetBean == null) { Method m = implementationMethod.getJavaMember(); Set<Bean<?>> beans = beanManager.getBeans(m.getDeclaringClass()); if (beans.size() == 1) { targetBean = beans.iterator().next(); } else if (beans.isEmpty()) { throw new IllegalStateException("Exception looking up authorizer method bean - " + "no beans found for method [" + m.getDeclaringClass() + "." + m.getName() + "]"); } else if (beans.size() > 1) { throw new IllegalStateException("Exception looking up authorizer method bean - " + "multiple beans found for method [" + m.getDeclaringClass().getName() + "." + m.getName() + "]"); } injectableMethod = new InjectableMethod(implementationMethod, targetBean, beanManager); } } public boolean matchesBinding(Annotation annotation) { if (!annotation.annotationType().equals(binding.annotationType())) { return false; } for (Method m : annotation.annotationType().getDeclaredMethods()) { if (m.isAnnotationPresent(Nonbinding.class)) continue; if (!memberValues.containsKey(m)) { return false; } try { Object value = m.invoke(annotation); if (!memberValues.get(m).equals(value)) { return false; } } catch (InvocationTargetException ex) { throw new SecurityDefinitionException("Error reading security binding members", ex); } catch (IllegalAccessException ex) { throw new SecurityDefinitionException("Error reading security binding members", ex); } } return true; } public Method getImplementationMethod() { return implementationMethod.getJavaMember(); } @Override public boolean equals(Object value) { return false; } @Override public int hashCode() { return 0; } } /** * Contains all known authorizers */ private Set<Authorizer> authorizers = new HashSet<Authorizer>(); /** * Contains all known secured types */ private Set<AnnotatedType<?>> securedTypes = new HashSet<AnnotatedType<?>>(); /** * A mapping between a secured method of a class and its authorizers */ private Map<Class<?>, Map<Method, Set<Authorizer>>> methodAuthorizers = new HashMap<Class<?>, Map<Method, Set<Authorizer>>>(); /** * @param <X> * @param event * @param beanManager */ public <X> void processAnnotatedType(@Observes ProcessAnnotatedType<X> event, final BeanManager beanManager) { AnnotatedTypeBuilder<X> builder = null; AnnotatedType<X> type = event.getAnnotatedType(); boolean isSecured = false; // Add the security interceptor to the class if the class is annotated // with a security binding type for (final Annotation annotation : type.getAnnotations()) { if (annotation.annotationType().isAnnotationPresent(SecurityBindingType.class)) { builder = new AnnotatedTypeBuilder<X>().readFromType(type); builder.addToClass(SecurityInterceptorBindingLiteral.INSTANCE); isSecured = true; } } // If the class isn't annotated with a security binding type, check if // any of its methods are, and if so, add the security interceptor to the // method if (!isSecured) { for (final AnnotatedMethod<? super X> m : type.getMethods()) { if (m.isAnnotationPresent(Secures.class)) { registerAuthorizer(m); continue; } for (final Annotation annotation : m.getAnnotations()) { if (annotation.annotationType().isAnnotationPresent(SecurityBindingType.class)) { if (builder == null) { builder = new AnnotatedTypeBuilder<X>().readFromType(type); } builder.addToMethod(m, SecurityInterceptorBindingLiteral.INSTANCE); isSecured = true; break; } } } } // If either the bean or any of its methods are secured, register it if (isSecured) { securedTypes.add(type); } if (builder != null) { event.setAnnotatedType(builder.create()); } } public void validateBindings(@Observes AfterBeanDiscovery event, BeanManager beanManager) { this.beanManager = beanManager; for (final AnnotatedType<?> type : securedTypes) { // Here we simply want to validate that each type that is annotated with // one or more security bindings has a valid authorizer for each binding for (final Annotation annotation : type.getJavaClass().getAnnotations()) { boolean found = false; if (annotation.annotationType().isAnnotationPresent(SecurityBindingType.class)) { // Validate the authorizer for (Authorizer auth : authorizers) { if (auth.matchesBinding(annotation)) { found = true; break; } } if (!found) { event.addDefinitionError(new SecurityDefinitionException("Secured type " + type.getJavaClass().getName() + " has no matching authorizer method for security binding @" + annotation.annotationType().getName())); } } } for (final AnnotatedMethod<?> method : type.getMethods()) { for (final Annotation annotation : method.getAnnotations()) { if (annotation.annotationType().isAnnotationPresent(SecurityBindingType.class)) { registerSecuredMethod(method.getJavaMember(), type.getJavaClass()); break; } } } } // Clear securedTypes, we don't require it any more securedTypes.clear(); securedTypes = null; } /** * This method is invoked by the security interceptor to obtain the authorizer stack for a secured method * * @param m * @return */ public Set<Authorizer> lookupAuthorizerStack(Method m, Class<?> targetClass) { if (!methodAuthorizers.containsKey(targetClass) || !methodAuthorizers.get(targetClass).containsKey(m)) { registerSecuredMethod(m, targetClass); } return methodAuthorizers.get(targetClass).get(m); } void checkAuthorization(Annotation binding) { boolean authorized = false; for (Authorizer authorizer : authorizers) { if (authorizer.matchesBinding(binding)) { authorizer.authorize(); authorized = true; } } if (!authorized) { throw new AuthorizationException("Failed to process authorization request - no matching authorizer " + "method for specified binding type [" + binding.annotationType().getClass().getName() + "]"); } } protected synchronized void registerSecuredMethod(Method method, Class<?> targetClass) { if (!methodAuthorizers.containsKey(targetClass)) { methodAuthorizers.put(targetClass, new HashMap<Method, Set<Authorizer>>()); } Map<Method, Set<Authorizer>> authz = methodAuthorizers.get(targetClass); if (!authz.containsKey(method)) { // Build a list of all security bindings on both the method and its declaring class Set<Annotation> bindings = new HashSet<Annotation>(); Class<?> cls = targetClass; while (!cls.equals(Object.class)) { for (final Annotation annotation : cls.getAnnotations()) { if (annotation.annotationType().isAnnotationPresent(SecurityBindingType.class)) { bindings.add(annotation); } } cls = cls.getSuperclass(); } for (final Annotation annotation : method.getAnnotations()) { if (annotation.annotationType().isAnnotationPresent(SecurityBindingType.class)) { bindings.add(annotation); } } Set<Authorizer> authorizerStack = new HashSet<Authorizer>(); for (Annotation binding : bindings) { boolean found = false; // For each security binding, find a valid authorizer for (Authorizer authorizer : authorizers) { if (authorizer.matchesBinding(binding)) { if (found) { StringBuilder sb = new StringBuilder(); sb.append("Matching authorizer methods found: ["); sb.append(authorizer.getImplementationMethod().getDeclaringClass().getName()); sb.append("."); sb.append(authorizer.getImplementationMethod().getName()); sb.append("]"); for (Authorizer a : authorizerStack) { if (a.matchesBinding(binding)) { sb.append(", ["); sb.append(a.getImplementationMethod().getDeclaringClass().getName()); sb.append("."); sb.append(a.getImplementationMethod().getName()); sb.append("]"); } } throw new SecurityDefinitionException("Ambiguous authorizers found for security binding type [@" + binding.annotationType().getName() + "] on method [" + method.getDeclaringClass().getName() + "." + method.getName() + "]. " + sb.toString()); } authorizerStack.add(authorizer); found = true; } } if (!found) { throw new SecurityDefinitionException("No matching authorizer found for security binding type [@" + binding.annotationType().getName() + "] on method [" + method.getDeclaringClass().getName() + "." + method.getName() + "]."); } } authz.put(method, authorizerStack); } } /** * Registers the specified authorizer method (i.e. a method annotated with the @Secures annotation) * * @param m * @throws IllegalAccessException * @throws InvocationTargetException */ protected void registerAuthorizer(AnnotatedMethod<?> m) { if (!m.getJavaMember().getReturnType().equals(Boolean.class) && !m.getJavaMember().getReturnType().equals(Boolean.TYPE)) { throw new SecurityDefinitionException("Invalid authorizer method [" + m.getJavaMember().getDeclaringClass().getName() + "." + m.getJavaMember().getName() + "] - does not return a boolean."); } // Locate the binding type Annotation binding = null; for (Annotation a : m.getAnnotations()) { if (a.annotationType().isAnnotationPresent(SecurityBindingType.class)) { if (binding != null) { throw new SecurityDefinitionException("Invalid authorizer method [" + m.getJavaMember().getDeclaringClass().getName() + "." + m.getJavaMember().getName() + "] - declares multiple security binding types"); } binding = a; } } Authorizer authorizer = new Authorizer(binding, m); authorizers.add(authorizer); } /** * Ensures that any implementations of the Authenticator interface are not stateless session beans. * * @param event */ public void validateAuthenticatorImplementation(@Observes ProcessSessionBean<Authenticator> event) { if (SessionBeanType.STATELESS.equals(event.getSessionBeanType())) { event.addDefinitionError(new IllegalStateException("Authenticator " + event.getBean().getClass() + " cannot be a Stateless Session Bean")); } } }