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
021package org.granite.spring.security;
022
023import java.lang.reflect.InvocationTargetException;
024import java.lang.reflect.Method;
025import java.util.Arrays;
026import java.util.List;
027import java.util.Map;
028
029import javax.servlet.http.HttpServletRequest;
030import javax.servlet.http.HttpServletResponse;
031import javax.servlet.http.HttpSession;
032
033import org.granite.context.GraniteContext;
034import org.granite.logging.Logger;
035import org.granite.messaging.service.security.AbstractSecurityContext;
036import org.granite.messaging.service.security.AbstractSecurityService;
037import org.granite.messaging.service.security.SecurityServiceException;
038import org.granite.messaging.webapp.HttpGraniteContext;
039import org.granite.messaging.webapp.ServletGraniteContext;
040import org.springframework.beans.factory.BeanFactoryUtils;
041import org.springframework.context.ApplicationContext;
042import org.springframework.context.ApplicationContextAware;
043import org.springframework.security.access.AccessDeniedException;
044import org.springframework.security.authentication.AnonymousAuthenticationToken;
045import org.springframework.security.authentication.AuthenticationManager;
046import org.springframework.security.authentication.AuthenticationTrustResolver;
047import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
048import org.springframework.security.authentication.BadCredentialsException;
049import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
050import org.springframework.security.authentication.encoding.PasswordEncoder;
051import org.springframework.security.core.Authentication;
052import org.springframework.security.core.AuthenticationException;
053import org.springframework.security.core.GrantedAuthority;
054import org.springframework.security.core.context.SecurityContext;
055import org.springframework.security.core.context.SecurityContextHolder;
056import org.springframework.security.core.userdetails.UsernameNotFoundException;
057import org.springframework.security.web.authentication.session.SessionAuthenticationException;
058import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
059import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
060import org.springframework.security.web.context.HttpRequestResponseHolder;
061import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
062import org.springframework.security.web.context.SecurityContextRepository;
063import org.springframework.web.context.support.WebApplicationContextUtils;
064
065
066/**
067 * @author Bouiaw
068 * @author wdrai
069 */
070public 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                        log.debug("Define authentication and save to repo: %s", authentication != null ? authentication.getName() : "none");
184                HttpRequestResponseHolder holder = new HttpRequestResponseHolder(graniteContext.getRequest(), graniteContext.getResponse());
185                SecurityContext securityContext = securityContextRepository.loadContext(holder);
186                securityContext.setAuthentication(authentication);
187                SecurityContextHolder.setContext(securityContext);
188                    try {
189                        securityContextRepository.saveContext(securityContext, (HttpServletRequest)getRequest.invoke(holder), (HttpServletResponse)getResponse.invoke(holder));
190                    }
191                    catch (Exception e) {
192                        log.error(e, "Could not save context after authentication");
193                    }
194
195                    endLogin(credentials, charset);
196            } 
197            catch (AuthenticationException e) {
198                handleAuthenticationExceptions(e);
199            }
200            finally {
201                        log.debug("Clear authentication");
202                    SecurityContextHolder.clearContext();
203            }
204        }
205
206        log.debug("User %s logged in", user);
207    }
208    
209    protected void handleAuthenticationExceptions(AuthenticationException e) {
210        if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException)
211            throw SecurityServiceException.newInvalidCredentialsException(e.getMessage());
212        
213        throw SecurityServiceException.newAuthenticationFailedException(e.getMessage());
214    }
215    
216    public void lookupAuthenticationManager(ApplicationContext ctx, String authenticationManagerBeanName) throws SecurityServiceException {
217        if (this.authenticationManager != null)
218                return;
219        
220        Map<String, AuthenticationManager> authManagers = BeanFactoryUtils.beansOfTypeIncludingAncestors(ctx, AuthenticationManager.class);
221        
222        if (authenticationManagerBeanName != null) {
223                this.authenticationManager = authManagers.get(authenticationManagerBeanName);
224                if (authenticationManager == null) {
225                        log.error("AuthenticationManager bean not found " + authenticationManagerBeanName);
226                        throw SecurityServiceException.newAuthenticationFailedException("Authentication failed");
227                }
228                return;
229        }
230        else if (authManagers.size() > 1) {
231                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>");
232                throw SecurityServiceException.newAuthenticationFailedException("Authentication failed");
233        }
234        
235        this.authenticationManager = authManagers.values().iterator().next();
236    }
237
238    
239    public Object authorize(AbstractSecurityContext context) throws Exception {
240        log.debug("Authorize %s on destination %s (secured: %b)", context, context.getDestination().getId(), context.getDestination().isSecured());
241
242        startAuthorization(context);
243        
244        ServletGraniteContext graniteContext = (ServletGraniteContext)GraniteContext.getCurrentInstance();
245        
246        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
247        HttpRequestResponseHolder holder = null;
248        
249        if (graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null) {
250                if (graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) == null) {
251                        holder = new HttpRequestResponseHolder(graniteContext.getRequest(), graniteContext.getResponse());
252                        SecurityContext contextBeforeChainExecution = securityContextRepository.loadContext(holder);
253                            SecurityContextHolder.setContext(contextBeforeChainExecution);
254                            if (isAuthenticated(authentication)) {
255                                log.debug("Restore authentication: %s", authentication.getName());
256                                contextBeforeChainExecution.setAuthentication(authentication);
257                            }
258                            else {
259                                authentication = contextBeforeChainExecution.getAuthentication();
260                                log.debug("Restore authentication from repository: %s", authentication != null ? authentication.getName() : "none");
261                            }
262                            
263                            graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, 0);
264                }
265                else {
266                        log.debug("Increment service reentrance counter");
267                            graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, (Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED)+1);
268                }
269        }
270        
271        if (context.getDestination().isSecured()) {
272            if (!isAuthenticated(authentication) || (!allowAnonymousAccess && authentication instanceof AnonymousAuthenticationToken)) {
273                log.debug("User not authenticated!");
274                throw SecurityServiceException.newNotLoggedInException("User not logged in");
275            }
276            if (!userCanAccessService(context, authentication)) { 
277                log.debug("Access denied for user %s", authentication != null ? authentication.getName() : "not authenticated");
278                throw SecurityServiceException.newAccessDeniedException("User not in required role");
279            }
280        }
281
282        try {
283                Object returnedObject = securityInterceptor != null 
284                        ? securityInterceptor.invoke(context)
285                        : endAuthorization(context);
286            
287            return returnedObject;
288        }
289        catch (AccessDeniedException e) {
290                throw SecurityServiceException.newAccessDeniedException(e.getMessage());
291        }
292        catch (InvocationTargetException e) {
293            handleAuthorizationExceptions(e);
294            throw e;
295        }
296        finally {
297            if (graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null) {
298                if ((Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) == 0) {
299                            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
300                        log.debug("Clear authentication and save to repo: %s", contextAfterChainExecution.getAuthentication() != null ? contextAfterChainExecution.getAuthentication().getName() : "none");
301                            SecurityContextHolder.clearContext();
302                            try {
303                                securityContextRepository.saveContext(contextAfterChainExecution, (HttpServletRequest)getRequest.invoke(holder), (HttpServletResponse)getResponse.invoke(holder));
304                            }
305                            catch (Exception e) {
306                                log.error(e, "Could not extract wrapped context from holder");
307                            }
308                            graniteContext.getRequest().removeAttribute(SECURITY_SERVICE_APPLIED);
309                }
310                else if (graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) == null) {
311                        log.debug("Clear authentication");
312                            SecurityContextHolder.clearContext();
313                }
314                else {
315                        log.debug("Decrement service reentrance counter");
316                        graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, (Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED)-1);
317                }
318            }
319        }
320    }
321    
322    @Override
323    public boolean acceptsContext() {
324        return GraniteContext.getCurrentInstance() instanceof ServletGraniteContext;
325    }
326
327    public void logout() {
328        ServletGraniteContext context = (ServletGraniteContext)GraniteContext.getCurrentInstance();
329        HttpSession session = context.getSession(false);
330        if (session != null && securityContextRepository.containsContext(context.getRequest()))                 
331                session.invalidate();
332        
333        SecurityContextHolder.clearContext();
334    }
335
336    protected boolean isUserInRole(Authentication authentication, String role) {
337        for (GrantedAuthority ga : authentication.getAuthorities()) {
338            if (ga.getAuthority().matches(role))
339                return true;
340        }
341        return false;
342    }
343
344    protected boolean isAuthenticated(Authentication authentication) {
345        return authentication != null && authentication.isAuthenticated();
346    }
347
348    protected boolean userCanAccessService(AbstractSecurityContext context, Authentication authentication) {
349        log.debug("Is authenticated as: %s", authentication.getName());
350
351        for (String role : context.getDestination().getRoles()) {
352            if (isUserInRole(authentication, role)) {
353                log.debug("Allowed access to %s in role %s", authentication.getName(), role);
354                return true;
355            }
356            log.debug("Access denied for %s not in role %s", authentication.getName(), role);
357        }
358        return false;
359    }
360
361    protected void handleAuthorizationExceptions(InvocationTargetException e) {
362        for (Throwable t = e; t != null; t = t.getCause()) {
363            // Don't create a dependency to javax.ejb in SecurityService...
364            if (t instanceof SecurityException ||
365                t instanceof AccessDeniedException ||
366                "javax.ejb.EJBAccessException".equals(t.getClass().getName())) {
367                throw SecurityServiceException.newAccessDeniedException(t.getMessage());
368            }
369            else if (t instanceof AuthenticationException) {
370                throw SecurityServiceException.newNotLoggedInException(t.getMessage());
371            }
372        }
373    }
374
375}