001    /*
002      GRANITE DATA SERVICES
003      Copyright (C) 2011 GRANITE DATA SERVICES S.A.S.
004    
005      This file is part of Granite Data Services.
006    
007      Granite Data Services is free software; you can redistribute it and/or modify
008      it under the terms of the GNU Library General Public License as published by
009      the Free Software Foundation; either version 2 of the License, or (at your
010      option) any later version.
011    
012      Granite Data Services is distributed in the hope that it will be useful, but
013      WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014      FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015      for more details.
016    
017      You should have received a copy of the GNU Library General Public License
018      along with this library; if not, see <http://www.gnu.org/licenses/>.
019    */
020    
021    package org.granite.spring.security;
022    
023    import java.lang.reflect.InvocationTargetException;
024    import java.lang.reflect.Method;
025    import java.util.Arrays;
026    import java.util.List;
027    import java.util.Map;
028    
029    import javax.servlet.http.HttpServletRequest;
030    import javax.servlet.http.HttpServletResponse;
031    import javax.servlet.http.HttpSession;
032    
033    import org.granite.context.GraniteContext;
034    import org.granite.logging.Logger;
035    import org.granite.messaging.service.security.AbstractSecurityContext;
036    import org.granite.messaging.service.security.AbstractSecurityService;
037    import org.granite.messaging.service.security.SecurityServiceException;
038    import org.granite.messaging.webapp.HttpGraniteContext;
039    import org.granite.messaging.webapp.ServletGraniteContext;
040    import org.springframework.beans.factory.BeanFactoryUtils;
041    import org.springframework.context.ApplicationContext;
042    import org.springframework.context.ApplicationContextAware;
043    import org.springframework.security.access.AccessDeniedException;
044    import org.springframework.security.authentication.AnonymousAuthenticationToken;
045    import org.springframework.security.authentication.AuthenticationManager;
046    import org.springframework.security.authentication.AuthenticationTrustResolver;
047    import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
048    import org.springframework.security.authentication.BadCredentialsException;
049    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
050    import org.springframework.security.authentication.encoding.PasswordEncoder;
051    import org.springframework.security.core.Authentication;
052    import org.springframework.security.core.AuthenticationException;
053    import org.springframework.security.core.GrantedAuthority;
054    import org.springframework.security.core.context.SecurityContext;
055    import org.springframework.security.core.context.SecurityContextHolder;
056    import org.springframework.security.core.userdetails.UsernameNotFoundException;
057    import org.springframework.security.web.authentication.session.SessionAuthenticationException;
058    import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
059    import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
060    import org.springframework.security.web.context.HttpRequestResponseHolder;
061    import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
062    import org.springframework.security.web.context.SecurityContextRepository;
063    import org.springframework.web.context.support.WebApplicationContextUtils;
064    
065    
066    /**
067     * @author Bouiaw
068     * @author wdrai
069     */
070    public class SpringSecurity3Service extends AbstractSecurityService implements ApplicationContextAware {
071            
072            private static final Logger log = Logger.getLogger(SpringSecurity3Service.class);
073            
074        private static final String FILTER_APPLIED = "__spring_security_scpf_applied";
075        private static final String SECURITY_SERVICE_APPLIED = "__spring_security_granite_service_applied";
076            
077        private ApplicationContext applicationContext = null;
078            private AuthenticationManager authenticationManager = null;
079            private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
080            private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
081            private AbstractSpringSecurity3Interceptor securityInterceptor = null;
082            private SessionAuthenticationStrategy sessionAuthenticationStrategy = new SessionFixationProtectionStrategy();
083            private PasswordEncoder passwordEncoder = null;
084            private String authenticationManagerBeanName = null;
085            private boolean allowAnonymousAccess = false;
086            private Method getRequest = null;
087            private Method getResponse = null;
088        
089            
090            public SpringSecurity3Service() {
091                    log.debug("Starting Spring 3 Security Service");
092                    try {
093                    getRequest = HttpRequestResponseHolder.class.getDeclaredMethod("getRequest");
094                    getRequest.setAccessible(true);
095                    getResponse = HttpRequestResponseHolder.class.getDeclaredMethod("getResponse");
096                    getResponse.setAccessible(true);
097                    }
098                    catch (Exception e) {
099                            throw new RuntimeException("Could not get methods from HttpRequestResponseHolder", e);
100                    }
101        }
102            
103            public void setApplicationContext(ApplicationContext applicationContext) {
104                    this.applicationContext = applicationContext;
105            }
106            
107            public void setAuthenticationManager(AuthenticationManager authenticationManager) {
108                    this.authenticationManager = authenticationManager;
109            }
110            
111            public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
112                    this.authenticationTrustResolver = authenticationTrustResolver;
113            }
114            
115            public void setAllowAnonymousAccess(boolean allowAnonymousAccess) {
116                    this.allowAnonymousAccess = allowAnonymousAccess;
117            }
118            
119            public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
120                    this.securityContextRepository = securityContextRepository;
121            }
122            
123            public void setSecurityInterceptor(AbstractSpringSecurity3Interceptor securityInterceptor) {
124                    this.securityInterceptor = securityInterceptor;
125            }
126            
127            public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
128                    if (sessionAuthenticationStrategy == null)
129                            throw new NullPointerException("SessionAuthenticationStrategy cannot be null");
130                    this.sessionAuthenticationStrategy = sessionAuthenticationStrategy;
131            }
132            
133            public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
134                    this.passwordEncoder = passwordEncoder;
135            }
136    
137        public void configure(Map<String, String> params) {
138            log.debug("Configuring with parameters %s: ", params);
139            if (params.containsKey("authentication-manager-bean-name"))
140                    authenticationManagerBeanName = params.get("authentication-manager-bean-name");
141            if (Boolean.TRUE.toString().equals(params.get("allow-anonymous-access")))
142                    allowAnonymousAccess = true;
143        }
144        
145        public void login(Object credentials, String charset) {
146            List<String> decodedCredentials = Arrays.asList(decodeBase64Credentials(credentials, charset));
147            
148            if (!(GraniteContext.getCurrentInstance() instanceof HttpGraniteContext)) {
149                    log.info("Login from non HTTP granite context ignored");
150                    return;
151            }
152            
153            HttpGraniteContext graniteContext = (HttpGraniteContext)GraniteContext.getCurrentInstance();
154            HttpServletRequest httpRequest = graniteContext.getRequest();
155    
156            String user = decodedCredentials.get(0);
157            String password = decodedCredentials.get(1);
158            if (passwordEncoder != null)
159                    password = passwordEncoder.encodePassword(password, null);
160            Authentication auth = new UsernamePasswordAuthenticationToken(user, password);
161            
162            ApplicationContext appContext = applicationContext != null ? applicationContext 
163                            : WebApplicationContextUtils.getWebApplicationContext(graniteContext.getServletContext());
164            
165            if (appContext != null) {
166                    lookupAuthenticationManager(appContext, authenticationManagerBeanName);
167                
168                try {
169                    Authentication authentication = authenticationManager.authenticate(auth);
170                    
171                    if (authentication != null && !authenticationTrustResolver.isAnonymous(authentication)) {
172                            try {
173                                    sessionAuthenticationStrategy.onAuthentication(authentication, httpRequest, graniteContext.getResponse());
174                        } 
175                            catch (SessionAuthenticationException e) {
176                            log.debug(e, "SessionAuthenticationStrategy rejected the authentication object");
177                            SecurityContextHolder.clearContext();
178                            handleAuthenticationExceptions(e);
179                            return;
180                        }                
181                    }
182                    
183                    HttpRequestResponseHolder holder = new HttpRequestResponseHolder(graniteContext.getRequest(), graniteContext.getResponse());
184                    SecurityContext securityContext = securityContextRepository.loadContext(holder);
185                    securityContext.setAuthentication(authentication);
186                    SecurityContextHolder.setContext(securityContext);
187                        try {
188                            securityContextRepository.saveContext(securityContext, (HttpServletRequest)getRequest.invoke(holder), (HttpServletResponse)getResponse.invoke(holder));
189                        }
190                        catch (Exception e) {
191                            log.error(e, "Could not save context after authentication");
192                        }
193    
194                        endLogin(credentials, charset);
195                } 
196                catch (AuthenticationException e) {
197                    handleAuthenticationExceptions(e);
198                }
199            }
200    
201            log.debug("User %s logged in", user);
202        }
203        
204        protected void handleAuthenticationExceptions(AuthenticationException e) {
205            if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException)
206                throw SecurityServiceException.newInvalidCredentialsException(e.getMessage());
207            
208            throw SecurityServiceException.newAuthenticationFailedException(e.getMessage());
209        }
210        
211        public void lookupAuthenticationManager(ApplicationContext ctx, String authenticationManagerBeanName) throws SecurityServiceException {
212            if (this.authenticationManager != null)
213                    return;
214            
215            Map<String, AuthenticationManager> authManagers = BeanFactoryUtils.beansOfTypeIncludingAncestors(ctx, AuthenticationManager.class);
216            
217            if (authenticationManagerBeanName != null) {
218                    this.authenticationManager = authManagers.get(authenticationManagerBeanName);
219                    if (authenticationManager == null) {
220                            log.error("AuthenticationManager bean not found " + authenticationManagerBeanName);
221                            throw SecurityServiceException.newAuthenticationFailedException("Authentication failed");
222                    }
223                    return;
224            }
225            else if (authManagers.size() > 1) {
226                    log.error("More than one AuthenticationManager beans found, specify which one to use in Spring config <graniteds:security-service authentication-manager='myAuthManager'/> or in granite-config.xml <security type='org.granite.spring.security.SpringSecurity3Service'><param name='authentication-manager-bean-name' value='myAuthManager'/></security>");
227                    throw SecurityServiceException.newAuthenticationFailedException("Authentication failed");
228            }
229            
230            this.authenticationManager = authManagers.values().iterator().next();
231        }
232    
233        
234        public Object authorize(AbstractSecurityContext context) throws Exception {
235            log.debug("Authorize %s on destination %s (secured: %b)", context, context.getDestination().getId(), context.getDestination().isSecured());
236    
237            startAuthorization(context);
238            
239            ServletGraniteContext graniteContext = (ServletGraniteContext)GraniteContext.getCurrentInstance();
240            
241            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
242            HttpRequestResponseHolder holder = null;
243            
244            if (graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null) {
245                    if (graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) == null) {
246                            holder = new HttpRequestResponseHolder(graniteContext.getRequest(), graniteContext.getResponse());
247                            SecurityContext contextBeforeChainExecution = securityContextRepository.loadContext(holder);
248                                SecurityContextHolder.setContext(contextBeforeChainExecution);
249                                if (isAuthenticated(authentication))
250                                    contextBeforeChainExecution.setAuthentication(authentication);
251                                else
252                                    authentication = contextBeforeChainExecution.getAuthentication();
253                                
254                                graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, 0);
255                    }
256                    else
257                                graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, (Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED)+1);
258            }
259            
260            if (context.getDestination().isSecured()) {
261                if (!isAuthenticated(authentication) || (!allowAnonymousAccess && authentication instanceof AnonymousAuthenticationToken)) {
262                    log.debug("User not authenticated!");
263                    throw SecurityServiceException.newNotLoggedInException("User not logged in");
264                }
265                if (!userCanAccessService(context, authentication)) { 
266                    log.debug("Access denied for user %s", authentication.getName());
267                    throw SecurityServiceException.newAccessDeniedException("User not in required role");
268                }
269            }
270    
271            try {
272                    Object returnedObject = securityInterceptor != null 
273                            ? securityInterceptor.invoke(context)
274                            : endAuthorization(context);
275                
276                return returnedObject;
277            }
278            catch (AccessDeniedException e) {
279                    throw SecurityServiceException.newAccessDeniedException(e.getMessage());
280            }
281            catch (InvocationTargetException e) {
282                handleAuthorizationExceptions(e);
283                throw e;
284            }
285            finally {
286                if (graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null) {
287                    if ((Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) == 0) {
288                                SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
289                                SecurityContextHolder.clearContext();
290                                try {
291                                    securityContextRepository.saveContext(contextAfterChainExecution, (HttpServletRequest)getRequest.invoke(holder), (HttpServletResponse)getResponse.invoke(holder));
292                                }
293                                catch (Exception e) {
294                                    log.error(e, "Could not extract wrapped context from holder");
295                                }
296                                graniteContext.getRequest().removeAttribute(SECURITY_SERVICE_APPLIED);
297                    }
298                    else
299                            graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, (Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED)-1);
300                }
301            }
302        }
303        
304        @Override
305        public boolean acceptsContext() {
306            return GraniteContext.getCurrentInstance() instanceof ServletGraniteContext;
307        }
308    
309        public void logout() {
310            ServletGraniteContext context = (ServletGraniteContext)GraniteContext.getCurrentInstance();
311            HttpSession session = context.getSession(false);
312            if (session != null && securityContextRepository.containsContext(context.getRequest()))                 
313                    session.invalidate();
314            
315            SecurityContextHolder.clearContext();
316        }
317    
318        protected boolean isUserInRole(Authentication authentication, String role) {
319            for (GrantedAuthority ga : authentication.getAuthorities()) {
320                if (ga.getAuthority().matches(role))
321                    return true;
322            }
323            return false;
324        }
325    
326        protected boolean isAuthenticated(Authentication authentication) {
327            return authentication != null && authentication.isAuthenticated();
328        }
329    
330        protected boolean userCanAccessService(AbstractSecurityContext context, Authentication authentication) {
331            log.debug("Is authenticated as: %s", authentication.getName());
332    
333            for (String role : context.getDestination().getRoles()) {
334                if (isUserInRole(authentication, role)) {
335                    log.debug("Allowed access to %s in role %s", authentication.getName(), role);
336                    return true;
337                }
338                log.debug("Access denied for %s not in role %s", authentication.getName(), role);
339            }
340            return false;
341        }
342    
343        protected void handleAuthorizationExceptions(InvocationTargetException e) {
344            for (Throwable t = e; t != null; t = t.getCause()) {
345                // Don't create a dependency to javax.ejb in SecurityService...
346                if (t instanceof SecurityException ||
347                    t instanceof AccessDeniedException ||
348                    "javax.ejb.EJBAccessException".equals(t.getClass().getName())) {
349                    throw SecurityServiceException.newAccessDeniedException(t.getMessage());
350                }
351                else if (t instanceof AuthenticationException) {
352                    throw SecurityServiceException.newNotLoggedInException(t.getMessage());
353                }
354            }
355        }
356    
357    }