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