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.springframework.beans.factory.BeanFactoryUtils;
040    import org.springframework.context.ApplicationContext;
041    import org.springframework.security.access.AccessDeniedException;
042    import org.springframework.security.authentication.AnonymousAuthenticationToken;
043    import org.springframework.security.authentication.AuthenticationManager;
044    import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
045    import org.springframework.security.authentication.BadCredentialsException;
046    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
047    import org.springframework.security.core.Authentication;
048    import org.springframework.security.core.AuthenticationException;
049    import org.springframework.security.core.GrantedAuthority;
050    import org.springframework.security.core.context.SecurityContext;
051    import org.springframework.security.core.context.SecurityContextHolder;
052    import org.springframework.security.core.userdetails.UsernameNotFoundException;
053    import org.springframework.security.web.authentication.session.SessionAuthenticationException;
054    import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
055    import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
056    import org.springframework.security.web.context.HttpRequestResponseHolder;
057    import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
058    import org.springframework.security.web.context.SecurityContextRepository;
059    import org.springframework.web.context.support.WebApplicationContextUtils;
060    
061    
062    /**
063     * @author Bouiaw
064     * @author wdrai
065     */
066    public class SpringSecurity3Service extends AbstractSecurityService {
067            
068            private static final Logger log = Logger.getLogger(SpringSecurity3Service.class);
069            
070        private static final String FILTER_APPLIED = "__spring_security_scpf_applied";
071        private static final String SECURITY_SERVICE_APPLIED = "__spring_security_granite_service_applied";
072            
073            private AuthenticationManager authenticationManager = null;
074            private AuthenticationTrustResolverImpl authenticationTrustResolver = new AuthenticationTrustResolverImpl();
075            private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
076            private AbstractSpringSecurity3Interceptor securityInterceptor = null;
077            private SessionAuthenticationStrategy sessionAuthenticationStrategy = new SessionFixationProtectionStrategy();
078            private String authenticationManagerBeanName = null;
079            private boolean allowAnonymousAccess = false;
080            private Method getRequest = null;
081            private Method getResponse = null;
082        
083            
084            public SpringSecurity3Service() {
085                    log.debug("Starting Spring 3 Security Service");
086                    try {
087                    getRequest = HttpRequestResponseHolder.class.getDeclaredMethod("getRequest");
088                    getRequest.setAccessible(true);
089                    getResponse = HttpRequestResponseHolder.class.getDeclaredMethod("getResponse");
090                    getResponse.setAccessible(true);
091                    }
092                    catch (Exception e) {
093                            throw new RuntimeException("Could not get methods from HttpRequestResponseHolder", e);
094                    }
095        }
096            
097            
098            public void setAuthenticationManager(AuthenticationManager authenticationManager) {
099                    this.authenticationManager = authenticationManager;
100            }
101            
102            public void setAllowAnonymousAccess(boolean allowAnonymousAccess) {
103                    this.allowAnonymousAccess = allowAnonymousAccess;
104            }
105            
106            public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
107                    this.securityContextRepository = securityContextRepository;
108            }
109            
110            public void setSecurityInterceptor(AbstractSpringSecurity3Interceptor securityInterceptor) {
111                    this.securityInterceptor = securityInterceptor;
112            }
113            
114            public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
115                    if (sessionAuthenticationStrategy == null)
116                            throw new NullPointerException("SessionAuthenticationStrategy cannot be null");
117                    this.sessionAuthenticationStrategy = sessionAuthenticationStrategy;
118            }
119    
120        public void configure(Map<String, String> params) {
121            log.debug("Configuring with parameters %s: ", params);
122            if (params.containsKey("authentication-manager-bean-name"))
123                    authenticationManagerBeanName = params.get("authentication-manager-bean-name");
124            if (Boolean.TRUE.toString().equals(params.get("allow-anonymous-access")))
125                    allowAnonymousAccess = true;
126        }
127        
128        public void login(Object credentials) {
129            List<String> decodedCredentials = Arrays.asList(decodeBase64Credentials(credentials));
130    
131            HttpGraniteContext graniteContext = (HttpGraniteContext)GraniteContext.getCurrentInstance();
132            HttpServletRequest httpRequest = graniteContext.getRequest();
133    
134            String user = decodedCredentials.get(0);
135            String password = decodedCredentials.get(1);
136            Authentication auth = new UsernamePasswordAuthenticationToken(user, password);
137    
138            ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(
139                httpRequest.getSession().getServletContext()
140            );
141            if (ctx != null) {
142                    lookupAuthenticationManager(ctx, authenticationManagerBeanName);
143                
144                try {
145                    Authentication authentication = authenticationManager.authenticate(auth);
146                    
147                    if (authentication != null && !authenticationTrustResolver.isAnonymous(authentication)) {
148                            try {
149                                    sessionAuthenticationStrategy.onAuthentication(authentication, httpRequest, graniteContext.getResponse());
150                        } 
151                            catch (SessionAuthenticationException e) {
152                            log.debug(e, "SessionAuthenticationStrategy rejected the authentication object");
153                            SecurityContextHolder.clearContext();
154                            handleAuthenticationExceptions(e);
155                            return;
156                        }                
157                    }
158                    
159                    HttpRequestResponseHolder holder = new HttpRequestResponseHolder(graniteContext.getRequest(), graniteContext.getResponse());
160                    SecurityContext securityContext = securityContextRepository.loadContext(holder);
161                    securityContext.setAuthentication(authentication);
162                    SecurityContextHolder.setContext(securityContext);
163                        try {
164                            securityContextRepository.saveContext(securityContext, (HttpServletRequest)getRequest.invoke(holder), (HttpServletResponse)getResponse.invoke(holder));
165                        }
166                        catch (Exception e) {
167                            log.error(e, "Could not save context after authentication");
168                        }
169                } 
170                catch (AuthenticationException e) {
171                    handleAuthenticationExceptions(e);
172                }
173            }
174    
175            log.debug("User %s logged in", user);
176        }
177        
178        protected void handleAuthenticationExceptions(AuthenticationException e) {
179            if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException)
180                throw SecurityServiceException.newInvalidCredentialsException(e.getMessage());
181            
182            throw SecurityServiceException.newAuthenticationFailedException(e.getMessage());
183        }
184        
185        public void lookupAuthenticationManager(ApplicationContext ctx, String authenticationManagerBeanName) throws SecurityServiceException {
186            if (this.authenticationManager != null)
187                    return;
188            
189            Map<String, AuthenticationManager> authManagers = BeanFactoryUtils.beansOfTypeIncludingAncestors(ctx, AuthenticationManager.class);
190            
191            if (authenticationManagerBeanName != null) {
192                    this.authenticationManager = authManagers.get(authenticationManagerBeanName);
193                    if (authenticationManager == null) {
194                            log.error("AuthenticationManager bean not found " + authenticationManagerBeanName);
195                            throw SecurityServiceException.newAuthenticationFailedException("Authentication failed");
196                    }
197                    return;
198            }
199            else if (authManagers.size() > 1) {
200                    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>");
201                    throw SecurityServiceException.newAuthenticationFailedException("Authentication failed");
202            }
203            
204            this.authenticationManager = authManagers.values().iterator().next();
205        }
206    
207        
208        public Object authorize(AbstractSecurityContext context) throws Exception {
209            log.debug("Authorize %s on destination %s (secured: %b)", context, context.getDestination().getId(), context.getDestination().isSecured());
210    
211            startAuthorization(context);
212            
213            HttpGraniteContext graniteContext = (HttpGraniteContext)GraniteContext.getCurrentInstance();
214            
215            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
216            HttpRequestResponseHolder holder = null;
217            
218            if (graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null) {
219                    if (graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) == null) {
220                            holder = new HttpRequestResponseHolder(graniteContext.getRequest(), graniteContext.getResponse());
221                            SecurityContext contextBeforeChainExecution = securityContextRepository.loadContext(holder);
222                                SecurityContextHolder.setContext(contextBeforeChainExecution);
223                                if (isAuthenticated(authentication))
224                                    contextBeforeChainExecution.setAuthentication(authentication);
225                                else
226                                    authentication = contextBeforeChainExecution.getAuthentication();
227                                
228                                graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, 0);
229                    }
230                    else
231                                graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, (Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED)+1);
232            }
233            
234            if (context.getDestination().isSecured()) {
235                if (!isAuthenticated(authentication) || (!allowAnonymousAccess && authentication instanceof AnonymousAuthenticationToken)) {
236                    log.debug("User not authenticated!");
237                    throw SecurityServiceException.newNotLoggedInException("User not logged in");
238                }
239                if (!userCanAccessService(context, authentication)) { 
240                    log.debug("Access denied for user %s", authentication.getName());
241                    throw SecurityServiceException.newAccessDeniedException("User not in required role");
242                }
243            }
244    
245            try {
246                    Object returnedObject = securityInterceptor != null 
247                            ? securityInterceptor.invoke(context)
248                            : endAuthorization(context);
249                
250                return returnedObject;
251            }
252            catch (AccessDeniedException e) {
253                    throw SecurityServiceException.newAccessDeniedException(e.getMessage());
254            }
255            catch (InvocationTargetException e) {
256                handleAuthorizationExceptions(e);
257                throw e;
258            }
259            finally {
260                if (graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null) {
261                    if ((Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) == 0) {
262                                SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
263                                SecurityContextHolder.clearContext();
264                                try {
265                                    securityContextRepository.saveContext(contextAfterChainExecution, (HttpServletRequest)getRequest.invoke(holder), (HttpServletResponse)getResponse.invoke(holder));
266                                }
267                                catch (Exception e) {
268                                    log.error(e, "Could not extract wrapped context from holder");
269                                }
270                                graniteContext.getRequest().removeAttribute(SECURITY_SERVICE_APPLIED);
271                    }
272                    else
273                            graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, (Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED)-1);
274                }
275            }
276        }
277    
278        public void logout() {
279            HttpGraniteContext context = (HttpGraniteContext)GraniteContext.getCurrentInstance();
280            HttpSession session = context.getSession(false);
281            if (session != null && securityContextRepository.containsContext(context.getRequest()))                 
282                    session.invalidate();
283            
284            SecurityContextHolder.clearContext();
285        }
286    
287        protected boolean isUserInRole(Authentication authentication, String role) {
288            for (GrantedAuthority ga : authentication.getAuthorities()) {
289                if (ga.getAuthority().matches(role))
290                    return true;
291            }
292            return false;
293        }
294    
295        protected boolean isAuthenticated(Authentication authentication) {
296            return authentication != null && authentication.isAuthenticated();
297        }
298    
299        protected boolean userCanAccessService(AbstractSecurityContext context, Authentication authentication) {
300            log.debug("Is authenticated as: %s", authentication.getName());
301    
302            for (String role : context.getDestination().getRoles()) {
303                if (isUserInRole(authentication, role)) {
304                    log.debug("Allowed access to %s in role %s", authentication.getName(), role);
305                    return true;
306                }
307                log.debug("Access denied for %s not in role %s", authentication.getName(), role);
308            }
309            return false;
310        }
311    
312        protected void handleAuthorizationExceptions(InvocationTargetException e) {
313            for (Throwable t = e; t != null; t = t.getCause()) {
314                // Don't create a dependency to javax.ejb in SecurityService...
315                if (t instanceof SecurityException ||
316                    t instanceof AccessDeniedException ||
317                    "javax.ejb.EJBAccessException".equals(t.getClass().getName()))
318                    throw SecurityServiceException.newAccessDeniedException(t.getMessage());
319            }
320        }
321    
322    }