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