/*
* eXist Open Source Native XML Database
* Copyright (C) 2014 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* $Id$
*/
package org.exist.backup;
import org.apache.commons.io.FileUtils;
import org.exist.EXistException;
import org.exist.backup.restore.listener.RestoreListener;
import org.exist.jetty.JettyStart;
import org.exist.security.*;
import org.exist.security.SecurityManager;
import org.exist.security.internal.RealmImpl;
import org.exist.security.internal.aider.GroupAider;
import org.exist.security.internal.aider.UserAider;
import org.exist.storage.BrokerPool;
import org.exist.storage.journal.Journal;
import org.exist.util.Configuration;
import org.exist.util.ConfigurationHelper;
import org.exist.util.DatabaseConfigurationException;
import org.exist.xmldb.UserManagementService;
import org.junit.*;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.Resource;
import org.xmldb.api.base.ResourceSet;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.CollectionManagementService;
import org.xmldb.api.modules.XMLResource;
import org.xmldb.api.modules.XPathQueryService;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Observable;
import static org.junit.Assert.assertEquals;
public class BackupRestoreSecurityPrincipalsTest {
private File backupFile = null;
private static JettyStart server;
private final static String BACKUP_FILE = "exist.BackupRestoreSecurityPrincipalsTest.backup.zip";
private final static String FRANK_USER = "frank";
private final static String JOE_USER = "joe";
private final static String JACK_USER = "jack";
private void startupDatabase() throws EXistException, DatabaseConfigurationException {
server = new JettyStart();
server.run();
}
/**
* Creates a backup of a database with
* three users: 'frank', 'joe' and 'jack' present
*
* It then clears out the database (including those users)
* so that it is ready for further testing
*/
@Before
public void setup() throws PermissionDeniedException, EXistException, XMLDBException, SAXException, IOException, DatabaseConfigurationException, AuthenticationException {
startupDatabase();
createUser(FRANK_USER, FRANK_USER); //should have id RealmImpl.INITIAL_LAST_ACCOUNT_ID + 1
createUser(JOE_USER, JOE_USER); //should have id RealmImpl.INITIAL_LAST_ACCOUNT_ID + 2
createUser(JACK_USER, JACK_USER); //should have id RealmImpl.INITIAL_LAST_ACCOUNT_ID + 3
final File tmpDir = new File(System.getProperty("java.io.tmpdir"));
backupFile = new File(tmpDir, BACKUP_FILE);
backupFile.deleteOnExit();
final Backup backup = new Backup("admin", "", backupFile.getAbsolutePath());
backup.backup(false, null);
//reset database
resetDatabaseToClean();
}
private void resetDatabaseToClean() throws EXistException, DatabaseConfigurationException {
shutdownDatabase();
deleteDatabaseFiles();
startupDatabase();
}
private void deleteDatabaseFiles() throws DatabaseConfigurationException {
final File confFile = ConfigurationHelper.lookup("conf.xml");
final Configuration config = new Configuration(confFile.getAbsolutePath());
final File dataDir = new File(config.getProperty(BrokerPool.PROPERTY_DATA_DIR).toString());
if(dataDir.exists()) {
deleteAllDataFiles(dataDir);
}
final File journalDir = new File(config.getProperty(Journal.PROPERTY_RECOVERY_JOURNAL_DIR).toString());
if(journalDir.exists()) {
deleteAllDataFiles(journalDir);
}
}
/**
* Deletes all files except those named 'RECOVERY' or 'README'
*
* Typically executed in $EXIST_HOME/webapp/WEB-INF/data
* to clear the database
*/
private void deleteAllDataFiles(final File root) {
final File[] dataFiles = root.listFiles(new FilenameFilter() {
@Override
public boolean accept(final File dir, final String name) {
return !(name.equals("RECOVERY") || name.equals("README") || name.equals(".DO_NOT_DELETE"));
}
});
for(final File dataFile : dataFiles) {
FileUtils.deleteQuietly(dataFile);
}
}
@After
public void shutdownDatabase() {
server.shutdown();
server = null;
}
/**
* We start with an empty database and then we create
* two users: 'frank' and 'jack'.
*
* We then try and restore a database backup, which already
* contains users 'frank', 'joe' and 'jack':
*
* frank will have the same username and user id in the current
* database and the backup we are trying to restore.
*
* joe does not exist in the current database, but his user id
* in the backup will collide with that of jack in the current database.
*
* jack will have a different user id in the backup when compared to the current
* database, however he will have the same username.
*
* We want to make sure that after the restore, all three users are present
* that they have distinct and expected user ids and that any resources
* that were owner by them are still correctly owner by them (and not some other user).
*/
@Test
public void restoreConflictingUsername() throws PermissionDeniedException, EXistException, SAXException, ParserConfigurationException, IOException, URISyntaxException, XMLDBException, AuthenticationException {
final Collection root = DatabaseManager.getCollection("xmldb:exist:///db", "admin", "");
final XPathQueryService xqs = (XPathQueryService) root.getService("XPathQueryService", "1.0");
//create new users: 'frank' and 'jack'
createUser(FRANK_USER, FRANK_USER); // should have id RealmImpl.INITIAL_LAST_ACCOUNT_ID + 1
createUser(JACK_USER, JACK_USER); // should have id RealmImpl.INITIAL_LAST_ACCOUNT_ID + 2
final String accountQuery = "declare namespace c = 'http://exist-db.org/Configuration';\n" +
"for $account in //c:account\n" +
"return\n" +
"<user id='{$account/@id}' name='{$account/c:name}'/>";
//check the current user accounts
ResourceSet result = xqs.query(accountQuery);
assertUser(RealmImpl.ADMIN_ACCOUNT_ID, SecurityManager.DBA_USER, ((XMLResource) result.getResource(0)).getContentAsDOM());
assertUser(RealmImpl.GUEST_ACCOUNT_ID, SecurityManager.GUEST_USER, ((XMLResource) result.getResource(1)).getContentAsDOM());
assertUser(RealmImpl.INITIAL_LAST_ACCOUNT_ID + 1, "frank", ((XMLResource) result.getResource(2)).getContentAsDOM());
assertUser(RealmImpl.INITIAL_LAST_ACCOUNT_ID + 2, "jack", ((XMLResource) result.getResource(3)).getContentAsDOM());
//check the last user id
final String lastAccountIdQuery = "declare namespace c = 'http://exist-db.org/Configuration';\n" +
"//c:security-manager/string(@last-account-id)";
result = xqs.query(lastAccountIdQuery);
assertEquals(RealmImpl.INITIAL_LAST_ACCOUNT_ID + 2, Integer.parseInt(result.getResource(0).getContent().toString())); //last account id should be that of 'jack'
//create a test collection and give everyone access
final CollectionManagementService cms = (CollectionManagementService)root.getService("CollectionManagementService", "1.0");
final Collection test = cms.createCollection("test");
final UserManagementService testUms = (UserManagementService)test.getService("UserManagementService", "1.0");
testUms.chmod("rwxrwxrwx");
//create and store a new document as 'frank'
final Collection frankTest = DatabaseManager.getCollection("xmldb:exist:///db/test", FRANK_USER, FRANK_USER);
final String FRANKS_DOCUMENT = "franks-document.xml";
final Resource frankDoc = frankTest.createResource(FRANKS_DOCUMENT, XMLResource.RESOURCE_TYPE);
frankDoc.setContent("<hello>frank</hello>");
frankTest.storeResource(frankDoc);
//create and store a new document as 'jack'
final Collection jackTest = DatabaseManager.getCollection("xmldb:exist:///db/test", JACK_USER, JACK_USER);
final String JACKS_DOCUMENT = "jacks-document.xml";
final Resource jackDoc = jackTest.createResource(JACKS_DOCUMENT, XMLResource.RESOURCE_TYPE);
jackDoc.setContent("<hello>jack</hello>");
jackTest.storeResource(jackDoc);
//restore the database backup
final Restore restore = new Restore();
restore.restore(new NullRestoreListener(), "admin", "", null, backupFile, "xmldb:exist:///db");
//check the current user accounts after the restore
result = xqs.query(accountQuery);
assertUser(RealmImpl.ADMIN_ACCOUNT_ID, SecurityManager.DBA_USER, ((XMLResource) result.getResource(0)).getContentAsDOM());
assertUser(RealmImpl.GUEST_ACCOUNT_ID, SecurityManager.GUEST_USER, ((XMLResource) result.getResource(1)).getContentAsDOM());
assertUser(RealmImpl.INITIAL_LAST_ACCOUNT_ID + 1, "frank", ((XMLResource) result.getResource(2)).getContentAsDOM());
assertUser(RealmImpl.INITIAL_LAST_ACCOUNT_ID + 2, "jack", ((XMLResource) result.getResource(3)).getContentAsDOM());
assertUser(RealmImpl.INITIAL_LAST_ACCOUNT_ID + 4, "joe", ((XMLResource) result.getResource(4)).getContentAsDOM()); //this is `+ 4` because pre-allocating an id skips one
//check the last user id after the restore
result = xqs.query(lastAccountIdQuery);
assertEquals(RealmImpl.INITIAL_LAST_ACCOUNT_ID + 4, Integer.parseInt(result.getResource(0).getContent().toString())); //last account id should be that of 'joe'
//check the owner of frank's document after restore
final Resource fDoc = test.getResource(FRANKS_DOCUMENT);
final Permission franksDocPermissions = testUms.getPermissions(fDoc);
assertEquals(FRANK_USER, franksDocPermissions.getOwner().getName());
//check the owner of jack's document after restore
final Resource jDoc = test.getResource(JACKS_DOCUMENT);
final Permission jacksDocPermissions = testUms.getPermissions(jDoc);
assertEquals(JACK_USER, jacksDocPermissions.getOwner().getName());
}
private void assertUser(final int userId, final String userName, final Node account) {
assertEquals(userId, Integer.parseInt(account.getAttributes().getNamedItem("id").getNodeValue()));
assertEquals(userName, account.getAttributes().getNamedItem("name").getNodeValue());
}
private void createUser(final String username, final String password) throws XMLDBException, PermissionDeniedException {
final Collection root = DatabaseManager.getCollection("xmldb:exist:///db", "admin", "");
final UserManagementService ums = (UserManagementService) root.getService("UserManagementService", "1.0");
final Account user = new UserAider(username);
user.setPassword(password);
//create the personal group
Group group = new GroupAider(username);
group.setMetadataValue(EXistSchemaType.DESCRIPTION, "Personal group for " + username);
group.addManager(ums.getAccount("admin"));
ums.addGroup(group);
//add the personal group as the primary group
user.addGroup(username);
//create the account
ums.addAccount(user);
//add the new account as a manager of their personal group
ums.addGroupManager(username, group.getName());
}
private class NullRestoreListener implements RestoreListener {
@Override
public void createCollection(String collection) {
}
@Override
public void restored(String resource) {
}
@Override
public void info(String message) {
}
@Override
public void warn(String message) {
}
@Override
public void error(String message) {
}
@Override
public String warningsAndErrorsAsString() {
return null;
}
@Override
public boolean hasProblems() {
return false;
}
@Override
public void setCurrentCollection(String currentCollectionName) {
}
@Override
public void setCurrentResource(String currentResourceName) {
}
@Override
public void restoreStarting() {
}
@Override
public void restoreFinished() {
}
@Override
public void observe(Observable observable) {
}
@Override
public void setCurrentBackup(String currentBackup) {
}
}
}