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    }