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    }