001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.tynamo.security.services;
020    
021    import java.lang.annotation.Annotation;
022    import java.lang.reflect.Method;
023    import java.util.List;
024    
025    import org.apache.shiro.ShiroException;
026    import org.apache.shiro.util.ClassUtils;
027    import org.apache.shiro.util.ThreadContext;
028    import org.apache.shiro.web.mgt.WebSecurityManager;
029    import org.apache.tapestry5.internal.services.PageResponseRenderer;
030    import org.apache.tapestry5.internal.services.RequestPageCache;
031    import org.apache.tapestry5.ioc.Configuration;
032    import org.apache.tapestry5.ioc.Invocation;
033    import org.apache.tapestry5.ioc.MappedConfiguration;
034    import org.apache.tapestry5.ioc.MethodAdvice;
035    import org.apache.tapestry5.ioc.MethodAdviceReceiver;
036    import org.apache.tapestry5.ioc.OrderedConfiguration;
037    import org.apache.tapestry5.ioc.ServiceBinder;
038    import org.apache.tapestry5.ioc.annotations.InjectService;
039    import org.apache.tapestry5.ioc.annotations.Local;
040    import org.apache.tapestry5.ioc.annotations.Match;
041    import org.apache.tapestry5.ioc.annotations.Order;
042    import org.apache.tapestry5.services.ApplicationInitializer;
043    import org.apache.tapestry5.services.ApplicationInitializerFilter;
044    import org.apache.tapestry5.services.ComponentClassResolver;
045    import org.apache.tapestry5.services.ComponentClassTransformWorker;
046    import org.apache.tapestry5.services.ComponentRequestFilter;
047    import org.apache.tapestry5.services.Context;
048    import org.apache.tapestry5.services.HttpServletRequestFilter;
049    import org.apache.tapestry5.services.LibraryMapping;
050    import org.apache.tapestry5.services.RequestGlobals;
051    import org.apache.tapestry5.services.Response;
052    import org.slf4j.Logger;
053    import org.tynamo.common.ModuleProperties;
054    import org.tynamo.security.SecurityComponentRequestFilter;
055    import org.tynamo.security.SecuritySymbols;
056    import org.tynamo.security.ShiroAnnotationWorker;
057    import org.tynamo.security.ShiroExceptionHandler;
058    import org.tynamo.security.services.impl.ClassInterceptorsCacheImpl;
059    import org.tynamo.security.services.impl.PageServiceImpl;
060    import org.tynamo.security.services.impl.SecurityConfiguration;
061    import org.tynamo.security.services.impl.SecurityFilterChainFactoryImpl;
062    import org.tynamo.security.services.impl.SecurityServiceImpl;
063    import org.tynamo.shiro.extension.authz.aop.AopHelper;
064    import org.tynamo.shiro.extension.authz.aop.DefaultSecurityInterceptor;
065    import org.tynamo.shiro.extension.authz.aop.SecurityInterceptor;
066    
067    /**
068     * The main entry point for Security integration.
069     *
070     */
071    public class SecurityModule
072    {
073    
074            private static final String EXCEPTION_HANDLE_METHOD_NAME = "handleRequestException";
075            private static final String PATH_PREFIX = "security";
076            private static final String version = ModuleProperties.getVersion(SecurityModule.class);
077    
078            public static void bind(final ServiceBinder binder)
079            {
080    
081                    binder.bind(WebSecurityManager.class, TapestryRealmSecurityManager.class);
082                    binder.bind(HttpServletRequestFilter.class, SecurityConfiguration.class).withId("SecurityConfiguration");
083                    binder.bind(ClassInterceptorsCache.class, ClassInterceptorsCacheImpl.class);
084                    binder.bind(SecurityService.class, SecurityServiceImpl.class);
085                    binder.bind(SecurityFilterChainFactory.class, SecurityFilterChainFactoryImpl.class);
086                    binder.bind(ComponentRequestFilter.class, SecurityComponentRequestFilter.class);
087                    binder.bind(ShiroExceptionHandler.class);
088                    binder.bind(PageService.class, PageServiceImpl.class);
089            }
090    
091            public static void contributeFactoryDefaults(MappedConfiguration<String, String> configuration)
092            {
093                    configuration.add(SecuritySymbols.LOGIN_URL, "/" + PATH_PREFIX + "/login");
094                    configuration.add(SecuritySymbols.SUCCESS_URL, "/index");
095                    configuration.add(SecuritySymbols.UNAUTHORIZED_URL, "/" + PATH_PREFIX + "/unauthorized");
096            }
097    
098    
099            /**
100             * Create ClassInterceptorsCache through annotations on the class page,
101             * which then will use SecurityFilter.
102             * <p/>
103             */
104            public void contributeApplicationInitializer(OrderedConfiguration<ApplicationInitializerFilter> configuration,
105                                                         final ComponentClassResolver componentClassResolver,
106                                                         final ClassInterceptorsCache classInterceptorsCache)
107            {
108    
109                    configuration.add("SecurityApplicationInitializerFilter", new ApplicationInitializerFilter()
110                    {
111                            @Override
112                            public void initializeApplication(Context context, ApplicationInitializer initializer)
113                            {
114    
115                                    initializer.initializeApplication(context);
116    
117                                    for (String name : componentClassResolver.getPageNames())
118                                    {
119                                            String className = componentClassResolver.resolvePageNameToClassName(name);
120                                            Class<?> clazz = ClassUtils.forName(className);
121    
122                                            while (clazz != null)
123                                            {
124                                                    for (Class<? extends Annotation> annotationClass : AopHelper.getAutorizationAnnotationClasses())
125                                                    {
126                                                            Annotation classAnnotation = clazz.getAnnotation(annotationClass);
127                                                            if (classAnnotation != null)
128                                                            {
129                                                                    //Add in the cache which then will be used in RequestFilter
130                                                                    classInterceptorsCache.add(className, new DefaultSecurityInterceptor(classAnnotation));
131                                                            }
132                                                    }
133                                                    clazz = clazz.getSuperclass();
134                                            }
135                                    }
136                            }
137                    });
138            }
139    
140    
141            public static void contributeComponentRequestHandler(OrderedConfiguration<ComponentRequestFilter> configuration,
142                                                                 @Local ComponentRequestFilter filter)
143            {
144                     configuration.add("SecurityFilter", filter, "before:*");
145            }
146    
147            public static void contributeComponentClassTransformWorker(OrderedConfiguration<ComponentClassTransformWorker> configuration)
148            {
149                    configuration.addInstance(ShiroAnnotationWorker.class.getSimpleName(), ShiroAnnotationWorker.class);
150            }
151    
152            public static void contributeComponentClassResolver(Configuration<LibraryMapping> configuration)
153            {
154                    configuration.add(new LibraryMapping(PATH_PREFIX, "org.tynamo.security"));
155            }
156    
157            public static void contributeClasspathAssetAliasManager(MappedConfiguration<String, String> configuration)
158            {
159                    configuration.add(PATH_PREFIX + "-" + version, "org/tynamo/security");
160            }
161    
162            /**
163             * Secure all service methods that are marked with authorization annotations.
164             * <p/>
165             * <b>Restriction:</b> Only service interfaces can be annotated.
166             */
167            @Match("*")
168            @Order("before:*")
169            public static void adviseSecurityAssert(MethodAdviceReceiver receiver)
170            {
171    
172                    Class<?> serviceInterface = receiver.getInterface();
173    
174                    for (Method method : serviceInterface.getMethods())
175                    {
176    
177                            List<SecurityInterceptor> interceptors =
178                                            AopHelper.createSecurityInterceptorsSeeingInterfaces(method, serviceInterface);
179    
180                            for (final SecurityInterceptor interceptor : interceptors)
181                            {
182                                    MethodAdvice advice = new MethodAdvice()
183                                    {
184                                            @Override
185                                            public void advise(Invocation invocation)
186                                            {
187                                                    // Only (try to) intercept if subject is bound.
188                                                    // This is useful in case background or initializing operations
189                                                    // call service operations that are secure
190                                                    if (ThreadContext.getSubject() != null) interceptor.intercept();
191                                                    invocation.proceed();
192    
193                                            }
194                                    };
195                                    receiver.adviseMethod(method, advice);
196                            }
197    
198                    }
199            }
200    
201            /**
202             * Advise current RequestExceptionHandler for we can catch ShiroException exceptions and handle this.
203             *
204             * @see org.tynamo.security.ShiroExceptionHandler
205             */
206            public static void adviseRequestExceptionHandler(MethodAdviceReceiver receiver,
207                                                             final PageResponseRenderer renderer,
208                                                             final RequestPageCache pageCache,
209                                                             final Logger logger,
210                                                             final RequestGlobals requestGlobals,
211                                                             final Response response,
212                                                             final SecurityService securityService,
213                                                             final ShiroExceptionHandler handler)
214            {
215    
216                    Method handleMethod;
217    
218                    try
219                    {
220                            Class<?> serviceInterface = receiver.getInterface();
221                            handleMethod = serviceInterface.getMethod(EXCEPTION_HANDLE_METHOD_NAME, Throwable.class);
222                    } catch (Exception e)
223                    {
224                            throw new RuntimeException("Can't find method  " +
225                                            "RequestExceptionHandler." + EXCEPTION_HANDLE_METHOD_NAME + ". Changed API?", e);
226                    }
227    
228                    MethodAdvice advice = new MethodAdvice()
229                    {
230                            @Override
231                            public void advise(Invocation invocation)
232                            {
233                                    Throwable exception = (Throwable) invocation.getParameter(0);
234    
235                                    ShiroException shiroException = null;
236    
237                                    // TODO Maybe we should just loop through the chain as done in exceptionpage module
238                                    // Depending on where the error was thrown, there could be several levels of wrappers..
239                                    // For exceptions in component operations, it's OperationException -> ComponentEventException -> ShiroException
240                                    if (exception.getCause() instanceof ShiroException) shiroException = (ShiroException) exception.getCause();
241                                    else if (exception.getCause() !=null && exception.getCause().getCause() instanceof ShiroException) shiroException = (ShiroException) exception.getCause().getCause();
242                                    else if (exception instanceof ShiroException) shiroException = (ShiroException) exception;
243    
244                                    if (shiroException != null)
245                                    {
246    
247                                            try
248                                            {
249                                                    handler.handle(shiroException);
250                                            } catch (Exception e)
251                                            {
252                                                    logger.error("Error handling SecurityException", e);
253                                                    invocation.proceed();
254                                            }
255    
256                                    } else
257                                    {
258                                            invocation.proceed();
259                                    }
260                            }
261                    };
262                    receiver.adviseMethod(handleMethod, advice);
263            }
264    
265            public static void contributeHttpServletRequestHandler(OrderedConfiguration<HttpServletRequestFilter> configuration,
266                            @InjectService("SecurityConfiguration") HttpServletRequestFilter securityConfiguration) {
267                    configuration.add("SecurityConfiguration", securityConfiguration, "before:*");
268            }
269    }