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