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 }