From 95a04c0f391119660efdbacc82cc69e4e6705d72 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 11 Jul 2012 13:02:15 -0400 Subject: [PATCH 01/18] [DS-861] Begin to add password salting. --- .../main/java/org/dspace/eperson/EPerson.java | 37 +++++++++++++------ dspace/etc/h2/database_schema.sql | 4 +- dspace/etc/oracle/database_schema.sql | 4 +- dspace/etc/postgres/database_schema.sql | 4 +- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java index 2c54a53fd0..e8685a8e34 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java @@ -10,6 +10,8 @@ package org.dspace.eperson; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; import org.apache.log4j.Logger; import org.dspace.authorize.AuthorizeException; @@ -851,22 +853,23 @@ public class EPerson extends DSpaceObject } /** - * Set the EPerson's password + * Set the EPerson's password. * * @param s - * the new email + * the new password. */ public void setPassword(String s) { - // FIXME: encoding - String encoded = Utils.getMD5(s); - - myRow.setColumn("password", encoded); + PasswordHash hash = new PasswordHash(s); + myRow.setColumn("password", Utils.toHex(hash.getHash())); + myRow.setColumn("salt", Utils.toHex(hash.getSalt())); + myRow.setColumn("digest_algorithm", hash.getAlgorithm()); modified = true; } /** - * Set the EPerson's password hash + * Set the EPerson's password hash. + * FIXME include the salt and algorithm * * @param s * hash of the password @@ -878,7 +881,9 @@ public class EPerson extends DSpaceObject } /** - * Return the EPerson's password hash + * Return the EPerson's password hash. + * FIXME return an actual PasswordHash + * * @return hash of the password */ public String getPasswordHash() @@ -895,9 +900,19 @@ public class EPerson extends DSpaceObject */ public boolean checkPassword(String attempt) { - String encoded = Utils.getMD5(attempt); - - return (encoded.equals(myRow.getStringColumn("password"))); + PasswordHash myHash; + try + { + myHash = new PasswordHash( + myRow.getStringColumn("digest_algorithm"), + myRow.getStringColumn("salt"), + myRow.getStringColumn("password")); + } catch (DecoderException ex) + { + log.error(ex.getMessage()); + return false; + } + return myHash.matches(attempt); } /** diff --git a/dspace/etc/h2/database_schema.sql b/dspace/etc/h2/database_schema.sql index 392927fe76..abf6bae1b9 100644 --- a/dspace/etc/h2/database_schema.sql +++ b/dspace/etc/h2/database_schema.sql @@ -172,7 +172,9 @@ CREATE TABLE EPerson ( eperson_id INTEGER PRIMARY KEY, email VARCHAR(64), - password VARCHAR(64), + password VARCHAR(128), + salt VARCHAR(32), + digest_algorithm VARCHAR(16), firstname VARCHAR(64), lastname VARCHAR(64), can_log_in BOOL, diff --git a/dspace/etc/oracle/database_schema.sql b/dspace/etc/oracle/database_schema.sql index c5d4194673..169ef27034 100644 --- a/dspace/etc/oracle/database_schema.sql +++ b/dspace/etc/oracle/database_schema.sql @@ -126,7 +126,9 @@ CREATE TABLE EPerson ( eperson_id INTEGER PRIMARY KEY, email VARCHAR2(64) UNIQUE, - password VARCHAR2(64), + password VARCHAR2(128), + salt VARCHAR2(32), + digest_algorithm VARCHAR2(16), firstname VARCHAR2(64), lastname VARCHAR2(64), can_log_in NUMBER(1), diff --git a/dspace/etc/postgres/database_schema.sql b/dspace/etc/postgres/database_schema.sql index d9bd080632..cfecdbb829 100644 --- a/dspace/etc/postgres/database_schema.sql +++ b/dspace/etc/postgres/database_schema.sql @@ -163,7 +163,9 @@ CREATE TABLE EPerson ( eperson_id INTEGER PRIMARY KEY, email VARCHAR(64) UNIQUE, - password VARCHAR(64), + password VARCHAR(128), + salt VARCHAR(32), + digest_algorithm VARCHAR(16), firstname VARCHAR(64), lastname VARCHAR(64), can_log_in BOOL, From f3f9433bfef88e4e875a6282d1e0c7ff9db618ba Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 11 Jul 2012 13:04:24 -0400 Subject: [PATCH 02/18] [DS-861] Oops, forgot to add new files. --- .../java/org/dspace/eperson/PasswordHash.java | 220 ++++++++++++++++++ .../org/dspace/eperson/PasswordHashTest.java | 138 +++++++++++ dspace/etc/oracle/database_schema_18-30.sql | 28 +++ dspace/etc/postgres/database_schema_18-30.sql | 28 +++ 4 files changed, 414 insertions(+) create mode 100644 dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java create mode 100644 dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java create mode 100644 dspace/etc/oracle/database_schema_18-30.sql create mode 100644 dspace/etc/postgres/database_schema_18-30.sql diff --git a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java new file mode 100644 index 0000000000..fb88d613ed --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java @@ -0,0 +1,220 @@ + +package org.dspace.eperson; + +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.dspace.services.ConfigurationService; +import org.dspace.utils.DSpace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * For handling digested secrets (such as passwords). + * Use {@link PasswordHash(String, byte[], byte[])} to package and manipulate + * secrets that have already been hashed, and {@link PasswordHash(String)} for + * plaintext secrets. Compare a plaintext candidate to a hashed secret with + * {@link matches(String)}. + * + * @author mwood + */ +public class PasswordHash +{ + private static final Logger log = LoggerFactory.getLogger(PasswordHash.class); + private static final ConfigurationService config + = new DSpace().getConfigurationService(); + private static final Charset UTF_8 = Charset.forName("UTF-8"); // Should always succeed: UTF-8 is required + + private static final String DEFAULT_DIGEST_ALGORITHM = "SHA-512"; // XXX magic + private static final String ALGORITHM_PROPERTY = "passwordAuthentication.digestAlgorithm"; // FIXME better name? + private static final int SALT_BYTES = 128/8; // XXX magic we want 128 bits + private static final int HASH_ROUNDS = 1024; // XXX magic 1024 rounds + private static final int SEED_BYTES = 64; // XXX magic + + /** A secure random number generator instance. */ + private static SecureRandom rng = null; + + /** How many times has the RNG been called without re-seeding? */ + private static int rngUses; + + private String algorithm; + private byte[] salt; + private byte[] hash; + + /** Don't allow empty instances. */ + private PasswordHash() {} + + /** + * Construct a hash structure from existing data, just for passing around. + * + * @param algorithm the digest algorithm used in producing {@code hash}. + * If null or empty, assume MD5 (the original, only algorithm). + * @param salt the salt hashed with the secret. + * @param hash the hashed secret. + */ + public PasswordHash(String algorithm, byte[] salt, byte[] hash) + { + if ((null == algorithm) || algorithm.isEmpty()) + this.algorithm = "MD5"; + else + this.algorithm = algorithm; + + this.salt = salt; + + this.hash = hash; + } + + /** + * Convenience: like {@link PasswordHash(String, byte[], byte[])} but with + * hexadecimal-encoded {@code String}s. + * @param algorithm the digest algorithm used in producing {@code hash}. + * If null or empty, assume MD5. + * @param salt hexadecimal digits encoding the bytes of the salt. + * @param hash hexadecimal digits encoding the bytes of the hash. + * @throws DecoderException if salt or hash is not proper hexadecimal. + */ + public PasswordHash(String algorithm, String salt, String hash) + throws DecoderException + { + if ((null == algorithm) || algorithm.isEmpty()) + this.algorithm = "MD5"; + else + this.algorithm = algorithm; + + this.salt = Hex.decodeHex(salt.toCharArray()); + + this.hash = Hex.decodeHex(hash.toCharArray()); + } + + /** + * Construct a hash structure from a cleartext password using the configured + * digest algorithm. + * + * @param password the secret to be hashed. + */ + public PasswordHash(String password) + { + // Generate some salt + salt = generateSalt(); + + // What digest algorithm to use? + algorithm = config.getPropertyAsType(ALGORITHM_PROPERTY, DEFAULT_DIGEST_ALGORITHM); + + // Hash it! + try { + hash = digest(salt, algorithm, password); + } catch (NoSuchAlgorithmException e) { + log.error(e.getMessage()); + hash = new byte[] { 0 }; + } + } + + /** + * Is this the string whose hash I hold? + * + * @param secret string to be hashed and compared to this hash. + * @return true if secret hashes to the value held by this instance. + */ + public boolean matches(String secret) + { + byte[] candidate; + try { + candidate = digest(salt, algorithm, secret); + } catch (NoSuchAlgorithmException e) { + log.error(e.getMessage()); + return false; + } + return Arrays.equals(candidate, hash); + } + + /** + * Get the value of hash + * + * @return the value of hash + */ + public byte[] getHash() + { + return hash; + } + + /** + * Get the value of salt + * + * @return the value of salt + */ + public byte[] getSalt() + { + return salt; + } + + /** + * Get the value of algorithm + * + * @return the value of algorithm + */ + public String getAlgorithm() + { + return algorithm; + } + + /** Generate an array of random bytes. */ + private synchronized byte[] generateSalt() + { + // Initialize a random-number generator + if (null == rng) + { + rng = new SecureRandom(); + log.info("Initialized a random number stream using " + + rng.getAlgorithm() + " provided by " + rng.getProvider()); + rngUses = 0; + } + + if (rngUses++ > 100) + { // re-seed the generator periodically to break up possible patterns + rng.setSeed(rng.generateSeed(SEED_BYTES)); + rngUses = 0; + } + + salt = new byte[SALT_BYTES]; + rng.nextBytes(salt); + return salt; + } + + /** + * Generate a salted hash of a string using a given algorithm. + * + * @param salt random bytes to salt the hash. + * @param algorithm name of the digest algorithm to use. + * @param secret the string to be hashed. Null is treated as an empty string (""). + * @return hash bytes. + * @throws NoSuchAlgorithmException if algorithm is unknown. + */ + private byte[] digest(byte[] salt, String algorithm, String secret) + throws NoSuchAlgorithmException + { + if (null == secret) + secret = ""; + + // Set up a digest + MessageDigest digester = MessageDigest.getInstance(algorithm); + + // Grind up the salt with the password, yielding a hash + if (null != salt) + digester.update(salt); + + digester.update(secret.getBytes(UTF_8)); // Round 0 + + for (int round = 1; round < HASH_ROUNDS; round++) + { + byte[] lastRound = digester.digest(); + digester.reset(); + digester.update(lastRound); + } + + return digester.digest(); + } +} diff --git a/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java b/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java new file mode 100644 index 0000000000..3170a9c1a4 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java @@ -0,0 +1,138 @@ +package org.dspace.eperson; + +import org.dspace.servicemanager.DSpaceKernelInit; +import org.junit.*; +import static org.junit.Assert.*; + +/** + * + * @author mwood + */ +public class PasswordHashTest +{ + public PasswordHashTest() + { + } + + @BeforeClass + public static void setUpClass() + throws Exception + { + // Make certain that a default DSpaceKernel is started. + DSpaceKernelInit.getKernel(null).start(); + } + + @AfterClass + public static void tearDownClass() + throws Exception + { + } + + @Before + public void setUp() + { + } + + @After + public void tearDown() + { + } + + /** + * Test the constructors. + */ + @Test + public void testConstructors() + { + PasswordHash h1, h3; + + // Test null inputs, as from NULL database columns (old EPerson using + // unsalted hash, for example). + h3 = new PasswordHash(null, (byte[])null, (byte[]) null); + assertEquals("MD5", h3.getAlgorithm()); + assertNull(h3.getSalt()); + assertNull(h3.getHash()); + assertFalse(h3.matches(null)); + assertFalse(h3.matches("not null")); + + // Test single-argument constructor, which does the hashing. + String password = "I've got a secret."; + h1 = new PasswordHash(password); + assertEquals("SHA-512", h1.getAlgorithm()); + assertFalse(h1.matches("random rubbish")); + assertTrue(h1.matches(password)); + + // Test 3-argument constructor with non-null data. + h3 = new PasswordHash(h1.getAlgorithm(), h1.getSalt(), h1.getHash()); + assertTrue(h3.matches(password)); + } + + /** + * Test of matches method, of class PasswordHash. + */ + /* + @Test + public void testMatches() + { + System.out.println("matches"); + String secret = ""; + PasswordHash instance = null; + boolean expResult = false; + boolean result = instance.matches(secret); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + */ + + /** + * Test of getHash method, of class PasswordHash. + */ + /* + @Test + public void testGetHash() + { + System.out.println("getHash"); + PasswordHash instance = null; + byte[] expResult = null; + byte[] result = instance.getHash(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + */ + + /** + * Test of getSalt method, of class PasswordHash. + */ + /* + @Test + public void testGetSalt() + { + System.out.println("getSalt"); + PasswordHash instance = null; + byte[] expResult = null; + byte[] result = instance.getSalt(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + */ + + /** + * Test of getAlgorithm method, of class PasswordHash. + */ + /* + @Test + public void testGetAlgorithm() + { + System.out.println("getAlgorithm"); + PasswordHash instance = null; + String expResult = ""; + String result = instance.getAlgorithm(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + */ +} diff --git a/dspace/etc/oracle/database_schema_18-30.sql b/dspace/etc/oracle/database_schema_18-30.sql new file mode 100644 index 0000000000..ab68041003 --- /dev/null +++ b/dspace/etc/oracle/database_schema_18-30.sql @@ -0,0 +1,28 @@ +-- +-- database_schema_18-30.sql +-- +-- Version: $Revision$ +-- +-- Date: $Date$ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +-- +-- SQL commands to upgrade the database schema of a live DSpace 1.8 or 1.8.x +-- to the DSpace 3.0 database schema +-- +-- DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. +-- DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. +-- DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. +-- +------------------------------------------- +-- New columns and longer hash for salted password hashing DS-861 -- +------------------------------------------- +ALTER TABLE EPerson ALTER COLUMN password TYPE VARCHAR(128); +ALTER TABLE EPerson ADD salt VARCHAR(32); +ALTER TABLE EPerson ADD digest_algorithm VARCHAR(16); diff --git a/dspace/etc/postgres/database_schema_18-30.sql b/dspace/etc/postgres/database_schema_18-30.sql new file mode 100644 index 0000000000..ab68041003 --- /dev/null +++ b/dspace/etc/postgres/database_schema_18-30.sql @@ -0,0 +1,28 @@ +-- +-- database_schema_18-30.sql +-- +-- Version: $Revision$ +-- +-- Date: $Date$ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +-- +-- SQL commands to upgrade the database schema of a live DSpace 1.8 or 1.8.x +-- to the DSpace 3.0 database schema +-- +-- DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. +-- DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. +-- DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. DUMP YOUR DATABASE FIRST. +-- +------------------------------------------- +-- New columns and longer hash for salted password hashing DS-861 -- +------------------------------------------- +ALTER TABLE EPerson ALTER COLUMN password TYPE VARCHAR(128); +ALTER TABLE EPerson ADD salt VARCHAR(32); +ALTER TABLE EPerson ADD digest_algorithm VARCHAR(16); From 9b58faa601ae793037ea3e0b011f13564df652c2 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 11 Jul 2012 14:24:04 -0400 Subject: [PATCH 03/18] Password hashes are now class instances, not naked strings. --- .../content/packager/RoleDisseminator.java | 16 ++++++++- .../dspace/content/packager/RoleIngester.java | 31 +++++++++++++--- .../main/java/org/dspace/eperson/EPerson.java | 34 +++++++++++++----- .../java/org/dspace/eperson/PasswordHash.java | 35 +++++++++++++++++-- 4 files changed, 99 insertions(+), 17 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java index 772f298a7d..29169363c7 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java @@ -32,6 +32,7 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; +import org.dspace.eperson.PasswordHash; import org.jdom.Namespace; @@ -70,6 +71,8 @@ public class RoleDisseminator implements PackageDisseminator public static final String LAST_NAME = "LastName"; public static final String LANGUAGE = "Language"; public static final String PASSWORD_HASH = "PasswordHash"; + public static final String PASSWORD_DIGEST = "digest"; + public static final String PASSWORD_SALT = "salt"; public static final String CAN_LOGIN = "CanLogin"; public static final String REQUIRE_CERTIFICATE = "RequireCertificate"; public static final String SELF_REGISTERED = "SelfRegistered"; @@ -475,8 +478,19 @@ public class RoleDisseminator implements PackageDisseminator if (emitPassword) { + PasswordHash password = eperson.getPasswordHash(); + writer.writeStartElement(PASSWORD_HASH); - writer.writeCharacters(eperson.getPasswordHash()); + + String algorithm = password.getAlgorithm(); + if (null != algorithm) + writer.writeAttribute(PASSWORD_DIGEST, algorithm); + + String salt = password.getSaltString(); + if (null != salt) + writer.writeAttribute(PASSWORD_SALT, salt); + + writer.writeCharacters(password.getHashString()); writer.writeEndElement(); } diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java index 0be5710db3..5015433bd0 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java @@ -16,6 +16,7 @@ import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import org.apache.commons.codec.DecoderException; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; @@ -26,11 +27,10 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; +import org.dspace.eperson.PasswordHash; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; +import org.w3c.dom.*; import org.xml.sax.SAXException; /** @@ -171,7 +171,30 @@ public class RoleIngester implements PackageIngester data = user.getElementsByTagName(RoleDisseminator.PASSWORD_HASH); if (data.getLength() > 0) { - eperson.setPasswordHash(data.item(0).getTextContent()); + Node element = data.item(0); + NamedNodeMap attributes = element.getAttributes(); + + Node algorithm = attributes.getNamedItem(RoleDisseminator.PASSWORD_DIGEST); + String algorithmText; + if (null != algorithm) + algorithmText = algorithm.getNodeValue(); + else + algorithmText = null; + + Node salt = attributes.getNamedItem(RoleDisseminator.PASSWORD_SALT); + String saltText; + if (null != salt) + saltText = salt.getNodeValue(); + else + saltText = null; + + PasswordHash password; + try { + password = new PasswordHash(algorithmText, saltText, element.getTextContent()); + } catch (DecoderException ex) { + throw new PackageValidationException("Unable to decode hexadecimal password hash or salt", ex); + } + eperson.setPasswordHash(password); } else { diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java index e8685a8e34..1a179b0d4a 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java @@ -11,7 +11,6 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import org.apache.commons.codec.DecoderException; -import org.apache.commons.codec.binary.Hex; import org.apache.log4j.Logger; import org.dspace.authorize.AuthorizeException; @@ -869,26 +868,43 @@ public class EPerson extends DSpaceObject /** * Set the EPerson's password hash. - * FIXME include the salt and algorithm * - * @param s - * hash of the password + * @param password + * hashed password, or null to set row data to NULL. */ - public void setPasswordHash(String s) + public void setPasswordHash(PasswordHash password) { - myRow.setColumn("password", s); + if (null == password) + { + myRow.setColumnNull("digest_algorithm"); + myRow.setColumnNull("salt"); + myRow.setColumnNull("password"); + } + else + { + myRow.setColumn("digest_algorithm", password.getAlgorithm()); + myRow.setColumn("salt", password.getSaltString()); + myRow.setColumn("password", password.getHashString()); + } modified = true; } /** * Return the EPerson's password hash. - * FIXME return an actual PasswordHash * * @return hash of the password */ - public String getPasswordHash() + public PasswordHash getPasswordHash() { - return myRow.getStringColumn("password"); + PasswordHash hash = null; + try { + hash = new PasswordHash(myRow.getStringColumn("digestAlgorithm"), + myRow.getStringColumn("salt"), + myRow.getStringColumn("password")); + } catch (DecoderException ex) { + log.error("Problem decoding stored salt or hash: " + ex.getMessage()); + } + return hash; } /** diff --git a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java index fb88d613ed..8938a26cea 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java +++ b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java @@ -85,7 +85,10 @@ public class PasswordHash else this.algorithm = algorithm; - this.salt = Hex.decodeHex(salt.toCharArray()); + if (null == salt) + this.salt = null; + else + this.salt = Hex.decodeHex(salt.toCharArray()); this.hash = Hex.decodeHex(hash.toCharArray()); } @@ -132,7 +135,7 @@ public class PasswordHash } /** - * Get the value of hash + * Get the hash. * * @return the value of hash */ @@ -142,7 +145,20 @@ public class PasswordHash } /** - * Get the value of salt + * Get the hash, as a String. + * + * @return hash encoded as hexadecimal digits, or null if none. + */ + public String getHashString() + { + if (null != hash) + return new String(Hex.encodeHex(hash)); + else + return null; + } + + /** + * Get the salt. * * @return the value of salt */ @@ -151,6 +167,19 @@ public class PasswordHash return salt; } + /** + * Get the salt, as a String. + * + * @return salt encoded as hexadecimal digits, or null if none. + */ + public String getSaltString() + { + if (null != salt) + return new String(Hex.encodeHex(salt)); + else + return null; + } + /** * Get the value of algorithm * From ee6fe99d1ea8d241738489df37a78234af1a0621 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 11 Jul 2012 14:26:57 -0400 Subject: [PATCH 04/18] Fix CRLF line terminators --- .../content/packager/RoleDisseminator.java | 1326 ++++++++--------- .../dspace/content/packager/RoleIngester.java | 1090 +++++++------- 2 files changed, 1208 insertions(+), 1208 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java index 29169363c7..b871d6b4aa 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java @@ -1,663 +1,663 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.content.packager; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; - -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import org.apache.log4j.Logger; - -import org.dspace.authorize.AuthorizeException; -import org.dspace.content.Collection; -import org.dspace.content.Community; -import org.dspace.content.DSpaceObject; -import org.dspace.content.crosswalk.CrosswalkException; -import org.dspace.core.Constants; -import org.dspace.core.Context; -import org.dspace.eperson.EPerson; -import org.dspace.eperson.Group; -import org.dspace.eperson.PasswordHash; - -import org.jdom.Namespace; - -/** - * Plugin to export all Group and EPerson objects in XML, perhaps for reloading. - * - * @author Mark Wood - */ -public class RoleDisseminator implements PackageDisseminator -{ - - /** log4j category */ - private static final Logger log = Logger.getLogger(RoleDisseminator.class); - - /** - * DSpace Roles XML Namespace in JDOM form. - */ - public static final Namespace DSROLES_NS = - Namespace.getNamespace("dsroles", "http://www.dspace.org/xmlns/dspace/dspace-roles"); - - public static final String DSPACE_ROLES = "DSpaceRoles"; - public static final String ID = "ID"; - public static final String GROUPS = "Groups"; - public static final String GROUP = "Group"; - public static final String NAME = "Name"; - public static final String TYPE = "Type"; - public static final String MEMBERS = "Members"; - public static final String MEMBER = "Member"; - public static final String MEMBER_GROUPS = "MemberGroups"; - public static final String MEMBER_GROUP = "MemberGroup"; - public static final String EPERSONS = "People"; - public static final String EPERSON = "Person"; - public static final String EMAIL = "Email"; - public static final String NETID = "Netid"; - public static final String FIRST_NAME = "FirstName"; - public static final String LAST_NAME = "LastName"; - public static final String LANGUAGE = "Language"; - public static final String PASSWORD_HASH = "PasswordHash"; - public static final String PASSWORD_DIGEST = "digest"; - public static final String PASSWORD_SALT = "salt"; - public static final String CAN_LOGIN = "CanLogin"; - public static final String REQUIRE_CERTIFICATE = "RequireCertificate"; - public static final String SELF_REGISTERED = "SelfRegistered"; - - // Valid type values for Groups (only used when Group is associated with a Community or Collection) - public static final String GROUP_TYPE_ADMIN = "ADMIN"; - public static final String GROUP_TYPE_SUBMIT = "SUBMIT"; - public static final String GROUP_TYPE_WORKFLOW_STEP_1 = "WORKFLOW_STEP_1"; - public static final String GROUP_TYPE_WORKFLOW_STEP_2 = "WORKFLOW_STEP_2"; - public static final String GROUP_TYPE_WORKFLOW_STEP_3 = "WORKFLOW_STEP_3"; - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageDisseminator#disseminate(org.dspace - * .core.Context, org.dspace.content.DSpaceObject, - * org.dspace.content.packager.PackageParameters, java.io.File) - */ - @Override - public void disseminate(Context context, DSpaceObject object, - PackageParameters params, File pkgFile) - throws PackageException, CrosswalkException, - AuthorizeException, SQLException, IOException - { - boolean emitPasswords = params.containsKey("passwords"); - - FileOutputStream fileOut = null; - try - { - //open file stream for writing - fileOut = new FileOutputStream(pkgFile); - writeToStream(context, object, fileOut, emitPasswords); - } - finally - { - //close file stream & save - if (fileOut != null) - { - fileOut.close(); - } - } - } - - /** - * Make serialized users and groups available on an InputStream, for code - * which wants to read one. - * - * @param emitPasswords true if password hashes should be included. - * @return the stream of XML representing users and groups. - * @throws IOException - * if a PipedOutputStream or PipedInputStream cannot be created. - */ - InputStream asStream(Context context, DSpaceObject object, boolean emitPasswords) - throws IOException - { - // Create a PipedOutputStream to which to write some XML - PipedOutputStream outStream = new PipedOutputStream(); - PipedInputStream inStream = new PipedInputStream(outStream); - - // Create a new Thread to push serialized objects into the pipe - Serializer serializer = new Serializer(context, object, outStream, - emitPasswords); - new Thread(serializer).start(); - - return inStream; - } - - /** - * Embody a thread for serializing users and groups. - * - * @author mwood - */ - private class Serializer implements Runnable - { - private Context context; - private DSpaceObject object; - private OutputStream stream; - private boolean emitPasswords; - - @SuppressWarnings("unused") - private Serializer() {} - - /** - * @param context - * @param object the DSpaceObject - * @param stream receives serialized user and group objects. Will be - * closed when serialization is complete. - * @param emitPasswords true if password hashes should be included. - */ - Serializer(Context context, DSpaceObject object, OutputStream stream, boolean emitPasswords) - { - this.context = context; - this.object = object; - this.stream = stream; - this.emitPasswords = emitPasswords; - } - - @Override - public void run() - { - try - { - writeToStream(context, object, stream, emitPasswords); - stream.close(); - } - catch (IOException e) - { - log.error(e); - } - catch (PackageException e) - { - log.error(e); - } - } - } - - /** - * Serialize users and groups to a stream. - * - * @param context - * @param stream receives the output. Is not closed by this method. - * @param emitPasswords true if password hashes should be included. - * @throws XMLStreamException - * @throws SQLException - */ - private void writeToStream(Context context, DSpaceObject object, OutputStream stream, - boolean emitPasswords) - throws PackageException - { - try - { - //First, find all Groups/People associated with our current Object - Group[] groups = findAssociatedGroups(context, object); - EPerson[] people = findAssociatedPeople(context, object); - - //Only continue if we've found Groups or People which we need to disseminate - if((groups!=null && groups.length>0) || - (people!=null && people.length>0)) - { - XMLOutputFactory factory = XMLOutputFactory.newInstance(); - XMLStreamWriter writer; - - writer = factory.createXMLStreamWriter(stream, "UTF-8"); - writer.setDefaultNamespace(DSROLES_NS.getURI()); - writer.writeStartDocument("UTF-8", "1.0"); - writer.writeStartElement(DSPACE_ROLES); - - //Only disseminate a element if some groups exist - if(groups!=null) - { - writer.writeStartElement(GROUPS); - - for (Group group : groups) - { - writeGroup(context, object, group, writer); - } - - writer.writeEndElement(); // GROUPS - } - - //Only disseminate an element if some people exist - if(people!=null) - { - writer.writeStartElement(EPERSONS); - - for (EPerson eperson : people) - { - writeEPerson(eperson, writer, emitPasswords); - } - - writer.writeEndElement(); // EPERSONS - } - - writer.writeEndElement(); // DSPACE_ROLES - writer.writeEndDocument(); - writer.close(); - }//end if Groups or People exist - } - catch (Exception e) - { - throw new PackageException(e); - } - } - - /* (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageDisseminator#disseminateAll(org.dspace - * .core.Context, org.dspace.content.DSpaceObject, - * org.dspace.content.packager.PackageParameters, java.io.File) - */ - @Override - public List disseminateAll(Context context, DSpaceObject dso, - PackageParameters params, File pkgFile) - throws PackageException, CrosswalkException, - AuthorizeException, SQLException, IOException - { - throw new PackageException("disseminateAll() is not implemented, as disseminate() method already handles dissemination of all roles to an external file."); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageDisseminator#getMIMEType(org.dspace - * .content.packager.PackageParameters) - */ - @Override - public String getMIMEType(PackageParameters params) - { - return "application/xml"; - } - - /** - * Emit XML describing a single Group. - * - * @param context - * the DSpace Context - * @parm relatedObject - * the DSpaceObject related to this group (if any) - * @param group - * the Group to describe - * @param write - * the description to this stream - */ - private void writeGroup(Context context, DSpaceObject relatedObject, Group group, XMLStreamWriter writer) - throws XMLStreamException, PackageException - { - //Translate the Group name for export. This ensures that groups with Internal IDs in their names - // (e.g. COLLECTION_1_ADMIN) are properly translated using the corresponding Handle or external identifier. - String exportGroupName = PackageUtils.translateGroupNameForExport(context, group.getName()); - - //If translated group name is returned as "null", this means the Group name - // had an Internal Collection/Community ID embedded, which could not be - // translated properly to a Handle. We will NOT export these groups, - // as they could cause conflicts or data integrity problems if they are - // imported into another DSpace system. - if(exportGroupName==null) - { - return; - } - - writer.writeStartElement(GROUP); - writer.writeAttribute(ID, String.valueOf(group.getID())); - writer.writeAttribute(NAME, exportGroupName); - - String groupType = getGroupType(relatedObject, group); - if(groupType!=null && !groupType.isEmpty()) - { - writer.writeAttribute(TYPE, groupType); - } - - //Add People to Group (if any belong to this group) - if(group.getMembers().length>0) - { - writer.writeStartElement(MEMBERS); - for (EPerson member : group.getMembers()) - { - writer.writeEmptyElement(MEMBER); - writer.writeAttribute(ID, String.valueOf(member.getID())); - writer.writeAttribute(NAME, member.getName()); - } - writer.writeEndElement(); - } - - //Add Groups as Member Groups (if any belong to this group) - if(group.getMemberGroups().length>0) - { - writer.writeStartElement(MEMBER_GROUPS); - for (Group member : group.getMemberGroups()) - { - String exportMemberName = PackageUtils.translateGroupNameForExport(context, member.getName()); - //Only export member group if its name can be properly translated for export. As noted above, - // we don't want groups that are *unable* to be accurately translated causing issues on import. - if(exportMemberName!=null) - { - writer.writeEmptyElement(MEMBER_GROUP); - writer.writeAttribute(ID, String.valueOf(member.getID())); - writer.writeAttribute(NAME, exportMemberName); - } - } - writer.writeEndElement(); - } - - writer.writeEndElement(); - } - - /** - * Return a Group Type string (see RoleDisseminator.GROUP_TYPE_* constants) - * which describes the type of group and its relation to the given object. - *

- * As a basic example, if the Group is a Collection Administration group, - * the Group Type string returned should be "ADMIN" - *

- * If type string cannot be determined, null is returned. - * - * @param dso - * the related DSpaceObject - * @param group - * the group - * @return a group type string or null - */ - private String getGroupType(DSpaceObject dso, Group group) - { - if (dso == null || group == null) - { - return null; - } - - if( dso.getType()==Constants.COMMUNITY) - { - Community community = (Community) dso; - - //Check if this is the ADMIN group for this community - if (group.equals(community.getAdministrators())) - { - return GROUP_TYPE_ADMIN; - } - } - else if(dso.getType() == Constants.COLLECTION) - { - Collection collection = (Collection) dso; - - if (group.equals(collection.getAdministrators())) - { - //Check if this is the ADMIN group for this collection - return GROUP_TYPE_ADMIN; - } - else if (group.equals(collection.getSubmitters())) - { - //Check if Submitters group - return GROUP_TYPE_SUBMIT; - } - else if (group.equals(collection.getWorkflowGroup(1))) - { - //Check if workflow step 1 group - return GROUP_TYPE_WORKFLOW_STEP_1; - } - else if (group.equals(collection.getWorkflowGroup(2))) - { - //check if workflow step 2 group - return GROUP_TYPE_WORKFLOW_STEP_2; - } - else if (group.equals(collection.getWorkflowGroup(3))) - { - //check if workflow step 3 group - return GROUP_TYPE_WORKFLOW_STEP_3; - } - } - - //by default, return null - return null; - } - - /** - * Emit XML describing a single EPerson. - * - * @param eperson - * the EPerson to describe - * @param write - * the description to this stream - * @param emitPassword - * do not export the password hash unless true - */ - private void writeEPerson(EPerson eperson, XMLStreamWriter writer, - boolean emitPassword) throws XMLStreamException - { - writer.writeStartElement(EPERSON); - writer.writeAttribute(ID, String.valueOf(eperson.getID())); - - writer.writeStartElement(EMAIL); - writer.writeCharacters(eperson.getEmail()); - writer.writeEndElement(); - - if(eperson.getNetid()!=null) - { - writer.writeStartElement(NETID); - writer.writeCharacters(eperson.getNetid()); - writer.writeEndElement(); - } - - if(eperson.getFirstName()!=null) - { - writer.writeStartElement(FIRST_NAME); - writer.writeCharacters(eperson.getFirstName()); - writer.writeEndElement(); - } - - if(eperson.getLastName()!=null) - { - writer.writeStartElement(LAST_NAME); - writer.writeCharacters(eperson.getLastName()); - writer.writeEndElement(); - } - - if(eperson.getLanguage()!=null) - { - writer.writeStartElement(LANGUAGE); - writer.writeCharacters(eperson.getLanguage()); - writer.writeEndElement(); - } - - if (emitPassword) - { - PasswordHash password = eperson.getPasswordHash(); - - writer.writeStartElement(PASSWORD_HASH); - - String algorithm = password.getAlgorithm(); - if (null != algorithm) - writer.writeAttribute(PASSWORD_DIGEST, algorithm); - - String salt = password.getSaltString(); - if (null != salt) - writer.writeAttribute(PASSWORD_SALT, salt); - - writer.writeCharacters(password.getHashString()); - writer.writeEndElement(); - } - - if (eperson.canLogIn()) - { - writer.writeEmptyElement(CAN_LOGIN); - } - - if (eperson.getRequireCertificate()) - { - writer.writeEmptyElement(REQUIRE_CERTIFICATE); - } - - if (eperson.getSelfRegistered()) - { - writer.writeEmptyElement(SELF_REGISTERED); - } - - writer.writeEndElement(); - } - - /** - * Find all Groups associated with this DSpace Object. - *

- * If object is SITE, all groups are returned. - *

- * If object is COMMUNITY or COLLECTION, only groups associated with - * those objects are returned (if any). - *

- * For all other objects, null is returned. - * - * @param context The DSpace context - * @param object the DSpace object - * @return array of all associated groups - */ - private Group[] findAssociatedGroups(Context context, DSpaceObject object) - throws SQLException - { - if(object.getType()==Constants.SITE) - { - // @TODO FIXME -- if there was a way to ONLY export Groups which are NOT - // associated with a Community or Collection, we should be doing that instead! - return Group.findAll(context, Group.NAME); - } - else if(object.getType()==Constants.COMMUNITY) - { - Community community = (Community) object; - - ArrayList list = new ArrayList(); - - //check for admin group - if(community.getAdministrators()!=null) - { - list.add(community.getAdministrators()); - } - - // FINAL CATCH-ALL -> Find any other groups where name begins with "COMMUNITY__" - // (There should be none, but this code is here just in case) - Group[] matchingGroups = Group.search(context, "COMMUNITY\\_" + community.getID() + "\\_"); - for(Group g : matchingGroups) - { - if(!list.contains(g)) - { - list.add(g); - } - } - - if(list.size()>0) - { - Group[] groupArray = new Group[list.size()]; - groupArray = (Group[]) list.toArray(groupArray); - return groupArray; - } - } - else if(object.getType()==Constants.COLLECTION) - { - Collection collection = (Collection) object; - - ArrayList list = new ArrayList(); - - //check for admin group - if(collection.getAdministrators()!=null) - { - list.add(collection.getAdministrators()); - } - //check for submitters group - if(collection.getSubmitters()!=null) - { - list.add(collection.getSubmitters()); - } - //check for workflow step 1 group - if(collection.getWorkflowGroup(1)!=null) - { - list.add(collection.getWorkflowGroup(1)); - } - //check for workflow step 2 group - if(collection.getWorkflowGroup(2)!=null) - { - list.add(collection.getWorkflowGroup(2)); - } - //check for workflow step 3 group - if(collection.getWorkflowGroup(3)!=null) - { - list.add(collection.getWorkflowGroup(3)); - } - - // FINAL CATCH-ALL -> Find any other groups where name begins with "COLLECTION__" - // (Necessary cause XMLUI allows you to generate a 'COLLECTION__DEFAULT_READ' group) - Group[] matchingGroups = Group.search(context, "COLLECTION\\_" + collection.getID() + "\\_"); - for(Group g : matchingGroups) - { - if(!list.contains(g)) - { - list.add(g); - } - } - - if(list.size()>0) - { - Group[] groupArray = new Group[list.size()]; - groupArray = (Group[]) list.toArray(groupArray); - return groupArray; - } - } - - //by default, return nothing - return null; - } - - - /** - * Find all EPeople associated with this DSpace Object. - *

- * If object is SITE, all people are returned. - *

- * For all other objects, null is returned. - * - * @param context The DSpace context - * @param object the DSpace object - * @return array of all associated EPerson objects - */ - private EPerson[] findAssociatedPeople(Context context, DSpaceObject object) - throws SQLException - { - if(object.getType()==Constants.SITE) - { - return EPerson.findAll(context, EPerson.EMAIL); - } - - //by default, return nothing - return null; - } - - /** - * Returns a user help string which should describe the - * additional valid command-line options that this packager - * implementation will accept when using the -o or - * --option flags with the Packager script. - * - * @return a string describing additional command-line options available - * with this packager - */ - @Override - public String getParameterHelp() - { - return "* passwords=[boolean] " + - "If true, user password hashes are also exported (so that they can be later restored). If false, user passwords are not exported. (Default is false)"; - } - -} +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.packager; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.apache.log4j.Logger; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.crosswalk.CrosswalkException; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.PasswordHash; + +import org.jdom.Namespace; + +/** + * Plugin to export all Group and EPerson objects in XML, perhaps for reloading. + * + * @author Mark Wood + */ +public class RoleDisseminator implements PackageDisseminator +{ + + /** log4j category */ + private static final Logger log = Logger.getLogger(RoleDisseminator.class); + + /** + * DSpace Roles XML Namespace in JDOM form. + */ + public static final Namespace DSROLES_NS = + Namespace.getNamespace("dsroles", "http://www.dspace.org/xmlns/dspace/dspace-roles"); + + public static final String DSPACE_ROLES = "DSpaceRoles"; + public static final String ID = "ID"; + public static final String GROUPS = "Groups"; + public static final String GROUP = "Group"; + public static final String NAME = "Name"; + public static final String TYPE = "Type"; + public static final String MEMBERS = "Members"; + public static final String MEMBER = "Member"; + public static final String MEMBER_GROUPS = "MemberGroups"; + public static final String MEMBER_GROUP = "MemberGroup"; + public static final String EPERSONS = "People"; + public static final String EPERSON = "Person"; + public static final String EMAIL = "Email"; + public static final String NETID = "Netid"; + public static final String FIRST_NAME = "FirstName"; + public static final String LAST_NAME = "LastName"; + public static final String LANGUAGE = "Language"; + public static final String PASSWORD_HASH = "PasswordHash"; + public static final String PASSWORD_DIGEST = "digest"; + public static final String PASSWORD_SALT = "salt"; + public static final String CAN_LOGIN = "CanLogin"; + public static final String REQUIRE_CERTIFICATE = "RequireCertificate"; + public static final String SELF_REGISTERED = "SelfRegistered"; + + // Valid type values for Groups (only used when Group is associated with a Community or Collection) + public static final String GROUP_TYPE_ADMIN = "ADMIN"; + public static final String GROUP_TYPE_SUBMIT = "SUBMIT"; + public static final String GROUP_TYPE_WORKFLOW_STEP_1 = "WORKFLOW_STEP_1"; + public static final String GROUP_TYPE_WORKFLOW_STEP_2 = "WORKFLOW_STEP_2"; + public static final String GROUP_TYPE_WORKFLOW_STEP_3 = "WORKFLOW_STEP_3"; + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageDisseminator#disseminate(org.dspace + * .core.Context, org.dspace.content.DSpaceObject, + * org.dspace.content.packager.PackageParameters, java.io.File) + */ + @Override + public void disseminate(Context context, DSpaceObject object, + PackageParameters params, File pkgFile) + throws PackageException, CrosswalkException, + AuthorizeException, SQLException, IOException + { + boolean emitPasswords = params.containsKey("passwords"); + + FileOutputStream fileOut = null; + try + { + //open file stream for writing + fileOut = new FileOutputStream(pkgFile); + writeToStream(context, object, fileOut, emitPasswords); + } + finally + { + //close file stream & save + if (fileOut != null) + { + fileOut.close(); + } + } + } + + /** + * Make serialized users and groups available on an InputStream, for code + * which wants to read one. + * + * @param emitPasswords true if password hashes should be included. + * @return the stream of XML representing users and groups. + * @throws IOException + * if a PipedOutputStream or PipedInputStream cannot be created. + */ + InputStream asStream(Context context, DSpaceObject object, boolean emitPasswords) + throws IOException + { + // Create a PipedOutputStream to which to write some XML + PipedOutputStream outStream = new PipedOutputStream(); + PipedInputStream inStream = new PipedInputStream(outStream); + + // Create a new Thread to push serialized objects into the pipe + Serializer serializer = new Serializer(context, object, outStream, + emitPasswords); + new Thread(serializer).start(); + + return inStream; + } + + /** + * Embody a thread for serializing users and groups. + * + * @author mwood + */ + private class Serializer implements Runnable + { + private Context context; + private DSpaceObject object; + private OutputStream stream; + private boolean emitPasswords; + + @SuppressWarnings("unused") + private Serializer() {} + + /** + * @param context + * @param object the DSpaceObject + * @param stream receives serialized user and group objects. Will be + * closed when serialization is complete. + * @param emitPasswords true if password hashes should be included. + */ + Serializer(Context context, DSpaceObject object, OutputStream stream, boolean emitPasswords) + { + this.context = context; + this.object = object; + this.stream = stream; + this.emitPasswords = emitPasswords; + } + + @Override + public void run() + { + try + { + writeToStream(context, object, stream, emitPasswords); + stream.close(); + } + catch (IOException e) + { + log.error(e); + } + catch (PackageException e) + { + log.error(e); + } + } + } + + /** + * Serialize users and groups to a stream. + * + * @param context + * @param stream receives the output. Is not closed by this method. + * @param emitPasswords true if password hashes should be included. + * @throws XMLStreamException + * @throws SQLException + */ + private void writeToStream(Context context, DSpaceObject object, OutputStream stream, + boolean emitPasswords) + throws PackageException + { + try + { + //First, find all Groups/People associated with our current Object + Group[] groups = findAssociatedGroups(context, object); + EPerson[] people = findAssociatedPeople(context, object); + + //Only continue if we've found Groups or People which we need to disseminate + if((groups!=null && groups.length>0) || + (people!=null && people.length>0)) + { + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + XMLStreamWriter writer; + + writer = factory.createXMLStreamWriter(stream, "UTF-8"); + writer.setDefaultNamespace(DSROLES_NS.getURI()); + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement(DSPACE_ROLES); + + //Only disseminate a element if some groups exist + if(groups!=null) + { + writer.writeStartElement(GROUPS); + + for (Group group : groups) + { + writeGroup(context, object, group, writer); + } + + writer.writeEndElement(); // GROUPS + } + + //Only disseminate an element if some people exist + if(people!=null) + { + writer.writeStartElement(EPERSONS); + + for (EPerson eperson : people) + { + writeEPerson(eperson, writer, emitPasswords); + } + + writer.writeEndElement(); // EPERSONS + } + + writer.writeEndElement(); // DSPACE_ROLES + writer.writeEndDocument(); + writer.close(); + }//end if Groups or People exist + } + catch (Exception e) + { + throw new PackageException(e); + } + } + + /* (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageDisseminator#disseminateAll(org.dspace + * .core.Context, org.dspace.content.DSpaceObject, + * org.dspace.content.packager.PackageParameters, java.io.File) + */ + @Override + public List disseminateAll(Context context, DSpaceObject dso, + PackageParameters params, File pkgFile) + throws PackageException, CrosswalkException, + AuthorizeException, SQLException, IOException + { + throw new PackageException("disseminateAll() is not implemented, as disseminate() method already handles dissemination of all roles to an external file."); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageDisseminator#getMIMEType(org.dspace + * .content.packager.PackageParameters) + */ + @Override + public String getMIMEType(PackageParameters params) + { + return "application/xml"; + } + + /** + * Emit XML describing a single Group. + * + * @param context + * the DSpace Context + * @parm relatedObject + * the DSpaceObject related to this group (if any) + * @param group + * the Group to describe + * @param write + * the description to this stream + */ + private void writeGroup(Context context, DSpaceObject relatedObject, Group group, XMLStreamWriter writer) + throws XMLStreamException, PackageException + { + //Translate the Group name for export. This ensures that groups with Internal IDs in their names + // (e.g. COLLECTION_1_ADMIN) are properly translated using the corresponding Handle or external identifier. + String exportGroupName = PackageUtils.translateGroupNameForExport(context, group.getName()); + + //If translated group name is returned as "null", this means the Group name + // had an Internal Collection/Community ID embedded, which could not be + // translated properly to a Handle. We will NOT export these groups, + // as they could cause conflicts or data integrity problems if they are + // imported into another DSpace system. + if(exportGroupName==null) + { + return; + } + + writer.writeStartElement(GROUP); + writer.writeAttribute(ID, String.valueOf(group.getID())); + writer.writeAttribute(NAME, exportGroupName); + + String groupType = getGroupType(relatedObject, group); + if(groupType!=null && !groupType.isEmpty()) + { + writer.writeAttribute(TYPE, groupType); + } + + //Add People to Group (if any belong to this group) + if(group.getMembers().length>0) + { + writer.writeStartElement(MEMBERS); + for (EPerson member : group.getMembers()) + { + writer.writeEmptyElement(MEMBER); + writer.writeAttribute(ID, String.valueOf(member.getID())); + writer.writeAttribute(NAME, member.getName()); + } + writer.writeEndElement(); + } + + //Add Groups as Member Groups (if any belong to this group) + if(group.getMemberGroups().length>0) + { + writer.writeStartElement(MEMBER_GROUPS); + for (Group member : group.getMemberGroups()) + { + String exportMemberName = PackageUtils.translateGroupNameForExport(context, member.getName()); + //Only export member group if its name can be properly translated for export. As noted above, + // we don't want groups that are *unable* to be accurately translated causing issues on import. + if(exportMemberName!=null) + { + writer.writeEmptyElement(MEMBER_GROUP); + writer.writeAttribute(ID, String.valueOf(member.getID())); + writer.writeAttribute(NAME, exportMemberName); + } + } + writer.writeEndElement(); + } + + writer.writeEndElement(); + } + + /** + * Return a Group Type string (see RoleDisseminator.GROUP_TYPE_* constants) + * which describes the type of group and its relation to the given object. + *

+ * As a basic example, if the Group is a Collection Administration group, + * the Group Type string returned should be "ADMIN" + *

+ * If type string cannot be determined, null is returned. + * + * @param dso + * the related DSpaceObject + * @param group + * the group + * @return a group type string or null + */ + private String getGroupType(DSpaceObject dso, Group group) + { + if (dso == null || group == null) + { + return null; + } + + if( dso.getType()==Constants.COMMUNITY) + { + Community community = (Community) dso; + + //Check if this is the ADMIN group for this community + if (group.equals(community.getAdministrators())) + { + return GROUP_TYPE_ADMIN; + } + } + else if(dso.getType() == Constants.COLLECTION) + { + Collection collection = (Collection) dso; + + if (group.equals(collection.getAdministrators())) + { + //Check if this is the ADMIN group for this collection + return GROUP_TYPE_ADMIN; + } + else if (group.equals(collection.getSubmitters())) + { + //Check if Submitters group + return GROUP_TYPE_SUBMIT; + } + else if (group.equals(collection.getWorkflowGroup(1))) + { + //Check if workflow step 1 group + return GROUP_TYPE_WORKFLOW_STEP_1; + } + else if (group.equals(collection.getWorkflowGroup(2))) + { + //check if workflow step 2 group + return GROUP_TYPE_WORKFLOW_STEP_2; + } + else if (group.equals(collection.getWorkflowGroup(3))) + { + //check if workflow step 3 group + return GROUP_TYPE_WORKFLOW_STEP_3; + } + } + + //by default, return null + return null; + } + + /** + * Emit XML describing a single EPerson. + * + * @param eperson + * the EPerson to describe + * @param write + * the description to this stream + * @param emitPassword + * do not export the password hash unless true + */ + private void writeEPerson(EPerson eperson, XMLStreamWriter writer, + boolean emitPassword) throws XMLStreamException + { + writer.writeStartElement(EPERSON); + writer.writeAttribute(ID, String.valueOf(eperson.getID())); + + writer.writeStartElement(EMAIL); + writer.writeCharacters(eperson.getEmail()); + writer.writeEndElement(); + + if(eperson.getNetid()!=null) + { + writer.writeStartElement(NETID); + writer.writeCharacters(eperson.getNetid()); + writer.writeEndElement(); + } + + if(eperson.getFirstName()!=null) + { + writer.writeStartElement(FIRST_NAME); + writer.writeCharacters(eperson.getFirstName()); + writer.writeEndElement(); + } + + if(eperson.getLastName()!=null) + { + writer.writeStartElement(LAST_NAME); + writer.writeCharacters(eperson.getLastName()); + writer.writeEndElement(); + } + + if(eperson.getLanguage()!=null) + { + writer.writeStartElement(LANGUAGE); + writer.writeCharacters(eperson.getLanguage()); + writer.writeEndElement(); + } + + if (emitPassword) + { + PasswordHash password = eperson.getPasswordHash(); + + writer.writeStartElement(PASSWORD_HASH); + + String algorithm = password.getAlgorithm(); + if (null != algorithm) + writer.writeAttribute(PASSWORD_DIGEST, algorithm); + + String salt = password.getSaltString(); + if (null != salt) + writer.writeAttribute(PASSWORD_SALT, salt); + + writer.writeCharacters(password.getHashString()); + writer.writeEndElement(); + } + + if (eperson.canLogIn()) + { + writer.writeEmptyElement(CAN_LOGIN); + } + + if (eperson.getRequireCertificate()) + { + writer.writeEmptyElement(REQUIRE_CERTIFICATE); + } + + if (eperson.getSelfRegistered()) + { + writer.writeEmptyElement(SELF_REGISTERED); + } + + writer.writeEndElement(); + } + + /** + * Find all Groups associated with this DSpace Object. + *

+ * If object is SITE, all groups are returned. + *

+ * If object is COMMUNITY or COLLECTION, only groups associated with + * those objects are returned (if any). + *

+ * For all other objects, null is returned. + * + * @param context The DSpace context + * @param object the DSpace object + * @return array of all associated groups + */ + private Group[] findAssociatedGroups(Context context, DSpaceObject object) + throws SQLException + { + if(object.getType()==Constants.SITE) + { + // @TODO FIXME -- if there was a way to ONLY export Groups which are NOT + // associated with a Community or Collection, we should be doing that instead! + return Group.findAll(context, Group.NAME); + } + else if(object.getType()==Constants.COMMUNITY) + { + Community community = (Community) object; + + ArrayList list = new ArrayList(); + + //check for admin group + if(community.getAdministrators()!=null) + { + list.add(community.getAdministrators()); + } + + // FINAL CATCH-ALL -> Find any other groups where name begins with "COMMUNITY__" + // (There should be none, but this code is here just in case) + Group[] matchingGroups = Group.search(context, "COMMUNITY\\_" + community.getID() + "\\_"); + for(Group g : matchingGroups) + { + if(!list.contains(g)) + { + list.add(g); + } + } + + if(list.size()>0) + { + Group[] groupArray = new Group[list.size()]; + groupArray = (Group[]) list.toArray(groupArray); + return groupArray; + } + } + else if(object.getType()==Constants.COLLECTION) + { + Collection collection = (Collection) object; + + ArrayList list = new ArrayList(); + + //check for admin group + if(collection.getAdministrators()!=null) + { + list.add(collection.getAdministrators()); + } + //check for submitters group + if(collection.getSubmitters()!=null) + { + list.add(collection.getSubmitters()); + } + //check for workflow step 1 group + if(collection.getWorkflowGroup(1)!=null) + { + list.add(collection.getWorkflowGroup(1)); + } + //check for workflow step 2 group + if(collection.getWorkflowGroup(2)!=null) + { + list.add(collection.getWorkflowGroup(2)); + } + //check for workflow step 3 group + if(collection.getWorkflowGroup(3)!=null) + { + list.add(collection.getWorkflowGroup(3)); + } + + // FINAL CATCH-ALL -> Find any other groups where name begins with "COLLECTION__" + // (Necessary cause XMLUI allows you to generate a 'COLLECTION__DEFAULT_READ' group) + Group[] matchingGroups = Group.search(context, "COLLECTION\\_" + collection.getID() + "\\_"); + for(Group g : matchingGroups) + { + if(!list.contains(g)) + { + list.add(g); + } + } + + if(list.size()>0) + { + Group[] groupArray = new Group[list.size()]; + groupArray = (Group[]) list.toArray(groupArray); + return groupArray; + } + } + + //by default, return nothing + return null; + } + + + /** + * Find all EPeople associated with this DSpace Object. + *

+ * If object is SITE, all people are returned. + *

+ * For all other objects, null is returned. + * + * @param context The DSpace context + * @param object the DSpace object + * @return array of all associated EPerson objects + */ + private EPerson[] findAssociatedPeople(Context context, DSpaceObject object) + throws SQLException + { + if(object.getType()==Constants.SITE) + { + return EPerson.findAll(context, EPerson.EMAIL); + } + + //by default, return nothing + return null; + } + + /** + * Returns a user help string which should describe the + * additional valid command-line options that this packager + * implementation will accept when using the -o or + * --option flags with the Packager script. + * + * @return a string describing additional command-line options available + * with this packager + */ + @Override + public String getParameterHelp() + { + return "* passwords=[boolean] " + + "If true, user password hashes are also exported (so that they can be later restored). If false, user passwords are not exported. (Default is false)"; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java index 5015433bd0..90e1cc0c37 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java @@ -1,545 +1,545 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.content.packager; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.sql.SQLException; -import java.util.List; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import org.apache.commons.codec.DecoderException; - -import org.dspace.authorize.AuthorizeException; -import org.dspace.content.Collection; -import org.dspace.content.Community; -import org.dspace.content.DSpaceObject; -import org.dspace.content.crosswalk.CrosswalkException; -import org.dspace.core.Constants; -import org.dspace.core.Context; -import org.dspace.eperson.EPerson; -import org.dspace.eperson.Group; -import org.dspace.eperson.PasswordHash; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.w3c.dom.*; -import org.xml.sax.SAXException; - -/** - * Create EPersons and Groups from a file of external representations. - * - * @author mwood - */ -public class RoleIngester implements PackageIngester -{ - private static final Logger log = LoggerFactory - .getLogger(RoleIngester.class); - - /** - * Common code to ingest roles from a Document. - * - * @param context - * DSpace Context - * @param parent - * the Parent DSpaceObject - * @param document - * the XML Document - * @throws SQLException - * @throws AuthorizeException - * @throws PackageException - */ - static void ingestDocument(Context context, DSpaceObject parent, - PackageParameters params, Document document) - throws SQLException, AuthorizeException, PackageException - { - String myEmail = context.getCurrentUser().getEmail(); - String myNetid = context.getCurrentUser().getNetid(); - - // Ingest users (EPersons) first so Groups can use them - NodeList users = document - .getElementsByTagName(RoleDisseminator.EPERSON); - for (int i = 0; i < users.getLength(); i++) - { - Element user = (Element) users.item(i); - // int userID = Integer.valueOf(user.getAttribute("ID")); // FIXME - // no way to set ID! - NodeList emails = user.getElementsByTagName(RoleDisseminator.EMAIL); - NodeList netids = user.getElementsByTagName(RoleDisseminator.NETID); - EPerson eperson; - EPerson collider; - String email = null; - String netid = null; - String identity; - if (emails.getLength() > 0) - { - email = emails.item(0).getTextContent(); - if (email.equals(myEmail)) - { - continue; // Cannot operate on my own EPerson! - } - identity = email; - collider = EPerson.findByEmail(context, identity); - // collider = EPerson.find(context, userID); - } - else if (netids.getLength() > 0) - { - netid = netids.item(0).getTextContent(); - if (netid.equals(myNetid)) - { - continue; // Cannot operate on my own EPerson! - } - identity = netid; - collider = EPerson.findByNetid(context, identity); - } - else - { - throw new PackageException("EPerson has neither email nor netid."); - } - - if (null != collider) - if (params.replaceModeEnabled()) // -r -f - { - eperson = collider; - } - else if (params.keepExistingModeEnabled()) // -r -k - { - log.warn("Existing EPerson {} was not restored from the package.", identity); - continue; - } - else - { - throw new PackageException("EPerson " + identity + " already exists."); - } - else - { - eperson = EPerson.create(context); - log.info("Created EPerson {}.", identity); - } - - eperson.setEmail(email); - eperson.setNetid(netid); - - NodeList data; - - data = user.getElementsByTagName(RoleDisseminator.FIRST_NAME); - if (data.getLength() > 0) - { - eperson.setFirstName(data.item(0).getTextContent()); - } - else - { - eperson.setFirstName(null); - } - - data = user.getElementsByTagName(RoleDisseminator.LAST_NAME); - if (data.getLength() > 0) - { - eperson.setLastName(data.item(0).getTextContent()); - } - else - { - eperson.setLastName(null); - } - - data = user.getElementsByTagName(RoleDisseminator.LANGUAGE); - if (data.getLength() > 0) - { - eperson.setLanguage(data.item(0).getTextContent()); - } - else - { - eperson.setLanguage(null); - } - - data = user.getElementsByTagName(RoleDisseminator.CAN_LOGIN); - eperson.setCanLogIn(data.getLength() > 0); - - data = user.getElementsByTagName(RoleDisseminator.REQUIRE_CERTIFICATE); - eperson.setRequireCertificate(data.getLength() > 0); - - data = user.getElementsByTagName(RoleDisseminator.SELF_REGISTERED); - eperson.setSelfRegistered(data.getLength() > 0); - - data = user.getElementsByTagName(RoleDisseminator.PASSWORD_HASH); - if (data.getLength() > 0) - { - Node element = data.item(0); - NamedNodeMap attributes = element.getAttributes(); - - Node algorithm = attributes.getNamedItem(RoleDisseminator.PASSWORD_DIGEST); - String algorithmText; - if (null != algorithm) - algorithmText = algorithm.getNodeValue(); - else - algorithmText = null; - - Node salt = attributes.getNamedItem(RoleDisseminator.PASSWORD_SALT); - String saltText; - if (null != salt) - saltText = salt.getNodeValue(); - else - saltText = null; - - PasswordHash password; - try { - password = new PasswordHash(algorithmText, saltText, element.getTextContent()); - } catch (DecoderException ex) { - throw new PackageValidationException("Unable to decode hexadecimal password hash or salt", ex); - } - eperson.setPasswordHash(password); - } - else - { - eperson.setPasswordHash(null); - } - - // Actually write Eperson info to DB - // NOTE: this update() doesn't call a commit(). So, Eperson info - // may still be rolled back if a subsequent error occurs - eperson.update(); - } - - // Now ingest the Groups - NodeList groups = document.getElementsByTagName(RoleDisseminator.GROUP); - - // Create the groups and add their EPerson members - for (int groupx = 0; groupx < groups.getLength(); groupx++) - { - Element group = (Element) groups.item(groupx); - String name = group.getAttribute(RoleDisseminator.NAME); - - try - { - //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) - // TODO: is this necessary? can we leave it in format with Handle in place of ? - // For now, this is necessary, because we don't want to accidentally - // create a new group COLLECTION_hdl:123/34_ADMIN, which is equivalent - // to an existing COLLECTION_45_ADMIN group - name = PackageUtils.translateGroupNameForImport(context, name); - } - catch(PackageException pe) - { - // If an error is thrown, then this Group corresponds to a - // Community or Collection that doesn't currently exist in the - // system. So, log a warning & skip it for now. - log.warn("Skipping group named '" + name + "' as it seems to correspond to a Community or Collection that does not exist in the system. " + - "If you are performing an AIP restore, you can ignore this warning as the Community/Collection AIP will likely create this group once it is processed."); - continue; - } - - Group groupObj = null; // The group to restore - Group collider = Group.findByName(context, name); // Existing group? - if (null != collider) - { // Group already exists, so empty it - if (params.replaceModeEnabled()) // -r -f - { - for (Group member : collider.getMemberGroups()) - { - collider.removeMember(member); - } - for (EPerson member : collider.getMembers()) - { - // Remove all group members *EXCEPT* we don't ever want - // to remove the current user from the list of Administrators - // (otherwise remainder of ingest will fail) - if(!(collider.equals(Group.find(context, 1)) && - member.equals(context.getCurrentUser()))) - { - collider.removeMember(member); - } - } - log.info("Existing Group {} was cleared. Its members will be replaced.", name); - groupObj = collider; - } - else if (params.keepExistingModeEnabled()) // -r -k - { - log.warn("Existing Group {} was not replaced from the package.", - name); - continue; - } - else - { - throw new PackageException("Group " + name + " already exists"); - } - } - else - { // No such group exists -- so, we'll need to create it! - - // First Check if this is a "typed" group (i.e. Community or Collection associated Group) - // If so, we'll create it via the Community or Collection - String type = group.getAttribute(RoleDisseminator.TYPE); - if(type!=null && !type.isEmpty() && parent!=null) - { - //What type of dspace object is this group associated with - if(parent.getType()==Constants.COLLECTION) - { - Collection collection = (Collection) parent; - - // Create this Collection-associated group, based on its group type - if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) - { - groupObj = collection.createAdministrators(); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_SUBMIT)) - { - groupObj = collection.createSubmitters(); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_1)) - { - groupObj = collection.createWorkflowGroup(1); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_2)) - { - groupObj = collection.createWorkflowGroup(2); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_3)) - { - groupObj = collection.createWorkflowGroup(3); - } - } - else if(parent.getType()==Constants.COMMUNITY) - { - Community community = (Community) parent; - - // Create this Community-associated group, based on its group type - if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) - { - groupObj = community.createAdministrators(); - } - } - //Ignore all other dspace object types - } - - //If group not yet created, create it with the given name - if(groupObj==null) - { - groupObj = Group.create(context); - } - - // Always set the name: parent.createBlop() is guessing - groupObj.setName(name); - - log.info("Created Group {}.", groupObj.getName()); - } - - // Add EPeople to newly created Group - NodeList members = group.getElementsByTagName(RoleDisseminator.MEMBER); - for (int memberx = 0; memberx < members.getLength(); memberx++) - { - Element member = (Element) members.item(memberx); - String memberName = member.getAttribute(RoleDisseminator.NAME); - EPerson memberEPerson = EPerson.findByEmail(context, memberName); - if (null != memberEPerson) - groupObj.addMember(memberEPerson); - else - throw new PackageValidationException("EPerson " + memberName - + " not found, not added to " + name); - } - - // Actually write Group info to DB - // NOTE: this update() doesn't call a commit(). So, Group info - // may still be rolled back if a subsequent error occurs - groupObj.update(); - - } - - // Go back and add Group members, now that all groups exist - for (int groupx = 0; groupx < groups.getLength(); groupx++) - { - Element group = (Element) groups.item(groupx); - String name = group.getAttribute(RoleDisseminator.NAME); - try - { - // Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) - name = PackageUtils.translateGroupNameForImport(context, name); - } - catch(PackageException pe) - { - // If an error is thrown, then this Group corresponds to a - // Community or Collection that doesn't currently exist in the - // system. So,skip it for now. - // (NOTE: We already logged a warning about this group earlier as - // this is the second time we are looping through all groups) - continue; - } - - // Find previously created group - Group groupObj = Group.findByName(context, name); - NodeList members = group - .getElementsByTagName(RoleDisseminator.MEMBER_GROUP); - for (int memberx = 0; memberx < members.getLength(); memberx++) - { - Element member = (Element) members.item(memberx); - String memberName = member.getAttribute(RoleDisseminator.NAME); - //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) - memberName = PackageUtils.translateGroupNameForImport(context, memberName); - // Find previously created group - Group memberGroup = Group.findByName(context, memberName); - groupObj.addMember(memberGroup); - } - // Actually update Group info in DB - // NOTE: Group info may still be rolled back if a subsequent error occurs - groupObj.update(); - } - } - - /** - * Ingest roles from an InputStream. - * - * @param context - * DSpace Context - * @param parent - * the Parent DSpaceObject - * @param stream - * the XML Document InputStream - * @throws PackageException - * @throws SQLException - * @throws AuthorizeException - */ - public static void ingestStream(Context context, DSpaceObject parent, - PackageParameters params, InputStream stream) - throws PackageException, SQLException, AuthorizeException - { - Document document; - - try - { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - dbf.setIgnoringComments(true); - dbf.setCoalescing(true); - DocumentBuilder db = dbf.newDocumentBuilder(); - document = db.parse(stream); - } - catch (ParserConfigurationException e) - { - throw new PackageException(e); - } - catch (SAXException e) - { - throw new PackageException(e); - } - catch (IOException e) - { - throw new PackageException(e); - } - /* - * TODO ? finally { close(stream); } - */ - ingestDocument(context, parent, params, document); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#ingest(org.dspace.core.Context - * , org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters, java.lang.String) - */ - @Override - public DSpaceObject ingest(Context context, DSpaceObject parent, - File pkgFile, PackageParameters params, String license) - throws PackageException, CrosswalkException, AuthorizeException, - SQLException, IOException - { - Document document; - - try - { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - dbf.setIgnoringComments(true); - dbf.setCoalescing(true); - DocumentBuilder db = dbf.newDocumentBuilder(); - document = db.parse(pkgFile); - } - catch (ParserConfigurationException e) - { - throw new PackageException(e); - } - catch (SAXException e) - { - throw new PackageException(e); - } - ingestDocument(context, parent, params, document); - - /* Does not create a DSpaceObject */ - return null; - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#ingestAll(org.dspace.core - * .Context, org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters, java.lang.String) - */ - @Override - public List ingestAll(Context context, DSpaceObject parent, - File pkgFile, PackageParameters params, String license) - throws PackageException, UnsupportedOperationException, - CrosswalkException, AuthorizeException, SQLException, IOException - { - throw new PackageException( - "ingestAll() is not implemented, as ingest() method already handles ingestion of all roles from an external file."); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#replace(org.dspace.core.Context - * , org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters) - */ - @Override - public DSpaceObject replace(Context context, DSpaceObject dso, - File pkgFile, PackageParameters params) throws PackageException, - UnsupportedOperationException, CrosswalkException, - AuthorizeException, SQLException, IOException - { - //Just call ingest() -- this will perform a replacement as necessary - return ingest(context, dso, pkgFile, params, null); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#replaceAll(org.dspace.core - * .Context, org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters) - */ - @Override - public List replaceAll(Context context, DSpaceObject dso, - File pkgFile, PackageParameters params) throws PackageException, - UnsupportedOperationException, CrosswalkException, - AuthorizeException, SQLException, IOException - { - throw new PackageException( - "replaceAll() is not implemented, as replace() method already handles replacement of all roles from an external file."); - } - - /** - * Returns a user help string which should describe the - * additional valid command-line options that this packager - * implementation will accept when using the -o or - * --option flags with the Packager script. - * - * @return a string describing additional command-line options available - * with this packager - */ - @Override - public String getParameterHelp() - { - return "No additional options available."; - } -} +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.packager; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.commons.codec.DecoderException; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.crosswalk.CrosswalkException; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.PasswordHash; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.*; +import org.xml.sax.SAXException; + +/** + * Create EPersons and Groups from a file of external representations. + * + * @author mwood + */ +public class RoleIngester implements PackageIngester +{ + private static final Logger log = LoggerFactory + .getLogger(RoleIngester.class); + + /** + * Common code to ingest roles from a Document. + * + * @param context + * DSpace Context + * @param parent + * the Parent DSpaceObject + * @param document + * the XML Document + * @throws SQLException + * @throws AuthorizeException + * @throws PackageException + */ + static void ingestDocument(Context context, DSpaceObject parent, + PackageParameters params, Document document) + throws SQLException, AuthorizeException, PackageException + { + String myEmail = context.getCurrentUser().getEmail(); + String myNetid = context.getCurrentUser().getNetid(); + + // Ingest users (EPersons) first so Groups can use them + NodeList users = document + .getElementsByTagName(RoleDisseminator.EPERSON); + for (int i = 0; i < users.getLength(); i++) + { + Element user = (Element) users.item(i); + // int userID = Integer.valueOf(user.getAttribute("ID")); // FIXME + // no way to set ID! + NodeList emails = user.getElementsByTagName(RoleDisseminator.EMAIL); + NodeList netids = user.getElementsByTagName(RoleDisseminator.NETID); + EPerson eperson; + EPerson collider; + String email = null; + String netid = null; + String identity; + if (emails.getLength() > 0) + { + email = emails.item(0).getTextContent(); + if (email.equals(myEmail)) + { + continue; // Cannot operate on my own EPerson! + } + identity = email; + collider = EPerson.findByEmail(context, identity); + // collider = EPerson.find(context, userID); + } + else if (netids.getLength() > 0) + { + netid = netids.item(0).getTextContent(); + if (netid.equals(myNetid)) + { + continue; // Cannot operate on my own EPerson! + } + identity = netid; + collider = EPerson.findByNetid(context, identity); + } + else + { + throw new PackageException("EPerson has neither email nor netid."); + } + + if (null != collider) + if (params.replaceModeEnabled()) // -r -f + { + eperson = collider; + } + else if (params.keepExistingModeEnabled()) // -r -k + { + log.warn("Existing EPerson {} was not restored from the package.", identity); + continue; + } + else + { + throw new PackageException("EPerson " + identity + " already exists."); + } + else + { + eperson = EPerson.create(context); + log.info("Created EPerson {}.", identity); + } + + eperson.setEmail(email); + eperson.setNetid(netid); + + NodeList data; + + data = user.getElementsByTagName(RoleDisseminator.FIRST_NAME); + if (data.getLength() > 0) + { + eperson.setFirstName(data.item(0).getTextContent()); + } + else + { + eperson.setFirstName(null); + } + + data = user.getElementsByTagName(RoleDisseminator.LAST_NAME); + if (data.getLength() > 0) + { + eperson.setLastName(data.item(0).getTextContent()); + } + else + { + eperson.setLastName(null); + } + + data = user.getElementsByTagName(RoleDisseminator.LANGUAGE); + if (data.getLength() > 0) + { + eperson.setLanguage(data.item(0).getTextContent()); + } + else + { + eperson.setLanguage(null); + } + + data = user.getElementsByTagName(RoleDisseminator.CAN_LOGIN); + eperson.setCanLogIn(data.getLength() > 0); + + data = user.getElementsByTagName(RoleDisseminator.REQUIRE_CERTIFICATE); + eperson.setRequireCertificate(data.getLength() > 0); + + data = user.getElementsByTagName(RoleDisseminator.SELF_REGISTERED); + eperson.setSelfRegistered(data.getLength() > 0); + + data = user.getElementsByTagName(RoleDisseminator.PASSWORD_HASH); + if (data.getLength() > 0) + { + Node element = data.item(0); + NamedNodeMap attributes = element.getAttributes(); + + Node algorithm = attributes.getNamedItem(RoleDisseminator.PASSWORD_DIGEST); + String algorithmText; + if (null != algorithm) + algorithmText = algorithm.getNodeValue(); + else + algorithmText = null; + + Node salt = attributes.getNamedItem(RoleDisseminator.PASSWORD_SALT); + String saltText; + if (null != salt) + saltText = salt.getNodeValue(); + else + saltText = null; + + PasswordHash password; + try { + password = new PasswordHash(algorithmText, saltText, element.getTextContent()); + } catch (DecoderException ex) { + throw new PackageValidationException("Unable to decode hexadecimal password hash or salt", ex); + } + eperson.setPasswordHash(password); + } + else + { + eperson.setPasswordHash(null); + } + + // Actually write Eperson info to DB + // NOTE: this update() doesn't call a commit(). So, Eperson info + // may still be rolled back if a subsequent error occurs + eperson.update(); + } + + // Now ingest the Groups + NodeList groups = document.getElementsByTagName(RoleDisseminator.GROUP); + + // Create the groups and add their EPerson members + for (int groupx = 0; groupx < groups.getLength(); groupx++) + { + Element group = (Element) groups.item(groupx); + String name = group.getAttribute(RoleDisseminator.NAME); + + try + { + //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) + // TODO: is this necessary? can we leave it in format with Handle in place of ? + // For now, this is necessary, because we don't want to accidentally + // create a new group COLLECTION_hdl:123/34_ADMIN, which is equivalent + // to an existing COLLECTION_45_ADMIN group + name = PackageUtils.translateGroupNameForImport(context, name); + } + catch(PackageException pe) + { + // If an error is thrown, then this Group corresponds to a + // Community or Collection that doesn't currently exist in the + // system. So, log a warning & skip it for now. + log.warn("Skipping group named '" + name + "' as it seems to correspond to a Community or Collection that does not exist in the system. " + + "If you are performing an AIP restore, you can ignore this warning as the Community/Collection AIP will likely create this group once it is processed."); + continue; + } + + Group groupObj = null; // The group to restore + Group collider = Group.findByName(context, name); // Existing group? + if (null != collider) + { // Group already exists, so empty it + if (params.replaceModeEnabled()) // -r -f + { + for (Group member : collider.getMemberGroups()) + { + collider.removeMember(member); + } + for (EPerson member : collider.getMembers()) + { + // Remove all group members *EXCEPT* we don't ever want + // to remove the current user from the list of Administrators + // (otherwise remainder of ingest will fail) + if(!(collider.equals(Group.find(context, 1)) && + member.equals(context.getCurrentUser()))) + { + collider.removeMember(member); + } + } + log.info("Existing Group {} was cleared. Its members will be replaced.", name); + groupObj = collider; + } + else if (params.keepExistingModeEnabled()) // -r -k + { + log.warn("Existing Group {} was not replaced from the package.", + name); + continue; + } + else + { + throw new PackageException("Group " + name + " already exists"); + } + } + else + { // No such group exists -- so, we'll need to create it! + + // First Check if this is a "typed" group (i.e. Community or Collection associated Group) + // If so, we'll create it via the Community or Collection + String type = group.getAttribute(RoleDisseminator.TYPE); + if(type!=null && !type.isEmpty() && parent!=null) + { + //What type of dspace object is this group associated with + if(parent.getType()==Constants.COLLECTION) + { + Collection collection = (Collection) parent; + + // Create this Collection-associated group, based on its group type + if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) + { + groupObj = collection.createAdministrators(); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_SUBMIT)) + { + groupObj = collection.createSubmitters(); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_1)) + { + groupObj = collection.createWorkflowGroup(1); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_2)) + { + groupObj = collection.createWorkflowGroup(2); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_3)) + { + groupObj = collection.createWorkflowGroup(3); + } + } + else if(parent.getType()==Constants.COMMUNITY) + { + Community community = (Community) parent; + + // Create this Community-associated group, based on its group type + if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) + { + groupObj = community.createAdministrators(); + } + } + //Ignore all other dspace object types + } + + //If group not yet created, create it with the given name + if(groupObj==null) + { + groupObj = Group.create(context); + } + + // Always set the name: parent.createBlop() is guessing + groupObj.setName(name); + + log.info("Created Group {}.", groupObj.getName()); + } + + // Add EPeople to newly created Group + NodeList members = group.getElementsByTagName(RoleDisseminator.MEMBER); + for (int memberx = 0; memberx < members.getLength(); memberx++) + { + Element member = (Element) members.item(memberx); + String memberName = member.getAttribute(RoleDisseminator.NAME); + EPerson memberEPerson = EPerson.findByEmail(context, memberName); + if (null != memberEPerson) + groupObj.addMember(memberEPerson); + else + throw new PackageValidationException("EPerson " + memberName + + " not found, not added to " + name); + } + + // Actually write Group info to DB + // NOTE: this update() doesn't call a commit(). So, Group info + // may still be rolled back if a subsequent error occurs + groupObj.update(); + + } + + // Go back and add Group members, now that all groups exist + for (int groupx = 0; groupx < groups.getLength(); groupx++) + { + Element group = (Element) groups.item(groupx); + String name = group.getAttribute(RoleDisseminator.NAME); + try + { + // Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) + name = PackageUtils.translateGroupNameForImport(context, name); + } + catch(PackageException pe) + { + // If an error is thrown, then this Group corresponds to a + // Community or Collection that doesn't currently exist in the + // system. So,skip it for now. + // (NOTE: We already logged a warning about this group earlier as + // this is the second time we are looping through all groups) + continue; + } + + // Find previously created group + Group groupObj = Group.findByName(context, name); + NodeList members = group + .getElementsByTagName(RoleDisseminator.MEMBER_GROUP); + for (int memberx = 0; memberx < members.getLength(); memberx++) + { + Element member = (Element) members.item(memberx); + String memberName = member.getAttribute(RoleDisseminator.NAME); + //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) + memberName = PackageUtils.translateGroupNameForImport(context, memberName); + // Find previously created group + Group memberGroup = Group.findByName(context, memberName); + groupObj.addMember(memberGroup); + } + // Actually update Group info in DB + // NOTE: Group info may still be rolled back if a subsequent error occurs + groupObj.update(); + } + } + + /** + * Ingest roles from an InputStream. + * + * @param context + * DSpace Context + * @param parent + * the Parent DSpaceObject + * @param stream + * the XML Document InputStream + * @throws PackageException + * @throws SQLException + * @throws AuthorizeException + */ + public static void ingestStream(Context context, DSpaceObject parent, + PackageParameters params, InputStream stream) + throws PackageException, SQLException, AuthorizeException + { + Document document; + + try + { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setIgnoringComments(true); + dbf.setCoalescing(true); + DocumentBuilder db = dbf.newDocumentBuilder(); + document = db.parse(stream); + } + catch (ParserConfigurationException e) + { + throw new PackageException(e); + } + catch (SAXException e) + { + throw new PackageException(e); + } + catch (IOException e) + { + throw new PackageException(e); + } + /* + * TODO ? finally { close(stream); } + */ + ingestDocument(context, parent, params, document); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#ingest(org.dspace.core.Context + * , org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters, java.lang.String) + */ + @Override + public DSpaceObject ingest(Context context, DSpaceObject parent, + File pkgFile, PackageParameters params, String license) + throws PackageException, CrosswalkException, AuthorizeException, + SQLException, IOException + { + Document document; + + try + { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setIgnoringComments(true); + dbf.setCoalescing(true); + DocumentBuilder db = dbf.newDocumentBuilder(); + document = db.parse(pkgFile); + } + catch (ParserConfigurationException e) + { + throw new PackageException(e); + } + catch (SAXException e) + { + throw new PackageException(e); + } + ingestDocument(context, parent, params, document); + + /* Does not create a DSpaceObject */ + return null; + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#ingestAll(org.dspace.core + * .Context, org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters, java.lang.String) + */ + @Override + public List ingestAll(Context context, DSpaceObject parent, + File pkgFile, PackageParameters params, String license) + throws PackageException, UnsupportedOperationException, + CrosswalkException, AuthorizeException, SQLException, IOException + { + throw new PackageException( + "ingestAll() is not implemented, as ingest() method already handles ingestion of all roles from an external file."); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#replace(org.dspace.core.Context + * , org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters) + */ + @Override + public DSpaceObject replace(Context context, DSpaceObject dso, + File pkgFile, PackageParameters params) throws PackageException, + UnsupportedOperationException, CrosswalkException, + AuthorizeException, SQLException, IOException + { + //Just call ingest() -- this will perform a replacement as necessary + return ingest(context, dso, pkgFile, params, null); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#replaceAll(org.dspace.core + * .Context, org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters) + */ + @Override + public List replaceAll(Context context, DSpaceObject dso, + File pkgFile, PackageParameters params) throws PackageException, + UnsupportedOperationException, CrosswalkException, + AuthorizeException, SQLException, IOException + { + throw new PackageException( + "replaceAll() is not implemented, as replace() method already handles replacement of all roles from an external file."); + } + + /** + * Returns a user help string which should describe the + * additional valid command-line options that this packager + * implementation will accept when using the -o or + * --option flags with the Packager script. + * + * @return a string describing additional command-line options available + * with this packager + */ + @Override + public String getParameterHelp() + { + return "No additional options available."; + } +} From 5613bb6f572cee446246aeb7a87e66b6145cc487 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 12 Jul 2012 12:54:39 -0400 Subject: [PATCH 05/18] Add commandline tool to find unsalted password hashes --- .../main/java/org/dspace/eperson/Groomer.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 dspace-api/src/main/java/org/dspace/eperson/Groomer.java diff --git a/dspace-api/src/main/java/org/dspace/eperson/Groomer.java b/dspace-api/src/main/java/org/dspace/eperson/Groomer.java new file mode 100644 index 0000000000..6b9ffb7433 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/Groomer.java @@ -0,0 +1,64 @@ + +package org.dspace.eperson; + +import java.sql.SQLException; +import org.apache.commons.cli.*; +import org.dspace.core.Context; +import org.dspace.storage.rdbms.DatabaseManager; +import org.dspace.storage.rdbms.TableRow; +import org.dspace.storage.rdbms.TableRowIterator; + +/** + * Tools for manipulating EPersons and Groups. + * + * @author mwood + */ +public class Groomer +{ + /** + * Command line tool for "grooming" the EPerson collection. + */ + static public void main(String[] argv) + throws SQLException + { + final String USAGE = "EPerson -verb [option...]"; + + OptionGroup verbs = new OptionGroup(); + verbs.setRequired(true); + verbs.addOption(new Option("h", "help", false, "explain this tool")); + verbs.addOption(new Option("u", "unsalted", false, "list accounts with unsalted password hashes")); + + Options options = new Options(); + options.addOptionGroup(verbs); + + PosixParser parser = new PosixParser(); + CommandLine command = null; + try { + command = parser.parse(options, argv); + } catch (ParseException ex) { + System.err.println(ex.getMessage()); + if (! (ex instanceof MissingOptionException)) + new HelpFormatter().printHelp(USAGE, options); + System.exit(1); + } + + // Help the user + if (command.hasOption('h') || command.hasOption('?')) + { + new HelpFormatter().printHelp(USAGE, options); + } + // Scan for unsalted hashes + else if (command.hasOption('u')) + { + Context myContext = new Context(); + final TableRowIterator tri = DatabaseManager.query(myContext, + "SELECT email FROM EPerson WHERE password IS NOT NULL AND digest_algorithm IS NULL"); + for (TableRow row = tri.next(); tri.hasNext(); row = tri.next()) + System.out.println(row.getStringColumn("email")); + myContext.abort(); // No changes to commit + } + // Should not happen: verb option defined but no code! + else + System.err.println("Unimplemented verb: " + verbs.getSelected()); + } +} From 9e999640f60c5b9f341f6102dc7ab7072daf5933 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 1 Aug 2012 10:54:30 -0400 Subject: [PATCH 06/18] Special case for old 1-trip unsalted MD5 hashes --- .../java/org/dspace/eperson/PasswordHash.java | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java index 8938a26cea..0d5a45e88b 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java +++ b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java @@ -34,6 +34,7 @@ public class PasswordHash private static final int SALT_BYTES = 128/8; // XXX magic we want 128 bits private static final int HASH_ROUNDS = 1024; // XXX magic 1024 rounds private static final int SEED_BYTES = 64; // XXX magic + private static final int RESEED_INTERVAL = 100; // XXX magic /** A secure random number generator instance. */ private static SecureRandom rng = null; @@ -53,7 +54,7 @@ public class PasswordHash * * @param algorithm the digest algorithm used in producing {@code hash}. * If null or empty, assume MD5 (the original, only algorithm). - * @param salt the salt hashed with the secret. + * @param salt the salt hashed with the secret, or null. * @param hash the hashed secret. */ public PasswordHash(String algorithm, byte[] salt, byte[] hash) @@ -73,7 +74,7 @@ public class PasswordHash * hexadecimal-encoded {@code String}s. * @param algorithm the digest algorithm used in producing {@code hash}. * If null or empty, assume MD5. - * @param salt hexadecimal digits encoding the bytes of the salt. + * @param salt hexadecimal digits encoding the bytes of the salt, or null. * @param hash hexadecimal digits encoding the bytes of the hash. * @throws DecoderException if salt or hash is not proper hexadecimal. */ @@ -90,6 +91,8 @@ public class PasswordHash else this.salt = Hex.decodeHex(salt.toCharArray()); + if (null == hash) + throw new DecoderException("Hash may not be null"); this.hash = Hex.decodeHex(hash.toCharArray()); } @@ -197,13 +200,14 @@ public class PasswordHash if (null == rng) { rng = new SecureRandom(); - log.info("Initialized a random number stream using " - + rng.getAlgorithm() + " provided by " + rng.getProvider()); + log.info("Initialized a random number stream using {} provided by {}", + rng.getAlgorithm(), rng.getProvider()); rngUses = 0; } - if (rngUses++ > 100) + if (rngUses++ > RESEED_INTERVAL) { // re-seed the generator periodically to break up possible patterns + log.debug("Re-seeding the RNG"); rng.setSeed(rng.generateSeed(SEED_BYTES)); rngUses = 0; } @@ -217,7 +221,7 @@ public class PasswordHash * Generate a salted hash of a string using a given algorithm. * * @param salt random bytes to salt the hash. - * @param algorithm name of the digest algorithm to use. + * @param algorithm name of the digest algorithm to use. Assume unsalted MD5 if null. * @param secret the string to be hashed. Null is treated as an empty string (""). * @return hash bytes. * @throws NoSuchAlgorithmException if algorithm is unknown. @@ -225,11 +229,21 @@ public class PasswordHash private byte[] digest(byte[] salt, String algorithm, String secret) throws NoSuchAlgorithmException { + MessageDigest digester; + if (null == secret) secret = ""; + // Special case: old unsalted one-trip MD5 hash. + if (null == algorithm) + { + digester = MessageDigest.getInstance("MD5"); + digester.update(secret.getBytes(UTF_8)); + return digester.digest(); + } + // Set up a digest - MessageDigest digester = MessageDigest.getInstance(algorithm); + digester = MessageDigest.getInstance(algorithm); // Grind up the salt with the password, yielding a hash if (null != salt) From 2edac96df31a0ffab822b1a1559bc1b6b0a255b1 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 1 Aug 2012 10:55:22 -0400 Subject: [PATCH 07/18] Update old MD5 hashes on successful password check --- .../src/main/java/org/dspace/eperson/EPerson.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java index 1a179b0d4a..babc309423 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java @@ -908,7 +908,8 @@ public class EPerson extends DSpaceObject } /** - * Check EPerson's password + * Check EPerson's password. Side effect: original unsalted MD5 hashes are + * converted using the current algorithm. * * @param attempt * the password attempt @@ -928,7 +929,13 @@ public class EPerson extends DSpaceObject log.error(ex.getMessage()); return false; } - return myHash.matches(attempt); + boolean answer = myHash.matches(attempt); + + // If using the old unsalted hash, and this password is correct, update to a new hash + if (answer && (null == myRow.getStringColumn("digest_algorithm"))) + setPassword(attempt); + + return answer; } /** From fcaa35aa8f53453582cb382d6c36f02e83604732 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 2 Aug 2012 13:05:02 -0400 Subject: [PATCH 08/18] More tests, fix what they found, add license blocks --- .../main/java/org/dspace/eperson/Groomer.java | 7 +++ .../java/org/dspace/eperson/PasswordHash.java | 24 ++++++--- .../org/dspace/eperson/PasswordHashTest.java | 50 +++++++++++++------ 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/Groomer.java b/dspace-api/src/main/java/org/dspace/eperson/Groomer.java index 6b9ffb7433..435a954fd4 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/Groomer.java +++ b/dspace-api/src/main/java/org/dspace/eperson/Groomer.java @@ -1,3 +1,10 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ package org.dspace.eperson; diff --git a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java index 0d5a45e88b..dbea1c7b1d 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java +++ b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java @@ -1,3 +1,10 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ package org.dspace.eperson; @@ -53,14 +60,14 @@ public class PasswordHash * Construct a hash structure from existing data, just for passing around. * * @param algorithm the digest algorithm used in producing {@code hash}. - * If null or empty, assume MD5 (the original, only algorithm). + * If empty, set to null. Other methods will treat this as unsalted MD5. * @param salt the salt hashed with the secret, or null. * @param hash the hashed secret. */ public PasswordHash(String algorithm, byte[] salt, byte[] hash) { - if ((null == algorithm) || algorithm.isEmpty()) - this.algorithm = "MD5"; + if ((null != algorithm) && algorithm.isEmpty()) + this.algorithm = null; else this.algorithm = algorithm; @@ -73,7 +80,7 @@ public class PasswordHash * Convenience: like {@link PasswordHash(String, byte[], byte[])} but with * hexadecimal-encoded {@code String}s. * @param algorithm the digest algorithm used in producing {@code hash}. - * If null or empty, assume MD5. + * If empty, set to null. Other methods will treat this as unsalted MD5. * @param salt hexadecimal digits encoding the bytes of the salt, or null. * @param hash hexadecimal digits encoding the bytes of the hash. * @throws DecoderException if salt or hash is not proper hexadecimal. @@ -81,8 +88,8 @@ public class PasswordHash public PasswordHash(String algorithm, String salt, String hash) throws DecoderException { - if ((null == algorithm) || algorithm.isEmpty()) - this.algorithm = "MD5"; + if ((null != algorithm) && algorithm.isEmpty()) + this.algorithm = null; else this.algorithm = algorithm; @@ -92,8 +99,9 @@ public class PasswordHash this.salt = Hex.decodeHex(salt.toCharArray()); if (null == hash) - throw new DecoderException("Hash may not be null"); - this.hash = Hex.decodeHex(hash.toCharArray()); + this.hash = null; + else + this.hash = Hex.decodeHex(hash.toCharArray()); } /** diff --git a/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java b/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java index 3170a9c1a4..6edaa8b500 100644 --- a/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java +++ b/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java @@ -1,5 +1,15 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ package org.dspace.eperson; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import org.apache.commons.codec.DecoderException; import org.dspace.servicemanager.DSpaceKernelInit; import org.junit.*; import static org.junit.Assert.*; @@ -43,17 +53,26 @@ public class PasswordHashTest */ @Test public void testConstructors() + throws DecoderException { PasswordHash h1, h3; // Test null inputs, as from NULL database columns (old EPerson using // unsalted hash, for example). - h3 = new PasswordHash(null, (byte[])null, (byte[]) null); - assertEquals("MD5", h3.getAlgorithm()); - assertNull(h3.getSalt()); - assertNull(h3.getHash()); - assertFalse(h3.matches(null)); - assertFalse(h3.matches("not null")); + h3 = new PasswordHash(null, (byte[])null, (byte[])null); + assertNull("Null algorithm", h3.getAlgorithm()); + assertNull("Null salt", h3.getSalt()); + assertNull("Null hash", h3.getHash()); + assertFalse("Match null string?", h3.matches(null)); + assertFalse("Match non-null string?", h3.matches("not null")); + + // Test 3-argument constructor with string arguments + h3 = new PasswordHash(null, (String)null, (String)null); + assertNull("Null algorithm", h3.getAlgorithm()); + assertNull("Null salt", h3.getSalt()); + assertNull("Null hash", h3.getHash()); + assertFalse("Match null string?", h3.matches(null)); + assertFalse("Match non-null string?", h3.matches("not null")); // Test single-argument constructor, which does the hashing. String password = "I've got a secret."; @@ -70,20 +89,21 @@ public class PasswordHashTest /** * Test of matches method, of class PasswordHash. */ - /* @Test public void testMatches() + throws NoSuchAlgorithmException { System.out.println("matches"); - String secret = ""; - PasswordHash instance = null; - boolean expResult = false; - boolean result = instance.matches(secret); - assertEquals(expResult, result); - // TODO review the generated test code and remove the default call to fail. - fail("The test case is a prototype."); + final String secret = "Clark Kent is Superman"; + + // Test old 1-trip MD5 hash + MessageDigest digest = MessageDigest.getInstance("MD5"); + PasswordHash hash = new PasswordHash(null, null, digest.digest(secret.getBytes())); + boolean result = hash.matches(secret); + assertTrue("Old unsalted 1-trip MD5 hash", result); + + // 3-argument form: see constructor tests } - */ /** * Test of getHash method, of class PasswordHash. From 3e1f497e1813a6cebe22d0d1d331cb585756bc14 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 2 Aug 2012 13:23:58 -0400 Subject: [PATCH 09/18] Improve javadoc --- dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java index dbea1c7b1d..6e3726510e 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java +++ b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java @@ -61,6 +61,7 @@ public class PasswordHash * * @param algorithm the digest algorithm used in producing {@code hash}. * If empty, set to null. Other methods will treat this as unsalted MD5. + * If you want salted multi-round MD5, specify "MD5". * @param salt the salt hashed with the secret, or null. * @param hash the hashed secret. */ @@ -81,6 +82,7 @@ public class PasswordHash * hexadecimal-encoded {@code String}s. * @param algorithm the digest algorithm used in producing {@code hash}. * If empty, set to null. Other methods will treat this as unsalted MD5. + * If you want salted multi-round MD5, specify "MD5". * @param salt hexadecimal digits encoding the bytes of the salt, or null. * @param hash hexadecimal digits encoding the bytes of the hash. * @throws DecoderException if salt or hash is not proper hexadecimal. From a88d7ff1c69fb5b9f772ce24d8f2e1a03a49d6a8 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 2 Aug 2012 13:24:35 -0400 Subject: [PATCH 10/18] Document assertions --- .../test/java/org/dspace/eperson/PasswordHashTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java b/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java index 6edaa8b500..1082f51797 100644 --- a/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java +++ b/dspace-api/src/test/java/org/dspace/eperson/PasswordHashTest.java @@ -66,7 +66,7 @@ public class PasswordHashTest assertFalse("Match null string?", h3.matches(null)); assertFalse("Match non-null string?", h3.matches("not null")); - // Test 3-argument constructor with string arguments + // Test 3-argument constructor with null string arguments h3 = new PasswordHash(null, (String)null, (String)null); assertNull("Null algorithm", h3.getAlgorithm()); assertNull("Null salt", h3.getSalt()); @@ -78,12 +78,12 @@ public class PasswordHashTest String password = "I've got a secret."; h1 = new PasswordHash(password); assertEquals("SHA-512", h1.getAlgorithm()); - assertFalse(h1.matches("random rubbish")); - assertTrue(h1.matches(password)); + assertFalse("Match against a different string", h1.matches("random rubbish")); + assertTrue("Match against the correct string", h1.matches(password)); // Test 3-argument constructor with non-null data. h3 = new PasswordHash(h1.getAlgorithm(), h1.getSalt(), h1.getHash()); - assertTrue(h3.matches(password)); + assertTrue("Match a duplicate original made from getter values", h3.matches(password)); } /** From 925606abe9835c7f77f3ef45f6f906af0a7018ea Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 2 Aug 2012 14:12:31 -0400 Subject: [PATCH 11/18] Proper (modular) name for a configuration property of password authentication --- dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java index 6e3726510e..0e1cae5ae2 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java +++ b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java @@ -37,7 +37,7 @@ public class PasswordHash private static final Charset UTF_8 = Charset.forName("UTF-8"); // Should always succeed: UTF-8 is required private static final String DEFAULT_DIGEST_ALGORITHM = "SHA-512"; // XXX magic - private static final String ALGORITHM_PROPERTY = "passwordAuthentication.digestAlgorithm"; // FIXME better name? + private static final String ALGORITHM_PROPERTY = "authentication-password.digestAlgorithm"; private static final int SALT_BYTES = 128/8; // XXX magic we want 128 bits private static final int HASH_ROUNDS = 1024; // XXX magic 1024 rounds private static final int SEED_BYTES = 64; // XXX magic From b728a42739eb642c31d506d73004d79dde6f07fe Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 3 Aug 2012 13:40:39 -0400 Subject: [PATCH 12/18] Create a few unit tests for EPerson to check out new hashing --- .../java/org/dspace/eperson/PasswordHash.java | 10 + .../java/org/dspace/eperson/EPersonTest.java | 768 ++++++++++++++++++ 2 files changed, 778 insertions(+) create mode 100644 dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java diff --git a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java index 0e1cae5ae2..dc9255b817 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java +++ b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java @@ -203,6 +203,16 @@ public class PasswordHash return algorithm; } + /** + * The digest algorithm used if none is configured. + * + * @return name of the default digest. + */ + static public String getDefaultAlgorithm() + { + return DEFAULT_DIGEST_ALGORITHM; + } + /** Generate an array of random bytes. */ private synchronized byte[] generateSalt() { diff --git a/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java b/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java new file mode 100644 index 0000000000..0531c90192 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java @@ -0,0 +1,768 @@ + +package org.dspace.eperson; + +import java.sql.SQLException; +import java.util.ArrayList; +import mockit.UsingMocksAndStubs; +import org.apache.commons.codec.DecoderException; +import org.dspace.MockConfigurationManager; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.kernel.DSpaceKernel; +import org.dspace.servicemanager.DSpaceKernelImpl; +import org.dspace.servicemanager.DSpaceKernelInit; +import org.dspace.services.ConfigurationService; +import org.dspace.storage.rdbms.MockDatabaseManager; +import org.dspace.storage.rdbms.TableRow; +import org.junit.*; +import static org.junit.Assert.*; + +/** + * + * @author mwood + */ +@UsingMocksAndStubs(value={MockDatabaseManager.class, MockConfigurationManager.class}) +public class EPersonTest +{ + private static TableRow row1; + + private static DSpaceKernelImpl kernel; + + private static ConfigurationService config; + + public EPersonTest() + { + } + + @BeforeClass + public static void setUpClass() + throws Exception + { + // Build a TableRow for an EPerson to wrap + final ArrayList epersonColumns = new ArrayList(); + epersonColumns.add("eperson_id"); + epersonColumns.add("password"); + epersonColumns.add("salt"); + epersonColumns.add("digest_algorithm"); + + row1 = new TableRow("EPerson", epersonColumns); + + // Make certain that a default DSpaceKernel is started. + kernel = DSpaceKernelInit.getKernel(null); + kernel.start(); + + // Configure the kernel + config = kernel.getConfigurationService(); + config.setProperty("db.name", "H2"); + config.setProperty("db.driver", "org.h2.Driver"); + } + + @AfterClass + public static void tearDownClass() + throws Exception + { + } + + @Before + public void setUp() + { + } + + @After + public void tearDown() + { + } + + /** + * Test of equals method, of class EPerson. + */ +/* + @Test + public void testEquals() + { + System.out.println("equals"); + Object obj = null; + EPerson instance = null; + boolean expResult = false; + boolean result = instance.equals(obj); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of hashCode method, of class EPerson. + */ +/* + @Test + public void testHashCode() + { + System.out.println("hashCode"); + EPerson instance = null; + int expResult = 0; + int result = instance.hashCode(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of find method, of class EPerson. + */ +/* + @Test + public void testFind() + throws Exception + { + System.out.println("find"); + Context context = null; + int id = 0; + EPerson expResult = null; + EPerson result = EPerson.find(context, id); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of findByEmail method, of class EPerson. + */ +/* + @Test + public void testFindByEmail() + throws Exception + { + System.out.println("findByEmail"); + Context context = null; + String email = ""; + EPerson expResult = null; + EPerson result = EPerson.findByEmail(context, email); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of findByNetid method, of class EPerson. + */ +/* + @Test + public void testFindByNetid() + throws Exception + { + System.out.println("findByNetid"); + Context context = null; + String netid = ""; + EPerson expResult = null; + EPerson result = EPerson.findByNetid(context, netid); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of search method, of class EPerson. + */ +/* + @Test + public void testSearch_Context_String() + throws Exception + { + System.out.println("search"); + Context context = null; + String query = ""; + EPerson[] expResult = null; + EPerson[] result = EPerson.search(context, query); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of search method, of class EPerson. + */ +/* + @Test + public void testSearch_4args() + throws Exception + { + System.out.println("search"); + Context context = null; + String query = ""; + int offset = 0; + int limit = 0; + EPerson[] expResult = null; + EPerson[] result = EPerson.search(context, query, offset, limit); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of searchResultCount method, of class EPerson. + */ +/* + @Test + public void testSearchResultCount() + throws Exception + { + System.out.println("searchResultCount"); + Context context = null; + String query = ""; + int expResult = 0; + int result = EPerson.searchResultCount(context, query); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of findAll method, of class EPerson. + */ +/* + @Test + public void testFindAll() + throws Exception + { + System.out.println("findAll"); + Context context = null; + int sortField = 0; + EPerson[] expResult = null; + EPerson[] result = EPerson.findAll(context, sortField); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of create method, of class EPerson. + */ +/* + @Test + public void testCreate() + throws Exception + { + System.out.println("create"); + Context context = null; + EPerson expResult = null; + EPerson result = EPerson.create(context); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of delete method, of class EPerson. + */ +/* + @Test + public void testDelete() + throws Exception + { + System.out.println("delete"); + EPerson instance = null; + instance.delete(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getID method, of class EPerson. + */ +/* + @Test + public void testGetID() + { + System.out.println("getID"); + EPerson instance = null; + int expResult = 0; + int result = instance.getID(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getLanguage method, of class EPerson. + */ +/* + @Test + public void testGetLanguage() + { + System.out.println("getLanguage"); + EPerson instance = null; + String expResult = ""; + String result = instance.getLanguage(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setLanguage method, of class EPerson. + */ +/* + @Test + public void testSetLanguage() + { + System.out.println("setLanguage"); + String language = ""; + EPerson instance = null; + instance.setLanguage(language); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getHandle method, of class EPerson. + */ +/* + @Test + public void testGetHandle() + { + System.out.println("getHandle"); + EPerson instance = null; + String expResult = ""; + String result = instance.getHandle(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getEmail method, of class EPerson. + */ +/* + @Test + public void testGetEmail() + { + System.out.println("getEmail"); + EPerson instance = null; + String expResult = ""; + String result = instance.getEmail(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setEmail method, of class EPerson. + */ +/* + @Test + public void testSetEmail() + { + System.out.println("setEmail"); + String s = ""; + EPerson instance = null; + instance.setEmail(s); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getNetid method, of class EPerson. + */ +/* + @Test + public void testGetNetid() + { + System.out.println("getNetid"); + EPerson instance = null; + String expResult = ""; + String result = instance.getNetid(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setNetid method, of class EPerson. + */ +/* + @Test + public void testSetNetid() + { + System.out.println("setNetid"); + String s = ""; + EPerson instance = null; + instance.setNetid(s); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getFullName method, of class EPerson. + */ +/* + @Test + public void testGetFullName() + { + System.out.println("getFullName"); + EPerson instance = null; + String expResult = ""; + String result = instance.getFullName(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getFirstName method, of class EPerson. + */ +/* + @Test + public void testGetFirstName() + { + System.out.println("getFirstName"); + EPerson instance = null; + String expResult = ""; + String result = instance.getFirstName(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setFirstName method, of class EPerson. + */ +/* + @Test + public void testSetFirstName() + { + System.out.println("setFirstName"); + String firstname = ""; + EPerson instance = null; + instance.setFirstName(firstname); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getLastName method, of class EPerson. + */ +/* + @Test + public void testGetLastName() + { + System.out.println("getLastName"); + EPerson instance = null; + String expResult = ""; + String result = instance.getLastName(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setLastName method, of class EPerson. + */ +/* + @Test + public void testSetLastName() + { + System.out.println("setLastName"); + String lastname = ""; + EPerson instance = null; + instance.setLastName(lastname); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setCanLogIn method, of class EPerson. + */ +/* + @Test + public void testSetCanLogIn() + { + System.out.println("setCanLogIn"); + boolean login = false; + EPerson instance = null; + instance.setCanLogIn(login); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of canLogIn method, of class EPerson. + */ +/* + @Test + public void testCanLogIn() + { + System.out.println("canLogIn"); + EPerson instance = null; + boolean expResult = false; + boolean result = instance.canLogIn(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setRequireCertificate method, of class EPerson. + */ +/* + @Test + public void testSetRequireCertificate() + { + System.out.println("setRequireCertificate"); + boolean isrequired = false; + EPerson instance = null; + instance.setRequireCertificate(isrequired); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getRequireCertificate method, of class EPerson. + */ +/* + @Test + public void testGetRequireCertificate() + { + System.out.println("getRequireCertificate"); + EPerson instance = null; + boolean expResult = false; + boolean result = instance.getRequireCertificate(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setSelfRegistered method, of class EPerson. + */ +/* + @Test + public void testSetSelfRegistered() + { + System.out.println("setSelfRegistered"); + boolean sr = false; + EPerson instance = null; + instance.setSelfRegistered(sr); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getSelfRegistered method, of class EPerson. + */ +/* + @Test + public void testGetSelfRegistered() + { + System.out.println("getSelfRegistered"); + EPerson instance = null; + boolean expResult = false; + boolean result = instance.getSelfRegistered(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getMetadata method, of class EPerson. + */ +/* + @Test + public void testGetMetadata() + { + System.out.println("getMetadata"); + String field = ""; + EPerson instance = null; + String expResult = ""; + String result = instance.getMetadata(field); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setMetadata method, of class EPerson. + */ +/* + @Test + public void testSetMetadata() + { + System.out.println("setMetadata"); + String field = ""; + String value = ""; + EPerson instance = null; + instance.setMetadata(field, value); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setPassword method, of class EPerson. + */ +/* + @Test + public void testSetPassword() + { + System.out.println("setPassword"); + String s = ""; + EPerson instance = null; + instance.setPassword(s); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of setPasswordHash method, of class EPerson. + */ +/* + @Test + public void testSetPasswordHash() + { + System.out.println("setPasswordHash"); + PasswordHash password = null; + EPerson instance = null; + instance.setPasswordHash(password); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getPasswordHash method, of class EPerson. + */ +/* + @Test + public void testGetPasswordHash() + { + System.out.println("getPasswordHash"); + EPerson instance = null; + PasswordHash expResult = null; + PasswordHash result = instance.getPasswordHash(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of checkPassword method, of class EPerson. + */ + @Test + public void testCheckPassword() + throws SQLException, DecoderException + { + System.out.println("checkPassword"); + final String attempt = "secret"; + Context ctx = new Context(); + EPerson instance = new EPerson(ctx, row1); + + // Test old unsalted MD5 hash + final String hash = "5ebe2294ecd0e0f08eab7690d2a6ee69"; // MD5("secret"); + instance.setPasswordHash(new PasswordHash(null, null, hash)); + boolean result = instance.checkPassword(attempt); + assertTrue("check string with matching MD5 hash", result); + // It should have converted the password to the new hash + assertEquals("should have upgraded algorithm", + PasswordHash.getDefaultAlgorithm(), + instance.getPasswordHash().getAlgorithm()); + + // TODO test a salted multiround hash + } + + /** + * Test of update method, of class EPerson. + */ +/* + @Test + public void testUpdate() + throws Exception + { + System.out.println("update"); + EPerson instance = null; + instance.update(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getType method, of class EPerson. + */ + @Test + public void testGetType() + throws SQLException + { + System.out.println("getType"); + EPerson instance = new EPerson(new Context(), row1); + int expResult = Constants.EPERSON; + int result = instance.getType(); + assertEquals("Should return Constants.EPERSON", expResult, result); + } + + /** + * Test of getDeleteConstraints method, of class EPerson. + */ +/* + @Test + public void testGetDeleteConstraints() + throws Exception + { + System.out.println("getDeleteConstraints"); + EPerson instance = null; + List expResult = null; + List result = instance.getDeleteConstraints(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ + + /** + * Test of getName method, of class EPerson. + */ +/* + @Test + public void testGetName() + { + System.out.println("getName"); + EPerson instance = null; + String expResult = ""; + String result = instance.getName(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +*/ +} From f6d6db4343d56a1485a8152289e57c417d48cdeb Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 3 Aug 2012 13:41:45 -0400 Subject: [PATCH 13/18] Correct the name of the new digest_algorithm column --- dspace-api/src/main/java/org/dspace/eperson/EPerson.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java index babc309423..cca720ebf3 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java @@ -898,7 +898,7 @@ public class EPerson extends DSpaceObject { PasswordHash hash = null; try { - hash = new PasswordHash(myRow.getStringColumn("digestAlgorithm"), + hash = new PasswordHash(myRow.getStringColumn("digest_algorithm"), myRow.getStringColumn("salt"), myRow.getStringColumn("password")); } catch (DecoderException ex) { From 81db2cb79dc657fb3cfe79036e836a95dc334ec2 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 3 Aug 2012 13:59:00 -0400 Subject: [PATCH 14/18] Tidy up, fix licensing again --- .../src/test/java/org/dspace/eperson/EPersonTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java b/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java index 0531c90192..c95b042aa2 100644 --- a/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java +++ b/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java @@ -1,3 +1,10 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ package org.dspace.eperson; @@ -697,6 +704,8 @@ public class EPersonTest assertEquals("should have upgraded algorithm", PasswordHash.getDefaultAlgorithm(), instance.getPasswordHash().getAlgorithm()); + assertTrue("upgraded hash should still match", + instance.checkPassword(attempt)); // TODO test a salted multiround hash } From 67c962b5b63d6ffc021eb196308b65055f7d3155 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 3 Aug 2012 14:22:13 -0400 Subject: [PATCH 15/18] More tidies --- dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java b/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java index c95b042aa2..234cb0f772 100644 --- a/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java +++ b/dspace-api/src/test/java/org/dspace/eperson/EPersonTest.java @@ -15,7 +15,6 @@ import org.apache.commons.codec.DecoderException; import org.dspace.MockConfigurationManager; import org.dspace.core.Constants; import org.dspace.core.Context; -import org.dspace.kernel.DSpaceKernel; import org.dspace.servicemanager.DSpaceKernelImpl; import org.dspace.servicemanager.DSpaceKernelInit; import org.dspace.services.ConfigurationService; From 3f70aef792c789d6ee0fd2e528ff3940b2154927 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Fri, 3 Aug 2012 15:12:29 -0400 Subject: [PATCH 16/18] Upgraded hashes were not committed to the database --- .../src/main/java/org/dspace/eperson/EPerson.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java index cca720ebf3..7dcffe9b82 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPerson.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPerson.java @@ -933,7 +933,20 @@ public class EPerson extends DSpaceObject // If using the old unsalted hash, and this password is correct, update to a new hash if (answer && (null == myRow.getStringColumn("digest_algorithm"))) + { + log.info("Upgrading password hash for EPerson " + getID()); setPassword(attempt); + try { + myContext.turnOffAuthorisationSystem(); + update(); + } catch (SQLException ex) { + log.error("Could not update password hash", ex); + } catch (AuthorizeException ex) { + log.error("Could not update password hash", ex); + } finally { + myContext.restoreAuthSystemState(); + } + } return answer; } From 5129c095f3ed6b4e3b69e06138206a8701609380 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 9 Aug 2012 11:05:04 -0400 Subject: [PATCH 17/18] Document configuration of the digest algorithm and provide a commented-out example. --- dspace/config/modules/authentication-password.cfg | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dspace/config/modules/authentication-password.cfg b/dspace/config/modules/authentication-password.cfg index 5b0c1aa00e..db03e09ce4 100644 --- a/dspace/config/modules/authentication-password.cfg +++ b/dspace/config/modules/authentication-password.cfg @@ -22,4 +22,12 @@ # using the DSpace password system will automatically become members of # this group. This is useful if you want a group made up of all internal # authenticated users. -# login.specialgroup = group-name \ No newline at end of file +# login.specialgroup = group-name + +##### Password hashing algorithm ##### + +# You may select any digest algorithm available through +# java.security.MessageDigest on your system. At least MD2, MD5, SHA-1, +# SHA-256, SHA-384, and SHA-512 should be available, but you may have +# installed others. If not set, SHA-512 will be used. +# digestAlgorithm = SHA-512 From f4419374bb3ae693ec2e175560bf3089dd2cdc4e Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 15 Aug 2012 09:02:53 -0400 Subject: [PATCH 18/18] Revert "Fix CRLF line terminators" This reverts commit ee6fe99d1ea8d241738489df37a78234af1a0621. --- .../content/packager/RoleDisseminator.java | 1326 ++++++++--------- .../dspace/content/packager/RoleIngester.java | 1090 +++++++------- 2 files changed, 1208 insertions(+), 1208 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java index b871d6b4aa..29169363c7 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java @@ -1,663 +1,663 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.content.packager; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; - -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import org.apache.log4j.Logger; - -import org.dspace.authorize.AuthorizeException; -import org.dspace.content.Collection; -import org.dspace.content.Community; -import org.dspace.content.DSpaceObject; -import org.dspace.content.crosswalk.CrosswalkException; -import org.dspace.core.Constants; -import org.dspace.core.Context; -import org.dspace.eperson.EPerson; -import org.dspace.eperson.Group; -import org.dspace.eperson.PasswordHash; - -import org.jdom.Namespace; - -/** - * Plugin to export all Group and EPerson objects in XML, perhaps for reloading. - * - * @author Mark Wood - */ -public class RoleDisseminator implements PackageDisseminator -{ - - /** log4j category */ - private static final Logger log = Logger.getLogger(RoleDisseminator.class); - - /** - * DSpace Roles XML Namespace in JDOM form. - */ - public static final Namespace DSROLES_NS = - Namespace.getNamespace("dsroles", "http://www.dspace.org/xmlns/dspace/dspace-roles"); - - public static final String DSPACE_ROLES = "DSpaceRoles"; - public static final String ID = "ID"; - public static final String GROUPS = "Groups"; - public static final String GROUP = "Group"; - public static final String NAME = "Name"; - public static final String TYPE = "Type"; - public static final String MEMBERS = "Members"; - public static final String MEMBER = "Member"; - public static final String MEMBER_GROUPS = "MemberGroups"; - public static final String MEMBER_GROUP = "MemberGroup"; - public static final String EPERSONS = "People"; - public static final String EPERSON = "Person"; - public static final String EMAIL = "Email"; - public static final String NETID = "Netid"; - public static final String FIRST_NAME = "FirstName"; - public static final String LAST_NAME = "LastName"; - public static final String LANGUAGE = "Language"; - public static final String PASSWORD_HASH = "PasswordHash"; - public static final String PASSWORD_DIGEST = "digest"; - public static final String PASSWORD_SALT = "salt"; - public static final String CAN_LOGIN = "CanLogin"; - public static final String REQUIRE_CERTIFICATE = "RequireCertificate"; - public static final String SELF_REGISTERED = "SelfRegistered"; - - // Valid type values for Groups (only used when Group is associated with a Community or Collection) - public static final String GROUP_TYPE_ADMIN = "ADMIN"; - public static final String GROUP_TYPE_SUBMIT = "SUBMIT"; - public static final String GROUP_TYPE_WORKFLOW_STEP_1 = "WORKFLOW_STEP_1"; - public static final String GROUP_TYPE_WORKFLOW_STEP_2 = "WORKFLOW_STEP_2"; - public static final String GROUP_TYPE_WORKFLOW_STEP_3 = "WORKFLOW_STEP_3"; - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageDisseminator#disseminate(org.dspace - * .core.Context, org.dspace.content.DSpaceObject, - * org.dspace.content.packager.PackageParameters, java.io.File) - */ - @Override - public void disseminate(Context context, DSpaceObject object, - PackageParameters params, File pkgFile) - throws PackageException, CrosswalkException, - AuthorizeException, SQLException, IOException - { - boolean emitPasswords = params.containsKey("passwords"); - - FileOutputStream fileOut = null; - try - { - //open file stream for writing - fileOut = new FileOutputStream(pkgFile); - writeToStream(context, object, fileOut, emitPasswords); - } - finally - { - //close file stream & save - if (fileOut != null) - { - fileOut.close(); - } - } - } - - /** - * Make serialized users and groups available on an InputStream, for code - * which wants to read one. - * - * @param emitPasswords true if password hashes should be included. - * @return the stream of XML representing users and groups. - * @throws IOException - * if a PipedOutputStream or PipedInputStream cannot be created. - */ - InputStream asStream(Context context, DSpaceObject object, boolean emitPasswords) - throws IOException - { - // Create a PipedOutputStream to which to write some XML - PipedOutputStream outStream = new PipedOutputStream(); - PipedInputStream inStream = new PipedInputStream(outStream); - - // Create a new Thread to push serialized objects into the pipe - Serializer serializer = new Serializer(context, object, outStream, - emitPasswords); - new Thread(serializer).start(); - - return inStream; - } - - /** - * Embody a thread for serializing users and groups. - * - * @author mwood - */ - private class Serializer implements Runnable - { - private Context context; - private DSpaceObject object; - private OutputStream stream; - private boolean emitPasswords; - - @SuppressWarnings("unused") - private Serializer() {} - - /** - * @param context - * @param object the DSpaceObject - * @param stream receives serialized user and group objects. Will be - * closed when serialization is complete. - * @param emitPasswords true if password hashes should be included. - */ - Serializer(Context context, DSpaceObject object, OutputStream stream, boolean emitPasswords) - { - this.context = context; - this.object = object; - this.stream = stream; - this.emitPasswords = emitPasswords; - } - - @Override - public void run() - { - try - { - writeToStream(context, object, stream, emitPasswords); - stream.close(); - } - catch (IOException e) - { - log.error(e); - } - catch (PackageException e) - { - log.error(e); - } - } - } - - /** - * Serialize users and groups to a stream. - * - * @param context - * @param stream receives the output. Is not closed by this method. - * @param emitPasswords true if password hashes should be included. - * @throws XMLStreamException - * @throws SQLException - */ - private void writeToStream(Context context, DSpaceObject object, OutputStream stream, - boolean emitPasswords) - throws PackageException - { - try - { - //First, find all Groups/People associated with our current Object - Group[] groups = findAssociatedGroups(context, object); - EPerson[] people = findAssociatedPeople(context, object); - - //Only continue if we've found Groups or People which we need to disseminate - if((groups!=null && groups.length>0) || - (people!=null && people.length>0)) - { - XMLOutputFactory factory = XMLOutputFactory.newInstance(); - XMLStreamWriter writer; - - writer = factory.createXMLStreamWriter(stream, "UTF-8"); - writer.setDefaultNamespace(DSROLES_NS.getURI()); - writer.writeStartDocument("UTF-8", "1.0"); - writer.writeStartElement(DSPACE_ROLES); - - //Only disseminate a element if some groups exist - if(groups!=null) - { - writer.writeStartElement(GROUPS); - - for (Group group : groups) - { - writeGroup(context, object, group, writer); - } - - writer.writeEndElement(); // GROUPS - } - - //Only disseminate an element if some people exist - if(people!=null) - { - writer.writeStartElement(EPERSONS); - - for (EPerson eperson : people) - { - writeEPerson(eperson, writer, emitPasswords); - } - - writer.writeEndElement(); // EPERSONS - } - - writer.writeEndElement(); // DSPACE_ROLES - writer.writeEndDocument(); - writer.close(); - }//end if Groups or People exist - } - catch (Exception e) - { - throw new PackageException(e); - } - } - - /* (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageDisseminator#disseminateAll(org.dspace - * .core.Context, org.dspace.content.DSpaceObject, - * org.dspace.content.packager.PackageParameters, java.io.File) - */ - @Override - public List disseminateAll(Context context, DSpaceObject dso, - PackageParameters params, File pkgFile) - throws PackageException, CrosswalkException, - AuthorizeException, SQLException, IOException - { - throw new PackageException("disseminateAll() is not implemented, as disseminate() method already handles dissemination of all roles to an external file."); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageDisseminator#getMIMEType(org.dspace - * .content.packager.PackageParameters) - */ - @Override - public String getMIMEType(PackageParameters params) - { - return "application/xml"; - } - - /** - * Emit XML describing a single Group. - * - * @param context - * the DSpace Context - * @parm relatedObject - * the DSpaceObject related to this group (if any) - * @param group - * the Group to describe - * @param write - * the description to this stream - */ - private void writeGroup(Context context, DSpaceObject relatedObject, Group group, XMLStreamWriter writer) - throws XMLStreamException, PackageException - { - //Translate the Group name for export. This ensures that groups with Internal IDs in their names - // (e.g. COLLECTION_1_ADMIN) are properly translated using the corresponding Handle or external identifier. - String exportGroupName = PackageUtils.translateGroupNameForExport(context, group.getName()); - - //If translated group name is returned as "null", this means the Group name - // had an Internal Collection/Community ID embedded, which could not be - // translated properly to a Handle. We will NOT export these groups, - // as they could cause conflicts or data integrity problems if they are - // imported into another DSpace system. - if(exportGroupName==null) - { - return; - } - - writer.writeStartElement(GROUP); - writer.writeAttribute(ID, String.valueOf(group.getID())); - writer.writeAttribute(NAME, exportGroupName); - - String groupType = getGroupType(relatedObject, group); - if(groupType!=null && !groupType.isEmpty()) - { - writer.writeAttribute(TYPE, groupType); - } - - //Add People to Group (if any belong to this group) - if(group.getMembers().length>0) - { - writer.writeStartElement(MEMBERS); - for (EPerson member : group.getMembers()) - { - writer.writeEmptyElement(MEMBER); - writer.writeAttribute(ID, String.valueOf(member.getID())); - writer.writeAttribute(NAME, member.getName()); - } - writer.writeEndElement(); - } - - //Add Groups as Member Groups (if any belong to this group) - if(group.getMemberGroups().length>0) - { - writer.writeStartElement(MEMBER_GROUPS); - for (Group member : group.getMemberGroups()) - { - String exportMemberName = PackageUtils.translateGroupNameForExport(context, member.getName()); - //Only export member group if its name can be properly translated for export. As noted above, - // we don't want groups that are *unable* to be accurately translated causing issues on import. - if(exportMemberName!=null) - { - writer.writeEmptyElement(MEMBER_GROUP); - writer.writeAttribute(ID, String.valueOf(member.getID())); - writer.writeAttribute(NAME, exportMemberName); - } - } - writer.writeEndElement(); - } - - writer.writeEndElement(); - } - - /** - * Return a Group Type string (see RoleDisseminator.GROUP_TYPE_* constants) - * which describes the type of group and its relation to the given object. - *

- * As a basic example, if the Group is a Collection Administration group, - * the Group Type string returned should be "ADMIN" - *

- * If type string cannot be determined, null is returned. - * - * @param dso - * the related DSpaceObject - * @param group - * the group - * @return a group type string or null - */ - private String getGroupType(DSpaceObject dso, Group group) - { - if (dso == null || group == null) - { - return null; - } - - if( dso.getType()==Constants.COMMUNITY) - { - Community community = (Community) dso; - - //Check if this is the ADMIN group for this community - if (group.equals(community.getAdministrators())) - { - return GROUP_TYPE_ADMIN; - } - } - else if(dso.getType() == Constants.COLLECTION) - { - Collection collection = (Collection) dso; - - if (group.equals(collection.getAdministrators())) - { - //Check if this is the ADMIN group for this collection - return GROUP_TYPE_ADMIN; - } - else if (group.equals(collection.getSubmitters())) - { - //Check if Submitters group - return GROUP_TYPE_SUBMIT; - } - else if (group.equals(collection.getWorkflowGroup(1))) - { - //Check if workflow step 1 group - return GROUP_TYPE_WORKFLOW_STEP_1; - } - else if (group.equals(collection.getWorkflowGroup(2))) - { - //check if workflow step 2 group - return GROUP_TYPE_WORKFLOW_STEP_2; - } - else if (group.equals(collection.getWorkflowGroup(3))) - { - //check if workflow step 3 group - return GROUP_TYPE_WORKFLOW_STEP_3; - } - } - - //by default, return null - return null; - } - - /** - * Emit XML describing a single EPerson. - * - * @param eperson - * the EPerson to describe - * @param write - * the description to this stream - * @param emitPassword - * do not export the password hash unless true - */ - private void writeEPerson(EPerson eperson, XMLStreamWriter writer, - boolean emitPassword) throws XMLStreamException - { - writer.writeStartElement(EPERSON); - writer.writeAttribute(ID, String.valueOf(eperson.getID())); - - writer.writeStartElement(EMAIL); - writer.writeCharacters(eperson.getEmail()); - writer.writeEndElement(); - - if(eperson.getNetid()!=null) - { - writer.writeStartElement(NETID); - writer.writeCharacters(eperson.getNetid()); - writer.writeEndElement(); - } - - if(eperson.getFirstName()!=null) - { - writer.writeStartElement(FIRST_NAME); - writer.writeCharacters(eperson.getFirstName()); - writer.writeEndElement(); - } - - if(eperson.getLastName()!=null) - { - writer.writeStartElement(LAST_NAME); - writer.writeCharacters(eperson.getLastName()); - writer.writeEndElement(); - } - - if(eperson.getLanguage()!=null) - { - writer.writeStartElement(LANGUAGE); - writer.writeCharacters(eperson.getLanguage()); - writer.writeEndElement(); - } - - if (emitPassword) - { - PasswordHash password = eperson.getPasswordHash(); - - writer.writeStartElement(PASSWORD_HASH); - - String algorithm = password.getAlgorithm(); - if (null != algorithm) - writer.writeAttribute(PASSWORD_DIGEST, algorithm); - - String salt = password.getSaltString(); - if (null != salt) - writer.writeAttribute(PASSWORD_SALT, salt); - - writer.writeCharacters(password.getHashString()); - writer.writeEndElement(); - } - - if (eperson.canLogIn()) - { - writer.writeEmptyElement(CAN_LOGIN); - } - - if (eperson.getRequireCertificate()) - { - writer.writeEmptyElement(REQUIRE_CERTIFICATE); - } - - if (eperson.getSelfRegistered()) - { - writer.writeEmptyElement(SELF_REGISTERED); - } - - writer.writeEndElement(); - } - - /** - * Find all Groups associated with this DSpace Object. - *

- * If object is SITE, all groups are returned. - *

- * If object is COMMUNITY or COLLECTION, only groups associated with - * those objects are returned (if any). - *

- * For all other objects, null is returned. - * - * @param context The DSpace context - * @param object the DSpace object - * @return array of all associated groups - */ - private Group[] findAssociatedGroups(Context context, DSpaceObject object) - throws SQLException - { - if(object.getType()==Constants.SITE) - { - // @TODO FIXME -- if there was a way to ONLY export Groups which are NOT - // associated with a Community or Collection, we should be doing that instead! - return Group.findAll(context, Group.NAME); - } - else if(object.getType()==Constants.COMMUNITY) - { - Community community = (Community) object; - - ArrayList list = new ArrayList(); - - //check for admin group - if(community.getAdministrators()!=null) - { - list.add(community.getAdministrators()); - } - - // FINAL CATCH-ALL -> Find any other groups where name begins with "COMMUNITY__" - // (There should be none, but this code is here just in case) - Group[] matchingGroups = Group.search(context, "COMMUNITY\\_" + community.getID() + "\\_"); - for(Group g : matchingGroups) - { - if(!list.contains(g)) - { - list.add(g); - } - } - - if(list.size()>0) - { - Group[] groupArray = new Group[list.size()]; - groupArray = (Group[]) list.toArray(groupArray); - return groupArray; - } - } - else if(object.getType()==Constants.COLLECTION) - { - Collection collection = (Collection) object; - - ArrayList list = new ArrayList(); - - //check for admin group - if(collection.getAdministrators()!=null) - { - list.add(collection.getAdministrators()); - } - //check for submitters group - if(collection.getSubmitters()!=null) - { - list.add(collection.getSubmitters()); - } - //check for workflow step 1 group - if(collection.getWorkflowGroup(1)!=null) - { - list.add(collection.getWorkflowGroup(1)); - } - //check for workflow step 2 group - if(collection.getWorkflowGroup(2)!=null) - { - list.add(collection.getWorkflowGroup(2)); - } - //check for workflow step 3 group - if(collection.getWorkflowGroup(3)!=null) - { - list.add(collection.getWorkflowGroup(3)); - } - - // FINAL CATCH-ALL -> Find any other groups where name begins with "COLLECTION__" - // (Necessary cause XMLUI allows you to generate a 'COLLECTION__DEFAULT_READ' group) - Group[] matchingGroups = Group.search(context, "COLLECTION\\_" + collection.getID() + "\\_"); - for(Group g : matchingGroups) - { - if(!list.contains(g)) - { - list.add(g); - } - } - - if(list.size()>0) - { - Group[] groupArray = new Group[list.size()]; - groupArray = (Group[]) list.toArray(groupArray); - return groupArray; - } - } - - //by default, return nothing - return null; - } - - - /** - * Find all EPeople associated with this DSpace Object. - *

- * If object is SITE, all people are returned. - *

- * For all other objects, null is returned. - * - * @param context The DSpace context - * @param object the DSpace object - * @return array of all associated EPerson objects - */ - private EPerson[] findAssociatedPeople(Context context, DSpaceObject object) - throws SQLException - { - if(object.getType()==Constants.SITE) - { - return EPerson.findAll(context, EPerson.EMAIL); - } - - //by default, return nothing - return null; - } - - /** - * Returns a user help string which should describe the - * additional valid command-line options that this packager - * implementation will accept when using the -o or - * --option flags with the Packager script. - * - * @return a string describing additional command-line options available - * with this packager - */ - @Override - public String getParameterHelp() - { - return "* passwords=[boolean] " + - "If true, user password hashes are also exported (so that they can be later restored). If false, user passwords are not exported. (Default is false)"; - } - -} +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.packager; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.apache.log4j.Logger; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.crosswalk.CrosswalkException; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.PasswordHash; + +import org.jdom.Namespace; + +/** + * Plugin to export all Group and EPerson objects in XML, perhaps for reloading. + * + * @author Mark Wood + */ +public class RoleDisseminator implements PackageDisseminator +{ + + /** log4j category */ + private static final Logger log = Logger.getLogger(RoleDisseminator.class); + + /** + * DSpace Roles XML Namespace in JDOM form. + */ + public static final Namespace DSROLES_NS = + Namespace.getNamespace("dsroles", "http://www.dspace.org/xmlns/dspace/dspace-roles"); + + public static final String DSPACE_ROLES = "DSpaceRoles"; + public static final String ID = "ID"; + public static final String GROUPS = "Groups"; + public static final String GROUP = "Group"; + public static final String NAME = "Name"; + public static final String TYPE = "Type"; + public static final String MEMBERS = "Members"; + public static final String MEMBER = "Member"; + public static final String MEMBER_GROUPS = "MemberGroups"; + public static final String MEMBER_GROUP = "MemberGroup"; + public static final String EPERSONS = "People"; + public static final String EPERSON = "Person"; + public static final String EMAIL = "Email"; + public static final String NETID = "Netid"; + public static final String FIRST_NAME = "FirstName"; + public static final String LAST_NAME = "LastName"; + public static final String LANGUAGE = "Language"; + public static final String PASSWORD_HASH = "PasswordHash"; + public static final String PASSWORD_DIGEST = "digest"; + public static final String PASSWORD_SALT = "salt"; + public static final String CAN_LOGIN = "CanLogin"; + public static final String REQUIRE_CERTIFICATE = "RequireCertificate"; + public static final String SELF_REGISTERED = "SelfRegistered"; + + // Valid type values for Groups (only used when Group is associated with a Community or Collection) + public static final String GROUP_TYPE_ADMIN = "ADMIN"; + public static final String GROUP_TYPE_SUBMIT = "SUBMIT"; + public static final String GROUP_TYPE_WORKFLOW_STEP_1 = "WORKFLOW_STEP_1"; + public static final String GROUP_TYPE_WORKFLOW_STEP_2 = "WORKFLOW_STEP_2"; + public static final String GROUP_TYPE_WORKFLOW_STEP_3 = "WORKFLOW_STEP_3"; + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageDisseminator#disseminate(org.dspace + * .core.Context, org.dspace.content.DSpaceObject, + * org.dspace.content.packager.PackageParameters, java.io.File) + */ + @Override + public void disseminate(Context context, DSpaceObject object, + PackageParameters params, File pkgFile) + throws PackageException, CrosswalkException, + AuthorizeException, SQLException, IOException + { + boolean emitPasswords = params.containsKey("passwords"); + + FileOutputStream fileOut = null; + try + { + //open file stream for writing + fileOut = new FileOutputStream(pkgFile); + writeToStream(context, object, fileOut, emitPasswords); + } + finally + { + //close file stream & save + if (fileOut != null) + { + fileOut.close(); + } + } + } + + /** + * Make serialized users and groups available on an InputStream, for code + * which wants to read one. + * + * @param emitPasswords true if password hashes should be included. + * @return the stream of XML representing users and groups. + * @throws IOException + * if a PipedOutputStream or PipedInputStream cannot be created. + */ + InputStream asStream(Context context, DSpaceObject object, boolean emitPasswords) + throws IOException + { + // Create a PipedOutputStream to which to write some XML + PipedOutputStream outStream = new PipedOutputStream(); + PipedInputStream inStream = new PipedInputStream(outStream); + + // Create a new Thread to push serialized objects into the pipe + Serializer serializer = new Serializer(context, object, outStream, + emitPasswords); + new Thread(serializer).start(); + + return inStream; + } + + /** + * Embody a thread for serializing users and groups. + * + * @author mwood + */ + private class Serializer implements Runnable + { + private Context context; + private DSpaceObject object; + private OutputStream stream; + private boolean emitPasswords; + + @SuppressWarnings("unused") + private Serializer() {} + + /** + * @param context + * @param object the DSpaceObject + * @param stream receives serialized user and group objects. Will be + * closed when serialization is complete. + * @param emitPasswords true if password hashes should be included. + */ + Serializer(Context context, DSpaceObject object, OutputStream stream, boolean emitPasswords) + { + this.context = context; + this.object = object; + this.stream = stream; + this.emitPasswords = emitPasswords; + } + + @Override + public void run() + { + try + { + writeToStream(context, object, stream, emitPasswords); + stream.close(); + } + catch (IOException e) + { + log.error(e); + } + catch (PackageException e) + { + log.error(e); + } + } + } + + /** + * Serialize users and groups to a stream. + * + * @param context + * @param stream receives the output. Is not closed by this method. + * @param emitPasswords true if password hashes should be included. + * @throws XMLStreamException + * @throws SQLException + */ + private void writeToStream(Context context, DSpaceObject object, OutputStream stream, + boolean emitPasswords) + throws PackageException + { + try + { + //First, find all Groups/People associated with our current Object + Group[] groups = findAssociatedGroups(context, object); + EPerson[] people = findAssociatedPeople(context, object); + + //Only continue if we've found Groups or People which we need to disseminate + if((groups!=null && groups.length>0) || + (people!=null && people.length>0)) + { + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + XMLStreamWriter writer; + + writer = factory.createXMLStreamWriter(stream, "UTF-8"); + writer.setDefaultNamespace(DSROLES_NS.getURI()); + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement(DSPACE_ROLES); + + //Only disseminate a element if some groups exist + if(groups!=null) + { + writer.writeStartElement(GROUPS); + + for (Group group : groups) + { + writeGroup(context, object, group, writer); + } + + writer.writeEndElement(); // GROUPS + } + + //Only disseminate an element if some people exist + if(people!=null) + { + writer.writeStartElement(EPERSONS); + + for (EPerson eperson : people) + { + writeEPerson(eperson, writer, emitPasswords); + } + + writer.writeEndElement(); // EPERSONS + } + + writer.writeEndElement(); // DSPACE_ROLES + writer.writeEndDocument(); + writer.close(); + }//end if Groups or People exist + } + catch (Exception e) + { + throw new PackageException(e); + } + } + + /* (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageDisseminator#disseminateAll(org.dspace + * .core.Context, org.dspace.content.DSpaceObject, + * org.dspace.content.packager.PackageParameters, java.io.File) + */ + @Override + public List disseminateAll(Context context, DSpaceObject dso, + PackageParameters params, File pkgFile) + throws PackageException, CrosswalkException, + AuthorizeException, SQLException, IOException + { + throw new PackageException("disseminateAll() is not implemented, as disseminate() method already handles dissemination of all roles to an external file."); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageDisseminator#getMIMEType(org.dspace + * .content.packager.PackageParameters) + */ + @Override + public String getMIMEType(PackageParameters params) + { + return "application/xml"; + } + + /** + * Emit XML describing a single Group. + * + * @param context + * the DSpace Context + * @parm relatedObject + * the DSpaceObject related to this group (if any) + * @param group + * the Group to describe + * @param write + * the description to this stream + */ + private void writeGroup(Context context, DSpaceObject relatedObject, Group group, XMLStreamWriter writer) + throws XMLStreamException, PackageException + { + //Translate the Group name for export. This ensures that groups with Internal IDs in their names + // (e.g. COLLECTION_1_ADMIN) are properly translated using the corresponding Handle or external identifier. + String exportGroupName = PackageUtils.translateGroupNameForExport(context, group.getName()); + + //If translated group name is returned as "null", this means the Group name + // had an Internal Collection/Community ID embedded, which could not be + // translated properly to a Handle. We will NOT export these groups, + // as they could cause conflicts or data integrity problems if they are + // imported into another DSpace system. + if(exportGroupName==null) + { + return; + } + + writer.writeStartElement(GROUP); + writer.writeAttribute(ID, String.valueOf(group.getID())); + writer.writeAttribute(NAME, exportGroupName); + + String groupType = getGroupType(relatedObject, group); + if(groupType!=null && !groupType.isEmpty()) + { + writer.writeAttribute(TYPE, groupType); + } + + //Add People to Group (if any belong to this group) + if(group.getMembers().length>0) + { + writer.writeStartElement(MEMBERS); + for (EPerson member : group.getMembers()) + { + writer.writeEmptyElement(MEMBER); + writer.writeAttribute(ID, String.valueOf(member.getID())); + writer.writeAttribute(NAME, member.getName()); + } + writer.writeEndElement(); + } + + //Add Groups as Member Groups (if any belong to this group) + if(group.getMemberGroups().length>0) + { + writer.writeStartElement(MEMBER_GROUPS); + for (Group member : group.getMemberGroups()) + { + String exportMemberName = PackageUtils.translateGroupNameForExport(context, member.getName()); + //Only export member group if its name can be properly translated for export. As noted above, + // we don't want groups that are *unable* to be accurately translated causing issues on import. + if(exportMemberName!=null) + { + writer.writeEmptyElement(MEMBER_GROUP); + writer.writeAttribute(ID, String.valueOf(member.getID())); + writer.writeAttribute(NAME, exportMemberName); + } + } + writer.writeEndElement(); + } + + writer.writeEndElement(); + } + + /** + * Return a Group Type string (see RoleDisseminator.GROUP_TYPE_* constants) + * which describes the type of group and its relation to the given object. + *

+ * As a basic example, if the Group is a Collection Administration group, + * the Group Type string returned should be "ADMIN" + *

+ * If type string cannot be determined, null is returned. + * + * @param dso + * the related DSpaceObject + * @param group + * the group + * @return a group type string or null + */ + private String getGroupType(DSpaceObject dso, Group group) + { + if (dso == null || group == null) + { + return null; + } + + if( dso.getType()==Constants.COMMUNITY) + { + Community community = (Community) dso; + + //Check if this is the ADMIN group for this community + if (group.equals(community.getAdministrators())) + { + return GROUP_TYPE_ADMIN; + } + } + else if(dso.getType() == Constants.COLLECTION) + { + Collection collection = (Collection) dso; + + if (group.equals(collection.getAdministrators())) + { + //Check if this is the ADMIN group for this collection + return GROUP_TYPE_ADMIN; + } + else if (group.equals(collection.getSubmitters())) + { + //Check if Submitters group + return GROUP_TYPE_SUBMIT; + } + else if (group.equals(collection.getWorkflowGroup(1))) + { + //Check if workflow step 1 group + return GROUP_TYPE_WORKFLOW_STEP_1; + } + else if (group.equals(collection.getWorkflowGroup(2))) + { + //check if workflow step 2 group + return GROUP_TYPE_WORKFLOW_STEP_2; + } + else if (group.equals(collection.getWorkflowGroup(3))) + { + //check if workflow step 3 group + return GROUP_TYPE_WORKFLOW_STEP_3; + } + } + + //by default, return null + return null; + } + + /** + * Emit XML describing a single EPerson. + * + * @param eperson + * the EPerson to describe + * @param write + * the description to this stream + * @param emitPassword + * do not export the password hash unless true + */ + private void writeEPerson(EPerson eperson, XMLStreamWriter writer, + boolean emitPassword) throws XMLStreamException + { + writer.writeStartElement(EPERSON); + writer.writeAttribute(ID, String.valueOf(eperson.getID())); + + writer.writeStartElement(EMAIL); + writer.writeCharacters(eperson.getEmail()); + writer.writeEndElement(); + + if(eperson.getNetid()!=null) + { + writer.writeStartElement(NETID); + writer.writeCharacters(eperson.getNetid()); + writer.writeEndElement(); + } + + if(eperson.getFirstName()!=null) + { + writer.writeStartElement(FIRST_NAME); + writer.writeCharacters(eperson.getFirstName()); + writer.writeEndElement(); + } + + if(eperson.getLastName()!=null) + { + writer.writeStartElement(LAST_NAME); + writer.writeCharacters(eperson.getLastName()); + writer.writeEndElement(); + } + + if(eperson.getLanguage()!=null) + { + writer.writeStartElement(LANGUAGE); + writer.writeCharacters(eperson.getLanguage()); + writer.writeEndElement(); + } + + if (emitPassword) + { + PasswordHash password = eperson.getPasswordHash(); + + writer.writeStartElement(PASSWORD_HASH); + + String algorithm = password.getAlgorithm(); + if (null != algorithm) + writer.writeAttribute(PASSWORD_DIGEST, algorithm); + + String salt = password.getSaltString(); + if (null != salt) + writer.writeAttribute(PASSWORD_SALT, salt); + + writer.writeCharacters(password.getHashString()); + writer.writeEndElement(); + } + + if (eperson.canLogIn()) + { + writer.writeEmptyElement(CAN_LOGIN); + } + + if (eperson.getRequireCertificate()) + { + writer.writeEmptyElement(REQUIRE_CERTIFICATE); + } + + if (eperson.getSelfRegistered()) + { + writer.writeEmptyElement(SELF_REGISTERED); + } + + writer.writeEndElement(); + } + + /** + * Find all Groups associated with this DSpace Object. + *

+ * If object is SITE, all groups are returned. + *

+ * If object is COMMUNITY or COLLECTION, only groups associated with + * those objects are returned (if any). + *

+ * For all other objects, null is returned. + * + * @param context The DSpace context + * @param object the DSpace object + * @return array of all associated groups + */ + private Group[] findAssociatedGroups(Context context, DSpaceObject object) + throws SQLException + { + if(object.getType()==Constants.SITE) + { + // @TODO FIXME -- if there was a way to ONLY export Groups which are NOT + // associated with a Community or Collection, we should be doing that instead! + return Group.findAll(context, Group.NAME); + } + else if(object.getType()==Constants.COMMUNITY) + { + Community community = (Community) object; + + ArrayList list = new ArrayList(); + + //check for admin group + if(community.getAdministrators()!=null) + { + list.add(community.getAdministrators()); + } + + // FINAL CATCH-ALL -> Find any other groups where name begins with "COMMUNITY__" + // (There should be none, but this code is here just in case) + Group[] matchingGroups = Group.search(context, "COMMUNITY\\_" + community.getID() + "\\_"); + for(Group g : matchingGroups) + { + if(!list.contains(g)) + { + list.add(g); + } + } + + if(list.size()>0) + { + Group[] groupArray = new Group[list.size()]; + groupArray = (Group[]) list.toArray(groupArray); + return groupArray; + } + } + else if(object.getType()==Constants.COLLECTION) + { + Collection collection = (Collection) object; + + ArrayList list = new ArrayList(); + + //check for admin group + if(collection.getAdministrators()!=null) + { + list.add(collection.getAdministrators()); + } + //check for submitters group + if(collection.getSubmitters()!=null) + { + list.add(collection.getSubmitters()); + } + //check for workflow step 1 group + if(collection.getWorkflowGroup(1)!=null) + { + list.add(collection.getWorkflowGroup(1)); + } + //check for workflow step 2 group + if(collection.getWorkflowGroup(2)!=null) + { + list.add(collection.getWorkflowGroup(2)); + } + //check for workflow step 3 group + if(collection.getWorkflowGroup(3)!=null) + { + list.add(collection.getWorkflowGroup(3)); + } + + // FINAL CATCH-ALL -> Find any other groups where name begins with "COLLECTION__" + // (Necessary cause XMLUI allows you to generate a 'COLLECTION__DEFAULT_READ' group) + Group[] matchingGroups = Group.search(context, "COLLECTION\\_" + collection.getID() + "\\_"); + for(Group g : matchingGroups) + { + if(!list.contains(g)) + { + list.add(g); + } + } + + if(list.size()>0) + { + Group[] groupArray = new Group[list.size()]; + groupArray = (Group[]) list.toArray(groupArray); + return groupArray; + } + } + + //by default, return nothing + return null; + } + + + /** + * Find all EPeople associated with this DSpace Object. + *

+ * If object is SITE, all people are returned. + *

+ * For all other objects, null is returned. + * + * @param context The DSpace context + * @param object the DSpace object + * @return array of all associated EPerson objects + */ + private EPerson[] findAssociatedPeople(Context context, DSpaceObject object) + throws SQLException + { + if(object.getType()==Constants.SITE) + { + return EPerson.findAll(context, EPerson.EMAIL); + } + + //by default, return nothing + return null; + } + + /** + * Returns a user help string which should describe the + * additional valid command-line options that this packager + * implementation will accept when using the -o or + * --option flags with the Packager script. + * + * @return a string describing additional command-line options available + * with this packager + */ + @Override + public String getParameterHelp() + { + return "* passwords=[boolean] " + + "If true, user password hashes are also exported (so that they can be later restored). If false, user passwords are not exported. (Default is false)"; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java index 90e1cc0c37..5015433bd0 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java @@ -1,545 +1,545 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.content.packager; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.sql.SQLException; -import java.util.List; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import org.apache.commons.codec.DecoderException; - -import org.dspace.authorize.AuthorizeException; -import org.dspace.content.Collection; -import org.dspace.content.Community; -import org.dspace.content.DSpaceObject; -import org.dspace.content.crosswalk.CrosswalkException; -import org.dspace.core.Constants; -import org.dspace.core.Context; -import org.dspace.eperson.EPerson; -import org.dspace.eperson.Group; -import org.dspace.eperson.PasswordHash; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.w3c.dom.*; -import org.xml.sax.SAXException; - -/** - * Create EPersons and Groups from a file of external representations. - * - * @author mwood - */ -public class RoleIngester implements PackageIngester -{ - private static final Logger log = LoggerFactory - .getLogger(RoleIngester.class); - - /** - * Common code to ingest roles from a Document. - * - * @param context - * DSpace Context - * @param parent - * the Parent DSpaceObject - * @param document - * the XML Document - * @throws SQLException - * @throws AuthorizeException - * @throws PackageException - */ - static void ingestDocument(Context context, DSpaceObject parent, - PackageParameters params, Document document) - throws SQLException, AuthorizeException, PackageException - { - String myEmail = context.getCurrentUser().getEmail(); - String myNetid = context.getCurrentUser().getNetid(); - - // Ingest users (EPersons) first so Groups can use them - NodeList users = document - .getElementsByTagName(RoleDisseminator.EPERSON); - for (int i = 0; i < users.getLength(); i++) - { - Element user = (Element) users.item(i); - // int userID = Integer.valueOf(user.getAttribute("ID")); // FIXME - // no way to set ID! - NodeList emails = user.getElementsByTagName(RoleDisseminator.EMAIL); - NodeList netids = user.getElementsByTagName(RoleDisseminator.NETID); - EPerson eperson; - EPerson collider; - String email = null; - String netid = null; - String identity; - if (emails.getLength() > 0) - { - email = emails.item(0).getTextContent(); - if (email.equals(myEmail)) - { - continue; // Cannot operate on my own EPerson! - } - identity = email; - collider = EPerson.findByEmail(context, identity); - // collider = EPerson.find(context, userID); - } - else if (netids.getLength() > 0) - { - netid = netids.item(0).getTextContent(); - if (netid.equals(myNetid)) - { - continue; // Cannot operate on my own EPerson! - } - identity = netid; - collider = EPerson.findByNetid(context, identity); - } - else - { - throw new PackageException("EPerson has neither email nor netid."); - } - - if (null != collider) - if (params.replaceModeEnabled()) // -r -f - { - eperson = collider; - } - else if (params.keepExistingModeEnabled()) // -r -k - { - log.warn("Existing EPerson {} was not restored from the package.", identity); - continue; - } - else - { - throw new PackageException("EPerson " + identity + " already exists."); - } - else - { - eperson = EPerson.create(context); - log.info("Created EPerson {}.", identity); - } - - eperson.setEmail(email); - eperson.setNetid(netid); - - NodeList data; - - data = user.getElementsByTagName(RoleDisseminator.FIRST_NAME); - if (data.getLength() > 0) - { - eperson.setFirstName(data.item(0).getTextContent()); - } - else - { - eperson.setFirstName(null); - } - - data = user.getElementsByTagName(RoleDisseminator.LAST_NAME); - if (data.getLength() > 0) - { - eperson.setLastName(data.item(0).getTextContent()); - } - else - { - eperson.setLastName(null); - } - - data = user.getElementsByTagName(RoleDisseminator.LANGUAGE); - if (data.getLength() > 0) - { - eperson.setLanguage(data.item(0).getTextContent()); - } - else - { - eperson.setLanguage(null); - } - - data = user.getElementsByTagName(RoleDisseminator.CAN_LOGIN); - eperson.setCanLogIn(data.getLength() > 0); - - data = user.getElementsByTagName(RoleDisseminator.REQUIRE_CERTIFICATE); - eperson.setRequireCertificate(data.getLength() > 0); - - data = user.getElementsByTagName(RoleDisseminator.SELF_REGISTERED); - eperson.setSelfRegistered(data.getLength() > 0); - - data = user.getElementsByTagName(RoleDisseminator.PASSWORD_HASH); - if (data.getLength() > 0) - { - Node element = data.item(0); - NamedNodeMap attributes = element.getAttributes(); - - Node algorithm = attributes.getNamedItem(RoleDisseminator.PASSWORD_DIGEST); - String algorithmText; - if (null != algorithm) - algorithmText = algorithm.getNodeValue(); - else - algorithmText = null; - - Node salt = attributes.getNamedItem(RoleDisseminator.PASSWORD_SALT); - String saltText; - if (null != salt) - saltText = salt.getNodeValue(); - else - saltText = null; - - PasswordHash password; - try { - password = new PasswordHash(algorithmText, saltText, element.getTextContent()); - } catch (DecoderException ex) { - throw new PackageValidationException("Unable to decode hexadecimal password hash or salt", ex); - } - eperson.setPasswordHash(password); - } - else - { - eperson.setPasswordHash(null); - } - - // Actually write Eperson info to DB - // NOTE: this update() doesn't call a commit(). So, Eperson info - // may still be rolled back if a subsequent error occurs - eperson.update(); - } - - // Now ingest the Groups - NodeList groups = document.getElementsByTagName(RoleDisseminator.GROUP); - - // Create the groups and add their EPerson members - for (int groupx = 0; groupx < groups.getLength(); groupx++) - { - Element group = (Element) groups.item(groupx); - String name = group.getAttribute(RoleDisseminator.NAME); - - try - { - //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) - // TODO: is this necessary? can we leave it in format with Handle in place of ? - // For now, this is necessary, because we don't want to accidentally - // create a new group COLLECTION_hdl:123/34_ADMIN, which is equivalent - // to an existing COLLECTION_45_ADMIN group - name = PackageUtils.translateGroupNameForImport(context, name); - } - catch(PackageException pe) - { - // If an error is thrown, then this Group corresponds to a - // Community or Collection that doesn't currently exist in the - // system. So, log a warning & skip it for now. - log.warn("Skipping group named '" + name + "' as it seems to correspond to a Community or Collection that does not exist in the system. " + - "If you are performing an AIP restore, you can ignore this warning as the Community/Collection AIP will likely create this group once it is processed."); - continue; - } - - Group groupObj = null; // The group to restore - Group collider = Group.findByName(context, name); // Existing group? - if (null != collider) - { // Group already exists, so empty it - if (params.replaceModeEnabled()) // -r -f - { - for (Group member : collider.getMemberGroups()) - { - collider.removeMember(member); - } - for (EPerson member : collider.getMembers()) - { - // Remove all group members *EXCEPT* we don't ever want - // to remove the current user from the list of Administrators - // (otherwise remainder of ingest will fail) - if(!(collider.equals(Group.find(context, 1)) && - member.equals(context.getCurrentUser()))) - { - collider.removeMember(member); - } - } - log.info("Existing Group {} was cleared. Its members will be replaced.", name); - groupObj = collider; - } - else if (params.keepExistingModeEnabled()) // -r -k - { - log.warn("Existing Group {} was not replaced from the package.", - name); - continue; - } - else - { - throw new PackageException("Group " + name + " already exists"); - } - } - else - { // No such group exists -- so, we'll need to create it! - - // First Check if this is a "typed" group (i.e. Community or Collection associated Group) - // If so, we'll create it via the Community or Collection - String type = group.getAttribute(RoleDisseminator.TYPE); - if(type!=null && !type.isEmpty() && parent!=null) - { - //What type of dspace object is this group associated with - if(parent.getType()==Constants.COLLECTION) - { - Collection collection = (Collection) parent; - - // Create this Collection-associated group, based on its group type - if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) - { - groupObj = collection.createAdministrators(); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_SUBMIT)) - { - groupObj = collection.createSubmitters(); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_1)) - { - groupObj = collection.createWorkflowGroup(1); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_2)) - { - groupObj = collection.createWorkflowGroup(2); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_3)) - { - groupObj = collection.createWorkflowGroup(3); - } - } - else if(parent.getType()==Constants.COMMUNITY) - { - Community community = (Community) parent; - - // Create this Community-associated group, based on its group type - if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) - { - groupObj = community.createAdministrators(); - } - } - //Ignore all other dspace object types - } - - //If group not yet created, create it with the given name - if(groupObj==null) - { - groupObj = Group.create(context); - } - - // Always set the name: parent.createBlop() is guessing - groupObj.setName(name); - - log.info("Created Group {}.", groupObj.getName()); - } - - // Add EPeople to newly created Group - NodeList members = group.getElementsByTagName(RoleDisseminator.MEMBER); - for (int memberx = 0; memberx < members.getLength(); memberx++) - { - Element member = (Element) members.item(memberx); - String memberName = member.getAttribute(RoleDisseminator.NAME); - EPerson memberEPerson = EPerson.findByEmail(context, memberName); - if (null != memberEPerson) - groupObj.addMember(memberEPerson); - else - throw new PackageValidationException("EPerson " + memberName - + " not found, not added to " + name); - } - - // Actually write Group info to DB - // NOTE: this update() doesn't call a commit(). So, Group info - // may still be rolled back if a subsequent error occurs - groupObj.update(); - - } - - // Go back and add Group members, now that all groups exist - for (int groupx = 0; groupx < groups.getLength(); groupx++) - { - Element group = (Element) groups.item(groupx); - String name = group.getAttribute(RoleDisseminator.NAME); - try - { - // Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) - name = PackageUtils.translateGroupNameForImport(context, name); - } - catch(PackageException pe) - { - // If an error is thrown, then this Group corresponds to a - // Community or Collection that doesn't currently exist in the - // system. So,skip it for now. - // (NOTE: We already logged a warning about this group earlier as - // this is the second time we are looping through all groups) - continue; - } - - // Find previously created group - Group groupObj = Group.findByName(context, name); - NodeList members = group - .getElementsByTagName(RoleDisseminator.MEMBER_GROUP); - for (int memberx = 0; memberx < members.getLength(); memberx++) - { - Element member = (Element) members.item(memberx); - String memberName = member.getAttribute(RoleDisseminator.NAME); - //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) - memberName = PackageUtils.translateGroupNameForImport(context, memberName); - // Find previously created group - Group memberGroup = Group.findByName(context, memberName); - groupObj.addMember(memberGroup); - } - // Actually update Group info in DB - // NOTE: Group info may still be rolled back if a subsequent error occurs - groupObj.update(); - } - } - - /** - * Ingest roles from an InputStream. - * - * @param context - * DSpace Context - * @param parent - * the Parent DSpaceObject - * @param stream - * the XML Document InputStream - * @throws PackageException - * @throws SQLException - * @throws AuthorizeException - */ - public static void ingestStream(Context context, DSpaceObject parent, - PackageParameters params, InputStream stream) - throws PackageException, SQLException, AuthorizeException - { - Document document; - - try - { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - dbf.setIgnoringComments(true); - dbf.setCoalescing(true); - DocumentBuilder db = dbf.newDocumentBuilder(); - document = db.parse(stream); - } - catch (ParserConfigurationException e) - { - throw new PackageException(e); - } - catch (SAXException e) - { - throw new PackageException(e); - } - catch (IOException e) - { - throw new PackageException(e); - } - /* - * TODO ? finally { close(stream); } - */ - ingestDocument(context, parent, params, document); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#ingest(org.dspace.core.Context - * , org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters, java.lang.String) - */ - @Override - public DSpaceObject ingest(Context context, DSpaceObject parent, - File pkgFile, PackageParameters params, String license) - throws PackageException, CrosswalkException, AuthorizeException, - SQLException, IOException - { - Document document; - - try - { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - dbf.setIgnoringComments(true); - dbf.setCoalescing(true); - DocumentBuilder db = dbf.newDocumentBuilder(); - document = db.parse(pkgFile); - } - catch (ParserConfigurationException e) - { - throw new PackageException(e); - } - catch (SAXException e) - { - throw new PackageException(e); - } - ingestDocument(context, parent, params, document); - - /* Does not create a DSpaceObject */ - return null; - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#ingestAll(org.dspace.core - * .Context, org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters, java.lang.String) - */ - @Override - public List ingestAll(Context context, DSpaceObject parent, - File pkgFile, PackageParameters params, String license) - throws PackageException, UnsupportedOperationException, - CrosswalkException, AuthorizeException, SQLException, IOException - { - throw new PackageException( - "ingestAll() is not implemented, as ingest() method already handles ingestion of all roles from an external file."); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#replace(org.dspace.core.Context - * , org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters) - */ - @Override - public DSpaceObject replace(Context context, DSpaceObject dso, - File pkgFile, PackageParameters params) throws PackageException, - UnsupportedOperationException, CrosswalkException, - AuthorizeException, SQLException, IOException - { - //Just call ingest() -- this will perform a replacement as necessary - return ingest(context, dso, pkgFile, params, null); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#replaceAll(org.dspace.core - * .Context, org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters) - */ - @Override - public List replaceAll(Context context, DSpaceObject dso, - File pkgFile, PackageParameters params) throws PackageException, - UnsupportedOperationException, CrosswalkException, - AuthorizeException, SQLException, IOException - { - throw new PackageException( - "replaceAll() is not implemented, as replace() method already handles replacement of all roles from an external file."); - } - - /** - * Returns a user help string which should describe the - * additional valid command-line options that this packager - * implementation will accept when using the -o or - * --option flags with the Packager script. - * - * @return a string describing additional command-line options available - * with this packager - */ - @Override - public String getParameterHelp() - { - return "No additional options available."; - } -} +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.packager; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.commons.codec.DecoderException; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.crosswalk.CrosswalkException; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.PasswordHash; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.*; +import org.xml.sax.SAXException; + +/** + * Create EPersons and Groups from a file of external representations. + * + * @author mwood + */ +public class RoleIngester implements PackageIngester +{ + private static final Logger log = LoggerFactory + .getLogger(RoleIngester.class); + + /** + * Common code to ingest roles from a Document. + * + * @param context + * DSpace Context + * @param parent + * the Parent DSpaceObject + * @param document + * the XML Document + * @throws SQLException + * @throws AuthorizeException + * @throws PackageException + */ + static void ingestDocument(Context context, DSpaceObject parent, + PackageParameters params, Document document) + throws SQLException, AuthorizeException, PackageException + { + String myEmail = context.getCurrentUser().getEmail(); + String myNetid = context.getCurrentUser().getNetid(); + + // Ingest users (EPersons) first so Groups can use them + NodeList users = document + .getElementsByTagName(RoleDisseminator.EPERSON); + for (int i = 0; i < users.getLength(); i++) + { + Element user = (Element) users.item(i); + // int userID = Integer.valueOf(user.getAttribute("ID")); // FIXME + // no way to set ID! + NodeList emails = user.getElementsByTagName(RoleDisseminator.EMAIL); + NodeList netids = user.getElementsByTagName(RoleDisseminator.NETID); + EPerson eperson; + EPerson collider; + String email = null; + String netid = null; + String identity; + if (emails.getLength() > 0) + { + email = emails.item(0).getTextContent(); + if (email.equals(myEmail)) + { + continue; // Cannot operate on my own EPerson! + } + identity = email; + collider = EPerson.findByEmail(context, identity); + // collider = EPerson.find(context, userID); + } + else if (netids.getLength() > 0) + { + netid = netids.item(0).getTextContent(); + if (netid.equals(myNetid)) + { + continue; // Cannot operate on my own EPerson! + } + identity = netid; + collider = EPerson.findByNetid(context, identity); + } + else + { + throw new PackageException("EPerson has neither email nor netid."); + } + + if (null != collider) + if (params.replaceModeEnabled()) // -r -f + { + eperson = collider; + } + else if (params.keepExistingModeEnabled()) // -r -k + { + log.warn("Existing EPerson {} was not restored from the package.", identity); + continue; + } + else + { + throw new PackageException("EPerson " + identity + " already exists."); + } + else + { + eperson = EPerson.create(context); + log.info("Created EPerson {}.", identity); + } + + eperson.setEmail(email); + eperson.setNetid(netid); + + NodeList data; + + data = user.getElementsByTagName(RoleDisseminator.FIRST_NAME); + if (data.getLength() > 0) + { + eperson.setFirstName(data.item(0).getTextContent()); + } + else + { + eperson.setFirstName(null); + } + + data = user.getElementsByTagName(RoleDisseminator.LAST_NAME); + if (data.getLength() > 0) + { + eperson.setLastName(data.item(0).getTextContent()); + } + else + { + eperson.setLastName(null); + } + + data = user.getElementsByTagName(RoleDisseminator.LANGUAGE); + if (data.getLength() > 0) + { + eperson.setLanguage(data.item(0).getTextContent()); + } + else + { + eperson.setLanguage(null); + } + + data = user.getElementsByTagName(RoleDisseminator.CAN_LOGIN); + eperson.setCanLogIn(data.getLength() > 0); + + data = user.getElementsByTagName(RoleDisseminator.REQUIRE_CERTIFICATE); + eperson.setRequireCertificate(data.getLength() > 0); + + data = user.getElementsByTagName(RoleDisseminator.SELF_REGISTERED); + eperson.setSelfRegistered(data.getLength() > 0); + + data = user.getElementsByTagName(RoleDisseminator.PASSWORD_HASH); + if (data.getLength() > 0) + { + Node element = data.item(0); + NamedNodeMap attributes = element.getAttributes(); + + Node algorithm = attributes.getNamedItem(RoleDisseminator.PASSWORD_DIGEST); + String algorithmText; + if (null != algorithm) + algorithmText = algorithm.getNodeValue(); + else + algorithmText = null; + + Node salt = attributes.getNamedItem(RoleDisseminator.PASSWORD_SALT); + String saltText; + if (null != salt) + saltText = salt.getNodeValue(); + else + saltText = null; + + PasswordHash password; + try { + password = new PasswordHash(algorithmText, saltText, element.getTextContent()); + } catch (DecoderException ex) { + throw new PackageValidationException("Unable to decode hexadecimal password hash or salt", ex); + } + eperson.setPasswordHash(password); + } + else + { + eperson.setPasswordHash(null); + } + + // Actually write Eperson info to DB + // NOTE: this update() doesn't call a commit(). So, Eperson info + // may still be rolled back if a subsequent error occurs + eperson.update(); + } + + // Now ingest the Groups + NodeList groups = document.getElementsByTagName(RoleDisseminator.GROUP); + + // Create the groups and add their EPerson members + for (int groupx = 0; groupx < groups.getLength(); groupx++) + { + Element group = (Element) groups.item(groupx); + String name = group.getAttribute(RoleDisseminator.NAME); + + try + { + //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) + // TODO: is this necessary? can we leave it in format with Handle in place of ? + // For now, this is necessary, because we don't want to accidentally + // create a new group COLLECTION_hdl:123/34_ADMIN, which is equivalent + // to an existing COLLECTION_45_ADMIN group + name = PackageUtils.translateGroupNameForImport(context, name); + } + catch(PackageException pe) + { + // If an error is thrown, then this Group corresponds to a + // Community or Collection that doesn't currently exist in the + // system. So, log a warning & skip it for now. + log.warn("Skipping group named '" + name + "' as it seems to correspond to a Community or Collection that does not exist in the system. " + + "If you are performing an AIP restore, you can ignore this warning as the Community/Collection AIP will likely create this group once it is processed."); + continue; + } + + Group groupObj = null; // The group to restore + Group collider = Group.findByName(context, name); // Existing group? + if (null != collider) + { // Group already exists, so empty it + if (params.replaceModeEnabled()) // -r -f + { + for (Group member : collider.getMemberGroups()) + { + collider.removeMember(member); + } + for (EPerson member : collider.getMembers()) + { + // Remove all group members *EXCEPT* we don't ever want + // to remove the current user from the list of Administrators + // (otherwise remainder of ingest will fail) + if(!(collider.equals(Group.find(context, 1)) && + member.equals(context.getCurrentUser()))) + { + collider.removeMember(member); + } + } + log.info("Existing Group {} was cleared. Its members will be replaced.", name); + groupObj = collider; + } + else if (params.keepExistingModeEnabled()) // -r -k + { + log.warn("Existing Group {} was not replaced from the package.", + name); + continue; + } + else + { + throw new PackageException("Group " + name + " already exists"); + } + } + else + { // No such group exists -- so, we'll need to create it! + + // First Check if this is a "typed" group (i.e. Community or Collection associated Group) + // If so, we'll create it via the Community or Collection + String type = group.getAttribute(RoleDisseminator.TYPE); + if(type!=null && !type.isEmpty() && parent!=null) + { + //What type of dspace object is this group associated with + if(parent.getType()==Constants.COLLECTION) + { + Collection collection = (Collection) parent; + + // Create this Collection-associated group, based on its group type + if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) + { + groupObj = collection.createAdministrators(); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_SUBMIT)) + { + groupObj = collection.createSubmitters(); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_1)) + { + groupObj = collection.createWorkflowGroup(1); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_2)) + { + groupObj = collection.createWorkflowGroup(2); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_3)) + { + groupObj = collection.createWorkflowGroup(3); + } + } + else if(parent.getType()==Constants.COMMUNITY) + { + Community community = (Community) parent; + + // Create this Community-associated group, based on its group type + if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) + { + groupObj = community.createAdministrators(); + } + } + //Ignore all other dspace object types + } + + //If group not yet created, create it with the given name + if(groupObj==null) + { + groupObj = Group.create(context); + } + + // Always set the name: parent.createBlop() is guessing + groupObj.setName(name); + + log.info("Created Group {}.", groupObj.getName()); + } + + // Add EPeople to newly created Group + NodeList members = group.getElementsByTagName(RoleDisseminator.MEMBER); + for (int memberx = 0; memberx < members.getLength(); memberx++) + { + Element member = (Element) members.item(memberx); + String memberName = member.getAttribute(RoleDisseminator.NAME); + EPerson memberEPerson = EPerson.findByEmail(context, memberName); + if (null != memberEPerson) + groupObj.addMember(memberEPerson); + else + throw new PackageValidationException("EPerson " + memberName + + " not found, not added to " + name); + } + + // Actually write Group info to DB + // NOTE: this update() doesn't call a commit(). So, Group info + // may still be rolled back if a subsequent error occurs + groupObj.update(); + + } + + // Go back and add Group members, now that all groups exist + for (int groupx = 0; groupx < groups.getLength(); groupx++) + { + Element group = (Element) groups.item(groupx); + String name = group.getAttribute(RoleDisseminator.NAME); + try + { + // Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) + name = PackageUtils.translateGroupNameForImport(context, name); + } + catch(PackageException pe) + { + // If an error is thrown, then this Group corresponds to a + // Community or Collection that doesn't currently exist in the + // system. So,skip it for now. + // (NOTE: We already logged a warning about this group earlier as + // this is the second time we are looping through all groups) + continue; + } + + // Find previously created group + Group groupObj = Group.findByName(context, name); + NodeList members = group + .getElementsByTagName(RoleDisseminator.MEMBER_GROUP); + for (int memberx = 0; memberx < members.getLength(); memberx++) + { + Element member = (Element) members.item(memberx); + String memberName = member.getAttribute(RoleDisseminator.NAME); + //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) + memberName = PackageUtils.translateGroupNameForImport(context, memberName); + // Find previously created group + Group memberGroup = Group.findByName(context, memberName); + groupObj.addMember(memberGroup); + } + // Actually update Group info in DB + // NOTE: Group info may still be rolled back if a subsequent error occurs + groupObj.update(); + } + } + + /** + * Ingest roles from an InputStream. + * + * @param context + * DSpace Context + * @param parent + * the Parent DSpaceObject + * @param stream + * the XML Document InputStream + * @throws PackageException + * @throws SQLException + * @throws AuthorizeException + */ + public static void ingestStream(Context context, DSpaceObject parent, + PackageParameters params, InputStream stream) + throws PackageException, SQLException, AuthorizeException + { + Document document; + + try + { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setIgnoringComments(true); + dbf.setCoalescing(true); + DocumentBuilder db = dbf.newDocumentBuilder(); + document = db.parse(stream); + } + catch (ParserConfigurationException e) + { + throw new PackageException(e); + } + catch (SAXException e) + { + throw new PackageException(e); + } + catch (IOException e) + { + throw new PackageException(e); + } + /* + * TODO ? finally { close(stream); } + */ + ingestDocument(context, parent, params, document); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#ingest(org.dspace.core.Context + * , org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters, java.lang.String) + */ + @Override + public DSpaceObject ingest(Context context, DSpaceObject parent, + File pkgFile, PackageParameters params, String license) + throws PackageException, CrosswalkException, AuthorizeException, + SQLException, IOException + { + Document document; + + try + { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setIgnoringComments(true); + dbf.setCoalescing(true); + DocumentBuilder db = dbf.newDocumentBuilder(); + document = db.parse(pkgFile); + } + catch (ParserConfigurationException e) + { + throw new PackageException(e); + } + catch (SAXException e) + { + throw new PackageException(e); + } + ingestDocument(context, parent, params, document); + + /* Does not create a DSpaceObject */ + return null; + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#ingestAll(org.dspace.core + * .Context, org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters, java.lang.String) + */ + @Override + public List ingestAll(Context context, DSpaceObject parent, + File pkgFile, PackageParameters params, String license) + throws PackageException, UnsupportedOperationException, + CrosswalkException, AuthorizeException, SQLException, IOException + { + throw new PackageException( + "ingestAll() is not implemented, as ingest() method already handles ingestion of all roles from an external file."); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#replace(org.dspace.core.Context + * , org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters) + */ + @Override + public DSpaceObject replace(Context context, DSpaceObject dso, + File pkgFile, PackageParameters params) throws PackageException, + UnsupportedOperationException, CrosswalkException, + AuthorizeException, SQLException, IOException + { + //Just call ingest() -- this will perform a replacement as necessary + return ingest(context, dso, pkgFile, params, null); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#replaceAll(org.dspace.core + * .Context, org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters) + */ + @Override + public List replaceAll(Context context, DSpaceObject dso, + File pkgFile, PackageParameters params) throws PackageException, + UnsupportedOperationException, CrosswalkException, + AuthorizeException, SQLException, IOException + { + throw new PackageException( + "replaceAll() is not implemented, as replace() method already handles replacement of all roles from an external file."); + } + + /** + * Returns a user help string which should describe the + * additional valid command-line options that this packager + * implementation will accept when using the -o or + * --option flags with the Packager script. + * + * @return a string describing additional command-line options available + * with this packager + */ + @Override + public String getParameterHelp() + { + return "No additional options available."; + } +}