1//
2//  ========================================================================
3//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4//  ------------------------------------------------------------------------
5//  All rights reserved. This program and the accompanying materials
6//  are made available under the terms of the Eclipse Public License v1.0
7//  and Apache License v2.0 which accompanies this distribution.
8//
9//      The Eclipse Public License is available at
10//      http://www.eclipse.org/legal/epl-v10.html
11//
12//      The Apache License v2.0 is available at
13//      http://www.opensource.org/licenses/apache2.0.php
14//
15//  You may elect to redistribute this code under either of these licenses.
16//  ========================================================================
17//
18
19package org.eclipse.jetty.security;
20
21import java.io.File;
22import java.io.FilenameFilter;
23import java.io.IOException;
24import java.security.Principal;
25import java.util.ArrayList;
26import java.util.HashMap;
27import java.util.HashSet;
28import java.util.Iterator;
29import java.util.List;
30import java.util.Map;
31import java.util.Properties;
32import java.util.Set;
33
34import javax.security.auth.Subject;
35
36import org.eclipse.jetty.security.MappedLoginService.KnownUser;
37import org.eclipse.jetty.security.MappedLoginService.RolePrincipal;
38import org.eclipse.jetty.server.UserIdentity;
39import org.eclipse.jetty.util.Scanner;
40import org.eclipse.jetty.util.Scanner.BulkListener;
41import org.eclipse.jetty.util.component.AbstractLifeCycle;
42import org.eclipse.jetty.util.log.Log;
43import org.eclipse.jetty.util.log.Logger;
44import org.eclipse.jetty.util.resource.Resource;
45import org.eclipse.jetty.util.security.Credential;
46
47/**
48 * PropertyUserStore
49 *
50 * This class monitors a property file of the format mentioned below and notifies registered listeners of the changes to the the given file.
51 *
52 * <PRE>
53 *  username: password [,rolename ...]
54 * </PRE>
55 *
56 * Passwords may be clear text, obfuscated or checksummed. The class com.eclipse.Util.Password should be used to generate obfuscated passwords or password
57 * checksums.
58 *
59 * If DIGEST Authentication is used, the password must be in a recoverable format, either plain text or OBF:.
60 */
61public class PropertyUserStore extends AbstractLifeCycle
62{
63    private static final Logger LOG = Log.getLogger(PropertyUserStore.class);
64
65    private String _config;
66    private Resource _configResource;
67    private Scanner _scanner;
68    private int _refreshInterval = 0;// default is not to reload
69
70    private IdentityService _identityService = new DefaultIdentityService();
71    private boolean _firstLoad = true; // true if first load, false from that point on
72    private final List<String> _knownUsers = new ArrayList<String>();
73    private final Map<String, UserIdentity> _knownUserIdentities = new HashMap<String, UserIdentity>();
74    private List<UserListener> _listeners;
75
76    /* ------------------------------------------------------------ */
77    public String getConfig()
78    {
79        return _config;
80    }
81
82    /* ------------------------------------------------------------ */
83    public void setConfig(String config)
84    {
85        _config = config;
86    }
87
88    /* ------------------------------------------------------------ */
89        public UserIdentity getUserIdentity(String userName)
90        {
91            return _knownUserIdentities.get(userName);
92        }
93
94    /* ------------------------------------------------------------ */
95    /**
96     * returns the resource associated with the configured properties file, creating it if necessary
97     */
98    public Resource getConfigResource() throws IOException
99    {
100        if (_configResource == null)
101        {
102            _configResource = Resource.newResource(_config);
103        }
104
105        return _configResource;
106    }
107
108    /* ------------------------------------------------------------ */
109    /**
110     * sets the refresh interval (in seconds)
111     */
112    public void setRefreshInterval(int msec)
113    {
114        _refreshInterval = msec;
115    }
116
117    /* ------------------------------------------------------------ */
118    /**
119     * refresh interval in seconds for how often the properties file should be checked for changes
120     */
121    public int getRefreshInterval()
122    {
123        return _refreshInterval;
124    }
125
126    /* ------------------------------------------------------------ */
127    private void loadUsers() throws IOException
128    {
129        if (_config == null)
130            return;
131
132        if (LOG.isDebugEnabled())
133            LOG.debug("Load " + this + " from " + _config);
134        Properties properties = new Properties();
135        if (getConfigResource().exists())
136            properties.load(getConfigResource().getInputStream());
137        Set<String> known = new HashSet<String>();
138
139        for (Map.Entry<Object, Object> entry : properties.entrySet())
140        {
141            String username = ((String)entry.getKey()).trim();
142            String credentials = ((String)entry.getValue()).trim();
143            String roles = null;
144            int c = credentials.indexOf(',');
145            if (c > 0)
146            {
147                roles = credentials.substring(c + 1).trim();
148                credentials = credentials.substring(0,c).trim();
149            }
150
151            if (username != null && username.length() > 0 && credentials != null && credentials.length() > 0)
152            {
153                String[] roleArray = IdentityService.NO_ROLES;
154                if (roles != null && roles.length() > 0)
155                {
156                    roleArray = roles.split(",");
157                }
158                known.add(username);
159                Credential credential = Credential.getCredential(credentials);
160
161                Principal userPrincipal = new KnownUser(username,credential);
162                Subject subject = new Subject();
163                subject.getPrincipals().add(userPrincipal);
164                subject.getPrivateCredentials().add(credential);
165
166                if (roles != null)
167                {
168                    for (String role : roleArray)
169                    {
170                        subject.getPrincipals().add(new RolePrincipal(role));
171                    }
172                }
173
174                subject.setReadOnly();
175
176                _knownUserIdentities.put(username,_identityService.newUserIdentity(subject,userPrincipal,roleArray));
177                notifyUpdate(username,credential,roleArray);
178            }
179        }
180
181        synchronized (_knownUsers)
182        {
183            /*
184             * if its not the initial load then we want to process removed users
185             */
186            if (!_firstLoad)
187            {
188                Iterator<String> users = _knownUsers.iterator();
189                while (users.hasNext())
190                {
191                    String user = users.next();
192                    if (!known.contains(user))
193                    {
194                        _knownUserIdentities.remove(user);
195                        notifyRemove(user);
196                    }
197                }
198            }
199
200            /*
201             * reset the tracked _users list to the known users we just processed
202             */
203
204            _knownUsers.clear();
205            _knownUsers.addAll(known);
206
207        }
208
209        /*
210         * set initial load to false as there should be no more initial loads
211         */
212        _firstLoad = false;
213    }
214
215    /* ------------------------------------------------------------ */
216    /**
217     * Depending on the value of the refresh interval, this method will either start up a scanner thread that will monitor the properties file for changes after
218     * it has initially loaded it. Otherwise the users will be loaded and there will be no active monitoring thread so changes will not be detected.
219     *
220     *
221     * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
222     */
223    protected void doStart() throws Exception
224    {
225        super.doStart();
226
227        if (getRefreshInterval() > 0)
228        {
229            _scanner = new Scanner();
230            _scanner.setScanInterval(getRefreshInterval());
231            List<File> dirList = new ArrayList<File>(1);
232            dirList.add(getConfigResource().getFile().getParentFile());
233            _scanner.setScanDirs(dirList);
234            _scanner.setFilenameFilter(new FilenameFilter()
235            {
236                public boolean accept(File dir, String name)
237                {
238                    File f = new File(dir,name);
239                    try
240                    {
241                        if (f.compareTo(getConfigResource().getFile()) == 0)
242                        {
243                            return true;
244                        }
245                    }
246                    catch (IOException e)
247                    {
248                        return false;
249                    }
250
251                    return false;
252                }
253
254            });
255
256            _scanner.addListener(new BulkListener()
257            {
258                public void filesChanged(List<String> filenames) throws Exception
259                {
260                    if (filenames == null)
261                        return;
262                    if (filenames.isEmpty())
263                        return;
264                    if (filenames.size() == 1)
265                    {
266                        Resource r = Resource.newResource(filenames.get(0));
267                        if (r.getFile().equals(_configResource.getFile()))
268                            loadUsers();
269                    }
270                }
271
272                public String toString()
273                {
274                    return "PropertyUserStore$Scanner";
275                }
276
277            });
278
279            _scanner.setReportExistingFilesOnStartup(true);
280            _scanner.setRecursive(false);
281            _scanner.start();
282        }
283        else
284        {
285            loadUsers();
286        }
287    }
288
289    /* ------------------------------------------------------------ */
290    /**
291     * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
292     */
293    protected void doStop() throws Exception
294    {
295        super.doStop();
296        if (_scanner != null)
297            _scanner.stop();
298        _scanner = null;
299    }
300
301    /**
302     * Notifies the registered listeners of potential updates to a user
303     *
304     * @param username
305     * @param credential
306     * @param roleArray
307     */
308    private void notifyUpdate(String username, Credential credential, String[] roleArray)
309    {
310        if (_listeners != null)
311        {
312            for (Iterator<UserListener> i = _listeners.iterator(); i.hasNext();)
313            {
314                i.next().update(username,credential,roleArray);
315            }
316        }
317    }
318
319    /**
320     * notifies the registered listeners that a user has been removed.
321     *
322     * @param username
323     */
324    private void notifyRemove(String username)
325    {
326        if (_listeners != null)
327        {
328            for (Iterator<UserListener> i = _listeners.iterator(); i.hasNext();)
329            {
330                i.next().remove(username);
331            }
332        }
333    }
334
335    /**
336     * registers a listener to be notified of the contents of the property file
337     */
338    public void registerUserListener(UserListener listener)
339    {
340        if (_listeners == null)
341        {
342            _listeners = new ArrayList<UserListener>();
343        }
344        _listeners.add(listener);
345    }
346
347    /**
348     * UserListener
349     */
350    public interface UserListener
351    {
352        public void update(String username, Credential credential, String[] roleArray);
353
354        public void remove(String username);
355    }
356}
357