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