    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.user;
    020
    021import org.apache.commons.lang3.StringUtils;
    022import org.apache.wiki.api.core.Engine;
    023import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
    024import org.apache.wiki.auth.NoSuchPrincipalException;
    025import org.apache.wiki.auth.WikiPrincipal;
    026import org.apache.wiki.auth.WikiSecurityException;
    027import org.apache.wiki.util.Serializer;
    028import org.apache.wiki.util.TextUtil;
    029import org.w3c.dom.Document;
    030import org.w3c.dom.Element;
    031import org.w3c.dom.Node;
    032import org.w3c.dom.NodeList;
    033import org.w3c.dom.Text;
    034import org.xml.sax.SAXException;
    035
    036import javax.xml.parsers.DocumentBuilderFactory;
    037import javax.xml.parsers.ParserConfigurationException;
    038import java.io.BufferedWriter;
    039import java.io.File;
    040import java.io.FileInputStream;
    041import java.io.FileNotFoundException;
    042import java.io.IOException;
    043import java.io.OutputStreamWriter;
    044import java.io.Serializable;
    045import java.nio.charset.StandardCharsets;
    046import java.nio.file.Files;
    047import java.security.Principal;
    048import java.text.DateFormat;
    049import java.text.ParseException;
    050import java.text.SimpleDateFormat;
    051import java.util.Arrays;
    052import java.util.Date;
    053import java.util.Map;
    054import java.util.Properties;
    055import java.util.SortedSet;
    056import java.util.TreeSet;
    057import java.util.stream.Collectors;
    058import java.util.stream.IntStream;
    059import javax.xml.XMLConstants;
    060import org.apache.commons.codec.digest.DigestUtils;
    061import org.apache.commons.io.FileUtils;
    062
    063/**
    064 * <p>Manages {@link DefaultUserProfile} objects using XML files for persistence. Passwords are hashed using SHA1. User entries are simple
    065 * <code>&lt;user&gt;</code> elements under the root. User profile properties are attributes of the element. For example:</p>
    066 * <blockquote><code>
    067 * &lt;users&gt;<br/>
    068 * &nbsp;&nbsp;&lt;user loginName="janne" fullName="Janne Jalkanen"<br/>
    069 * &nbsp;&nbsp;&nbsp;&nbsp;wikiName="JanneJalkanen" email="janne@ecyrd.com"<br/>
    070 * &nbsp;&nbsp;&nbsp;&nbsp;password="{SHA}457b08e825da547c3b77fbc1ff906a1d00a7daee"/&gt;<br/>
    071 * &lt;/users&gt;
    072 * </code></blockquote>
    073 * <p>In this example, the un-hashed password is <code>myP@5sw0rd</code>. Passwords are hashed without salt.</p>
    074 * @since 2.3
    075 */
    076
    077// FIXME: If the DB is shared across multiple systems, it's possible to lose accounts
    078//        if two people add new accounts right after each other from different wikis.
    079public class XMLUserDatabase extends AbstractUserDatabase {
    080
    081    /** The jspwiki.properties property specifying the file system location of the user database. */
    082    public static final String  PROP_USERDATABASE = "jspwiki.xmlUserDatabaseFile";
    083    private static final String DEFAULT_USERDATABASE = "userdatabase.xml";
    084    private static final String ATTRIBUTES_TAG    = "attributes";
    085    private static final String OLD_HASHES_TAG    = "oldhashes";
    086    private static final String CREATED           = "created";
    087    private static final String EMAIL             = "email";
    088    private static final String FULL_NAME         = "fullName";
    089    private static final String LOGIN_NAME        = "loginName";
    090    private static final String LAST_MODIFIED     = "lastModified";
    091    private static final String LOCK_EXPIRY       = "lockExpiry";
    092    private static final String PASSWORD          = "password";
    093    private static final String UID               = "uid";
    094    private static final String USER_TAG          = "user";
    095    private static final String WIKI_NAME         = "wikiName";
    096    private static final String DATE_FORMAT       = "yyyy.MM.dd 'at' HH:mm:ss:SSS z";
    097    private Document            c_dom;
    098    private File                c_file;
    099    private int m_passwordReusedCount = -1;
    100
    101    /** {@inheritDoc} */
    102    @Override
    103    public synchronized void deleteByLoginName( final String loginName ) throws WikiSecurityException {
    104        if( c_dom == null ) {
    105            throw new WikiSecurityException( "FATAL: database does not exist" );
    106        }
    107
    108        final NodeList users = c_dom.getDocumentElement().getElementsByTagName( USER_TAG );
    109        for( int i = 0; i < users.getLength(); i++ ) {
    110            final Element user = ( Element )users.item( i );
    111            if( user.getAttribute( LOGIN_NAME ).equals( loginName ) ) {
    112                c_dom.getDocumentElement().removeChild( user );
    113
    114                // Commit to disk
    115                saveDOM();
    116                return;
    117            }
    118        }
    119        throw new NoSuchPrincipalException( "Not in database: " + loginName );
    120    }
    121
    122    /** {@inheritDoc} */
    123    @Override
    124    public UserProfile findByEmail( final String index ) throws NoSuchPrincipalException {
    125        return findBy( EMAIL, index );
    126    }
    127
    128    /** {@inheritDoc} */
    129    @Override
    130    public UserProfile findByFullName( final String index ) throws NoSuchPrincipalException {
    131        return findBy( FULL_NAME, index );
    132    }
    133
    134    /** {@inheritDoc} */
    135    @Override
    136    public UserProfile findByLoginName( final String index ) throws NoSuchPrincipalException {
    137        return findBy( LOGIN_NAME, index );
    138    }
    139
    140    /** {@inheritDoc} */
    141    @Override
    142    public UserProfile findByUid( final String uid ) throws NoSuchPrincipalException {
    143        return findBy( UID, uid );
    144    }
    145
    146    /** {@inheritDoc} */
    147    @Override
    148    public UserProfile findByWikiName( final String index ) throws NoSuchPrincipalException {
    149        return findBy( WIKI_NAME, index );
    150    }
    151
    152    public UserProfile findBy( final String attr, final String value ) throws NoSuchPrincipalException {
    153        final UserProfile profile = findByAttribute( attr, value );
    154        if ( profile != null ) {
    155            return profile;
    156        }
    157        throw new NoSuchPrincipalException( "Not in database: " + value );
    158    }
    159
    160    /** {@inheritDoc} */
    161    @Override
    162    public Principal[] getWikiNames() throws WikiSecurityException {
    163        if ( c_dom == null ) {
    164            throw new IllegalStateException( "FATAL: database does not exist" );
    165        }
    166        final SortedSet< WikiPrincipal > principals = new TreeSet<>();
    167        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
    168        for( int i = 0; i < users.getLength(); i++ ) {
    169            final Element user = ( Element )users.item( i );
    170            final String wikiName = user.getAttribute( WIKI_NAME );
    171            if( StringUtils.isEmpty( wikiName ) ) {
    172                LOG.warn( "Detected null or empty wiki name for {} in XMLUserDataBase. Check your user database.", user.getAttribute( LOGIN_NAME ) );
    173            } else {
    174                final WikiPrincipal principal = new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME );
    175                principals.add( principal );
    176            }
    177        }
    178        return principals.toArray( new Principal[0] );
    179    }
    180
    181    /** {@inheritDoc} */
    182    @Override
    183    public void initialize( final Engine engine, final Properties props ) throws NoRequiredPropertyException {
    184        m_engine = engine;
    185        final File defaultFile;
    186        if( engine.getRootPath() == null ) {
    187            LOG.warn( "Cannot identify JSPWiki root path" );
    188            defaultFile = new File( "WEB-INF/" + DEFAULT_USERDATABASE ).getAbsoluteFile();
    189        } else {
    190            defaultFile = new File( engine.getRootPath() + "/WEB-INF/" + DEFAULT_USERDATABASE );
    191        }
    192
    193        // Get database file location
    194        final String file = TextUtil.getStringProperty( props, PROP_USERDATABASE, defaultFile.getAbsolutePath() );
    195        if( file == null ) {
    196            LOG.warn( "XML user database property " + PROP_USERDATABASE + " not found; trying " + defaultFile );
    197            c_file = defaultFile;
    198        } else {
    199            c_file = new File( file );
    200        }
    201
    202        LOG.info( "XML user database at " + c_file.getAbsolutePath() );
    203        m_passwordReusedCount = TextUtil.getIntegerProperty( props, "jspwiki.credentials.reuseCount", -1);
    204        File checkFile = new File(c_file.getParent(), c_file.getName() + ".check");
    205        if (checkFile.exists()) {
    206
    207            byte[] computedHash = null;
    208            byte[] storedHash = null;
    209            try (FileInputStream fis = new FileInputStream(c_file)) {
    210                computedHash = DigestUtils.sha256(fis);
    211                storedHash = FileUtils.readFileToByteArray(checkFile);
    212            } catch (Exception ex) {
    213                throw new RuntimeException("Failed to compute integrity check. ", ex);
    214            }
    215            if (Arrays.equals(computedHash, storedHash)) {
    216                LOG.info("XML user database hash check passed. no modifications detected.");
    217            } else {
    218                throw new RuntimeException("XML user database has been modified outside of JSP Wiki. Refusing start up. An administrator will need to restore the file from backup");
    219            }
    220
    221        } else {
    222            LOG.info("XML user database check file does not exist. This is normal if JSPWIki was just installed.");
    223        }
    224
    225        buildDOM();
    226        sanitizeDOM();
    227    }
    228
    229    private void buildDOM() {
    230        // Read DOM
    231        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    232        factory.setValidating( false );
    233        factory.setExpandEntityReferences( false );
    234        factory.setIgnoringComments( true );
    235        factory.setNamespaceAware( false );
    236        factory.setAttribute( XMLConstants.ACCESS_EXTERNAL_DTD, "" );
    237        factory.setAttribute( XMLConstants.ACCESS_EXTERNAL_SCHEMA, "" );
    238        try {
    239            c_dom = factory.newDocumentBuilder().parse( c_file );
    240            LOG.debug( "Database successfully initialized" );
    241            c_lastModified = c_file.lastModified();
    242            c_lastCheck = System.currentTimeMillis();
    243        } catch( final ParserConfigurationException e ) {
    244            LOG.error( "Configuration error: {}", e.getMessage() );
    245        } catch( final SAXException e ) {
    246            LOG.error( "SAX error: {}", e.getMessage() );
    247        } catch( final FileNotFoundException e ) {
    248            LOG.info( "User database not found; creating from scratch..." );
    249        } catch( final IOException e ) {
    250            LOG.error( "IO error: {}", e.getMessage() );
    251        } catch( final Exception e ) {
    252            LOG.error( "Error initializing XML database from: {} {}", c_file.getAbsolutePath(), e.getMessage() );
    253        }
    254        if ( c_dom == null ) {
    255            try {
    256                //  Create the DOM from scratch
    257                c_dom = factory.newDocumentBuilder().newDocument();
    258                c_dom.appendChild( c_dom.createElement( "users" ) );
    259            } catch( final ParserConfigurationException e ) {
    260                LOG.fatal( "Could not create in-memory DOM" );
    261            }
    262        }
    263    }
    264
    265    private void saveDOM() throws WikiSecurityException {
    266        if( c_dom == null ) {
    267            throw new IllegalStateException( "FATAL: database does not exist" );
    268        }
    269
    270        final File newFile = new File( c_file.getAbsolutePath() + ".new" );
    271        try( final BufferedWriter io = new BufferedWriter( new OutputStreamWriter( Files.newOutputStream( newFile.toPath() ), StandardCharsets.UTF_8 ) ) ) {
    272
    273            // Write the file header and document root
    274            io.write( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" );
    275            io.write( "<users>\n" );
    276
    277            // Write each profile as a <user> node
    278            final Element root = c_dom.getDocumentElement();
    279            final NodeList nodes = root.getElementsByTagName( USER_TAG );
    280            for( int i = 0; i < nodes.getLength(); i++ ) {
    281                final Element user = ( Element )nodes.item( i );
    282                io.write( "    <" + USER_TAG + " " );
    283                io.write( UID );
    284                io.write( "=\"" + user.getAttribute( UID ) + "\" " );
    285                io.write( LOGIN_NAME );
    286                io.write( "=\"" + user.getAttribute( LOGIN_NAME ) + "\" " );
    287                io.write( WIKI_NAME );
    288                io.write( "=\"" + user.getAttribute( WIKI_NAME ) + "\" " );
    289                io.write( FULL_NAME );
    290                io.write( "=\"" + user.getAttribute( FULL_NAME ) + "\" " );
    291                io.write( EMAIL );
    292                io.write( "=\"" + user.getAttribute( EMAIL ) + "\" " );
    293                io.write( PASSWORD );
    294                io.write( "=\"" + user.getAttribute( PASSWORD ) + "\" " );
    295                io.write( CREATED );
    296                io.write( "=\"" + user.getAttribute( CREATED ) + "\" " );
    297                io.write( LAST_MODIFIED );
    298                io.write( "=\"" + user.getAttribute( LAST_MODIFIED ) + "\" " );
    299                io.write( LOCK_EXPIRY );
    300                io.write( "=\"" + user.getAttribute( LOCK_EXPIRY ) + "\" " );
    301                io.write( OLD_HASHES_TAG );
    302                io.write( "=\"" + user.getAttribute( OLD_HASHES_TAG ) + "\" " );
    303                io.write( ">" );
    304                final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG );
    305                for( int j = 0; j < attributes.getLength(); j++ ) {
    306                    final Element attribute = ( Element )attributes.item( j );
    307                    final String value = extractText( attribute );
    308                    io.write( "\n        <" + ATTRIBUTES_TAG + ">" );
    309                    io.write( value );
    310                    io.write( "</" + ATTRIBUTES_TAG + ">" );
    311                }
    312                io.write( "\n    </" + USER_TAG + ">\n" );
    313            }
    314            io.write( "</users>" );
    315        } catch( final IOException e ) {
    316            throw new WikiSecurityException( e.getLocalizedMessage(), e );
    317        }
    318
    319        // Copy new file over old version
    320        final File backup = new File( c_file.getAbsolutePath() + ".old" );
    321        if( backup.exists() ) {
    322            if( !backup.delete() ) {
    323                LOG.error( "Could not delete old user database backup: " + backup );
    324            }
    325        }
    326        if( !c_file.renameTo( backup ) ) {
    327            LOG.error( "Could not create user database backup: " + backup );
    328        }
    329        if( !newFile.renameTo( c_file ) ) {
    330            LOG.error( "Could not save database: " + backup + " restoring backup." );
    331            if( !backup.renameTo( c_file ) ) {
    332                LOG.error( "Restore failed. Check the file permissions." );
    333            }
    334            LOG.error( "Could not save database: " + c_file + ". Check the file permissions" );
    335        }
    336
    337        try (FileInputStream fis = new FileInputStream(c_file)) {
    338            byte[] hash = DigestUtils.sha256(fis);
    339            File checkFile = new File(c_file.getParent(), c_file.getName() + ".check");
    340            FileUtils.writeByteArrayToFile(checkFile, hash);
    341        } catch (Exception ex) {
    342            LOG.warn("Failed to recompute and/or save the check file", ex);
    343        }
    344    }
    345
    346    private long c_lastCheck;
    347    private long c_lastModified;
    348
    349    private void checkForRefresh() {
    350        final long time = System.currentTimeMillis();
    351        if( time - c_lastCheck > 60 * 1000L ) {
    352            final long lastModified = c_file.lastModified();
    353
    354            if( lastModified > c_lastModified ) {
    355                buildDOM();
    356            }
    357        }
    358    }
    359
    360    /**
    361     * {@inheritDoc}
    362     *
    363     * @see org.apache.wiki.auth.user.UserDatabase#rename(String, String)
    364     */
    365    @Override
    366    public synchronized void rename( final String loginName, final String newName) throws DuplicateUserException, WikiSecurityException {
    367        if( c_dom == null ) {
    368            LOG.fatal( "Could not rename profile '" + loginName + "'; database does not exist" );
    369            throw new IllegalStateException( "FATAL: database does not exist" );
    370        }
    371        checkForRefresh();
    372
    373        // Get the existing user; if not found, throws NoSuchPrincipalException
    374        final UserProfile profile = findByLoginName( loginName );
    375
    376        // Get user with the proposed name; if found, it's a collision
    377        try {
    378            final UserProfile otherProfile = findByLoginName( newName );
    379            if( otherProfile != null ) {
    380                throw new DuplicateUserException( "security.error.cannot.rename", newName );
    381            }
    382        } catch( final NoSuchPrincipalException e ) {
    383            LOG.debug(e.getMessage(), e);
    384            // Good! That means it's safe to save using the new name
    385        }
    386
    387        // Find the user with the old login id attribute, and change it
    388        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
    389        for( int i = 0; i < users.getLength(); i++ ) {
    390            final Element user = ( Element )users.item( i );
    391            if( user.getAttribute( LOGIN_NAME ).equals( loginName ) ) {
    392                final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
    393                final Date modDate = new Date( System.currentTimeMillis() );
    394                setAttribute( user, LOGIN_NAME, newName );
    395                setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) );
    396                profile.setLoginName( newName );
    397                profile.setLastModified( modDate );
    398                break;
    399            }
    400        }
    401
    402        // Commit to disk
    403        saveDOM();
    404    }
    405
    406    /** {@inheritDoc} */
    407    @Override
    408    public synchronized void save( final UserProfile profile ) throws WikiSecurityException {
    409        if ( c_dom == null ) {
    410            LOG.fatal( "Could not save profile " + profile + " database does not exist" );
    411            throw new IllegalStateException( "FATAL: database does not exist" );
    412        }
    413
    414        checkForRefresh();
    415
    416        final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
    417        final String index = profile.getLoginName();
    418        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
    419        Element user = IntStream.range(0, users.getLength()).mapToObj(i -> (Element) users.item(i)).filter(currentUser -> currentUser.getAttribute(LOGIN_NAME).equals(index)).findFirst().orElse(null);
    420
    421        boolean isNew = false;
    422
    423        final Date modDate = new Date( System.currentTimeMillis() );
    424        if( user == null ) {
    425            // Create new user node
    426            profile.setCreated( modDate );
    427            LOG.info( "Creating new user " + index );
    428            user = c_dom.createElement( USER_TAG );
    429            c_dom.getDocumentElement().appendChild( user );
    430            setAttribute( user, CREATED, c_format.format( profile.getCreated() ) );
    431            isNew = true;
    432        } else {
    433            // To update existing user node, delete old attributes first...
    434            final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG );
    435            for( int i = 0; i < attributes.getLength(); i++ ) {
    436                user.removeChild( attributes.item( i ) );
    437            }
    438        }
    439
    440        setAttribute( user, UID, profile.getUid() );
    441        setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) );
    442        setAttribute( user, LOGIN_NAME, profile.getLoginName() );
    443        setAttribute( user, FULL_NAME, profile.getFullname() );
    444        setAttribute( user, WIKI_NAME, profile.getWikiName() );
    445        setAttribute( user, EMAIL, profile.getEmail() );
    446        final Date lockExpiry = profile.getLockExpiry();
    447        setAttribute( user, LOCK_EXPIRY, lockExpiry == null ? "" : c_format.format( lockExpiry ) );
    448
    449        // Hash and save the new password if it's different from old one
    450        final String newPassword = profile.getPassword();
    451        if( newPassword != null && !newPassword.equals( "" ) ) {
    452            final String oldPassword = user.getAttribute( PASSWORD );
    453            if( !oldPassword.equals( newPassword ) ) {
    454                String newhash = getHash( newPassword );
    455                setAttribute( user, PASSWORD, newhash );
    456
    457                profile.getPreviousHashedCredentials().add(newhash);
    458                while (!profile.getPreviousHashedCredentials().isEmpty() &&
    459                        profile.getPreviousHashedCredentials().size() > m_passwordReusedCount) {
    460                    profile.getPreviousHashedCredentials().remove(0);
    461                }
    462
    463            }
    464        }
    465
    466        // Save the attributes as Base64 string
    467        if(!profile.getAttributes().isEmpty()) {
    468            try {
    469                final String encodedAttributes = Serializer.serializeToBase64( profile.getAttributes() );
    470                final Element attributes = c_dom.createElement( ATTRIBUTES_TAG );
    471                user.appendChild( attributes );
    472                final Text value = c_dom.createTextNode( encodedAttributes );
    473                attributes.appendChild( value );
    474            } catch( final IOException e ) {
    475                throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e );
    476            }
    477        }
    478        if (!profile.getPreviousHashedCredentials().isEmpty()) {
    479            setAttribute( user, OLD_HASHES_TAG, StringUtils.join(profile.getPreviousHashedCredentials(), "|"));
    480        }
    481
    482        // Set the profile timestamps
    483        if( isNew ) {
    484            profile.setCreated( modDate );
    485        }
    486        profile.setLastModified( modDate );
    487
    488        // Commit to disk
    489        saveDOM();
    490    }
    491
    492    /**
    493     * Private method that returns the first {@link UserProfile}matching a &lt;user&gt; element's supplied attribute. This method will also
    494     * set the UID if it has not yet been set.
    495     *
    496     * @param matchAttribute matching attribute
    497     * @param index value to match
    498     * @return the profile, or <code>null</code> if not found
    499     */
    500    private UserProfile findByAttribute( final String matchAttribute, String index ) {
    501        if ( c_dom == null ) {
    502            throw new IllegalStateException( "FATAL: database does not exist" );
    503        }
    504
    505        checkForRefresh();
    506        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
    507        if( users == null ) {
    508            return null;
    509        }
    510
    511        // check if we have to do a case-insensitive compare
    512        final boolean caseSensitiveCompare = !matchAttribute.equals( EMAIL );
    513
    514        for( int i = 0; i < users.getLength(); i++ ) {
    515            final Element user = (Element) users.item( i );
    516            String userAttribute = user.getAttribute( matchAttribute );
    517            if( !caseSensitiveCompare ) {
    518                userAttribute = StringUtils.lowerCase(userAttribute);
    519                index = StringUtils.lowerCase(index);
    520            }
    521            if( userAttribute.equals( index ) ) {
    522                final UserProfile profile = newProfile();
    523
    524                // Parse basic attributes
    525                profile.setUid( user.getAttribute( UID ) );
    526                if( profile.getUid() == null || profile.getUid().isEmpty() ) {
    527                    profile.setUid( generateUid( this ) );
    528                }
    529                profile.setLoginName( user.getAttribute( LOGIN_NAME ) );
    530                profile.setFullname( user.getAttribute( FULL_NAME ) );
    531                profile.setPassword( user.getAttribute( PASSWORD ) );
    532                profile.setEmail( user.getAttribute( EMAIL ) );
    533
    534                // Get created/modified timestamps
    535                final String created = user.getAttribute( CREATED );
    536                final String modified = user.getAttribute( LAST_MODIFIED );
    537                profile.setCreated( parseDate( profile, created ) );
    538                profile.setLastModified( parseDate( profile, modified ) );
    539
    540                // Is the profile locked?
    541                final String lockExpiry = user.getAttribute( LOCK_EXPIRY );
    542                if( StringUtils.isEmpty( lockExpiry ) || lockExpiry.isEmpty() ) {
    543                    profile.setLockExpiry( null );
    544                } else {
    545                    profile.setLockExpiry( new Date( Long.parseLong( lockExpiry ) ) );
    546                }
    547                final String oldHahes = user.getAttribute(OLD_HASHES_TAG);
    548                if (oldHahes != null && oldHahes.length() > 0) {
    549                    String[] parts = oldHahes.split("\\|");
    550                    for (String s : parts) {
    551                        profile.getPreviousHashedCredentials().add(s);
    552                    }
    553                }
    554
    555                // Extract all the user's attributes (should only be one attributes tag, but you never know!)
    556                final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG );
    557                for( int j = 0; j < attributes.getLength(); j++ ) {
    558                    final Element attribute = ( Element )attributes.item( j );
    559                    final String serializedMap = extractText( attribute );
    560                    try {
    561                        final Map< String, ? extends Serializable > map = Serializer.deserializeFromBase64( serializedMap );
    562                        profile.getAttributes().putAll( map );
    563                    } catch( final IOException e ) {
    564                        LOG.error( "Could not parse user profile attributes!", e );
    565                    }
    566                }
    567
    568                return profile;
    569            }
    570        }
    571        return null;
    572    }
    573
    574    /**
    575     * Extracts all the text nodes that are immediate children of an Element.
    576     *
    577     * @param element the base element
    578     * @return the text nodes that are immediate children of the base element, concatenated together
    579     */
    580    private String extractText( final Element element ) {
    581        String text = "";
    582        if( element.getChildNodes().getLength() > 0 ) {
    583            final NodeList children = element.getChildNodes();
    584            text = IntStream.range(0, children.getLength()).mapToObj(children::item).filter(child -> child.getNodeType() == Node.TEXT_NODE).map(child -> ((Text) child).getData()).collect(Collectors.joining());
    585        }
    586        return text;
    587    }
    588
    589    /**
    590     *  Tries to parse a date using the default format - then, for backwards compatibility reasons, tries the platform default.
    591     *
    592     *  @param profile profile associated to the date.
    593     *  @param date date to be parsed.
    594     *  @return A parsed date, or null, if both parse attempts fail.
    595     */
    596    private Date parseDate( final UserProfile profile, final String date ) {
    597        try {
    598            final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
    599            return c_format.parse( date );
    600        } catch( final ParseException e ) {
    601            try {
    602                return DateFormat.getDateTimeInstance().parse( date );
    603            } catch( final ParseException e2 ) {
    604                LOG.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + profile.getLoginName() + "'." +
    605                          " It may have been tampered with.", e2 );
    606            }
    607        }
    608        return null;
    609    }
    610
    611    /**
    612     * After loading the DOM, this method sanity-checks the dates in the DOM and makes sure they are formatted properly. This is sort-of
    613     * hacky, but it should work.
    614     */
    615    private void sanitizeDOM() {
    616        if( c_dom == null ) {
    617            throw new IllegalStateException( "FATAL: database does not exist" );
    618        }
    619
    620        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
    621        for( int i = 0; i < users.getLength(); i++ ) {
    622            final Element user = ( Element )users.item( i );
    623
    624            // Sanitize UID (and generate a new one if one does not exist)
    625            String uid = user.getAttribute( UID ).trim();
    626            if( StringUtils.isEmpty( uid ) || "-1".equals( uid ) ) {
    627                uid = String.valueOf( generateUid( this ) );
    628                user.setAttribute( UID, uid );
    629            }
    630
    631            // Sanitize dates
    632            final String loginName = user.getAttribute( LOGIN_NAME );
    633            String created = user.getAttribute( CREATED );
    634            String modified = user.getAttribute( LAST_MODIFIED );
    635            final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
    636            try {
    637                created = c_format.format( c_format.parse( created ) );
    638                modified = c_format.format( c_format.parse( modified ) );
    639                user.setAttribute( CREATED, created );
    640                user.setAttribute( LAST_MODIFIED, modified );
    641            } catch( final ParseException e ) {
    642                try {
    643                    created = c_format.format( DateFormat.getDateTimeInstance().parse( created ) );
    644                    modified = c_format.format( DateFormat.getDateTimeInstance().parse( modified ) );
    645                    user.setAttribute( CREATED, created );
    646                    user.setAttribute( LAST_MODIFIED, modified );
    647                } catch( final ParseException e2 ) {
    648                    LOG.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + loginName + "'."
    649                            + " It may have been tampered with." );
    650                }
    651            }
    652        }
    653    }
    654
    655    /**
    656     * Private method that sets an attribute value for a supplied DOM element.
    657     *
    658     * @param element the element whose attribute is to be set
    659     * @param attribute the name of the attribute to set
    660     * @param value the desired attribute value
    661     */
    662    private void setAttribute( final Element element, final String attribute, final String value ) {
    663        if( value != null ) {
    664            element.setAttribute( attribute, value );
    665        }
    666    }
    667
    668}