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.console.core.security;
019
020 import java.io.File;
021 import java.io.FileOutputStream;
022 import java.io.InputStream;
023 import java.io.IOException;
024 import java.io.OutputStream;
025 import java.net.URL;
026 import java.net.URLConnection;
027 import java.net.UnknownServiceException;
028 import java.security.MessageDigest;
029 import java.security.NoSuchAlgorithmException;
030 import java.util.Arrays;
031 import java.util.Enumeration;
032 import java.util.HashSet;
033 import java.util.Hashtable;
034 import java.util.Properties;
035 import java.util.Set;
036
037 import org.apache.commons.logging.Log;
038 import org.apache.commons.logging.LogFactory;
039 import org.apache.geronimo.common.GeronimoSecurityException;
040 import org.apache.geronimo.gbean.GBeanInfo;
041 import org.apache.geronimo.gbean.GBeanInfoBuilder;
042 import org.apache.geronimo.gbean.GBeanLifecycle;
043 import org.apache.geronimo.j2ee.j2eeobjectnames.NameFactory;
044 import org.apache.geronimo.security.jaas.LoginModuleGBean;
045 import org.apache.geronimo.security.jaas.LoginModuleSettings;
046 import org.apache.geronimo.system.serverinfo.ServerInfo;
047 import org.apache.geronimo.crypto.encoders.Base64;
048 import org.apache.geronimo.crypto.encoders.HexTranslator;
049 import org.apache.geronimo.crypto.SimpleEncryption;
050 import org.apache.geronimo.crypto.EncryptionManager;
051
052 /**
053 * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $
054 */
055 public class PropertiesLoginModuleManager implements GBeanLifecycle {
056 private static Log log = LogFactory.getLog(PropertiesLoginModuleManager.class);
057
058 private ServerInfo serverInfo;
059
060 private LoginModuleSettings loginModule;
061
062 private Properties users = new Properties();
063
064 private Properties groups = new Properties();
065
066 private static final String usersKey = "usersURI";
067
068 private static final String groupsKey = "groupsURI";
069
070 private static final String digestKey = "digest";
071
072 private final static String encodingKey = "encoding";
073
074 public PropertiesLoginModuleManager(ServerInfo serverInfo, LoginModuleSettings loginModule) {
075 this.serverInfo = serverInfo;
076 this.loginModule = loginModule;
077 }
078
079 private void refreshUsers() throws GeronimoSecurityException {
080 users.clear();
081 InputStream in = null;
082 try {
083 in = serverInfo.resolveServer(getUsersURI()).toURL().openStream();
084 users.load(in);
085 } catch (Exception e) {
086 throw new GeronimoSecurityException(e);
087 } finally {
088 if (in != null) {
089 try {
090 in.close();
091 } catch (IOException ignored) {
092 // ignored
093 }
094 }
095 }
096 }
097
098 private void refreshGroups() throws GeronimoSecurityException {
099 groups.clear();
100 InputStream in = null;
101 try {
102 in = serverInfo.resolveServer(getGroupsURI()).toURL().openStream();
103 groups.load(in);
104 } catch (Exception e) {
105 throw new GeronimoSecurityException(e);
106 } finally {
107 if (in != null) {
108 try {
109 in.close();
110 } catch (IOException ignored) {
111 // ignored
112 }
113 }
114 }
115 }
116
117 private void clearAll() {
118 users.clear();
119 groups.clear();
120 }
121
122 public void refreshAll() throws GeronimoSecurityException {
123 refreshGroups();
124 refreshUsers();
125 }
126
127 public String[] getUsers() throws GeronimoSecurityException {
128 refreshUsers();
129 return (String[]) users.keySet().toArray(new String[0]);
130 }
131
132 public String[] getGroups() throws GeronimoSecurityException {
133 refreshGroups();
134 return (String[]) groups.keySet().toArray(new String[0]);
135 }
136
137 public void addUserPrincipal(Hashtable properties)
138 throws GeronimoSecurityException {
139
140 refreshUsers();
141 String name = (String) properties.get("UserName");
142 if (users.getProperty(name) != null) {
143 log.warn("addUserPrincipal() UserName="+name+" already exists.");
144 throw new GeronimoSecurityException("User principal="+name+" already exists.");
145 }
146 try {
147 String realPassword = (String) properties.get("Password");
148 if (realPassword != null) {
149 String digest = getDigest();
150 if(digest != null && !digest.equals("")) {
151 realPassword = digestPassword(realPassword, digest, getEncoding());
152 }
153 realPassword = EncryptionManager.encrypt(realPassword);
154 }
155 users.setProperty(name, realPassword);
156 store(users, serverInfo.resolveServer(getUsersURI()).toURL());
157 } catch (Exception e) {
158 throw new GeronimoSecurityException("Cannot add user principal: "
159 + e.getMessage(), e);
160 }
161 }
162
163 public void removeUserPrincipal(String userPrincipal)
164 throws GeronimoSecurityException {
165 refreshUsers();
166 try {
167 users.remove(userPrincipal);
168 store(users, serverInfo.resolveServer(getUsersURI()).toURL());
169 } catch (Exception e) {
170 throw new GeronimoSecurityException("Cannot remove user principal "
171 + userPrincipal + ": " + e.getMessage(), e);
172 }
173 }
174
175 public void updateUserPrincipal(Hashtable properties)
176 throws GeronimoSecurityException {
177 refreshUsers();
178 String name = (String) properties.get("UserName");
179 if (users.getProperty(name) == null) {
180 log.warn("updateUserPrincipal() UserName="+name+" does not exist.");
181 throw new GeronimoSecurityException("User principal="+name+" does not exist.");
182 }
183 try {
184 String realPassword = (String) properties.get("Password");
185 if (realPassword != null) {
186 String digest = getDigest();
187 if(digest != null && !digest.equals("")) {
188 realPassword = digestPassword(realPassword, digest, getEncoding());
189 }
190 realPassword = EncryptionManager.encrypt(realPassword);
191 }
192 users.setProperty(name, realPassword);
193 store(users, serverInfo.resolveServer(getUsersURI()).toURL());
194 } catch (Exception e) {
195 throw new GeronimoSecurityException("Cannot update user principal: "
196 + e.getMessage(), e);
197 }
198 }
199
200 public void addGroupPrincipal(Hashtable properties)
201 throws GeronimoSecurityException {
202 refreshGroups();
203 String group = (String) properties.get("GroupName");
204 if (groups.getProperty(group) != null) {
205 log.warn("addGroupPrincipal() GroupName="+group+" already exists.");
206 throw new GeronimoSecurityException("Group principal="+group+" already exists.");
207 }
208 try {
209 groups.setProperty(group, (String) properties.get("Members"));
210 store(groups, serverInfo.resolveServer(getGroupsURI()).toURL());
211 } catch (Exception e) {
212 throw new GeronimoSecurityException("Cannot add group principal: "
213 + e.getMessage(), e);
214 }
215 }
216
217 public void removeGroupPrincipal(String groupPrincipal)
218 throws GeronimoSecurityException {
219 refreshGroups();
220 try {
221 groups.remove(groupPrincipal);
222 store(groups, serverInfo.resolveServer(getGroupsURI()).toURL());
223 } catch (Exception e) {
224 throw new GeronimoSecurityException(
225 "Cannot remove group principal: " + e.getMessage(), e);
226 }
227 }
228
229 public void updateGroupPrincipal(Hashtable properties)
230 throws GeronimoSecurityException {
231 //same as add group principal
232 refreshGroups();
233 String group = (String) properties.get("GroupName");
234 if (groups.getProperty(group) == null) {
235 log.warn("updateGroupPrincipal() GroupName="+group+" does not exist.");
236 throw new GeronimoSecurityException("Group principal="+group+" does not exist.");
237 }
238 try {
239 groups.setProperty(group, (String) properties.get("Members"));
240 store(groups, serverInfo.resolveServer(getGroupsURI()).toURL());
241 } catch (Exception e) {
242 throw new GeronimoSecurityException("Cannot update group principal: "
243 + e.getMessage(), e);
244 }
245 }
246
247 public void addToGroup(String userPrincipal, String groupPrincipal)
248 throws GeronimoSecurityException {
249 throw new GeronimoSecurityException(
250 "Not implemented for properties file security realm...");
251 }
252
253 public void removeFromGroup(String userPrincipal, String groupPrincipal)
254 throws GeronimoSecurityException {
255 throw new GeronimoSecurityException(
256 "Not implemented for properties file security realm...");
257 }
258
259 public String getPassword(String userPrincipal)
260 throws GeronimoSecurityException {
261 refreshUsers();
262 if (users.getProperty(userPrincipal) == null) {
263 log.warn("getPassword() User="+userPrincipal+" does not exist.");
264 throw new GeronimoSecurityException("User principal="+userPrincipal+" does not exist.");
265 }
266 String realPassword = users.getProperty(userPrincipal);
267 if (realPassword != null) {
268 realPassword = (String) EncryptionManager.decrypt(realPassword);
269 }
270 return realPassword;
271 }
272
273 public Set getGroupMembers(String groupPrincipal)
274 throws GeronimoSecurityException {
275 Set memberSet = new HashSet();
276 // return nothing when the groupPrincipal is null or empty
277 if (groupPrincipal == null || groupPrincipal.equals("")) {
278 return memberSet;
279 }
280 refreshGroups();
281 if (groups.getProperty(groupPrincipal) == null) {
282 log.warn("getGroupMembers() Group="+groupPrincipal+" does not exist.");
283 return memberSet;
284 }
285 String[] members = ((String)groups.getProperty(groupPrincipal)).split(",");
286
287 memberSet.addAll(Arrays.asList(members));
288 return memberSet;
289 }
290
291 private String getUsersURI() {
292 return (String) loginModule.getOptions().get(usersKey);
293 }
294
295 private String getGroupsURI() {
296 return (String) loginModule.getOptions().get(groupsKey);
297 }
298
299 private String getDigest() {
300 return (String) loginModule.getOptions().get(digestKey);
301 }
302
303 private String getEncoding() {
304 return (String) loginModule.getOptions().get(encodingKey);
305 }
306
307 /**
308 * Allows the GBean at startup to request that all unencrypted passwords
309 * be updated.
310 */
311 private void encryptAllPasswords() throws GeronimoSecurityException {
312 log.debug("Checking passwords to see if any need encrypting");
313 refreshAll();
314 try {
315 String name;
316 boolean bUpdates=false;
317
318 for (Enumeration e=users.keys(); e.hasMoreElements(); ) {
319 name=(String)e.nextElement();
320 String realPassword = users.getProperty(name);
321 // Encrypt the password if needed, so we can compare it with the supplied one
322 if (realPassword != null) {
323 String pw = EncryptionManager.encrypt(realPassword);
324 if (!realPassword.equals(pw)) {
325 users.setProperty(name, pw);
326 bUpdates = true;
327 }
328 }
329 }
330
331 // rewrite the users.properties file if we had passwords to encrypt
332 if (bUpdates)
333 {
334 log.debug("Found password(s) that needed encrypting");
335 store(users, serverInfo.resolveServer(getUsersURI()).toURL());
336 }
337 } catch (Exception e) {
338 log.error("encryptAllPasswords failed", e);
339 throw new GeronimoSecurityException(e);
340 }
341 }
342
343 private void store(Properties props, URL url) throws Exception {
344 OutputStream out = null;
345 log.debug("Updating properties file="+url.toExternalForm());
346 try {
347 try {
348 URLConnection con = url.openConnection();
349 con.setDoOutput(true);
350 out = con.getOutputStream();
351 } catch (Exception e) {
352 if ("file".equalsIgnoreCase(url.getProtocol()) && e instanceof UnknownServiceException) {
353 out = new FileOutputStream(new File(url.getFile()));
354 } else {
355 throw e;
356 }
357 }
358 props.store(out, null);
359 } finally {
360 if (out != null) {
361 try {
362 out.close();
363 } catch (IOException ignored) {
364 // ignored
365 }
366 }
367 }
368 }
369
370 /**
371 * This method returns the message digest of a specified string.
372 * @param password The string that is to be digested
373 * @param algorithm Name of the Message Digest algorithm
374 * @param encoding Encoding to be used for digest data. Hex by default.
375 * @return encoded digest bytes
376 * @throws NoSuchAlgorithmException if the Message Digest algorithm is not available
377 */
378 private String digestPassword(String password, String algorithm, String encoding) throws NoSuchAlgorithmException {
379 MessageDigest md = MessageDigest.getInstance(algorithm);
380 byte[] data = md.digest(password.getBytes());
381 if(encoding == null || "hex".equalsIgnoreCase(encoding)) {
382 // Convert bytes to hex digits
383 byte[] hexData = new byte[data.length * 2];
384 HexTranslator ht = new HexTranslator();
385 ht.encode(data, 0, data.length, hexData, 0);
386 return new String(hexData);
387 } else if("base64".equalsIgnoreCase(encoding)) {
388 return new String(Base64.encode(data));
389 }
390 return "";
391 }
392
393 public void doFail() {
394 log.warn("Failed");
395 }
396
397 public void doStart() throws Exception {
398 log.debug("Starting gbean");
399 encryptAllPasswords();
400 log.debug("Started gbean");
401 }
402
403 public void doStop() throws Exception {
404 log.debug("Stopping gbean");
405 clearAll();
406 log.debug("Stopped gbean");
407 }
408
409 public static final GBeanInfo GBEAN_INFO;
410
411 static {
412 GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic("PropertiesLoginModuleManager", PropertiesLoginModuleManager.class);
413
414 infoFactory.addOperation("addUserPrincipal", new Class[]{Hashtable.class});
415 infoFactory.addOperation("removeUserPrincipal", new Class[]{String.class});
416 infoFactory.addOperation("updateUserPrincipal", new Class[]{Hashtable.class});
417 infoFactory.addOperation("getGroups");
418 infoFactory.addOperation("getUsers");
419 infoFactory.addOperation("refreshAll");
420
421 infoFactory.addOperation("updateUserPrincipal", new Class[]{Hashtable.class});
422
423 infoFactory.addOperation("getPassword", new Class[]{String.class});
424 infoFactory.addOperation("getGroupMembers", new Class[]{String.class});
425 infoFactory.addOperation("addGroupPrincipal", new Class[]{Hashtable.class});
426 infoFactory.addOperation("removeGroupPrincipal", new Class[]{String.class});
427 infoFactory.addOperation("updateGroupPrincipal", new Class[]{Hashtable.class});
428 infoFactory.addOperation("addToGroup", new Class[]{String.class, String.class});
429 infoFactory.addOperation("removeFromGroup", new Class[]{String.class, String.class});
430
431 infoFactory.addReference("ServerInfo", ServerInfo.class, NameFactory.GERONIMO_SERVICE);
432 infoFactory.addReference("LoginModule", LoginModuleSettings.class, NameFactory.LOGIN_MODULE);
433
434 infoFactory.setConstructor(new String[]{"ServerInfo", "LoginModule"});
435
436 GBEAN_INFO = infoFactory.getBeanInfo();
437 }
438
439 public static GBeanInfo getGBeanInfo() {
440 return GBEAN_INFO;
441 }
442
443 }