/** * The contents of this file are subject to the OpenMRS Public License * Version 1.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://license.openmrs.org * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ package org.openmrs.web.filter.update; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import liquibase.ChangeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.Appender; import org.apache.log4j.Logger; import org.openmrs.api.context.Context; import org.openmrs.util.DatabaseUpdateException; import org.openmrs.util.DatabaseUpdater; import org.openmrs.util.InputRequiredException; import org.openmrs.util.MemoryAppender; import org.openmrs.util.OpenmrsConstants; import org.openmrs.util.Security; import org.openmrs.util.DatabaseUpdater.ChangeSetExecutorCallback; import org.openmrs.web.Listener; import org.openmrs.web.filter.StartupFilter; import org.springframework.web.context.ContextLoader; /** * This is the second filter that is processed. It is only active when OpenMRS has some liquibase * updates that need to be run. If updates are needed, this filter/wizard asks for a super user to * authenticate and review the updates before continuing. */ public class UpdateFilter extends StartupFilter { protected final Log log = LogFactory.getLog(getClass()); /** * The velocity macro page to redirect to if an error occurs or on initial startup */ private final String DEFAULT_PAGE = "maintenance.vm"; /** * The page that lists off all the currently unexecuted changes */ private final String REVIEW_CHANGES = "reviewchanges.vm"; private final String PROGRESS_VM_AJAXREQUEST = "updateProgress.vm.ajaxRequest"; /** * The model object behind this set of screens */ private UpdateFilterModel model = null; /** * Variable set as soon as the update is done or verified to not be needed so that future calls * through this filter are a simple boolean check */ private static boolean updatesRequired = true; /** * Used on all pages after the first to make sure the user isn't trying to cheat and do some url * magic to hack in. */ private boolean authenticatedSuccessfully = false; private UpdateFilterCompletion updateJob; /** * Variable set to true as soon as the update begins and set to false when the process ends * This thread should only be accesses thorugh the sychronized method. */ private static boolean isDatabaseUpdateInProgress = false; /** * Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on GET requests * * @param httpRequest * @param httpResponse */ @Override protected void doGet(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException { Map<String, Object> referenceMap = new HashMap<String, Object>(); // do step one of the wizard renderTemplate(DEFAULT_PAGE, referenceMap, httpResponse); } /** * Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on POST requests * * @see org.openmrs.web.filter.StartupFilter#doPost(javax.servlet.http.HttpServletRequest, * javax.servlet.http.HttpServletResponse) */ @Override protected void doPost(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException { String page = httpRequest.getParameter("page"); Map<String, Object> referenceMap = new HashMap<String, Object>(); // step one if (DEFAULT_PAGE.equals(page)) { String username = httpRequest.getParameter("username"); String password = httpRequest.getParameter("password"); log.debug("Attempting to authenticate user: " + username); if (authenticateAsSuperUser(username, password)) { log.debug("Authentication successful. Redirecting to 'reviewupdates' page."); // set a variable so we know that the user started here authenticatedSuccessfully = true; //Set variable to tell us whether updates are already in progress referenceMap.put("isDatabaseUpdateInProgress", isDatabaseUpdateInProgress); renderTemplate(REVIEW_CHANGES, referenceMap, httpResponse); } else { // if not authenticated, show main page again try { log.debug("Sleeping for 3 seconds because of a bad username/password"); Thread.sleep(3000); } catch (InterruptedException e) { log.error("Unable to sleep", e); throw new ServletException("Got interrupted while trying to sleep thread", e); } errors.add("Unable to authenticate as a User with the " + OpenmrsConstants.SUPERUSER_ROLE + " role. Invalid username or password"); renderTemplate(DEFAULT_PAGE, referenceMap, httpResponse); } } // step two else if (REVIEW_CHANGES.equals(page)) { if (!authenticatedSuccessfully) { // throw the user back to the main page because they are cheating renderTemplate(DEFAULT_PAGE, referenceMap, httpResponse); return; } //if no one has run any required updates if (!isDatabaseUpdateInProgress) { isDatabaseUpdateInProgress = true; updateJob = new UpdateFilterCompletion(); updateJob.start(); referenceMap.put("updateJobStarted", true); } else{ referenceMap.put("isDatabaseUpdateInProgress", true); referenceMap.put("updateJobStarted", false); } renderTemplate(REVIEW_CHANGES, referenceMap, httpResponse); } else if (PROGRESS_VM_AJAXREQUEST.equals(page)) { httpResponse.setContentType("text/json"); httpResponse.setHeader("Cache-Control", "no-cache"); Map<String, Object> result = new HashMap<String, Object>(); if (updateJob != null) { result.put("hasErrors", updateJob.hasErrors()); if (updateJob.hasErrors()) { errors.addAll(updateJob.getErrors()); } result.put("updatesRequired", updatesRequired()); result.put("message", updateJob.getMessage()); result.put("changesetIds", updateJob.getChangesetIds()); result.put("executingChangesetId", updateJob.getExecutingChangesetId()); Appender appender = Logger.getRootLogger().getAppender("MEMORY_APPENDER"); if (appender instanceof MemoryAppender) { MemoryAppender memoryAppender = (MemoryAppender) appender; result.put("logLines", memoryAppender.getLogLines()); } else { result.put("logLines", new ArrayList<String>()); } } String jsonText = toJSONString(result); httpResponse.getWriter().write(jsonText); } } /** * Look in the users table for a user with this username and password and see if they have a * role of {@link OpenmrsConstants#SUPERUSER_ROLE}. * * @param usernameOrSystemId user entered username * @param password user entered password * @return true if this user has the super user role * @see #isSuperUser(Connection, Integer) * @should return false if given invalid credentials * @should return false if given user is not superuser * @should return true if given user is superuser * @should not authorize retired superusers * @should authenticate with systemId */ protected boolean authenticateAsSuperUser(String usernameOrSystemId, String password) throws ServletException { Connection connection = null; try { connection = DatabaseUpdater.getConnection(); String select = "select user_id, password, salt from users where (username = ? or system_id = ?) and retired = 0"; PreparedStatement statement = connection.prepareStatement(select); statement.setString(1, usernameOrSystemId); statement.setString(2, usernameOrSystemId); if (statement.execute()) { ResultSet results = statement.getResultSet(); if (results.next()) { Integer userId = results.getInt(1); String storedPassword = results.getString(2); String salt = results.getString(3); String passwordToHash = password + salt; return Security.hashMatches(storedPassword, passwordToHash) && isSuperUser(connection, userId); } } } catch (Throwable t) { log .error( "Error while trying to authenticate as super user. Ignore this if you are upgrading from OpenMRS 1.5 to 1.6", t); // we may not have upgraded User to have retired instead of voided yet, so if the query above fails, we try // again the old way try { String select = "select user_id, password, salt from users where (username = ? or system_id = ?) and voided = 0"; PreparedStatement statement = connection.prepareStatement(select); statement.setString(1, usernameOrSystemId); statement.setString(2, usernameOrSystemId); if (statement.execute()) { ResultSet results = statement.getResultSet(); if (results.next()) { Integer userId = results.getInt(1); String storedPassword = results.getString(2); String salt = results.getString(3); String passwordToHash = password + salt; return Security.hashMatches(storedPassword, passwordToHash) && isSuperUser(connection, userId); } } } catch (Throwable t2) { log.error("Error while trying to authenticate as super user (voided version)", t); } } finally { if (connection != null) try { connection.close(); } catch (SQLException e) { log.debug("Error while closing the database", e); } } return false; } /** * Checks the given user to see if they have been given the * {@link OpenmrsConstants#SUPERUSER_ROLE} role. This method does not look at child roles. * * @param connection the java sql connection to use * @param userId the user id to look at * @return true if the given user is a super user * @should return true if given user has superuser role * @should return false if given user does not have the super user role */ protected boolean isSuperUser(Connection connection, Integer userId) throws Exception { // the 'Administrator' part of this string is necessary because if the database was upgraded // by OpenMRS 1.6 alpha then System Developer was renamed to that. This has to be here so we // can roll back that change in 1.6 beta+ String select = "select 1 from user_role where user_id = ? and (role = ? or role = 'Administrator')"; PreparedStatement statement = connection.prepareStatement(select); statement.setInt(1, userId); statement.setString(2, OpenmrsConstants.SUPERUSER_ROLE); if (statement.execute()) { ResultSet results = statement.getResultSet(); if (results.next()) { return results.getInt(1) == 1; } } return false; } /** * Do everything to get openmrs going. * * @param servletContext the servletContext from the filterconfig * @see Listener#startOpenmrs(ServletContext) */ private void startOpenmrs(ServletContext servletContext) throws IOException, ServletException { // start spring // after this point, all errors need to also call: contextLoader.closeWebApplicationContext(event.getServletContext()) // logic copied from org.springframework.web.context.ContextLoaderListener ContextLoader contextLoader = new ContextLoader(); contextLoader.initWebApplicationContext(servletContext); try { Listener.startOpenmrs(servletContext); } catch (ServletException servletException) { contextLoader.closeWebApplicationContext(servletContext); throw servletException; } } /** * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) */ @Override public void init(FilterConfig filterConfig) throws ServletException { super.init(filterConfig); log.debug("Initializing the UpdateFilter"); Properties properties = Listener.getRuntimeProperties(); if (properties != null) { model = new UpdateFilterModel(); Context.setRuntimeProperties(properties); try { if (model.changes == null) updatesRequired = false; else { log.debug("Setting updates required to " + (model.changes.size() > 0) + " because of the size of unrun changes"); updatesRequired = model.changes.size() > 0; } } catch (Exception e) { throw new ServletException("Unable to determine if updates are required", e); } } else { // the wizard runs the updates, so they will not need any updates. log.debug("Setting updates required to false because the user doesn't have any runtime properties yet"); setUpdatesRequired(false); } } /** * @see org.openmrs.web.filter.StartupFilter#getModel() */ @Override protected Object getModel() { // this object was initialized in the #init(FilterConfig) method return model; } /** * @see org.openmrs.web.filter.StartupFilter#skipFilter() */ @Override public boolean skipFilter(HttpServletRequest httpRequest) { return !PROGRESS_VM_AJAXREQUEST.equals(httpRequest.getParameter("page")) && !updatesRequired(); } /** * Used by the Listener to know if this filter wants to do its magic * * @return true if updates have been determined to be required * @see #init(FilterConfig) * @see Listener#setupNeeded */ public static synchronized boolean updatesRequired() { return updatesRequired; } /** * @param updatesRequired the updatesRequired to set */ protected static synchronized void setUpdatesRequired(boolean updatesRequired) { UpdateFilter.updatesRequired = updatesRequired; } /** * @see org.openmrs.web.filter.StartupFilter#getTemplatePrefix() */ @Override protected String getTemplatePrefix() { return "org/openmrs/web/filter/update/"; } /** * This class controls the final steps and is used by the ajax calls to know what updates have * been executed. TODO: Break this out into a separate (non-inner) class */ private class UpdateFilterCompletion { private Thread thread; private String executingChangesetId = null; private List<String> changesetIds = new ArrayList<String>(); private List<String> errors = new ArrayList<String>(); private String message = null; private boolean erroneous = false; synchronized public void reportError(String error) { List<String> errors = new ArrayList<String>(); errors.add(error); reportErrors(errors); } synchronized public void reportErrors(List<String> errs) { errors.addAll(errs); erroneous = true; } synchronized public boolean hasErrors() { return erroneous; } synchronized public List<String> getErrors() { return errors; } /** * Start the completion stage. This fires up the thread to do all the work. */ public void start() { setUpdatesRequired(true); thread.start(); } public void waitForCompletion() { try { thread.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block log.error("Error generated", e); } } synchronized public void setMessage(String message) { this.message = message; } synchronized public String getMessage() { return message; } synchronized public void addChangesetId(String changesetid) { this.changesetIds.add(changesetid); this.executingChangesetId = changesetid; } synchronized public List<String> getChangesetIds() { return changesetIds; } synchronized public String getExecutingChangesetId() { return executingChangesetId; } /** * This class does all the work of creating the desired database, user, updates, etc */ public UpdateFilterCompletion() { Runnable r = new Runnable() { /** * TODO split this up into multiple testable methods * * @see java.lang.Runnable#run() */ public void run() { try { /** * A callback class that prints out info about liquibase changesets */ class PrintingChangeSetExecutorCallback implements ChangeSetExecutorCallback { private String message; public PrintingChangeSetExecutorCallback(String message) { this.message = message; } /** * @see org.openmrs.util.DatabaseUpdater.ChangeSetExecutorCallback#executing(liquibase.ChangeSet, * int) */ public void executing(ChangeSet changeSet, int numChangeSetsToRun) { addChangesetId(changeSet.getId()); setMessage(message); } } try { setMessage("Updating the database to the latest version"); DatabaseUpdater.executeChangelog(null, null, new PrintingChangeSetExecutorCallback( "Updating database tables to latest version ")); executingChangesetId = null; // clear out the last changeset } catch (InputRequiredException inputRequired) { // the user would be stepped through the questions returned here. log.error("Not implemented", inputRequired); model.updateChanges(); reportError("Input during database updates is not yet implemented. " + inputRequired.getMessage()); return; } catch (DatabaseUpdateException e) { log.error("Unable to update the database", e); List<String> errors = new ArrayList<String>(); errors.add("Unable to update the database. See server error logs for the full stacktrace."); errors.addAll(Arrays.asList(e.getMessage().split("\n"))); model.updateChanges(); reportErrors(errors); return; } setMessage("Starting OpenMRS"); try { startOpenmrs(filterConfig.getServletContext()); } catch (Throwable t) { log.error("Unable to complete the startup.", t); reportError("Unable to complete the startup. See the server error log for the complete stacktrace." + t.getMessage()); return; } // set this so that the wizard isn't run again on next page load setUpdatesRequired(false); } finally { if (!hasErrors()) { setUpdatesRequired(false); } //reset to let other user's make requests after updates are run isDatabaseUpdateInProgress = false; } } }; thread = new Thread(r); } } }