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.messaging.service.security;
022    
023    import java.io.UnsupportedEncodingException;
024    
025    import org.granite.clustering.GraniteDistributedData;
026    import org.granite.clustering.GraniteDistributedDataFactory;
027    import org.granite.logging.Logger;
028    import org.granite.messaging.amf.process.AMF3MessageProcessor;
029    import org.granite.util.Base64;
030    
031    import flex.messaging.messages.Message;
032    
033    /**
034     * Abstract implementation of the {@link SecurityService} interface. This class mainly contains
035     * utility methods helping with actual implementations.
036     * 
037     * @author Franck WOLFF
038     */
039    public abstract class AbstractSecurityService implements SecurityService {
040    
041        private static final Logger log = Logger.getLogger(AbstractSecurityService.class);
042    
043        public static final String AUTH_TYPE = "granite-security";
044    
045        /**
046         * A default implementation of the basic login method, passing null as the extra charset
047         * parameter. Mainly here for compatibility purpose.
048         * 
049         * @param credentials the login:password pair (must be a base64/ISO-8859-1 encoded string).
050         */
051        public void login(Object credentials) throws SecurityServiceException {
052            login(credentials, null);
053            }
054    
055            /**
056         * Try to login by using remote credentials (see Flex method RemoteObject.setRemoteCredentials()).
057         * This method must be called at the beginning of {@link SecurityService#authorize(AbstractSecurityContext)}.
058         * 
059         * @param context the current security context.
060         * @throws SecurityServiceException if login fails.
061         */
062        protected void startAuthorization(AbstractSecurityContext context) throws SecurityServiceException {
063            // Get credentials set with RemoteObject.setRemoteCredentials() and login.
064            Object credentials = context.getMessage().getHeader(Message.REMOTE_CREDENTIALS_HEADER);
065            if (credentials != null && !("".equals(credentials)))
066                login(credentials, (String)context.getMessage().getHeader(Message.REMOTE_CREDENTIALS_CHARSET_HEADER));
067        }
068    
069        /**
070         * Invoke a service method (EJB3, Spring, Seam, etc...) after a successful authorization.
071         * This method must be called at the end of {@link SecurityService#authorize(AbstractSecurityContext)}.
072         * 
073         * @param context the current security context.
074         * @throws Exception if anything goes wrong with service invocation.
075         */
076        protected Object endAuthorization(AbstractSecurityContext context) throws Exception {
077            return context.invoke();
078        }
079    
080        /**
081         * Decode credentails encoded in base 64 (in the form of "username:password"), as they have been
082         * sent by a RemoteObject.
083         * 
084         * @param credentials base 64 encoded credentials.
085         * @return an array containing two decoded Strings, username and password.
086         * @throws IllegalArgumentException if credentials isn't a String.
087         * @throws SecurityServiceException if credentials are invalid (bad encoding or missing ':').
088         */
089        protected String[] decodeBase64Credentials(Object credentials, String charset) {
090            if (!(credentials instanceof String))
091                throw new IllegalArgumentException("Credentials should be a non null String: " +
092                    (credentials != null ? credentials.getClass().getName() : null));
093    
094            if (charset == null)
095                    charset = "ISO-8859-1";
096            
097            byte[] bytes = Base64.decode((String)credentials);
098            String decoded;
099            try {
100                    decoded = new String(bytes, charset);
101            }
102            catch (UnsupportedEncodingException e) {
103                throw SecurityServiceException.newInvalidCredentialsException("ISO-8859-1 encoding not supported ???");
104            }
105    
106            int colon = decoded.indexOf(':');
107            if (colon == -1)
108                throw SecurityServiceException.newInvalidCredentialsException("No colon");
109    
110            return new String[] {decoded.substring(0, colon), decoded.substring(colon + 1)};
111        }
112        
113        /**
114         * Handle a security exception. This method is called in
115         * {@link AMF3MessageProcessor#processCommandMessage(flex.messaging.messages.CommandMessage)}
116         * whenever a SecurityService occurs and does nothing by default.
117         * 
118         * @param e the security exception.
119         */
120            public void handleSecurityException(SecurityServiceException e) {
121        }
122    
123        /**
124         * Try to save current credentials in distributed data, typically a user session attribute. This method
125         * must be called at the end of a successful {@link SecurityService#login(Object)} operation and is useful
126         * in clustered environments with session replication in order to transparently re-authenticate the
127         * user when failing over.
128         * 
129         * @param credentials the credentials to be saved in distributed data.
130         */
131            protected void endLogin(Object credentials, String charset) {
132                    try {
133                            GraniteDistributedData gdd = GraniteDistributedDataFactory.getInstance();
134                            if (gdd != null) {
135                                    gdd.setCredentials(credentials);
136                                    gdd.setCredentialsCharset(charset);
137                            }
138                    }
139                    catch (Exception e) {
140                            log.error(e, "Could not save credentials in distributed data");
141                    }
142        }
143        
144            /**
145             * Try to re-authenticate the current user with credentials previously saved in distributed data.
146             * This method must be called in the {@link SecurityService#authorize(AbstractSecurityContext)}
147             * method when the current user principal is null.
148             * 
149             * @return <tt>true</tt> if relogin was successful, <tt>false</tt> otherwise.
150             * 
151             * @see #endLogin(Object, String)
152             */
153        protected boolean tryRelogin() {
154            try {
155                            GraniteDistributedData gdd = GraniteDistributedDataFactory.getInstance();
156                            if (gdd != null) {
157                                    Object credentials = gdd.getCredentials();
158                            if (credentials != null) {
159                                    String charset = gdd.getCredentialsCharset();
160                                    try {
161                                            login(credentials, charset);
162                                            return true;
163                                    }
164                                    catch (SecurityServiceException e) {
165                                    }
166                            }
167                            }
168            }
169            catch (Exception e) {
170                    log.error(e, "Could not relogin with credentials found in distributed data");
171            }
172            return false;
173        }
174    
175        /**
176         * Try to remove credentials previously saved in distributed data. This method must be called in the
177         * {@link SecurityService#logout()} method.
178         * 
179             * @see #endLogin(Object, String)
180         */
181            protected void endLogout() {
182                    try {
183                            GraniteDistributedData gdd = GraniteDistributedDataFactory.getInstance();
184                            if (gdd != null) {
185                                    gdd.removeCredentials();
186                                    gdd.removeCredentialsCharset();
187                            }
188                    }
189                    catch (Exception e) {
190                            log.error(e, "Could not remove credentials from distributed data");
191                    }
192        }
193    }