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.WikiEngine; 043import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 044import org.apache.wiki.auth.NoSuchPrincipalException; 045import org.apache.wiki.auth.WikiPrincipal; 046import org.apache.wiki.auth.WikiSecurityException; 047import org.apache.wiki.util.TextUtil; 048import org.w3c.dom.Document; 049import org.w3c.dom.Element; 050import org.w3c.dom.NodeList; 051import org.xml.sax.SAXException; 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<>(); 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 @Deprecated 125 @Override 126 public void commit() throws WikiSecurityException 127 { } 128 129 /** 130 * Looks up and deletes a {@link Group} from the group database. If the 131 * group database does not contain the supplied Group. this method throws a 132 * {@link NoSuchPrincipalException}. The method commits the results 133 * of the delete to persistent storage. 134 * @param group the group to remove 135 * @throws WikiSecurityException if the database does not contain the 136 * supplied group (thrown as {@link NoSuchPrincipalException}) or if 137 * the commit did not succeed 138 */ 139 @Override 140 public void delete( Group group ) throws WikiSecurityException 141 { 142 String index = group.getName(); 143 boolean exists = m_groups.containsKey( index ); 144 145 if ( !exists ) 146 { 147 throw new NoSuchPrincipalException( "Not in database: " + group.getName() ); 148 } 149 150 m_groups.remove( index ); 151 152 // Commit to disk 153 saveDOM(); 154 } 155 156 /** 157 * Returns all wiki groups that are stored in the GroupDatabase as an array 158 * of Group objects. If the database does not contain any groups, this 159 * method will return a zero-length array. This method causes back-end 160 * storage to load the entire set of group; thus, it should be called 161 * infrequently (e.g., at initialization time). 162 * @return the wiki groups 163 * @throws WikiSecurityException if the groups cannot be returned by the back-end 164 */ 165 @Override 166 public Group[] groups() throws WikiSecurityException 167 { 168 buildDOM(); 169 Collection<Group> groups = m_groups.values(); 170 return groups.toArray( new Group[groups.size()] ); 171 } 172 173 /** 174 * Initializes the group database based on values from a Properties object. 175 * The properties object must contain a file path to the XML database file 176 * whose key is {@link #PROP_DATABASE}. 177 * @param engine the wiki engine 178 * @param props the properties used to initialize the group database 179 * @throws NoRequiredPropertyException if the user database cannot be 180 * located, parsed, or opened 181 * @throws WikiSecurityException if the database could not be initialized successfully 182 */ 183 @Override 184 public void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException 185 { 186 m_engine = engine; 187 188 File defaultFile = null; 189 if ( engine.getRootPath() == null ) 190 { 191 log.warn( "Cannot identify JSPWiki root path" ); 192 defaultFile = new File( "WEB-INF/" + DEFAULT_DATABASE ).getAbsoluteFile(); 193 } 194 else 195 { 196 defaultFile = new File( engine.getRootPath() + "/WEB-INF/" + DEFAULT_DATABASE ); 197 } 198 199 // Get database file location 200 String file = TextUtil.getStringProperty(props, PROP_DATABASE , defaultFile.getAbsolutePath()); 201 if ( file == null ) 202 { 203 log.warn( "XML group database property " + PROP_DATABASE + " not found; trying " + defaultFile ); 204 m_file = defaultFile; 205 } 206 else 207 { 208 m_file = new File( file ); 209 } 210 211 log.info( "XML group database at " + m_file.getAbsolutePath() ); 212 213 // Read DOM 214 buildDOM(); 215 } 216 217 /** 218 * Saves a Group to the group database. Note that this method <em>must</em> 219 * fail, and throw an <code>IllegalArgumentException</code>, if the 220 * proposed group is the same name as one of the built-in Roles: e.g., 221 * Admin, Authenticated, etc. The database is responsible for setting 222 * create/modify timestamps, upon a successful save, to the Group. 223 * The method commits the results of the delete to persistent storage. 224 * @param group the Group to save 225 * @param modifier the user who saved the Group 226 * @throws WikiSecurityException if the Group could not be saved successfully 227 */ 228 @Override 229 public void save( Group group, Principal modifier ) throws WikiSecurityException 230 { 231 if ( group == null || modifier == null ) 232 { 233 throw new IllegalArgumentException( "Group or modifier cannot be null." ); 234 } 235 236 checkForRefresh(); 237 238 String index = group.getName(); 239 boolean isNew = !( m_groups.containsKey( index ) ); 240 Date modDate = new Date( System.currentTimeMillis() ); 241 if ( isNew ) 242 { 243 // If new, set created info 244 group.setCreated( modDate ); 245 group.setCreator( modifier.getName() ); 246 } 247 group.setModifier( modifier.getName() ); 248 group.setLastModified( modDate ); 249 250 // Add the group to the 'saved' list 251 m_groups.put( index, group ); 252 253 // Commit to disk 254 saveDOM(); 255 } 256 257 private void buildDOM() throws WikiSecurityException 258 { 259 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 260 factory.setValidating( false ); 261 factory.setExpandEntityReferences( false ); 262 factory.setIgnoringComments( true ); 263 factory.setNamespaceAware( false ); 264 try 265 { 266 m_dom = factory.newDocumentBuilder().parse( m_file ); 267 log.debug( "Database successfully initialized" ); 268 m_lastModified = m_file.lastModified(); 269 m_lastCheck = System.currentTimeMillis(); 270 } 271 catch( ParserConfigurationException e ) 272 { 273 log.error( "Configuration error: " + e.getMessage() ); 274 } 275 catch( SAXException e ) 276 { 277 log.error( "SAX error: " + e.getMessage() ); 278 } 279 catch( FileNotFoundException e ) 280 { 281 log.info( "Group database not found; creating from scratch..." ); 282 } 283 catch( IOException e ) 284 { 285 log.error( "IO error: " + e.getMessage() ); 286 } 287 if ( m_dom == null ) 288 { 289 try 290 { 291 // 292 // Create the DOM from scratch 293 // 294 m_dom = factory.newDocumentBuilder().newDocument(); 295 m_dom.appendChild( m_dom.createElement( "groups" ) ); 296 } 297 catch( ParserConfigurationException e ) 298 { 299 log.fatal( "Could not create in-memory DOM" ); 300 } 301 } 302 303 // Ok, now go and read this sucker in 304 if ( m_dom != null ) 305 { 306 NodeList groupNodes = m_dom.getElementsByTagName( GROUP_TAG ); 307 for( int i = 0; i < groupNodes.getLength(); i++ ) 308 { 309 Element groupNode = (Element) groupNodes.item( i ); 310 String groupName = groupNode.getAttribute( GROUP_NAME ); 311 if ( groupName == null ) 312 { 313 log.warn( "Detected null group name in XMLGroupDataBase. Check your group database." ); 314 } 315 else 316 { 317 Group group = buildGroup( groupNode, groupName ); 318 m_groups.put( groupName, group ); 319 } 320 } 321 } 322 } 323 324 private long m_lastCheck = 0; 325 private long m_lastModified = 0; 326 327 private void checkForRefresh() 328 { 329 long time = System.currentTimeMillis(); 330 331 if( time - m_lastCheck > 60*1000L ) 332 { 333 long lastModified = m_file.lastModified(); 334 335 if( lastModified > m_lastModified ) 336 { 337 try 338 { 339 buildDOM(); 340 } 341 catch( WikiSecurityException e ) 342 { 343 log.error("Could not refresh DOM",e); 344 } 345 } 346 } 347 } 348 /** 349 * Constructs a Group based on a DOM group node. 350 * @param groupNode the node in the DOM containing the node 351 * @param name the name of the group 352 * @throws NoSuchPrincipalException 353 * @throws WikiSecurityException 354 */ 355 private Group buildGroup( Element groupNode, String name ) 356 { 357 // It's an error if either param is null (very odd) 358 if ( groupNode == null || name == null ) 359 { 360 throw new IllegalArgumentException( "DOM element or name cannot be null." ); 361 } 362 363 // Construct a new group 364 Group group = new Group( name, m_engine.getApplicationName() ); 365 366 // Get the users for this group, and add them 367 NodeList members = groupNode.getElementsByTagName( MEMBER_TAG ); 368 for( int i = 0; i < members.getLength(); i++ ) 369 { 370 Element memberNode = (Element) members.item( i ); 371 String principalName = memberNode.getAttribute( PRINCIPAL ); 372 Principal member = new WikiPrincipal( principalName ); 373 group.add( member ); 374 } 375 376 // Add the created/last-modified info 377 String creator = groupNode.getAttribute( CREATOR ); 378 String created = groupNode.getAttribute( CREATED ); 379 String modifier = groupNode.getAttribute( MODIFIER ); 380 String modified = groupNode.getAttribute( LAST_MODIFIED ); 381 try 382 { 383 group.setCreated( m_format.parse( created ) ); 384 group.setLastModified( m_format.parse( modified ) ); 385 } 386 catch ( ParseException e ) 387 { 388 // If parsing failed, use the platform default 389 try 390 { 391 group.setCreated( m_defaultFormat.parse( created ) ); 392 group.setLastModified( m_defaultFormat.parse( modified ) ); 393 } 394 catch ( ParseException e2) 395 { 396 log.warn( "Could not parse 'created' or 'lastModified' " + "attribute for " + " group'" 397 + group.getName() + "'." + " It may have been tampered with." ); 398 } 399 } 400 group.setCreator( creator ); 401 group.setModifier( modifier ); 402 return group; 403 } 404 405 private void saveDOM() throws WikiSecurityException 406 { 407 if ( m_dom == null ) 408 { 409 log.fatal( "Group database doesn't exist in memory." ); 410 } 411 412 File newFile = new File( m_file.getAbsolutePath() + ".new" ); 413 try 414 { 415 BufferedWriter io = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( newFile ), "UTF-8" ) ); 416 417 // Write the file header and document root 418 io.write( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ); 419 io.write( "<groups>\n" ); 420 421 // Write each profile as a <group> node 422 for( Group group : m_groups.values() ) 423 { 424 io.write( " <" + GROUP_TAG + " " ); 425 io.write( GROUP_NAME ); 426 io.write( "=\"" + StringEscapeUtils.escapeXml( group.getName() )+ "\" " ); 427 io.write( CREATOR ); 428 io.write( "=\"" + StringEscapeUtils.escapeXml( group.getCreator() ) + "\" " ); 429 io.write( CREATED ); 430 io.write( "=\"" + m_format.format( group.getCreated() ) + "\" " ); 431 io.write( MODIFIER ); 432 io.write( "=\"" + group.getModifier() + "\" " ); 433 io.write( LAST_MODIFIED ); 434 io.write( "=\"" + m_format.format( group.getLastModified() ) + "\"" ); 435 io.write( ">\n" ); 436 437 // Write each member as a <member> node 438 for( Principal member : group.members() ) 439 { 440 io.write( " <" + MEMBER_TAG + " " ); 441 io.write( PRINCIPAL ); 442 io.write( "=\"" + StringEscapeUtils.escapeXml(member.getName()) + "\" " ); 443 io.write( "/>\n" ); 444 } 445 446 // Close tag 447 io.write( " </" + GROUP_TAG + ">\n" ); 448 } 449 io.write( "</groups>" ); 450 io.close(); 451 } 452 catch( IOException e ) 453 { 454 throw new WikiSecurityException( e.getLocalizedMessage(), e ); 455 } 456 457 // Copy new file over old version 458 File backup = new File( m_file.getAbsolutePath() + ".old" ); 459 if ( backup.exists() && !backup.delete()) 460 { 461 log.error( "Could not delete old group database backup: " + backup ); 462 } 463 if ( !m_file.renameTo( backup ) ) 464 { 465 log.error( "Could not create group database backup: " + backup ); 466 } 467 if ( !newFile.renameTo( m_file ) ) 468 { 469 log.error( "Could not save database: " + backup + " restoring backup." ); 470 if ( !backup.renameTo( m_file ) ) 471 { 472 log.error( "Restore failed. Check the file permissions." ); 473 } 474 log.error( "Could not save database: " + m_file + ". Check the file permissions" ); 475 } 476 } 477 478}