001    /**
002     *  Licensed to the Apache Software Foundation (ASF) under one or more
003     *  contributor license agreements.  See the NOTICE file distributed with
004     *  this work for additional information regarding copyright ownership.
005     *  The ASF licenses this file to You under the Apache License, Version 2.0
006     *  (the "License"); you may not use this file except in compliance with
007     *  the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package org.apache.geronimo.security.realm.providers;
019    
020    import org.apache.commons.logging.Log;
021    import org.apache.commons.logging.LogFactory;
022    import org.apache.geronimo.common.GeronimoSecurityException;
023    import org.apache.geronimo.security.jaas.JaasLoginModuleUse;
024    import org.apache.geronimo.system.serverinfo.ServerInfo;
025    import org.apache.geronimo.util.encoders.HexTranslator;
026    
027    import javax.security.auth.Subject;
028    import javax.security.auth.callback.Callback;
029    import javax.security.auth.callback.CallbackHandler;
030    import javax.security.auth.callback.NameCallback;
031    import javax.security.auth.callback.PasswordCallback;
032    import javax.security.auth.callback.UnsupportedCallbackException;
033    import javax.security.auth.login.FailedLoginException;
034    import javax.security.auth.login.LoginException;
035    import javax.security.auth.spi.LoginModule;
036    import java.io.IOException;
037    import java.io.InputStream;
038    import java.net.URI;
039    import java.security.MessageDigest;
040    import java.security.NoSuchAlgorithmException;
041    import java.util.Enumeration;
042    import java.util.HashMap;
043    import java.util.HashSet;
044    import java.util.Iterator;
045    import java.util.Map;
046    import java.util.Properties;
047    import java.util.Set;
048    
049    
050    /**
051     * A LoginModule that reads a list of users and group from files on disk.  The
052     * files should be formatted using standard Java properties syntax.  Expects
053     * to be run by a GenericSecurityRealm (doesn't work on its own).
054     *
055     * @version $Rev: 487175 $ $Date: 2006-12-14 03:10:31 -0800 (Thu, 14 Dec 2006) $
056     */
057    public class PropertiesFileLoginModule implements LoginModule {
058        public final static String USERS_URI = "usersURI";
059        public final static String GROUPS_URI = "groupsURI";
060        public final static String DIGEST = "digest";
061        private static Log log = LogFactory.getLog(PropertiesFileLoginModule.class);
062        final Properties users = new Properties();
063        final Map groups = new HashMap();
064        private String digest;
065    
066        Subject subject;
067        CallbackHandler handler;
068        String username;
069        String password;
070    
071        public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
072            this.subject = subject;
073            this.handler = callbackHandler;
074            try {
075                ServerInfo serverInfo = (ServerInfo) options.get(JaasLoginModuleUse.SERVERINFO_LM_OPTION);
076                final String users = (String)options.get(USERS_URI);
077                final String groups = (String)options.get(GROUPS_URI);
078                digest = (String) options.get(DIGEST);
079                if(digest != null && !digest.equals("")) {
080                    // Check if the digest algorithm is available
081                    try {
082                        MessageDigest.getInstance(digest);
083                    } catch(NoSuchAlgorithmException e) {
084                        log.error("Initialization failed. Digest algorithm "+digest+" is not available.", e);
085                        throw new IllegalArgumentException("Unable to configure properties file login module: "+e.getMessage());
086                    }
087                }
088                if(users == null || groups == null) {
089                    throw new IllegalArgumentException("Both "+USERS_URI+" and "+GROUPS_URI+" must be provided!");
090                }
091                URI usersURI = new URI(users);
092                URI groupsURI = new URI(groups);
093                loadProperties(serverInfo, usersURI, groupsURI);
094            } catch (Exception e) {
095                log.error("Initialization failed", e);
096                throw new IllegalArgumentException("Unable to configure properties file login module: "+e.getMessage());
097            }
098        }
099    
100        public void loadProperties(ServerInfo serverInfo, URI userURI, URI groupURI) throws GeronimoSecurityException {
101            try {
102                URI userFile = serverInfo.resolveServer(userURI);
103                URI groupFile = serverInfo.resolveServer(groupURI);
104                InputStream stream = userFile.toURL().openStream();
105                users.load(stream);
106                stream.close();
107    
108                Properties temp = new Properties();
109                stream = groupFile.toURL().openStream();
110                temp.load(stream);
111                stream.close();
112    
113                Enumeration e = temp.keys();
114                while (e.hasMoreElements()) {
115                    String groupName = (String) e.nextElement();
116                    String[] userList = ((String) temp.get(groupName)).split(",");
117    
118                    Set userset = (Set) groups.get(groupName);
119                    if (userset == null) {
120                        userset = new HashSet();
121                        groups.put(groupName, userset);
122                    }
123    
124                    for (int i = 0; i < userList.length; i++) {
125                        userset.add(userList[i]);
126                    }
127                }
128    
129            } catch (Exception e) {
130                log.error("Properties File Login Module - data load failed", e);
131                throw new GeronimoSecurityException(e);
132            }
133        }
134    
135    
136        public boolean login() throws LoginException {
137            Callback[] callbacks = new Callback[2];
138    
139            callbacks[0] = new NameCallback("User name");
140            callbacks[1] = new PasswordCallback("Password", false);
141            try {
142                handler.handle(callbacks);
143            } catch (IOException ioe) {
144                throw (LoginException) new LoginException().initCause(ioe);
145            } catch (UnsupportedCallbackException uce) {
146                throw (LoginException) new LoginException().initCause(uce);
147            }
148            assert callbacks.length == 2;
149            username = ((NameCallback) callbacks[0]).getName();
150            if(username == null || username.equals("")) {
151                return false;
152            }
153            String realPassword = users.getProperty(username);
154            char[] entered = ((PasswordCallback) callbacks[1]).getPassword();
155            password = entered == null ? null : new String(entered);
156            boolean result = (realPassword == null && password == null) ||
157                    (realPassword != null && password != null && checkPassword(realPassword, password));
158            if(!result) {
159                throw new FailedLoginException();
160            }
161            return true;
162        }
163    
164        public boolean commit() throws LoginException {
165            Set principals = subject.getPrincipals();
166    
167            principals.add(new GeronimoUserPrincipal(username));
168    
169            Iterator e = groups.keySet().iterator();
170            while (e.hasNext()) {
171                String groupName = (String) e.next();
172                Set users = (Set) groups.get(groupName);
173                Iterator iter = users.iterator();
174                while (iter.hasNext()) {
175                    String user = (String) iter.next();
176                    if (username.equals(user)) {
177                        principals.add(new GeronimoGroupPrincipal(groupName));
178                        break;
179                    }
180                }
181            }
182    
183            return true;
184        }
185    
186        public boolean abort() throws LoginException {
187            username = null;
188            password = null;
189    
190            return true;
191        }
192    
193        public boolean logout() throws LoginException {
194            username = null;
195            password = null;
196            //todo: should remove principals added by commit
197            return true;
198        }
199    
200        /**
201         * Gets the names of all principal classes that may be populated into
202         * a Subject.
203         */
204        public String[] getPrincipalClassNames() {
205            return new String[]{GeronimoUserPrincipal.class.getName(), GeronimoGroupPrincipal.class.getName()};
206        }
207    
208        /**
209         * Gets a list of all the principals of a particular type (identified by
210         * the principal class).  These are available for manual role mapping.
211         */
212        public String[] getPrincipalsOfClass(String className) {
213            Set s;
214            if(className.equals(GeronimoGroupPrincipal.class.getName())) {
215                s = groups.keySet();
216            } else if(className.equals(GeronimoUserPrincipal.class.getName())) {
217                s = users.keySet();
218            } else {
219                throw new IllegalArgumentException("No such principal class "+className);
220            }
221            return (String[]) s.toArray(new String[s.size()]);
222        }
223    
224        /**
225         * This method checks if the provided password is correct.  The original password may have been digested.
226         * @param real      Original password in digested form if applicable
227         * @param provided  User provided password in clear text
228         * @return true     If the password is correct
229         */
230        private boolean checkPassword(String real, String provided){
231            if(digest == null || digest.equals("")) {
232                // No digest algorithm is used
233                return real.equals(provided);
234            }
235            try {
236                // Digest the user provided password
237                MessageDigest md = MessageDigest.getInstance(digest);
238                byte[] data = md.digest(provided.getBytes());
239                // Convert bytes to hex digits
240                byte[] hexData = new byte[data.length * 2];
241                HexTranslator ht = new HexTranslator();
242                ht.encode(data, 0, data.length, hexData, 0);
243                // Compare the digested provided password with the actual one
244                return real.equalsIgnoreCase(new String(hexData));
245            } catch (NoSuchAlgorithmException e) {
246                // Should not occur.  Availability of algorithm has been checked at initialization
247                log.error("Should not occur.  Availability of algorithm has been checked at initialization.", e);
248            }
249            return false;
250        }
251    }