All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.wiki.auth.user.XMLUserDatabase Maven / Gradle / Ivy

/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
 */
package org.apache.wiki.auth.user;

import org.apache.commons.lang3.StringUtils;
import org.apache.wiki.api.core.Engine;
import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
import org.apache.wiki.auth.NoSuchPrincipalException;
import org.apache.wiki.auth.WikiPrincipal;
import org.apache.wiki.auth.WikiSecurityException;
import org.apache.wiki.util.Serializer;
import org.apache.wiki.util.TextUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.Principal;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * 

Manages {@link DefaultUserProfile} objects using XML files for persistence. Passwords are hashed using SHA1. User entries are simple * <user> elements under the root. User profile properties are attributes of the element. For example:

*
* <users>
*   <user loginName="janne" fullName="Janne Jalkanen"
*     wikiName="JanneJalkanen" email="[email protected]"
*     password="{SHA}457b08e825da547c3b77fbc1ff906a1d00a7daee"/>
* </users> *
*

In this example, the un-hashed password is myP@5sw0rd. Passwords are hashed without salt.

* @since 2.3 */ // FIXME: If the DB is shared across multiple systems, it's possible to lose accounts // if two people add new accounts right after each other from different wikis. public class XMLUserDatabase extends AbstractUserDatabase { /** The jspwiki.properties property specifying the file system location of the user database. */ public static final String PROP_USERDATABASE = "jspwiki.xmlUserDatabaseFile"; private static final String DEFAULT_USERDATABASE = "userdatabase.xml"; private static final String ATTRIBUTES_TAG = "attributes"; private static final String CREATED = "created"; private static final String EMAIL = "email"; private static final String FULL_NAME = "fullName"; private static final String LOGIN_NAME = "loginName"; private static final String LAST_MODIFIED = "lastModified"; private static final String LOCK_EXPIRY = "lockExpiry"; private static final String PASSWORD = "password"; private static final String UID = "uid"; private static final String USER_TAG = "user"; private static final String WIKI_NAME = "wikiName"; private static final String DATE_FORMAT = "yyyy.MM.dd 'at' HH:mm:ss:SSS z"; private Document c_dom; private File c_file; /** {@inheritDoc} */ @Override public synchronized void deleteByLoginName( final String loginName ) throws WikiSecurityException { if( c_dom == null ) { throw new WikiSecurityException( "FATAL: database does not exist" ); } final NodeList users = c_dom.getDocumentElement().getElementsByTagName( USER_TAG ); for( int i = 0; i < users.getLength(); i++ ) { final Element user = ( Element )users.item( i ); if( user.getAttribute( LOGIN_NAME ).equals( loginName ) ) { c_dom.getDocumentElement().removeChild( user ); // Commit to disk saveDOM(); return; } } throw new NoSuchPrincipalException( "Not in database: " + loginName ); } /** {@inheritDoc} */ @Override public UserProfile findByEmail( final String index ) throws NoSuchPrincipalException { return findBy( EMAIL, index ); } /** {@inheritDoc} */ @Override public UserProfile findByFullName( final String index ) throws NoSuchPrincipalException { return findBy( FULL_NAME, index ); } /** {@inheritDoc} */ @Override public UserProfile findByLoginName( final String index ) throws NoSuchPrincipalException { return findBy( LOGIN_NAME, index ); } /** {@inheritDoc} */ @Override public UserProfile findByUid( final String uid ) throws NoSuchPrincipalException { return findBy( UID, uid ); } /** {@inheritDoc} */ @Override public UserProfile findByWikiName( final String index ) throws NoSuchPrincipalException { return findBy( WIKI_NAME, index ); } public UserProfile findBy( final String attr, final String value ) throws NoSuchPrincipalException { final UserProfile profile = findByAttribute( attr, value ); if ( profile != null ) { return profile; } throw new NoSuchPrincipalException( "Not in database: " + value ); } /** {@inheritDoc} */ @Override public Principal[] getWikiNames() throws WikiSecurityException { if ( c_dom == null ) { throw new IllegalStateException( "FATAL: database does not exist" ); } final SortedSet< WikiPrincipal > principals = new TreeSet<>(); final NodeList users = c_dom.getElementsByTagName( USER_TAG ); for( int i = 0; i < users.getLength(); i++ ) { final Element user = ( Element )users.item( i ); final String wikiName = user.getAttribute( WIKI_NAME ); if( StringUtils.isEmpty( wikiName ) ) { LOG.warn( "Detected null or empty wiki name for {} in XMLUserDataBase. Check your user database.", user.getAttribute( LOGIN_NAME ) ); } else { final WikiPrincipal principal = new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME ); principals.add( principal ); } } return principals.toArray( new Principal[0] ); } /** {@inheritDoc} */ @Override public void initialize( final Engine engine, final Properties props ) throws NoRequiredPropertyException { final File defaultFile; if( engine.getRootPath() == null ) { LOG.warn( "Cannot identify JSPWiki root path" ); defaultFile = new File( "WEB-INF/" + DEFAULT_USERDATABASE ).getAbsoluteFile(); } else { defaultFile = new File( engine.getRootPath() + "/WEB-INF/" + DEFAULT_USERDATABASE ); } // Get database file location final String file = TextUtil.getStringProperty( props, PROP_USERDATABASE, defaultFile.getAbsolutePath() ); if( file == null ) { LOG.warn( "XML user database property " + PROP_USERDATABASE + " not found; trying " + defaultFile ); c_file = defaultFile; } else { c_file = new File( file ); } LOG.info( "XML user database at " + c_file.getAbsolutePath() ); buildDOM(); sanitizeDOM(); } private void buildDOM() { // Read DOM final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating( false ); factory.setExpandEntityReferences( false ); factory.setIgnoringComments( true ); factory.setNamespaceAware( false ); //factory.setAttribute( XMLConstants.ACCESS_EXTERNAL_DTD, "" ); //factory.setAttribute( XMLConstants.ACCESS_EXTERNAL_SCHEMA, "" ); try { c_dom = factory.newDocumentBuilder().parse( c_file ); LOG.debug( "Database successfully initialized" ); c_lastModified = c_file.lastModified(); c_lastCheck = System.currentTimeMillis(); } catch( final ParserConfigurationException e ) { LOG.error( "Configuration error: {}", e.getMessage() ); } catch( final SAXException e ) { LOG.error( "SAX error: {}", e.getMessage() ); } catch( final FileNotFoundException e ) { LOG.info( "User database not found; creating from scratch..." ); } catch( final IOException e ) { LOG.error( "IO error: {}", e.getMessage() ); } if( c_dom == null ) { try { // Create the DOM from scratch c_dom = factory.newDocumentBuilder().newDocument(); c_dom.appendChild( c_dom.createElement( "users" ) ); } catch( final ParserConfigurationException e ) { LOG.fatal( "Could not create in-memory DOM" ); } } } private void saveDOM() throws WikiSecurityException { if( c_dom == null ) { throw new IllegalStateException( "FATAL: database does not exist" ); } final File newFile = new File( c_file.getAbsolutePath() + ".new" ); try( final BufferedWriter io = new BufferedWriter( new OutputStreamWriter( Files.newOutputStream( newFile.toPath() ), StandardCharsets.UTF_8 ) ) ) { // Write the file header and document root io.write( "\n" ); io.write( "\n" ); // Write each profile as a node final Element root = c_dom.getDocumentElement(); final NodeList nodes = root.getElementsByTagName( USER_TAG ); for( int i = 0; i < nodes.getLength(); i++ ) { final Element user = ( Element )nodes.item( i ); io.write( " <" + USER_TAG + " " ); io.write( UID ); io.write( "=\"" + user.getAttribute( UID ) + "\" " ); io.write( LOGIN_NAME ); io.write( "=\"" + user.getAttribute( LOGIN_NAME ) + "\" " ); io.write( WIKI_NAME ); io.write( "=\"" + user.getAttribute( WIKI_NAME ) + "\" " ); io.write( FULL_NAME ); io.write( "=\"" + user.getAttribute( FULL_NAME ) + "\" " ); io.write( EMAIL ); io.write( "=\"" + user.getAttribute( EMAIL ) + "\" " ); io.write( PASSWORD ); io.write( "=\"" + user.getAttribute( PASSWORD ) + "\" " ); io.write( CREATED ); io.write( "=\"" + user.getAttribute( CREATED ) + "\" " ); io.write( LAST_MODIFIED ); io.write( "=\"" + user.getAttribute( LAST_MODIFIED ) + "\" " ); io.write( LOCK_EXPIRY ); io.write( "=\"" + user.getAttribute( LOCK_EXPIRY ) + "\" " ); io.write( ">" ); final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); for( int j = 0; j < attributes.getLength(); j++ ) { final Element attribute = ( Element )attributes.item( j ); final String value = extractText( attribute ); io.write( "\n <" + ATTRIBUTES_TAG + ">" ); io.write( value ); io.write( "" ); } io.write( "\n \n" ); } io.write( "" ); } catch( final IOException e ) { throw new WikiSecurityException( e.getLocalizedMessage(), e ); } // Copy new file over old version final File backup = new File( c_file.getAbsolutePath() + ".old" ); if( backup.exists() ) { if( !backup.delete() ) { LOG.error( "Could not delete old user database backup: " + backup ); } } if( !c_file.renameTo( backup ) ) { LOG.error( "Could not create user database backup: " + backup ); } if( !newFile.renameTo( c_file ) ) { LOG.error( "Could not save database: " + backup + " restoring backup." ); if( !backup.renameTo( c_file ) ) { LOG.error( "Restore failed. Check the file permissions." ); } LOG.error( "Could not save database: " + c_file + ". Check the file permissions" ); } } private long c_lastCheck; private long c_lastModified; private void checkForRefresh() { final long time = System.currentTimeMillis(); if( time - c_lastCheck > 60 * 1000L ) { final long lastModified = c_file.lastModified(); if( lastModified > c_lastModified ) { buildDOM(); } } } /** * {@inheritDoc} * * @see org.apache.wiki.auth.user.UserDatabase#rename(String, String) */ @Override public synchronized void rename( final String loginName, final String newName) throws DuplicateUserException, WikiSecurityException { if( c_dom == null ) { LOG.fatal( "Could not rename profile '" + loginName + "'; database does not exist" ); throw new IllegalStateException( "FATAL: database does not exist" ); } checkForRefresh(); // Get the existing user; if not found, throws NoSuchPrincipalException final UserProfile profile = findByLoginName( loginName ); // Get user with the proposed name; if found, it's a collision try { final UserProfile otherProfile = findByLoginName( newName ); if( otherProfile != null ) { throw new DuplicateUserException( "security.error.cannot.rename", newName ); } } catch( final NoSuchPrincipalException e ) { // Good! That means it's safe to save using the new name } // Find the user with the old login id attribute, and change it final NodeList users = c_dom.getElementsByTagName( USER_TAG ); for( int i = 0; i < users.getLength(); i++ ) { final Element user = ( Element )users.item( i ); if( user.getAttribute( LOGIN_NAME ).equals( loginName ) ) { final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); final Date modDate = new Date( System.currentTimeMillis() ); setAttribute( user, LOGIN_NAME, newName ); setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) ); profile.setLoginName( newName ); profile.setLastModified( modDate ); break; } } // Commit to disk saveDOM(); } /** {@inheritDoc} */ @Override public synchronized void save( final UserProfile profile ) throws WikiSecurityException { if ( c_dom == null ) { LOG.fatal( "Could not save profile " + profile + " database does not exist" ); throw new IllegalStateException( "FATAL: database does not exist" ); } checkForRefresh(); final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); final String index = profile.getLoginName(); final NodeList users = c_dom.getElementsByTagName( USER_TAG ); Element user = IntStream.range(0, users.getLength()).mapToObj(i -> (Element) users.item(i)).filter(currentUser -> currentUser.getAttribute(LOGIN_NAME).equals(index)).findFirst().orElse(null); boolean isNew = false; final Date modDate = new Date( System.currentTimeMillis() ); if( user == null ) { // Create new user node profile.setCreated( modDate ); LOG.info( "Creating new user " + index ); user = c_dom.createElement( USER_TAG ); c_dom.getDocumentElement().appendChild( user ); setAttribute( user, CREATED, c_format.format( profile.getCreated() ) ); isNew = true; } else { // To update existing user node, delete old attributes first... final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); for( int i = 0; i < attributes.getLength(); i++ ) { user.removeChild( attributes.item( i ) ); } } setAttribute( user, UID, profile.getUid() ); setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) ); setAttribute( user, LOGIN_NAME, profile.getLoginName() ); setAttribute( user, FULL_NAME, profile.getFullname() ); setAttribute( user, WIKI_NAME, profile.getWikiName() ); setAttribute( user, EMAIL, profile.getEmail() ); final Date lockExpiry = profile.getLockExpiry(); setAttribute( user, LOCK_EXPIRY, lockExpiry == null ? "" : c_format.format( lockExpiry ) ); // Hash and save the new password if it's different from old one final String newPassword = profile.getPassword(); if( newPassword != null && !newPassword.equals( "" ) ) { final String oldPassword = user.getAttribute( PASSWORD ); if( !oldPassword.equals( newPassword ) ) { setAttribute( user, PASSWORD, getHash( newPassword ) ); } } // Save the attributes as Base64 string if(!profile.getAttributes().isEmpty()) { try { final String encodedAttributes = Serializer.serializeToBase64( profile.getAttributes() ); final Element attributes = c_dom.createElement( ATTRIBUTES_TAG ); user.appendChild( attributes ); final Text value = c_dom.createTextNode( encodedAttributes ); attributes.appendChild( value ); } catch( final IOException e ) { throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e ); } } // Set the profile timestamps if( isNew ) { profile.setCreated( modDate ); } profile.setLastModified( modDate ); // Commit to disk saveDOM(); } /** * Private method that returns the first {@link UserProfile}matching a <user> element's supplied attribute. This method will also * set the UID if it has not yet been set. * * @param matchAttribute matching attribute * @param index value to match * @return the profile, or null if not found */ private UserProfile findByAttribute( final String matchAttribute, String index ) { if ( c_dom == null ) { throw new IllegalStateException( "FATAL: database does not exist" ); } checkForRefresh(); final NodeList users = c_dom.getElementsByTagName( USER_TAG ); if( users == null ) { return null; } // check if we have to do a case-insensitive compare final boolean caseSensitiveCompare = !matchAttribute.equals( EMAIL ); for( int i = 0; i < users.getLength(); i++ ) { final Element user = (Element) users.item( i ); String userAttribute = user.getAttribute( matchAttribute ); if( !caseSensitiveCompare ) { userAttribute = StringUtils.lowerCase(userAttribute); index = StringUtils.lowerCase(index); } if( userAttribute.equals( index ) ) { final UserProfile profile = newProfile(); // Parse basic attributes profile.setUid( user.getAttribute( UID ) ); if( profile.getUid() == null || profile.getUid().isEmpty() ) { profile.setUid( generateUid( this ) ); } profile.setLoginName( user.getAttribute( LOGIN_NAME ) ); profile.setFullname( user.getAttribute( FULL_NAME ) ); profile.setPassword( user.getAttribute( PASSWORD ) ); profile.setEmail( user.getAttribute( EMAIL ) ); // Get created/modified timestamps final String created = user.getAttribute( CREATED ); final String modified = user.getAttribute( LAST_MODIFIED ); profile.setCreated( parseDate( profile, created ) ); profile.setLastModified( parseDate( profile, modified ) ); // Is the profile locked? final String lockExpiry = user.getAttribute( LOCK_EXPIRY ); if( StringUtils.isEmpty( lockExpiry ) || lockExpiry.isEmpty() ) { profile.setLockExpiry( null ); } else { profile.setLockExpiry( new Date( Long.parseLong( lockExpiry ) ) ); } // Extract all the user's attributes (should only be one attributes tag, but you never know!) final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); for( int j = 0; j < attributes.getLength(); j++ ) { final Element attribute = ( Element )attributes.item( j ); final String serializedMap = extractText( attribute ); try { final Map< String, ? extends Serializable > map = Serializer.deserializeFromBase64( serializedMap ); profile.getAttributes().putAll( map ); } catch( final IOException e ) { LOG.error( "Could not parse user profile attributes!", e ); } } return profile; } } return null; } /** * Extracts all the text nodes that are immediate children of an Element. * * @param element the base element * @return the text nodes that are immediate children of the base element, concatenated together */ private String extractText( final Element element ) { String text = ""; if( element.getChildNodes().getLength() > 0 ) { final NodeList children = element.getChildNodes(); text = IntStream.range(0, children.getLength()).mapToObj(children::item).filter(child -> child.getNodeType() == Node.TEXT_NODE).map(child -> ((Text) child).getData()).collect(Collectors.joining()); } return text; } /** * Tries to parse a date using the default format - then, for backwards compatibility reasons, tries the platform default. * * @param profile profile associated to the date. * @param date date to be parsed. * @return A parsed date, or null, if both parse attempts fail. */ private Date parseDate( final UserProfile profile, final String date ) { try { final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); return c_format.parse( date ); } catch( final ParseException e ) { try { return DateFormat.getDateTimeInstance().parse( date ); } catch( final ParseException e2 ) { LOG.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + profile.getLoginName() + "'." + " It may have been tampered with.", e2 ); } } return null; } /** * After loading the DOM, this method sanity-checks the dates in the DOM and makes sure they are formatted properly. This is sort-of * hacky, but it should work. */ private void sanitizeDOM() { if( c_dom == null ) { throw new IllegalStateException( "FATAL: database does not exist" ); } final NodeList users = c_dom.getElementsByTagName( USER_TAG ); for( int i = 0; i < users.getLength(); i++ ) { final Element user = ( Element )users.item( i ); // Sanitize UID (and generate a new one if one does not exist) String uid = user.getAttribute( UID ).trim(); if( StringUtils.isEmpty( uid ) || "-1".equals( uid ) ) { uid = String.valueOf( generateUid( this ) ); user.setAttribute( UID, uid ); } // Sanitize dates final String loginName = user.getAttribute( LOGIN_NAME ); String created = user.getAttribute( CREATED ); String modified = user.getAttribute( LAST_MODIFIED ); final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); try { created = c_format.format( c_format.parse( created ) ); modified = c_format.format( c_format.parse( modified ) ); user.setAttribute( CREATED, created ); user.setAttribute( LAST_MODIFIED, modified ); } catch( final ParseException e ) { try { created = c_format.format( DateFormat.getDateTimeInstance().parse( created ) ); modified = c_format.format( DateFormat.getDateTimeInstance().parse( modified ) ); user.setAttribute( CREATED, created ); user.setAttribute( LAST_MODIFIED, modified ); } catch( final ParseException e2 ) { LOG.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + loginName + "'." + " It may have been tampered with." ); } } } } /** * Private method that sets an attribute value for a supplied DOM element. * * @param element the element whose attribute is to be set * @param attribute the name of the attribute to set * @param value the desired attribute value */ private void setAttribute( final Element element, final String attribute, final String value ) { if( value != null ) { element.setAttribute( attribute, value ); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy