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 */
022package org.granite.spring.security;
023
024import java.lang.reflect.InvocationTargetException;
025import java.lang.reflect.Method;
026import java.util.Arrays;
027import java.util.List;
028import java.util.Map;
029
030import javax.servlet.http.HttpServletRequest;
031import javax.servlet.http.HttpServletResponse;
032import javax.servlet.http.HttpSession;
033
034import org.granite.context.GraniteContext;
035import org.granite.logging.Logger;
036import org.granite.messaging.service.security.AbstractSecurityContext;
037import org.granite.messaging.service.security.AbstractSecurityService;
038import org.granite.messaging.service.security.SecurityServiceException;
039import org.granite.messaging.webapp.HttpGraniteContext;
040import org.granite.messaging.webapp.ServletGraniteContext;
041import org.springframework.beans.factory.BeanFactoryUtils;
042import org.springframework.context.ApplicationContext;
043import org.springframework.context.ApplicationContextAware;
044import org.springframework.context.ApplicationEventPublisher;
045import org.springframework.context.ApplicationEventPublisherAware;
046import org.springframework.security.access.AccessDeniedException;
047import org.springframework.security.authentication.AnonymousAuthenticationToken;
048import org.springframework.security.authentication.AuthenticationManager;
049import org.springframework.security.authentication.AuthenticationTrustResolver;
050import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
051import org.springframework.security.authentication.BadCredentialsException;
052import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
053import org.springframework.security.authentication.encoding.PasswordEncoder;
054import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
055import org.springframework.security.core.Authentication;
056import org.springframework.security.core.AuthenticationException;
057import org.springframework.security.core.GrantedAuthority;
058import org.springframework.security.core.context.SecurityContext;
059import org.springframework.security.core.context.SecurityContextHolder;
060import org.springframework.security.core.userdetails.UsernameNotFoundException;
061import org.springframework.security.web.authentication.session.SessionAuthenticationException;
062import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
063import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
064import org.springframework.security.web.context.HttpRequestResponseHolder;
065import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
066import org.springframework.security.web.context.SecurityContextRepository;
067import org.springframework.web.context.support.WebApplicationContextUtils;
068
069
070/**
071 * @author Bouiaw
072 * @author wdrai
073 */
074@SuppressWarnings("deprecation")
075public 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 AuthenticationManager authenticationManager = null;
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 String authenticationManagerBeanName = null;
091        private boolean allowAnonymousAccess = false;
092        private Method getRequest = null;
093        private Method getResponse = null;
094    
095        
096        public SpringSecurity3Service() {
097                log.debug("Starting Spring 3 Security Service");
098                try {
099                getRequest = HttpRequestResponseHolder.class.getDeclaredMethod("getRequest");
100                getRequest.setAccessible(true);
101                getResponse = HttpRequestResponseHolder.class.getDeclaredMethod("getResponse");
102                getResponse.setAccessible(true);
103                }
104                catch (Exception e) {
105                        throw new RuntimeException("Could not get methods from HttpRequestResponseHolder", e);
106                }
107    }
108        
109        public void setApplicationContext(ApplicationContext applicationContext) {
110                this.applicationContext = applicationContext;
111        }
112        
113        public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
114                this.eventPublisher = eventPublisher;
115        }
116        
117        public void setAuthenticationManager(AuthenticationManager authenticationManager) {
118                this.authenticationManager = authenticationManager;
119        }
120        
121        public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
122                this.authenticationTrustResolver = authenticationTrustResolver;
123        }
124        
125        public void setAllowAnonymousAccess(boolean allowAnonymousAccess) {
126                this.allowAnonymousAccess = allowAnonymousAccess;
127        }
128        
129        public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
130                this.securityContextRepository = securityContextRepository;
131        }
132        
133        public void setSecurityInterceptor(AbstractSpringSecurity3Interceptor securityInterceptor) {
134                this.securityInterceptor = securityInterceptor;
135        }
136        
137        public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
138                if (sessionAuthenticationStrategy == null)
139                        throw new NullPointerException("SessionAuthenticationStrategy cannot be null");
140                this.sessionAuthenticationStrategy = sessionAuthenticationStrategy;
141        }
142        
143        public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
144                this.passwordEncoder = passwordEncoder;
145        }
146
147    public void configure(Map<String, String> params) {
148        log.debug("Configuring with parameters %s: ", params);
149        if (params.containsKey("authentication-manager-bean-name"))
150                authenticationManagerBeanName = params.get("authentication-manager-bean-name");
151        if (Boolean.TRUE.toString().equals(params.get("allow-anonymous-access")))
152                allowAnonymousAccess = true;
153    }
154    
155    public void login(Object credentials, String charset) {
156        List<String> decodedCredentials = Arrays.asList(decodeBase64Credentials(credentials, charset));
157        
158        if (!(GraniteContext.getCurrentInstance() instanceof HttpGraniteContext)) {
159                log.info("Login from non HTTP granite context ignored");
160                return;
161        }
162        
163        HttpGraniteContext graniteContext = (HttpGraniteContext)GraniteContext.getCurrentInstance();
164        HttpServletRequest httpRequest = graniteContext.getRequest();
165
166        String user = decodedCredentials.get(0);
167        String password = decodedCredentials.get(1);
168        if (passwordEncoder != null)
169                password = passwordEncoder.encodePassword(password, null);
170        Authentication auth = new UsernamePasswordAuthenticationToken(user, password);
171        
172        ApplicationContext appContext = applicationContext != null ? applicationContext 
173                        : WebApplicationContextUtils.getWebApplicationContext(graniteContext.getServletContext());
174        
175        if (appContext != null) {
176                lookupAuthenticationManager(appContext, authenticationManagerBeanName);
177            
178            try {
179                Authentication authentication = authenticationManager.authenticate(auth);
180                
181                if (authentication != null && !authenticationTrustResolver.isAnonymous(authentication)) {
182                        try {
183                                sessionAuthenticationStrategy.onAuthentication(authentication, httpRequest, graniteContext.getResponse());
184                    } 
185                        catch (SessionAuthenticationException e) {
186                        log.debug(e, "SessionAuthenticationStrategy rejected the authentication object");
187                        SecurityContextHolder.clearContext();
188                        handleAuthenticationExceptions(e);
189                        return;
190                    }                
191                }
192                
193                        log.debug("Define authentication and save to repo: %s", authentication != null ? authentication.getName() : "none");
194                HttpRequestResponseHolder holder = new HttpRequestResponseHolder(graniteContext.getRequest(), graniteContext.getResponse());
195                SecurityContext securityContext = securityContextRepository.loadContext(holder);
196                securityContext.setAuthentication(authentication);
197                SecurityContextHolder.setContext(securityContext);
198                    try {
199                        securityContextRepository.saveContext(securityContext, (HttpServletRequest)getRequest.invoke(holder), (HttpServletResponse)getResponse.invoke(holder));
200                    }
201                    catch (Exception e) {
202                        log.error(e, "Could not save context after authentication");
203                    }
204                    
205                    if (eventPublisher != null)
206                        eventPublisher.publishEvent(new AuthenticationSuccessEvent(authentication));
207                    
208                    endLogin(credentials, charset);
209            } 
210            catch (AuthenticationException e) {
211                handleAuthenticationExceptions(e);
212            }
213            finally {
214                        log.debug("Clear authentication");
215                    SecurityContextHolder.clearContext();
216            }
217        }
218
219        log.debug("User %s logged in", user);
220    }
221    
222    protected void handleAuthenticationExceptions(AuthenticationException e) {
223        if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException)
224            throw SecurityServiceException.newInvalidCredentialsException(e.getMessage());
225        
226        throw SecurityServiceException.newAuthenticationFailedException(e.getMessage());
227    }
228    
229    public void lookupAuthenticationManager(ApplicationContext ctx, String authenticationManagerBeanName) throws SecurityServiceException {
230        if (this.authenticationManager != null)
231                return;
232        
233        Map<String, AuthenticationManager> authManagers = BeanFactoryUtils.beansOfTypeIncludingAncestors(ctx, AuthenticationManager.class);
234        
235        if (authenticationManagerBeanName != null) {
236                this.authenticationManager = authManagers.get(authenticationManagerBeanName);
237                if (authenticationManager == null) {
238                        log.error("AuthenticationManager bean not found " + authenticationManagerBeanName);
239                        throw SecurityServiceException.newAuthenticationFailedException("Authentication failed");
240                }
241                return;
242        }
243        else if (authManagers.size() > 1) {
244                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>");
245                throw SecurityServiceException.newAuthenticationFailedException("Authentication failed");
246        }
247        
248        this.authenticationManager = authManagers.values().iterator().next();
249    }
250
251    
252    public Object authorize(AbstractSecurityContext context) throws Exception {
253        log.debug("Authorize %s on destination %s (secured: %b)", context, context.getDestination().getId(), context.getDestination().isSecured());
254
255        startAuthorization(context);
256        
257        ServletGraniteContext graniteContext = (ServletGraniteContext)GraniteContext.getCurrentInstance();
258        
259        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
260        HttpRequestResponseHolder holder = null;
261        
262        if (graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null) {
263                if (graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) == null) {
264                        holder = new HttpRequestResponseHolder(graniteContext.getRequest(), graniteContext.getResponse());
265                        SecurityContext contextBeforeChainExecution = securityContextRepository.loadContext(holder);
266                            SecurityContextHolder.setContext(contextBeforeChainExecution);
267                            if (isAuthenticated(authentication)) {
268                                log.debug("Restore authentication: %s", authentication.getName());
269                                contextBeforeChainExecution.setAuthentication(authentication);
270                            }
271                            else {
272                                authentication = contextBeforeChainExecution.getAuthentication();
273                                log.debug("Restore authentication from repository: %s", authentication != null ? authentication.getName() : "none");
274                            }
275                            
276                            graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, 0);
277                }
278                else {
279                        log.debug("Increment service reentrance counter");
280                            graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, (Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED)+1);
281                }
282        }
283        
284        try {
285                if (context.getDestination().isSecured()) {
286                    if (!isAuthenticated(authentication) || (!allowAnonymousAccess && authentication instanceof AnonymousAuthenticationToken)) {
287                        log.debug("User not authenticated!");
288                        throw SecurityServiceException.newNotLoggedInException("User not logged in");
289                    }
290                    if (!userCanAccessService(context, authentication)) { 
291                        log.debug("Access denied for user %s", authentication != null ? authentication.getName() : "not authenticated");
292                        throw SecurityServiceException.newAccessDeniedException("User not in required role");
293                    }
294                }
295                
296                Object returnedObject = securityInterceptor != null 
297                        ? securityInterceptor.invoke(context)
298                        : endAuthorization(context);
299            
300            return returnedObject;
301        }
302        catch (SecurityServiceException e) {
303                throw e;
304        }
305        catch (AccessDeniedException e) {
306                throw SecurityServiceException.newAccessDeniedException(e.getMessage());
307        }
308        catch (InvocationTargetException e) {
309            handleAuthorizationExceptions(e);
310            throw e;
311        }
312        finally {
313            if (graniteContext.getRequest().getAttribute(FILTER_APPLIED) == null) {
314                if ((Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) == 0) {
315                            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
316                        log.debug("Clear authentication and save to repo: %s", contextAfterChainExecution.getAuthentication() != null ? contextAfterChainExecution.getAuthentication().getName() : "none");
317                            SecurityContextHolder.clearContext();
318                            try {
319                                securityContextRepository.saveContext(contextAfterChainExecution, (HttpServletRequest)getRequest.invoke(holder), (HttpServletResponse)getResponse.invoke(holder));
320                            }
321                            catch (Exception e) {
322                                log.error(e, "Could not extract wrapped context from holder");
323                            }
324                            graniteContext.getRequest().removeAttribute(SECURITY_SERVICE_APPLIED);
325                }
326                else if (graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED) == null) {
327                        log.debug("Clear authentication");
328                            SecurityContextHolder.clearContext();
329                }
330                else {
331                        log.debug("Decrement service reentrance counter");
332                        graniteContext.getRequest().setAttribute(SECURITY_SERVICE_APPLIED, (Integer)graniteContext.getRequest().getAttribute(SECURITY_SERVICE_APPLIED)-1);
333                }
334            }
335        }
336    }
337    
338    @Override
339    public boolean acceptsContext() {
340        return GraniteContext.getCurrentInstance() instanceof ServletGraniteContext;
341    }
342
343    public void logout() {
344        ServletGraniteContext context = (ServletGraniteContext)GraniteContext.getCurrentInstance();
345        HttpSession session = context.getSession(false);
346        if (session != null && securityContextRepository.containsContext(context.getRequest()))                 
347                session.invalidate();
348        
349        SecurityContextHolder.clearContext();
350    }
351
352    protected boolean isUserInRole(Authentication authentication, String role) {
353        for (GrantedAuthority ga : authentication.getAuthorities()) {
354            if (ga.getAuthority().matches(role))
355                return true;
356        }
357        return false;
358    }
359
360    protected boolean isAuthenticated(Authentication authentication) {
361        return authentication != null && authentication.isAuthenticated();
362    }
363
364    protected boolean userCanAccessService(AbstractSecurityContext context, Authentication authentication) {
365        log.debug("Is authenticated as: %s", authentication.getName());
366
367        for (String role : context.getDestination().getRoles()) {
368            if (isUserInRole(authentication, role)) {
369                log.debug("Allowed access to %s in role %s", authentication.getName(), role);
370                return true;
371            }
372            log.debug("Access denied for %s not in role %s", authentication.getName(), role);
373        }
374        return false;
375    }
376
377    protected void handleAuthorizationExceptions(InvocationTargetException e) {
378        for (Throwable t = e; t != null; t = t.getCause()) {
379            // Don't create a dependency to javax.ejb in SecurityService...
380            if (t instanceof SecurityException ||
381                t instanceof AccessDeniedException ||
382                "javax.ejb.EJBAccessException".equals(t.getClass().getName())) {
383                throw SecurityServiceException.newAccessDeniedException(t.getMessage());
384            }
385            else if (t instanceof AuthenticationException) {
386                throw SecurityServiceException.newNotLoggedInException(t.getMessage());
387            }
388        }
389    }
390
391}