001 /* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019 package org.apache.wiki.auth.authorize; 020 021 import java.io.BufferedWriter; 022 import java.io.File; 023 import java.io.FileNotFoundException; 024 import java.io.FileOutputStream; 025 import java.io.IOException; 026 import java.io.OutputStreamWriter; 027 import java.security.Principal; 028 import java.text.DateFormat; 029 import java.text.ParseException; 030 import java.text.SimpleDateFormat; 031 import java.util.Collection; 032 import java.util.Date; 033 import java.util.HashMap; 034 import java.util.Map; 035 import java.util.Properties; 036 037 import javax.xml.parsers.DocumentBuilderFactory; 038 import javax.xml.parsers.ParserConfigurationException; 039 040 import org.apache.commons.lang.StringEscapeUtils; 041 import org.apache.log4j.Logger; 042 import org.w3c.dom.Document; 043 import org.w3c.dom.Element; 044 import org.w3c.dom.NodeList; 045 import org.xml.sax.SAXException; 046 import org.apache.wiki.WikiEngine; 047 import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 048 import org.apache.wiki.auth.NoSuchPrincipalException; 049 import org.apache.wiki.auth.WikiPrincipal; 050 import org.apache.wiki.auth.WikiSecurityException; 051 052 /** 053 * <p> 054 * GroupDatabase implementation for loading, persisting and storing wiki groups, 055 * using an XML file for persistence. Group entries are simple 056 * <code><group></code> elements under the root. Each group member is 057 * representated by a <code><member></code> element. For example: 058 * </p> 059 * <blockquote><code> 060 * <groups><br/> 061 * <group name="TV" created="Jun 20, 2006 2:50:54 PM" lastModified="Jan 21, 2006 2:50:54 PM"><br/> 062 * <member principal="Archie Bunker" /><br/> 063 * <member principal="BullwinkleMoose" /><br/> 064 * <member principal="Fred Friendly" /><br/> 065 * </group><br/> 066 * <group name="Literature" created="Jun 22, 2006 2:50:54 PM" lastModified="Jan 23, 2006 2:50:54 PM"><br/> 067 * <member principal="Charles Dickens" /><br/> 068 * <member principal="Homer" /><br/> 069 * </group><br/> 070 * </groups> 071 * </code></blockquote> 072 * @since 2.4.17 073 */ 074 public class XMLGroupDatabase implements GroupDatabase 075 { 076 protected static final Logger log = Logger.getLogger( XMLGroupDatabase.class ); 077 078 /** 079 * The jspwiki.properties property specifying the file system location of 080 * the group database. 081 */ 082 public static final String PROP_DATABASE = "jspwiki.xmlGroupDatabaseFile"; 083 084 private static final String DEFAULT_DATABASE = "groupdatabase.xml"; 085 086 private static final String CREATED = "created"; 087 088 private static final String CREATOR = "creator"; 089 090 private static final String GROUP_TAG = "group"; 091 092 private static final String GROUP_NAME = "name"; 093 094 private static final String LAST_MODIFIED = "lastModified"; 095 096 private static final String MODIFIER = "modifier"; 097 098 private static final String MEMBER_TAG = "member"; 099 100 private static final String PRINCIPAL = "principal"; 101 102 private Document m_dom = null; 103 104 private DateFormat m_defaultFormat = DateFormat.getDateTimeInstance(); 105 106 private DateFormat m_format = new SimpleDateFormat("yyyy.MM.dd 'at' HH:mm:ss:SSS z"); 107 108 private File m_file = null; 109 110 private WikiEngine m_engine = null; 111 112 private Map<String, Group> m_groups = new HashMap<String, Group>(); 113 114 /** 115 * No-op method that in previous versions of JSPWiki was intended to 116 * atomically commit changes to the user database. Now, the 117 * {@link #save(Group, Principal)} and {@link #delete(Group)} methods 118 * are atomic themselves. 119 * @throws WikiSecurityException never... 120 * @deprecated there is no need to call this method because the save and 121 * delete methods contain their own commit logic 122 */ 123 @SuppressWarnings("deprecation") 124 public void commit() throws WikiSecurityException 125 { } 126 127 /** 128 * Looks up and deletes a {@link Group} from the group database. If the 129 * group database does not contain the supplied Group. this method throws a 130 * {@link NoSuchPrincipalException}. The method commits the results 131 * of the delete to persistent storage. 132 * @param group the group to remove 133 * @throws WikiSecurityException if the database does not contain the 134 * supplied group (thrown as {@link NoSuchPrincipalException}) or if 135 * the commit did not succeed 136 */ 137 public void delete( Group group ) throws WikiSecurityException 138 { 139 String index = group.getName(); 140 boolean exists = m_groups.containsKey( index ); 141 142 if ( !exists ) 143 { 144 throw new NoSuchPrincipalException( "Not in database: " + group.getName() ); 145 } 146 147 m_groups.remove( index ); 148 149 // Commit to disk 150 saveDOM(); 151 } 152 153 /** 154 * Returns all wiki groups that are stored in the GroupDatabase as an array 155 * of Group objects. If the database does not contain any groups, this 156 * method will return a zero-length array. This method causes back-end 157 * storage to load the entire set of group; thus, it should be called 158 * infrequently (e.g., at initialization time). 159 * @return the wiki groups 160 * @throws WikiSecurityException if the groups cannot be returned by the back-end 161 */ 162 public Group[] groups() throws WikiSecurityException 163 { 164 buildDOM(); 165 Collection<Group> groups = m_groups.values(); 166 return groups.toArray( new Group[groups.size()] ); 167 } 168 169 /** 170 * Initializes the group database based on values from a Properties object. 171 * The properties object must contain a file path to the XML database file 172 * whose key is {@link #PROP_DATABASE}. 173 * @param engine the wiki engine 174 * @param props the properties used to initialize the group database 175 * @throws NoRequiredPropertyException if the user database cannot be 176 * located, parsed, or opened 177 * @throws WikiSecurityException if the database could not be initialized successfully 178 */ 179 public void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException 180 { 181 m_engine = engine; 182 183 File defaultFile = null; 184 if ( engine.getRootPath() == null ) 185 { 186 log.warn( "Cannot identify JSPWiki root path" ); 187 defaultFile = new File( "WEB-INF/" + DEFAULT_DATABASE ).getAbsoluteFile(); 188 } 189 else 190 { 191 defaultFile = new File( engine.getRootPath() + "/WEB-INF/" + DEFAULT_DATABASE ); 192 } 193 194 // Get database file location 195 String file = props.getProperty( PROP_DATABASE ); 196 if ( file == null ) 197 { 198 log.warn( "XML group database property " + PROP_DATABASE + " not found; trying " + defaultFile ); 199 m_file = defaultFile; 200 } 201 else 202 { 203 m_file = new File( file ); 204 } 205 206 log.info( "XML group database at " + m_file.getAbsolutePath() ); 207 208 // Read DOM 209 buildDOM(); 210 } 211 212 /** 213 * Saves a Group to the group database. Note that this method <em>must</em> 214 * fail, and throw an <code>IllegalArgumentException</code>, if the 215 * proposed group is the same name as one of the built-in Roles: e.g., 216 * Admin, Authenticated, etc. The database is responsible for setting 217 * create/modify timestamps, upon a successful save, to the Group. 218 * The method commits the results of the delete to persistent storage. 219 * @param group the Group to save 220 * @param modifier the user who saved the Group 221 * @throws WikiSecurityException if the Group could not be saved successfully 222 */ 223 public void save( Group group, Principal modifier ) throws WikiSecurityException 224 { 225 if ( group == null || modifier == null ) 226 { 227 throw new IllegalArgumentException( "Group or modifier cannot be null." ); 228 } 229 230 checkForRefresh(); 231 232 String index = group.getName(); 233 boolean isNew = !( m_groups.containsKey( index ) ); 234 Date modDate = new Date( System.currentTimeMillis() ); 235 if ( isNew ) 236 { 237 // If new, set created info 238 group.setCreated( modDate ); 239 group.setCreator( modifier.getName() ); 240 } 241 group.setModifier( modifier.getName() ); 242 group.setLastModified( modDate ); 243 244 // Add the group to the 'saved' list 245 m_groups.put( index, group ); 246 247 // Commit to disk 248 saveDOM(); 249 } 250 251 private void buildDOM() throws WikiSecurityException 252 { 253 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 254 factory.setValidating( false ); 255 factory.setExpandEntityReferences( false ); 256 factory.setIgnoringComments( true ); 257 factory.setNamespaceAware( false ); 258 try 259 { 260 m_dom = factory.newDocumentBuilder().parse( m_file ); 261 log.debug( "Database successfully initialized" ); 262 m_lastModified = m_file.lastModified(); 263 m_lastCheck = System.currentTimeMillis(); 264 } 265 catch( ParserConfigurationException e ) 266 { 267 log.error( "Configuration error: " + e.getMessage() ); 268 } 269 catch( SAXException e ) 270 { 271 log.error( "SAX error: " + e.getMessage() ); 272 } 273 catch( FileNotFoundException e ) 274 { 275 log.info( "Group database not found; creating from scratch..." ); 276 } 277 catch( IOException e ) 278 { 279 log.error( "IO error: " + e.getMessage() ); 280 } 281 if ( m_dom == null ) 282 { 283 try 284 { 285 // 286 // Create the DOM from scratch 287 // 288 m_dom = factory.newDocumentBuilder().newDocument(); 289 m_dom.appendChild( m_dom.createElement( "groups" ) ); 290 } 291 catch( ParserConfigurationException e ) 292 { 293 log.fatal( "Could not create in-memory DOM" ); 294 } 295 } 296 297 // Ok, now go and read this sucker in 298 if ( m_dom != null ) 299 { 300 NodeList groupNodes = m_dom.getElementsByTagName( GROUP_TAG ); 301 for( int i = 0; i < groupNodes.getLength(); i++ ) 302 { 303 Element groupNode = (Element) groupNodes.item( i ); 304 String groupName = groupNode.getAttribute( GROUP_NAME ); 305 if ( groupName == null ) 306 { 307 log.warn( "Detected null group name in XMLGroupDataBase. Check your group database." ); 308 } 309 else 310 { 311 Group group = buildGroup( groupNode, groupName ); 312 m_groups.put( groupName, group ); 313 } 314 } 315 } 316 } 317 318 private long m_lastCheck = 0; 319 private long m_lastModified = 0; 320 321 private void checkForRefresh() 322 { 323 long time = System.currentTimeMillis(); 324 325 if( time - m_lastCheck > 60*1000L ) 326 { 327 long lastModified = m_file.lastModified(); 328 329 if( lastModified > m_lastModified ) 330 { 331 try 332 { 333 buildDOM(); 334 } 335 catch( WikiSecurityException e ) 336 { 337 log.error("Could not refresh DOM",e); 338 } 339 } 340 } 341 } 342 /** 343 * Constructs a Group based on a DOM group node. 344 * @param groupNode the node in the DOM containing the node 345 * @param name the name of the group 346 * @throws NoSuchPrincipalException 347 * @throws WikiSecurityException 348 */ 349 private Group buildGroup( Element groupNode, String name ) 350 { 351 // It's an error if either param is null (very odd) 352 if ( groupNode == null || name == null ) 353 { 354 throw new IllegalArgumentException( "DOM element or name cannot be null." ); 355 } 356 357 // Construct a new group 358 Group group = new Group( name, m_engine.getApplicationName() ); 359 360 // Get the users for this group, and add them 361 NodeList members = groupNode.getElementsByTagName( MEMBER_TAG ); 362 for( int i = 0; i < members.getLength(); i++ ) 363 { 364 Element memberNode = (Element) members.item( i ); 365 String principalName = memberNode.getAttribute( PRINCIPAL ); 366 Principal member = new WikiPrincipal( principalName ); 367 group.add( member ); 368 } 369 370 // Add the created/last-modified info 371 String creator = groupNode.getAttribute( CREATOR ); 372 String created = groupNode.getAttribute( CREATED ); 373 String modifier = groupNode.getAttribute( MODIFIER ); 374 String modified = groupNode.getAttribute( LAST_MODIFIED ); 375 try 376 { 377 group.setCreated( m_format.parse( created ) ); 378 group.setLastModified( m_format.parse( modified ) ); 379 } 380 catch ( ParseException e ) 381 { 382 // If parsing failed, use the platform default 383 try 384 { 385 group.setCreated( m_defaultFormat.parse( created ) ); 386 group.setLastModified( m_defaultFormat.parse( modified ) ); 387 } 388 catch ( ParseException e2) 389 { 390 log.warn( "Could not parse 'created' or 'lastModified' " + "attribute for " + " group'" 391 + group.getName() + "'." + " It may have been tampered with." ); 392 } 393 } 394 group.setCreator( creator ); 395 group.setModifier( modifier ); 396 return group; 397 } 398 399 private void saveDOM() throws WikiSecurityException 400 { 401 if ( m_dom == null ) 402 { 403 log.fatal( "Group database doesn't exist in memory." ); 404 } 405 406 File newFile = new File( m_file.getAbsolutePath() + ".new" ); 407 try 408 { 409 BufferedWriter io = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( newFile ), "UTF-8" ) ); 410 411 // Write the file header and document root 412 io.write( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ); 413 io.write( "<groups>\n" ); 414 415 // Write each profile as a <group> node 416 for( Group group : m_groups.values() ) 417 { 418 io.write( " <" + GROUP_TAG + " " ); 419 io.write( GROUP_NAME ); 420 io.write( "=\"" + StringEscapeUtils.escapeXml( group.getName() )+ "\" " ); 421 io.write( CREATOR ); 422 io.write( "=\"" + StringEscapeUtils.escapeXml( group.getCreator() ) + "\" " ); 423 io.write( CREATED ); 424 io.write( "=\"" + m_format.format( group.getCreated() ) + "\" " ); 425 io.write( MODIFIER ); 426 io.write( "=\"" + group.getModifier() + "\" " ); 427 io.write( LAST_MODIFIED ); 428 io.write( "=\"" + m_format.format( group.getLastModified() ) + "\"" ); 429 io.write( ">\n" ); 430 431 // Write each member as a <member> node 432 for( Principal member : group.members() ) 433 { 434 io.write( " <" + MEMBER_TAG + " " ); 435 io.write( PRINCIPAL ); 436 io.write( "=\"" + StringEscapeUtils.escapeXml(member.getName()) + "\" " ); 437 io.write( "/>\n" ); 438 } 439 440 // Close tag 441 io.write( " </" + GROUP_TAG + ">\n" ); 442 } 443 io.write( "</groups>" ); 444 io.close(); 445 } 446 catch( IOException e ) 447 { 448 throw new WikiSecurityException( e.getLocalizedMessage(), e ); 449 } 450 451 // Copy new file over old version 452 File backup = new File( m_file.getAbsolutePath() + ".old" ); 453 if ( backup.exists() && !backup.delete()) 454 { 455 log.error( "Could not delete old group database backup: " + backup ); 456 } 457 if ( !m_file.renameTo( backup ) ) 458 { 459 log.error( "Could not create group database backup: " + backup ); 460 } 461 if ( !newFile.renameTo( m_file ) ) 462 { 463 log.error( "Could not save database: " + backup + " restoring backup." ); 464 if ( !backup.renameTo( m_file ) ) 465 { 466 log.error( "Restore failed. Check the file permissions." ); 467 } 468 log.error( "Could not save database: " + m_file + ". Check the file permissions" ); 469 } 470 } 471 472 }