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 }