Sat, 30 Dec 2017 20:35:23 +0100
adds DatabaseFacade
--- a/setup/postgres/psql_create_database.sql Tue Dec 26 19:45:31 2017 +0100 +++ b/setup/postgres/psql_create_database.sql Sat Dec 30 20:35:23 2017 +0100 @@ -1,9 +1,5 @@ --- Create a database owner role, which has no login permissions. --- You can either: --- 1) login as default user and switch the user --- 2) decide to override this decision and give login permissions --- 3) use your superuser of choice to manage the database (not recommended!) -create role lightpit_dbo with password 'lpit_dbo_changeme'; +-- Create a database owner role, which is also a privileged user +create user lightpit_dbo with password 'lpit_dbo_changeme'; -- Create the actual (unprivileged) database user create user lightpit_user with password 'lpit_user_changeme';
--- a/src/java/de/uapcore/lightpit/Constants.java Tue Dec 26 19:45:31 2017 +0100 +++ b/src/java/de/uapcore/lightpit/Constants.java Sat Dec 30 20:35:23 2017 +0100 @@ -49,6 +49,16 @@ public static final String CTX_ATTR_LANGUAGES = "available-languages"; /** + * Name for the context parameter optionally specifying the JNDI context; + */ + public static final String CTX_ATTR_JNDI_CONTEXT = "jndi-context"; + + /** + * Name for the context parameter optionally specifying a database schema. + */ + public static final String CTX_ATTR_DB_SCHEMA = "db-schema"; + + /** * Key for the request attribute containing the class name of the currently dispatching module. */ public static final String REQ_ATTR_MODULE_CLASSNAME = fqn(AbstractLightPITServlet.class, "moduleClassname");
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java/de/uapcore/lightpit/DatabaseFacade.java Sat Dec 30 20:35:23 2017 +0100 @@ -0,0 +1,211 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2017 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +package de.uapcore.lightpit; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Optional; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.annotation.WebListener; +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides access to different privilege layers within the database. + */ +@WebListener +public final class DatabaseFacade implements ServletContextListener { + + private static final Logger LOG = LoggerFactory.getLogger(DatabaseFacade.class); + + /** + * Timeout in seconds for the validation test. + */ + private static final int DB_TEST_TIMEOUT = 10; + + /** + * The default schema to test against when validating the connection. + * + * May be overridden by context parameter. + */ + private static final String DB_DEFAULT_SCHEMA = "lightpit"; + + /** + * The attribute name in the servlet context under which an instance of this class can be found. + */ + public static final String SC_ATTR_NAME = DatabaseFacade.class.getName(); + private ServletContext sc; + + private static final String PRIVILEGED_DS_JNDI_NAME = "jdbc/lightpit/dbo"; + private Optional<DataSource> privilegedDataSource; + + private static final String UNPRIVILEGED_DS_JNDI_NAME = "jdbc/lightpit/app"; + private Optional<DataSource> unprivilegedDataSource; + + + /** + * Returns an optional privileged data source. + * + * Privileged data sources should be able to execute any kind of DDL + * statements to perform installation or configuration steps. + * + * This optional should always be empty in live operation. Modules which + * provide installation or configuration steps MUST check the presence of + * a privileged data source and SHOULD display an informative message if + * it is currently disabled. + * + * @return an optional privileged data source + */ + public Optional<DataSource> getPrivilegedDataSource() { + return privilegedDataSource; + } + + /** + * Returns an optional unprivileged data source. + * + * The Optional returned should never be empty. However, if something goes + * wrong during initialization, the data source might be absent. + * Hence, users of this data source are forced to check the existence. + * + * @return an optional unprivileged data source + */ + public Optional<DataSource> getUnprivilegedDataSource() { + return unprivilegedDataSource; + } + + /** + * Returns the JNDI resource name of the privileged data source. + * + * Modules may use this information to provide useful information to the user. + * + * @return the JNDI resource name of the privileged data source + */ + public String getPrivilegedDataSourceJNDIName() { + return PRIVILEGED_DS_JNDI_NAME; + } + + /** + * Returns the JNDI resource name of the unprivileged data source. + * + * Modules may use this information to provide useful information to the user. + * + * @return the JNDI resource name of the unprivileged data source + */ + public String getUnprivilegedDataSourceJNDIName() { + return UNPRIVILEGED_DS_JNDI_NAME; + } + + private static void checkConnection(DataSource ds, String testSchema, String errMsg) { + try (Connection conn = ds.getConnection()) { + if (!conn.isValid(DB_TEST_TIMEOUT)) { + throw new SQLException("Validation check failed."); + } + if (conn.isReadOnly()) { + throw new SQLException("Connection is read-only and thus unusable."); + } + if (!conn.getSchema().equals(testSchema)) { + throw new SQLException(String.format("Connection is not configured to use the schema %s.", testSchema)); + } + DatabaseMetaData metaData = conn.getMetaData(); + LOG.info("Connections as {} to {}/{} ready to go.", metaData.getUserName(), metaData.getURL(), conn.getSchema()); + } catch (SQLException ex) { + LOG.error(errMsg, ex); + } + } + + private static Optional<DataSource> retrievePrivilegedDataSource(Context ctx) { + DataSource ret = null; + try { + ret = (DataSource)ctx.lookup(PRIVILEGED_DS_JNDI_NAME); + LOG.info("Privileged data source {} retrieved from context.", PRIVILEGED_DS_JNDI_NAME); + LOG.warn("Your application may be vulnerable due to privileged database access. Make sure that privileged data sources are only available during installation or configuration."); + } catch (NamingException ex) { + LOG.info("Privileged data source not available. This is perfectly OK. Activate only, if you need to do installation or configuration."); + /* in case the absence of the DataSource is not intended, log something more useful on debug level */ + LOG.debug("Reason for the missing data source: ", ex); + } + return Optional.ofNullable(ret); + } + + private static Optional<DataSource> retrieveUnprivilegedDataSource(Context ctx) { + DataSource ret = null; + try { + ret = (DataSource)ctx.lookup(UNPRIVILEGED_DS_JNDI_NAME); + LOG.info("Unprivileged data source retrieved."); + } catch (NamingException ex) { + LOG.error("Unprivileged data source {} not available.", UNPRIVILEGED_DS_JNDI_NAME); + /* for the unprivileged DataSource log the exception on error level (ordinary admins could find this useful) */ + LOG.error("Reason for the missing data source: ", ex); + } + return Optional.ofNullable(ret); + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + sc = sce.getServletContext(); + + privilegedDataSource = unprivilegedDataSource = null; + + final String contextName = Optional + .ofNullable(sc.getInitParameter(Constants.CTX_ATTR_JNDI_CONTEXT)) + .orElse("java:comp/env"); + final String dbSchema = Optional + .ofNullable(sc.getInitParameter(Constants.CTX_ATTR_DB_SCHEMA)) + .orElse(DB_DEFAULT_SCHEMA); + + try { + LOG.debug("Trying to access JNDI context {}...", contextName); + Context initialCtx = new InitialContext(); + Context ctx = (Context) initialCtx.lookup(contextName); + + privilegedDataSource = retrievePrivilegedDataSource(ctx); + unprivilegedDataSource = retrieveUnprivilegedDataSource(ctx); + + privilegedDataSource.ifPresent((ds) -> checkConnection(ds, dbSchema, "Checking privileged connection failed")); + unprivilegedDataSource.ifPresent((ds) -> checkConnection(ds, dbSchema, "Checking unprivileged connection failed")); + } catch (NamingException | ClassCastException ex) { + LOG.error("Cannot access JNDI resources.", ex); + } + + sc.setAttribute(SC_ATTR_NAME, this); + LOG.info("Database facade injected into ServletContext."); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + privilegedDataSource = unprivilegedDataSource = null; + } +}
--- a/src/java/log4j2.properties Tue Dec 26 19:45:31 2017 +0100 +++ b/src/java/log4j2.properties Sat Dec 30 20:35:23 2017 +0100 @@ -29,7 +29,7 @@ appender.console.type = Console appender.console.name = STDOUT appender.console.layout.type = PatternLayout -appender.console.layout.pattern = %d{ISO8601} %p - %M: %m %n +appender.console.layout.pattern = %d{ISO8601} [%p] %m %n rootLogger.appenderRef.stdout.ref = STDOUT
--- a/web/META-INF/context.xml Tue Dec 26 19:45:31 2017 +0100 +++ b/web/META-INF/context.xml Sat Dec 30 20:35:23 2017 +0100 @@ -1,2 +1,11 @@ <?xml version="1.0" encoding="UTF-8"?> -<Context path="/lightpit" /> +<Context path="/lightpit"> + <ResourceLink name="jdbc/lightpit/app" + global="jdbc/lightpit/app" + type="javax.sql.DataSource" /> + + <!-- Remove this link after installation and configuration --> + <ResourceLink name="jdbc/lightpit/dbo" + global="jdbc/lightpit/dbo" + type="javax.sql.DataSource" /> +</Context>