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.seam.security;
022
023import java.lang.reflect.Constructor;
024import java.lang.reflect.InvocationTargetException;
025import java.lang.reflect.Method;
026import java.util.ArrayList;
027import java.util.List;
028import java.util.Map;
029
030import javax.faces.application.FacesMessage;
031import javax.security.auth.login.LoginException;
032
033import org.granite.logging.Logger;
034import org.granite.messaging.service.security.AbstractSecurityContext;
035import org.granite.messaging.service.security.AbstractSecurityService;
036import org.granite.messaging.service.security.SecurityServiceException;
037import org.jboss.seam.contexts.Contexts;
038import org.jboss.seam.faces.FacesMessages;
039import org.jboss.seam.security.AuthorizationException;
040import org.jboss.seam.security.Identity;
041import org.jboss.seam.security.NotLoggedInException;
042
043/**
044 * @author Venkat DANDA
045 */
046public class SeamSecurityService extends AbstractSecurityService {
047
048        private static final Logger log = Logger.getLogger(SeamSecurityService.class);
049        
050    public void configure(Map<String, String> params) {
051    }
052
053    public void login(Object credentials, String charset) throws SecurityServiceException {
054        String[] decoded = decodeBase64Credentials(credentials, charset);
055        
056        Contexts.getSessionContext().set("org.granite.seam.login", Boolean.TRUE);
057        
058        Identity identity = Identity.instance();
059        
060        // Unauthenticate if username has changed (otherwise the previous session/principal is reused)
061        if (identity.isLoggedIn(false) && !decoded[0].equals(identity.getUsername())) {
062                try {
063                        Method method = identity.getClass().getDeclaredMethod("unAuthenticate");
064                        method.setAccessible(true);
065                        method.invoke(identity);
066                } catch (Exception e) {
067                        log.error(e, "Could not call unAuthenticate method on: %s", identity.getClass());
068                }
069        }
070
071        identity.setUsername(decoded[0]);
072        identity.setPassword(decoded[1]);
073        try {
074            identity.authenticate();
075
076            endLogin(credentials, charset);
077        }
078        catch (LoginException e) {
079            identity.login();   // Force add of login error messages
080            
081            handleAuthenticationExceptions(e);
082        }
083    }
084    
085    protected void handleAuthenticationExceptions(LoginException e) {
086        throw SecurityServiceException.newInvalidCredentialsException("User authentication failed", e.getMessage());
087    }
088
089    public Object authorize(AbstractSecurityContext context) throws Exception {
090        startAuthorization(context);
091        
092        if (context.getDestination().isSecured()) {
093
094            Identity identity = Identity.instance();
095            if (!identity.isLoggedIn()) {
096                // TODO: Session expiration detection...
097                // throw SecurityServiceException.newSessionExpiredException("Session expired");
098                throw SecurityServiceException.newNotLoggedInException("User not logged in");
099            }
100
101            boolean accessDenied = true;
102            for (String role : context.getDestination().getRoles()) {
103                if (identity.hasRole(role)) {
104                    accessDenied = false;
105                    break;
106                }
107            }
108            if (accessDenied)
109                throw SecurityServiceException.newAccessDeniedException("User not in required role");
110        }
111
112        try {
113            return endAuthorization(context);
114        } catch (InvocationTargetException e) {
115            for (Throwable t = e; t != null; t = t.getCause()) {
116                // If destination is not secured...
117                if (t instanceof NotLoggedInException)
118                    throw SecurityServiceException.newNotLoggedInException("User not logged in");
119                // Don't create a dependency to javax.ejb in SecurityService...
120                if (t instanceof SecurityException ||
121                    t instanceof AuthorizationException ||
122                    "javax.ejb.EJBAccessException".equals(t.getClass().getName()))
123                    throw SecurityServiceException.newAccessDeniedException(t.getMessage());
124            }
125            throw e;
126        }
127    }
128
129    public void logout() throws SecurityServiceException {
130        // if (Identity.instance().isLoggedIn(false)) ?
131        Identity.instance().logout();
132    }
133    
134    
135    @Override
136    public void handleSecurityException(SecurityServiceException e) {
137        // Add messages
138        //Prepare for the messages. First step is convert the tasks to Seam FacesMessages
139        FacesMessages.afterPhase();
140        //Second step is add the Seam FacesMessages to JSF FacesContext Messages
141        FacesMessages.instance().beforeRenderResponse();
142        
143        List<FacesMessage> facesMessages = FacesMessages.instance().getCurrentMessages();
144        
145        try {
146            Class<?> c = Thread.currentThread().getContextClassLoader().loadClass("org.granite.tide.TideMessage");
147            Constructor<?> co = c.getConstructor(String.class, String.class, String.class);
148            
149            List<Object> tideMessages = new ArrayList<Object>(facesMessages.size());
150            for (FacesMessage fm : facesMessages) {
151                String severity = null;
152                if (fm.getSeverity() == FacesMessage.SEVERITY_INFO)
153                    severity = "INFO";
154                else if (fm.getSeverity() == FacesMessage.SEVERITY_WARN)
155                    severity = "WARNING";
156                else if (fm.getSeverity() == FacesMessage.SEVERITY_ERROR)
157                    severity = "ERROR";
158                else if (fm.getSeverity() == FacesMessage.SEVERITY_FATAL)
159                    severity = "FATAL";
160                
161                tideMessages.add(co.newInstance(severity, fm.getSummary(), fm.getDetail()));
162            }
163            
164            e.getExtendedData().put("messages", tideMessages);
165        }
166        catch (Throwable t) {
167            e.getExtendedData().put("messages", facesMessages);
168        }
169    }
170}