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