Java tutorial
/* * Copyright (C) 2013 denkbares GmbH * * This is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. * * This software is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with this software; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF * site: http://www.fsf.org. */ package de.knowwe.defi.usermanager; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Serializable; 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 javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.lang.StringUtils; import org.apache.wiki.WikiEngine; 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.auth.user.AbstractUserDatabase; import org.apache.wiki.auth.user.DuplicateUserException; import org.apache.wiki.auth.user.UserProfile; import org.apache.wiki.util.Serializer; 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 com.denkbares.utils.Log; /** * <p>Manages {@link org.apache.wiki.auth.user.DefaultUserProfile} objects using XML files for persistence. * Passwords are hashed using SHA1. User entries are simple <code><user></code> * elements under the root. User profile properties are attributes of the * element. For example:</p> * <blockquote><code> * <users><br/> * <user loginName="janne" fullName="Janne Jalkanen"<br/> * wikiName="JanneJalkanen" email="janne@ecyrd.com"<br/> * password="{SHA}457b08e825da547c3b77fbc1ff906a1d00a7daee"/><br/> * </users> * </code></blockquote> * <p>In this example, the un-hashed password is <code>myP@5sw0rd</code>. Passwords are hashed without salt.</p> * @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 = null; private File c_file = null; /** * Looks up and deletes the first {@link org.apache.wiki.auth.user.UserProfile} in the user database * that matches a profile having a given login name. If the user database * does not contain a user with a matching attribute, throws a * {@link org.apache.wiki.auth.NoSuchPrincipalException}. * @param loginName the login name of the user profile that shall be deleted */ @Override public synchronized void deleteByLoginName(String loginName) throws WikiSecurityException { if (c_dom == null) { throw new WikiSecurityException("FATAL: database does not exist"); } NodeList users = c_dom.getDocumentElement().getElementsByTagName(USER_TAG); for (int i = 0; i < users.getLength(); i++) { 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); } /** * Looks up and returns the first {@link org.apache.wiki.auth.user.UserProfile}in the user database * that matches a profile having a given e-mail address. If the user * database does not contain a user with a matching attribute, throws a * {@link org.apache.wiki.auth.NoSuchPrincipalException}. * @param index the e-mail address of the desired user profile * @return the user profile * @see org.apache.wiki.auth.user.UserDatabase#findByEmail(String) */ @Override public UserProfile findByEmail(String index) throws NoSuchPrincipalException { UserProfile profile = findByAttribute(EMAIL, index); if (profile != null) { return profile; } throw new NoSuchPrincipalException("Not in database: " + index); } /** * Looks up and returns the first {@link org.apache.wiki.auth.user.UserProfile}in the user database * that matches a profile having a given full name. If the user database * does not contain a user with a matching attribute, throws a * {@link org.apache.wiki.auth.NoSuchPrincipalException}. * @param index the fill name of the desired user profile * @return the user profile * @see org.apache.wiki.auth.user.UserDatabase#findByFullName(String) */ @Override public UserProfile findByFullName(String index) throws NoSuchPrincipalException { UserProfile profile = findByAttribute(FULL_NAME, index); if (profile != null) { return profile; } throw new NoSuchPrincipalException("Not in database: " + index); } /** * Looks up and returns the first {@link org.apache.wiki.auth.user.UserProfile}in the user database * that matches a profile having a given login name. If the user database * does not contain a user with a matching attribute, throws a * {@link org.apache.wiki.auth.NoSuchPrincipalException}. * @param index the login name of the desired user profile * @return the user profile * @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(String) */ @Override public UserProfile findByLoginName(String index) throws NoSuchPrincipalException { UserProfile profile = findByAttribute(LOGIN_NAME, index); if (profile != null) { return profile; } throw new NoSuchPrincipalException("Not in database: " + index); } /** * {@inheritDoc} */ @Override public UserProfile findByUid(String uid) throws NoSuchPrincipalException { UserProfile profile = findByAttribute(UID, uid); if (profile != null) { return profile; } throw new NoSuchPrincipalException("Not in database: " + uid); } /** * Looks up and returns the first {@link org.apache.wiki.auth.user.UserProfile}in the user database * that matches a profile having a given wiki name. If the user database * does not contain a user with a matching attribute, throws a * {@link org.apache.wiki.auth.NoSuchPrincipalException}. * @param index the wiki name of the desired user profile * @return the user profile * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(String) */ @Override public UserProfile findByWikiName(String index) throws NoSuchPrincipalException { UserProfile profile = findByAttribute(WIKI_NAME, index); if (profile != null) { return profile; } throw new NoSuchPrincipalException("Not in database: " + index); } /** * Returns all WikiNames that are stored in the UserDatabase * as an array of WikiPrincipal objects. If the database does not * contain any profiles, this method will return a zero-length * array. * @return the WikiNames * @throws org.apache.wiki.auth.WikiSecurityException In case things fail. */ @Override public Principal[] getWikiNames() throws WikiSecurityException { if (c_dom == null) { throw new IllegalStateException("FATAL: database does not exist"); } SortedSet<Principal> principals = new TreeSet<>(); NodeList users = c_dom.getElementsByTagName(USER_TAG); for (int i = 0; i < users.getLength(); i++) { Element user = (Element) users.item(i); String wikiName = user.getAttribute(WIKI_NAME); if (wikiName == null) { Log.warning("Detected null wiki name in XMLUserDataBase. Check your user database."); } else { Principal principal = new WikiPrincipal(wikiName, WikiPrincipal.WIKI_NAME); principals.add(principal); } } return principals.toArray(new Principal[principals.size()]); } /** * Initializes the user database based on values from a Properties object. * The properties object must contain a file path to the XML database file * whose key is {@link #PROP_USERDATABASE}. * @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.WikiEngine, * java.util.Properties) * @throws org.apache.wiki.api.exceptions.NoRequiredPropertyException if the user database cannot be located, parsed, or opened */ @Override public void initialize(WikiEngine engine, Properties props) throws NoRequiredPropertyException { File defaultFile = null; if (engine.getRootPath() == null) { Log.warning("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 String file = props.getProperty(PROP_USERDATABASE); if (file == null) { Log.warning("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 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(false); factory.setExpandEntityReferences(false); factory.setIgnoringComments(true); factory.setNamespaceAware(false); try { c_dom = factory.newDocumentBuilder().parse(c_file); Log.fine("Database successfully initialized"); c_lastModified = c_file.lastModified(); c_lastCheck = System.currentTimeMillis(); } catch (ParserConfigurationException e) { Log.severe("Configuration error: " + e.getMessage()); } catch (SAXException e) { Log.severe("SAX error: " + e.getMessage()); } catch (FileNotFoundException e) { Log.info("User database not found; creating from scratch..."); } catch (IOException e) { Log.severe("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 (ParserConfigurationException e) { Log.severe("Could not create in-memory DOM"); } } } private void saveDOM() throws WikiSecurityException { if (c_dom == null) { Log.severe("User database doesn't exist in memory."); } File newFile = new File(c_file.getAbsolutePath() + ".new"); try { BufferedWriter io = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newFile), "UTF-8")); // Write the file header and document root io.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); io.write("<users>\n"); // Write each profile as a <user> node Element root = c_dom.getDocumentElement(); NodeList nodes = root.getElementsByTagName(USER_TAG); for (int i = 0; i < nodes.getLength(); i++) { 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(">"); NodeList attributes = user.getElementsByTagName(ATTRIBUTES_TAG); for (int j = 0; j < attributes.getLength(); j++) { Element attribute = (Element) attributes.item(j); String value = extractText(attribute); io.write("\n <" + ATTRIBUTES_TAG + ">"); io.write(value); io.write("</" + ATTRIBUTES_TAG + ">"); } io.write("\n </" + USER_TAG + ">\n"); } io.write("</users>"); io.close(); } catch (IOException e) { throw new WikiSecurityException(e.getLocalizedMessage(), e); } // Copy new file over old version File backup = new File(c_file.getAbsolutePath() + ".old"); if (backup.exists()) { if (!backup.delete()) { Log.severe("Could not delete old user database backup: " + backup); } } if (!c_file.renameTo(backup)) { Log.severe("Could not create user database backup: " + backup); } if (!newFile.renameTo(c_file)) { Log.severe("Could not save database: " + backup + " restoring backup."); if (!backup.renameTo(c_file)) { Log.severe("Restore failed. Check the file permissions."); } Log.severe("Could not save database: " + c_file + ". Check the file permissions"); } } private long c_lastCheck = 0; private long c_lastModified = 0; private void checkForRefresh() { long time = System.currentTimeMillis(); if (time - c_lastCheck > 60 * 1000L) { long lastModified = c_file.lastModified(); if (lastModified > c_lastModified) { buildDOM(); } } } /** * @see org.apache.wiki.auth.user.UserDatabase#rename(String, String) */ @Override public synchronized void rename(String loginName, String newName) throws DuplicateUserException, WikiSecurityException { if (c_dom == null) { Log.severe("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 UserProfile profile = findByLoginName(loginName); // Get user with the proposed name; if found, it's a collision try { UserProfile otherProfile = findByLoginName(newName); if (otherProfile != null) { throw new DuplicateUserException("security.error.cannot.rename", newName); } } catch (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 NodeList users = c_dom.getElementsByTagName(USER_TAG); for (int i = 0; i < users.getLength(); i++) { Element user = (Element) users.item(i); if (user.getAttribute(LOGIN_NAME).equals(loginName)) { DateFormat c_format = new SimpleDateFormat(DATE_FORMAT); 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(); } /** * Saves a {@link org.apache.wiki.auth.user.UserProfile}to the user database, overwriting the * existing profile if it exists. The user name under which the profile * should be saved is returned by the supplied profile's * {@link org.apache.wiki.auth.user.UserProfile#getLoginName()}method. * @param profile the user profile to save * @throws org.apache.wiki.auth.WikiSecurityException if the profile cannot be saved */ @Override public synchronized void save(UserProfile profile) throws WikiSecurityException { if (c_dom == null) { Log.severe("Could not save profile " + profile + " database does not exist"); throw new IllegalStateException("FATAL: database does not exist"); } checkForRefresh(); DateFormat c_format = new SimpleDateFormat(DATE_FORMAT); String index = profile.getLoginName(); NodeList users = c_dom.getElementsByTagName(USER_TAG); Element user = null; for (int i = 0; i < users.getLength(); i++) { Element currentUser = (Element) users.item(i); if (currentUser.getAttribute(LOGIN_NAME).equals(index)) { user = currentUser; break; } } boolean isNew = false; 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... 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()); 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 String newPassword = profile.getPassword(); if (newPassword != null && !newPassword.equals("")) { String oldPassword = user.getAttribute(PASSWORD); if (!oldPassword.equals(newPassword)) { setAttribute(user, PASSWORD, getHash(newPassword)); } } // Save the attributes as as Base64 string if (!profile.getAttributes().isEmpty()) { try { String encodedAttributes = Serializer.serializeToBase64(profile.getAttributes()); Element attributes = c_dom.createElement(ATTRIBUTES_TAG); user.appendChild(attributes); Text value = c_dom.createTextNode(encodedAttributes); attributes.appendChild(value); } catch (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 org.apache.wiki.auth.user.UserProfile}matching a * <user> element's supplied attribute. This method will also * set the UID if it has not yet been set. * @param matchAttribute * @param index * @return the profile, or <code>null</code> if not found */ private UserProfile findByAttribute(String matchAttribute, String index) { if (c_dom == null) { throw new IllegalStateException("FATAL: database does not exist"); } checkForRefresh(); NodeList users = c_dom.getElementsByTagName(USER_TAG); if (users == null) return null; // check if we have to do a case insensitive compare boolean caseSensitiveCompare = true; if (matchAttribute.equals(EMAIL)) { caseSensitiveCompare = false; } for (int i = 0; i < users.getLength(); i++) { 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)) { 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 String created = user.getAttribute(CREATED); String modified = user.getAttribute(LAST_MODIFIED); profile.setCreated(parseDate(profile, created)); profile.setLastModified(parseDate(profile, modified)); // Is the profile locked? String lockExpiry = user.getAttribute(LOCK_EXPIRY); if (lockExpiry == null || lockExpiry.isEmpty()) { profile.setLockExpiry(null); } else { profile.setLockExpiry(parseDate(profile, lockExpiry)); } // Extract all of the user's attributes (should only be one attributes tag, but you never know!) NodeList attributes = user.getElementsByTagName(ATTRIBUTES_TAG); for (int j = 0; j < attributes.getLength(); j++) { Element attribute = (Element) attributes.item(j); String serializedMap = extractText(attribute); try { Map<String, ? extends Serializable> map = Serializer.deserializeFromBase64(serializedMap); profile.getAttributes().putAll(map); } catch (IOException e) { Log.severe("Could not parse user profile attributes!", e); } } return profile; } } return null; } /** * Extracts all of 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(Element element) { String text = ""; if (element.getChildNodes().getLength() > 0) { NodeList children = element.getChildNodes(); for (int k = 0; k < children.getLength(); k++) { Node child = children.item(k); if (child.getNodeType() == Node.TEXT_NODE) { text = text + ((Text) child).getData(); } } } return text; } /** * Tries to parse a date using the default format - then, for backwards * compatibility reasons, tries the platform default. * * @param profile * @param date * @return A parsed date, or null, if both parse attempts fail. */ private Date parseDate(UserProfile profile, String date) { try { DateFormat c_format = new SimpleDateFormat(DATE_FORMAT); return c_format.parse(date); } catch (ParseException e) { try { return DateFormat.getDateTimeInstance().parse(date); } catch (ParseException e2) { Log.warning("Could not parse 'created' or 'lastModified' " + "attribute for " + " profile '" + profile.getLoginName() + "'." + " It may have been tampered with."); } } 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"); } NodeList users = c_dom.getElementsByTagName(USER_TAG); for (int i = 0; i < users.getLength(); i++) { 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 (uid == null || uid.isEmpty() || "-1".equals(uid)) { uid = String.valueOf(generateUid(this)); user.setAttribute(UID, uid); } // Sanitize dates String loginName = user.getAttribute(LOGIN_NAME); String created = user.getAttribute(CREATED); String modified = user.getAttribute(LAST_MODIFIED); 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 (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 (ParseException e2) { Log.warning("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(Element element, String attribute, String value) { if (value != null) { element.setAttribute(attribute, value); } } }