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