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.seam21.security;
023
024import java.lang.reflect.Constructor;
025import java.lang.reflect.InvocationTargetException;
026import java.lang.reflect.Method;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.List;
030import java.util.Map;
031
032import javax.security.auth.login.LoginException;
033
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.jboss.seam.contexts.Contexts;
039import org.jboss.seam.core.Events;
040import org.jboss.seam.international.StatusMessage;
041import org.jboss.seam.international.StatusMessages;
042import org.jboss.seam.security.AuthorizationException;
043import org.jboss.seam.security.Identity;
044import org.jboss.seam.security.NotLoggedInException;
045
046/**
047 * @author Venkat DANDA
048 */
049public class Seam21SecurityService extends AbstractSecurityService {
050
051        private static final Logger log = Logger.getLogger(Seam21SecurityService.class);
052        
053    public void configure(Map<String, String> params) {
054    }
055
056    @SuppressWarnings("deprecation")
057        public void login(Object credentials, String charset) throws SecurityServiceException {
058        String[] decoded = decodeBase64Credentials(credentials, charset);
059        
060        Contexts.getSessionContext().set("org.granite.seam.login", Boolean.TRUE);
061        
062        Identity identity = Identity.instance();
063        
064        // Unauthenticate if username has changed (otherwise the previous session/principal is reused)
065        if (identity.isLoggedIn(false) && !decoded[0].equals(identity.getUsername())) {
066                try {
067                        Method method = identity.getClass().getDeclaredMethod("unAuthenticate");
068                        method.setAccessible(true);
069                        method.invoke(identity);
070                } catch (Exception e) {
071                        log.error(e, "Could not call unAuthenticate method on: %s", identity.getClass());
072                }
073        }
074
075        identity.setUsername(decoded[0]);
076        identity.setPassword(decoded[1]);
077        try {
078            identity.authenticate();
079
080            endLogin(credentials, charset);
081        }
082        catch (LoginException e) {
083                // Force add of login error messages
084                try {
085                        Method method = identity.getClass().getMethod("getCredentials");
086                        Object cred = method.invoke(identity);
087                        method = cred.getClass().getMethod("invalidate");
088                        method.invoke(cred);
089                } catch (Exception f) {
090                        log.error(f, "Could not call getCredentials().invalidate() method on: %s", identity.getClass());
091                }
092            
093            if (Events.exists()) 
094                Events.instance().raiseEvent("org.jboss.seam.security.loginFailed", e);
095            
096            throw SecurityServiceException.newInvalidCredentialsException("User authentication failed");
097        }
098    }
099
100    public Object authorize(AbstractSecurityContext context) throws Exception {
101        startAuthorization(context);
102        
103        if (context.getDestination().isSecured()) {
104
105            Identity identity = Identity.instance();
106            if (!identity.isLoggedIn()) {
107                // TODO: Session expiration detection...
108                // throw SecurityServiceException.newSessionExpiredException("Session expired");
109                throw SecurityServiceException.newNotLoggedInException("User not logged in");
110            }
111
112            boolean accessDenied = true;
113            for (String role : context.getDestination().getRoles()) {
114                if (identity.hasRole(role)) {
115                    accessDenied = false;
116                    break;
117                }
118            }
119            if (accessDenied)
120                throw SecurityServiceException.newAccessDeniedException("User not in required role");
121        }
122
123        try {
124            return endAuthorization(context);
125        } 
126        catch (InvocationTargetException e) {
127            for (Throwable t = e; t != null; t = t.getCause()) {
128                // If destination is not secured...
129                if (t instanceof NotLoggedInException)
130                    throw SecurityServiceException.newNotLoggedInException("User not logged in");
131                // Don't create a dependency to javax.ejb in SecurityService...
132                if (t instanceof SecurityException ||
133                    t instanceof AuthorizationException ||
134                    "javax.ejb.EJBAccessException".equals(t.getClass().getName()))
135                    throw SecurityServiceException.newAccessDeniedException(t.getMessage());
136            }
137            throw e;
138        }
139    }
140
141    public void logout() throws SecurityServiceException {
142        // if (Identity.instance().isLoggedIn(false)) ?
143        Identity.instance().logout();
144    }
145    
146    
147    @Override
148    @SuppressWarnings("unchecked")
149    public void handleSecurityException(SecurityServiceException e) {
150        List<StatusMessage> messages = Collections.emptyList();
151        
152        try {
153            StatusMessages statusMessages = StatusMessages.instance();
154            if (statusMessages != null) {
155                // Execute and get the messages (once again reflection hack to use protected methods) 
156                Method m = StatusMessages.class.getDeclaredMethod("doRunTasks");
157                m.setAccessible(true);
158                m.invoke(statusMessages);
159                
160                Method m2 = StatusMessages.class.getDeclaredMethod("getMessages");
161                m2.setAccessible(true);
162                messages = (List<StatusMessage>)m2.invoke(statusMessages);
163            }
164        }
165        catch (Exception se) {
166            log.error("Could not get status messages", se);
167        }
168        
169        List<Object> tideMessages = new ArrayList<Object>(messages.size());
170        
171        try {
172            Class<?> c = Thread.currentThread().getContextClassLoader().loadClass("org.granite.tide.TideMessage");
173            Constructor<?> co = c.getConstructor(String.class, String.class, String.class);
174            
175            for (StatusMessage fm : messages) {
176                String severity = null;
177                if (fm.getSeverity() == StatusMessage.Severity.INFO)
178                    severity = "INFO";
179                else if (fm.getSeverity() == StatusMessage.Severity.WARN)
180                    severity = "WARNING";
181                else if (fm.getSeverity() == StatusMessage.Severity.ERROR)
182                    severity = "ERROR";
183                else if (fm.getSeverity() == StatusMessage.Severity.FATAL)
184                    severity = "FATAL";
185                
186                tideMessages.add(co.newInstance(severity, fm.getSummary(), fm.getDetail()));
187            }
188            
189            e.getExtendedData().put("messages", tideMessages);
190        }
191        catch (Throwable t) {
192            e.getExtendedData().put("messages", tideMessages);
193        }
194    }
195}