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