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 }