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 org.apache.shiro.ShiroException;
022    import org.apache.shiro.util.ClassUtils;
023    import org.apache.shiro.util.ThreadContext;
024    import org.apache.shiro.web.mgt.WebSecurityManager;
025    import org.apache.tapestry5.internal.services.PageResponseRenderer;
026    import org.apache.tapestry5.internal.services.RequestPageCache;
027    import org.apache.tapestry5.ioc.*;
028    import org.apache.tapestry5.ioc.annotations.InjectService;
029    import org.apache.tapestry5.ioc.annotations.Local;
030    import org.apache.tapestry5.ioc.annotations.Match;
031    import org.apache.tapestry5.ioc.annotations.Order;
032    import org.apache.tapestry5.services.*;
033    import org.slf4j.Logger;
034    import org.tynamo.security.SecurityComponentRequestFilter;
035    import org.tynamo.security.SecuritySymbols;
036    import org.tynamo.security.ShiroAnnotationWorker;
037    import org.tynamo.security.ShiroExceptionHandler;
038    import org.tynamo.security.filter.SecurityRequestFilter;
039    import org.tynamo.security.services.impl.ClassInterceptorsCacheImpl;
040    import org.tynamo.security.services.impl.PageServiceImpl;
041    import org.tynamo.security.services.impl.SecurityServiceImpl;
042    import org.tynamo.shiro.extension.authz.aop.AopHelper;
043    import org.tynamo.shiro.extension.authz.aop.DefaultSecurityInterceptor;
044    import org.tynamo.shiro.extension.authz.aop.SecurityInterceptor;
045    
046    import java.io.IOException;
047    import java.lang.annotation.Annotation;
048    import java.lang.reflect.Method;
049    import java.util.List;
050    import java.util.Properties;
051    
052    /**
053     * The main entry point for Security integration.
054     *
055     */
056    public class SecurityModule
057    {
058    
059            private static final String EXCEPTION_HANDLE_METHOD_NAME = "handleRequestException";
060            private static final String PATH_PREFIX = "security";
061            private static String version = "unversioned";
062    
063            static
064            {
065                    Properties moduleProperties = new Properties();
066                    try
067                    {
068                            moduleProperties.load(SecurityModule.class.getResourceAsStream("module.properties"));
069                            version = moduleProperties.getProperty("module.version");
070                    } catch (IOException e)
071                    {
072                            // ignore
073                    }
074            }
075    
076            public static void bind(final ServiceBinder binder)
077            {
078    
079                    binder.bind(WebSecurityManager.class, TapestryRealmSecurityManager.class);
080                    binder.bind(HttpServletRequestFilter.class, SecurityRequestFilter.class).withId("SecurityRequestFilter");
081                    binder.bind(ClassInterceptorsCache.class, ClassInterceptorsCacheImpl.class);
082                    binder.bind(SecurityService.class, SecurityServiceImpl.class);
083                    binder.bind(ComponentRequestFilter.class, SecurityComponentRequestFilter.class);
084                    binder.bind(ShiroExceptionHandler.class);
085                    binder.bind(PageService.class, PageServiceImpl.class);
086            }
087    
088            public static void contributeFactoryDefaults(MappedConfiguration<String, String> configuration)
089            {
090                    configuration.add(SecuritySymbols.LOGIN_URL, "/" + PATH_PREFIX + "/login");
091                    configuration.add(SecuritySymbols.SUCCESS_URL, "/index");
092                    configuration.add(SecuritySymbols.UNAUTHORIZED_URL, "/" + PATH_PREFIX + "/unauthorized");
093                    configuration.add(SecuritySymbols.DEFAULTSIGNINPAGE, "/defaultSignInPage");
094                    configuration.add(SecuritySymbols.CONFIG_PATH, "classpath:shiro.ini");
095                    configuration.add(SecuritySymbols.SHOULD_LOAD_INI_FROM_CONFIG_PATH, "false");
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("SecurityRequestFilter") HttpServletRequestFilter securityRequestFilter)
267            {
268                    configuration.add("SecurityRequestFilter", securityRequestFilter, "before:*");
269            }
270    
271    }