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 }