diff --git a/webapp/build.xml b/webapp/build.xml index 41b3269c7..ee1a4280a 100644 --- a/webapp/build.xml +++ b/webapp/build.xml @@ -114,8 +114,8 @@ deploy - Deploy the application directly into the Tomcat webapps directory. message="${deploy.properties.file} must contain a value for VitroConnection.DataSource.username" /> - + diff --git a/webapp/config/example.deploy.properties b/webapp/config/example.deploy.properties index 9148818b3..339405801 100644 --- a/webapp/config/example.deploy.properties +++ b/webapp/config/example.deploy.properties @@ -83,11 +83,11 @@ VitroConnection.DataSource.validationQuery = SELECT 1 # vitro.local.solr.url = # -# The name of your first admin user for the Vitro application. The password -# for this user is initially set to "defaultAdmin", but you will be asked to +# The email address of the root user for the VIVO application. The password +# for this user is initially set to "rootPassword", but you will be asked to # change the password the first time you log in. # -initialAdminUser = defaultAdmin +rootUser.emailAddress = root@myDomain.com # # How is a logged-in user associated with a particular Individual? One way is diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/identifier/common/CommonIdentifierBundleFactory.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/identifier/common/CommonIdentifierBundleFactory.java index af51ba1d6..b2353e6ae 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/identifier/common/CommonIdentifierBundleFactory.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/identifier/common/CommonIdentifierBundleFactory.java @@ -24,6 +24,7 @@ import edu.cornell.mannlib.vitro.webapp.beans.Individual; import edu.cornell.mannlib.vitro.webapp.beans.SelfEditingConfiguration; import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; +import edu.cornell.mannlib.vitro.webapp.dao.UserAccountsDao; import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; /** @@ -49,6 +50,7 @@ public class CommonIdentifierBundleFactory implements IdentifierBundleFactory { ArrayIdentifierBundle bundle = new ArrayIdentifierBundle(); bundle.addAll(createUserIdentifiers(req)); + bundle.addAll(createRootUserIdentifiers(req)); bundle.addAll(createRoleLevelIdentifiers(req)); bundle.addAll(createBlacklistOrAssociatedIndividualIdentifiers(req)); @@ -68,6 +70,16 @@ public class CommonIdentifierBundleFactory implements IdentifierBundleFactory { } } + private Collection createRootUserIdentifiers( + HttpServletRequest req) { + UserAccount user = LoginStatusBean.getCurrentUser(req); + if (isRootUser(user)) { + return Collections.singleton(new IsRootUser()); + } else { + return Collections.emptySet(); + } + } + /** * Create an identifier that shows the role level of the current user, or * PUBLIC if the user is not logged in. @@ -131,6 +143,25 @@ public class CommonIdentifierBundleFactory implements IdentifierBundleFactory { return individuals; } + /** + * Is this user a root user? + */ + private boolean isRootUser(UserAccount user) { + if (user == null) { + return false; + } + + WebappDaoFactory wdf = (WebappDaoFactory) context + .getAttribute("webappDaoFactory"); + if (wdf == null) { + log.error("Could not get a WebappDaoFactory from the ServletContext"); + return false; + } + + UserAccountsDao uaDao = wdf.getUserAccountsDao(); + return uaDao.isRootUser(user); + } + @Override public String toString() { return this.getClass().getSimpleName() + " - " + hashCode(); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/identifier/common/IsRootUser.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/identifier/common/IsRootUser.java new file mode 100644 index 000000000..a41c2ab04 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/identifier/common/IsRootUser.java @@ -0,0 +1,21 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.auth.identifier.common; + +import edu.cornell.mannlib.vitro.webapp.auth.identifier.Identifier; +import edu.cornell.mannlib.vitro.webapp.auth.identifier.IdentifierBundle; + +/** + * The current user is a root user. + */ +public class IsRootUser extends AbstractCommonIdentifier implements Identifier { + public static boolean isRootUser(IdentifierBundle ids) { + return !getIdentifiersForClass(ids, IsRootUser.class).isEmpty(); + } + + @Override + public String toString() { + return "IsRootUser"; + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/RootUserPolicy.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/RootUserPolicy.java new file mode 100644 index 000000000..362fcba1e --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/policy/RootUserPolicy.java @@ -0,0 +1,164 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.auth.policy; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.hp.hpl.jena.ontology.OntModel; +import com.hp.hpl.jena.rdf.model.Resource; +import com.hp.hpl.jena.shared.Lock; +import com.hp.hpl.jena.vocabulary.RDF; + +import edu.cornell.mannlib.vitro.webapp.auth.identifier.IdentifierBundle; +import edu.cornell.mannlib.vitro.webapp.auth.identifier.common.IsRootUser; +import edu.cornell.mannlib.vitro.webapp.auth.policy.ifaces.Authorization; +import edu.cornell.mannlib.vitro.webapp.auth.policy.ifaces.PolicyDecision; +import edu.cornell.mannlib.vitro.webapp.auth.policy.ifaces.PolicyIface; +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.ifaces.RequestedAction; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount.Status; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.controller.authenticate.Authenticator; +import edu.cornell.mannlib.vitro.webapp.dao.UserAccountsDao; +import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; +import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; +import edu.cornell.mannlib.vitro.webapp.dao.jena.ModelContext; +import edu.cornell.mannlib.vitro.webapp.servlet.setup.AbortStartup; + +/** + * If the user has an IsRootUser identifier, they can do anything! + * + * On setup, check to see that there is a root user. If not, create one. If we + * can't create one, abort. + */ +public class RootUserPolicy implements PolicyIface { + private static final Log log = LogFactory.getLog(RootUserPolicy.class); + + private static final String PROPERTY_ROOT_USER_EMAIL = "rootUser.emailAddress"; + private static final String ROOT_USER_INITIAL_PASSWORD = "rootPassword"; + + /** + * This is the entire policy. If you are a root user, you are authorized. + */ + @Override + public PolicyDecision isAuthorized(IdentifierBundle whoToAuth, + RequestedAction whatToAuth) { + if (IsRootUser.isRootUser(whoToAuth)) { + return new BasicPolicyDecision(Authorization.AUTHORIZED, + "RootUserPolicy: approved"); + } else { + return new BasicPolicyDecision(Authorization.INCONCLUSIVE, + "not root user"); + } + } + + // ---------------------------------------------------------------------- + // Setup class + // ---------------------------------------------------------------------- + + public static class Setup implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + ServletContext ctx = sce.getServletContext(); + + if (AbortStartup.isStartupAborted(ctx)) { + return; + } + + try { + UserAccountsDao uaDao = getUserAccountsDao(ctx); + OntModel userAccountsModel = getUserAccountsModel(ctx); + + if (!rootUserExists(uaDao)) { + createRootUser(ctx, uaDao, userAccountsModel); + } + + ServletPolicyList.addPolicy(ctx, new RootUserPolicy()); + } catch (Exception e) { + log.error("could not run " + this.getClass().getSimpleName() + + ": " + e); + AbortStartup.abortStartup(ctx); + throw new RuntimeException(e); + } + } + + private UserAccountsDao getUserAccountsDao(ServletContext ctx) { + WebappDaoFactory wadf = (WebappDaoFactory) ctx + .getAttribute("webappDaoFactory"); + if (wadf == null) { + throw new IllegalStateException( + "No webappDaoFactory on the servlet context"); + } + return wadf.getUserAccountsDao(); + } + + private OntModel getUserAccountsModel(ServletContext ctx) { + return ModelContext.getBaseOntModelSelector(ctx) + .getUserAccountsModel(); + } + + private boolean rootUserExists(UserAccountsDao uaDao) { + for (UserAccount ua : uaDao.getAllUserAccounts()) { + if (uaDao.isRootUser(ua)) { + return true; + } + } + return false; + } + + /** + * TODO The first and last name should be left blank, so the user will + * be forced to edit them. However, that's not in place yet. + */ + private void createRootUser(ServletContext ctx, UserAccountsDao uaDao, + OntModel userAccountsModel) { + String emailAddress = ConfigurationProperties.getBean(ctx) + .getProperty(PROPERTY_ROOT_USER_EMAIL); + if (emailAddress == null) { + throw new IllegalStateException( + "deploy.properties must contain a value for '" + + PROPERTY_ROOT_USER_EMAIL + "'"); + } + + if (null != uaDao.getUserAccountByEmail(emailAddress)) { + throw new IllegalStateException("Can't create root user - " + + "an account already exists with email address '" + + emailAddress + "'"); + } + + UserAccount ua = new UserAccount(); + ua.setEmailAddress(emailAddress); + ua.setFirstName("root"); + ua.setLastName("user"); + ua.setMd5Password(Authenticator + .applyMd5Encoding(ROOT_USER_INITIAL_PASSWORD)); + ua.setPasswordChangeRequired(true); + ua.setStatus(Status.ACTIVE); + + uaDao.insertUserAccount(ua); + + userAccountsModel.enterCriticalSection(Lock.WRITE); + try { + Resource r = userAccountsModel.getResource(ua.getUri()); + Resource t = userAccountsModel + .getResource(VitroVocabulary.USERACCOUNT_ROOT_USER); + userAccountsModel.add(r, RDF.type, t); + } finally { + userAccountsModel.leaveCriticalSection(); + } + + log.info("Created root user as '" + emailAddress + "'"); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + // Nothing to destroy + } + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/UserAccountsDao.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/UserAccountsDao.java index 927538a31..46115df77 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/UserAccountsDao.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/UserAccountsDao.java @@ -40,6 +40,11 @@ public interface UserAccountsDao { */ UserAccount getUserAccountByExternalAuthId(String externalAuthId); + /** + * Is this UserAccount a root user? + */ + boolean isRootUser(UserAccount userAccount); + /** * Create a new UserAccount in the model. * diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java index f92bba41e..706b665cb 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java @@ -174,6 +174,7 @@ public class VitroVocabulary { // =============== Vitro UserAccount and PermissionSet vocabulary =========== public static final String USERACCOUNT = VITRO_AUTH + "UserAccount"; + public static final String USERACCOUNT_ROOT_USER = VITRO_AUTH + "RootUserAccount"; public static final String USERACCOUNT_EMAIL_ADDRESS = VITRO_AUTH + "emailAddress"; public static final String USERACCOUNT_FIRST_NAME = VITRO_AUTH + "firstName"; public static final String USERACCOUNT_LAST_NAME = VITRO_AUTH + "lastName"; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/filtering/UserAccountsDaoFiltering.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/filtering/UserAccountsDaoFiltering.java index 2a08eb9f8..12b725c5b 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/filtering/UserAccountsDaoFiltering.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/filtering/UserAccountsDaoFiltering.java @@ -47,6 +47,11 @@ public class UserAccountsDaoFiltering extends BaseFiltering implements return innerDao.getUserAccountByExternalAuthId(externalAuthId); } + @Override + public boolean isRootUser(UserAccount userAccount) { + return innerDao.isRootUser(userAccount); + } + @Override public String insertUserAccount(UserAccount userAccount) { return innerDao.insertUserAccount(userAccount); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java index c455add30..c29a19e54 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java @@ -155,6 +155,7 @@ public class JenaBaseDaoCon { /* ***************** User Account Model constants ***************** */ protected OntClass USERACCOUNT = _constModel.createClass(VitroVocabulary.USERACCOUNT); + protected OntClass USERACCOUNT_ROOT_USER = _constModel.createClass(VitroVocabulary.USERACCOUNT_ROOT_USER); protected DatatypeProperty USERACCOUNT_EMAIL_ADDRESS = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_EMAIL_ADDRESS); protected DatatypeProperty USERACCOUNT_FIRST_NAME = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_FIRST_NAME); protected DatatypeProperty USERACCOUNT_LAST_NAME = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_LAST_NAME); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java index 82613c2da..48b53a856 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java @@ -157,6 +157,21 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao return getUserAccountByUri(userUri); } + @Override + public boolean isRootUser(UserAccount userAccount) { + if (userAccount == null) { + return false; + } + + getOntModel().enterCriticalSection(Lock.READ); + try { + OntResource r = getOntModel().getOntResource(userAccount.getUri()); + return isResourceOfType(r, USERACCOUNT_ROOT_USER); + } finally { + getOntModel().leaveCriticalSection(); + } + } + @Override public String insertUserAccount(UserAccount userAccount) { if (userAccount == null) { @@ -361,6 +376,13 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao * There should already be a lock on the model when this is called. */ private boolean isResourceOfType(OntResource r, OntClass type) { + if (r == null) { + return false; + } + if (type == null) { + return false; + } + StmtIterator stmts = getOntModel().listStatements(r, RDF.type, type); if (stmts.hasNext()) { stmts.close(); diff --git a/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/dao/UserAccountsDaoStub.java b/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/dao/UserAccountsDaoStub.java index 42aa6a104..c0a96befe 100644 --- a/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/dao/UserAccountsDaoStub.java +++ b/webapp/test/stubs/edu/cornell/mannlib/vitro/webapp/dao/UserAccountsDaoStub.java @@ -48,6 +48,11 @@ public class UserAccountsDaoStub implements UserAccountsDao { "UserAccountsDaoStub.getUserAccountByEmail() not implemented."); } + @Override + public boolean isRootUser(UserAccount userAccount) { + throw new RuntimeException("UserAccountsDao.isRootUser() not implemented."); + } + @Override public String insertUserAccount(UserAccount userAccount) { throw new RuntimeException(