blob: 74e689f15b8e4f9d0300cdadc662874e35fefccd [file] [log] [blame]
Jake Slack03928ae2014-05-13 18:41:56 -07001//
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}