Sat, 30 Dec 2017 20:35:23 +0100
adds DatabaseFacade
1.1 --- a/setup/postgres/psql_create_database.sql Tue Dec 26 19:45:31 2017 +0100 1.2 +++ b/setup/postgres/psql_create_database.sql Sat Dec 30 20:35:23 2017 +0100 1.3 @@ -1,9 +1,5 @@ 1.4 --- Create a database owner role, which has no login permissions. 1.5 --- You can either: 1.6 --- 1) login as default user and switch the user 1.7 --- 2) decide to override this decision and give login permissions 1.8 --- 3) use your superuser of choice to manage the database (not recommended!) 1.9 -create role lightpit_dbo with password 'lpit_dbo_changeme'; 1.10 +-- Create a database owner role, which is also a privileged user 1.11 +create user lightpit_dbo with password 'lpit_dbo_changeme'; 1.12 1.13 -- Create the actual (unprivileged) database user 1.14 create user lightpit_user with password 'lpit_user_changeme';
2.1 --- a/src/java/de/uapcore/lightpit/Constants.java Tue Dec 26 19:45:31 2017 +0100 2.2 +++ b/src/java/de/uapcore/lightpit/Constants.java Sat Dec 30 20:35:23 2017 +0100 2.3 @@ -49,6 +49,16 @@ 2.4 public static final String CTX_ATTR_LANGUAGES = "available-languages"; 2.5 2.6 /** 2.7 + * Name for the context parameter optionally specifying the JNDI context; 2.8 + */ 2.9 + public static final String CTX_ATTR_JNDI_CONTEXT = "jndi-context"; 2.10 + 2.11 + /** 2.12 + * Name for the context parameter optionally specifying a database schema. 2.13 + */ 2.14 + public static final String CTX_ATTR_DB_SCHEMA = "db-schema"; 2.15 + 2.16 + /** 2.17 * Key for the request attribute containing the class name of the currently dispatching module. 2.18 */ 2.19 public static final String REQ_ATTR_MODULE_CLASSNAME = fqn(AbstractLightPITServlet.class, "moduleClassname");
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/src/java/de/uapcore/lightpit/DatabaseFacade.java Sat Dec 30 20:35:23 2017 +0100 3.3 @@ -0,0 +1,211 @@ 3.4 +/* 3.5 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 3.6 + * 3.7 + * Copyright 2017 Mike Becker. All rights reserved. 3.8 + * 3.9 + * Redistribution and use in source and binary forms, with or without 3.10 + * modification, are permitted provided that the following conditions are met: 3.11 + * 3.12 + * 1. Redistributions of source code must retain the above copyright 3.13 + * notice, this list of conditions and the following disclaimer. 3.14 + * 3.15 + * 2. Redistributions in binary form must reproduce the above copyright 3.16 + * notice, this list of conditions and the following disclaimer in the 3.17 + * documentation and/or other materials provided with the distribution. 3.18 + * 3.19 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 3.20 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 3.21 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 3.22 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 3.23 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 3.24 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 3.25 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 3.26 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 3.27 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 3.28 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 3.29 + * POSSIBILITY OF SUCH DAMAGE. 3.30 + * 3.31 + */ 3.32 +package de.uapcore.lightpit; 3.33 + 3.34 +import java.sql.Connection; 3.35 +import java.sql.DatabaseMetaData; 3.36 +import java.sql.SQLException; 3.37 +import java.util.Optional; 3.38 +import javax.naming.Context; 3.39 +import javax.naming.InitialContext; 3.40 +import javax.naming.NamingException; 3.41 +import javax.servlet.ServletContext; 3.42 +import javax.servlet.ServletContextEvent; 3.43 +import javax.servlet.ServletContextListener; 3.44 +import javax.servlet.annotation.WebListener; 3.45 +import javax.sql.DataSource; 3.46 +import org.slf4j.Logger; 3.47 +import org.slf4j.LoggerFactory; 3.48 + 3.49 +/** 3.50 + * Provides access to different privilege layers within the database. 3.51 + */ 3.52 +@WebListener 3.53 +public final class DatabaseFacade implements ServletContextListener { 3.54 + 3.55 + private static final Logger LOG = LoggerFactory.getLogger(DatabaseFacade.class); 3.56 + 3.57 + /** 3.58 + * Timeout in seconds for the validation test. 3.59 + */ 3.60 + private static final int DB_TEST_TIMEOUT = 10; 3.61 + 3.62 + /** 3.63 + * The default schema to test against when validating the connection. 3.64 + * 3.65 + * May be overridden by context parameter. 3.66 + */ 3.67 + private static final String DB_DEFAULT_SCHEMA = "lightpit"; 3.68 + 3.69 + /** 3.70 + * The attribute name in the servlet context under which an instance of this class can be found. 3.71 + */ 3.72 + public static final String SC_ATTR_NAME = DatabaseFacade.class.getName(); 3.73 + private ServletContext sc; 3.74 + 3.75 + private static final String PRIVILEGED_DS_JNDI_NAME = "jdbc/lightpit/dbo"; 3.76 + private Optional<DataSource> privilegedDataSource; 3.77 + 3.78 + private static final String UNPRIVILEGED_DS_JNDI_NAME = "jdbc/lightpit/app"; 3.79 + private Optional<DataSource> unprivilegedDataSource; 3.80 + 3.81 + 3.82 + /** 3.83 + * Returns an optional privileged data source. 3.84 + * 3.85 + * Privileged data sources should be able to execute any kind of DDL 3.86 + * statements to perform installation or configuration steps. 3.87 + * 3.88 + * This optional should always be empty in live operation. Modules which 3.89 + * provide installation or configuration steps MUST check the presence of 3.90 + * a privileged data source and SHOULD display an informative message if 3.91 + * it is currently disabled. 3.92 + * 3.93 + * @return an optional privileged data source 3.94 + */ 3.95 + public Optional<DataSource> getPrivilegedDataSource() { 3.96 + return privilegedDataSource; 3.97 + } 3.98 + 3.99 + /** 3.100 + * Returns an optional unprivileged data source. 3.101 + * 3.102 + * The Optional returned should never be empty. However, if something goes 3.103 + * wrong during initialization, the data source might be absent. 3.104 + * Hence, users of this data source are forced to check the existence. 3.105 + * 3.106 + * @return an optional unprivileged data source 3.107 + */ 3.108 + public Optional<DataSource> getUnprivilegedDataSource() { 3.109 + return unprivilegedDataSource; 3.110 + } 3.111 + 3.112 + /** 3.113 + * Returns the JNDI resource name of the privileged data source. 3.114 + * 3.115 + * Modules may use this information to provide useful information to the user. 3.116 + * 3.117 + * @return the JNDI resource name of the privileged data source 3.118 + */ 3.119 + public String getPrivilegedDataSourceJNDIName() { 3.120 + return PRIVILEGED_DS_JNDI_NAME; 3.121 + } 3.122 + 3.123 + /** 3.124 + * Returns the JNDI resource name of the unprivileged data source. 3.125 + * 3.126 + * Modules may use this information to provide useful information to the user. 3.127 + * 3.128 + * @return the JNDI resource name of the unprivileged data source 3.129 + */ 3.130 + public String getUnprivilegedDataSourceJNDIName() { 3.131 + return UNPRIVILEGED_DS_JNDI_NAME; 3.132 + } 3.133 + 3.134 + private static void checkConnection(DataSource ds, String testSchema, String errMsg) { 3.135 + try (Connection conn = ds.getConnection()) { 3.136 + if (!conn.isValid(DB_TEST_TIMEOUT)) { 3.137 + throw new SQLException("Validation check failed."); 3.138 + } 3.139 + if (conn.isReadOnly()) { 3.140 + throw new SQLException("Connection is read-only and thus unusable."); 3.141 + } 3.142 + if (!conn.getSchema().equals(testSchema)) { 3.143 + throw new SQLException(String.format("Connection is not configured to use the schema %s.", testSchema)); 3.144 + } 3.145 + DatabaseMetaData metaData = conn.getMetaData(); 3.146 + LOG.info("Connections as {} to {}/{} ready to go.", metaData.getUserName(), metaData.getURL(), conn.getSchema()); 3.147 + } catch (SQLException ex) { 3.148 + LOG.error(errMsg, ex); 3.149 + } 3.150 + } 3.151 + 3.152 + private static Optional<DataSource> retrievePrivilegedDataSource(Context ctx) { 3.153 + DataSource ret = null; 3.154 + try { 3.155 + ret = (DataSource)ctx.lookup(PRIVILEGED_DS_JNDI_NAME); 3.156 + LOG.info("Privileged data source {} retrieved from context.", PRIVILEGED_DS_JNDI_NAME); 3.157 + 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."); 3.158 + } catch (NamingException ex) { 3.159 + LOG.info("Privileged data source not available. This is perfectly OK. Activate only, if you need to do installation or configuration."); 3.160 + /* in case the absence of the DataSource is not intended, log something more useful on debug level */ 3.161 + LOG.debug("Reason for the missing data source: ", ex); 3.162 + } 3.163 + return Optional.ofNullable(ret); 3.164 + } 3.165 + 3.166 + private static Optional<DataSource> retrieveUnprivilegedDataSource(Context ctx) { 3.167 + DataSource ret = null; 3.168 + try { 3.169 + ret = (DataSource)ctx.lookup(UNPRIVILEGED_DS_JNDI_NAME); 3.170 + LOG.info("Unprivileged data source retrieved."); 3.171 + } catch (NamingException ex) { 3.172 + LOG.error("Unprivileged data source {} not available.", UNPRIVILEGED_DS_JNDI_NAME); 3.173 + /* for the unprivileged DataSource log the exception on error level (ordinary admins could find this useful) */ 3.174 + LOG.error("Reason for the missing data source: ", ex); 3.175 + } 3.176 + return Optional.ofNullable(ret); 3.177 + } 3.178 + 3.179 + @Override 3.180 + public void contextInitialized(ServletContextEvent sce) { 3.181 + sc = sce.getServletContext(); 3.182 + 3.183 + privilegedDataSource = unprivilegedDataSource = null; 3.184 + 3.185 + final String contextName = Optional 3.186 + .ofNullable(sc.getInitParameter(Constants.CTX_ATTR_JNDI_CONTEXT)) 3.187 + .orElse("java:comp/env"); 3.188 + final String dbSchema = Optional 3.189 + .ofNullable(sc.getInitParameter(Constants.CTX_ATTR_DB_SCHEMA)) 3.190 + .orElse(DB_DEFAULT_SCHEMA); 3.191 + 3.192 + try { 3.193 + LOG.debug("Trying to access JNDI context {}...", contextName); 3.194 + Context initialCtx = new InitialContext(); 3.195 + Context ctx = (Context) initialCtx.lookup(contextName); 3.196 + 3.197 + privilegedDataSource = retrievePrivilegedDataSource(ctx); 3.198 + unprivilegedDataSource = retrieveUnprivilegedDataSource(ctx); 3.199 + 3.200 + privilegedDataSource.ifPresent((ds) -> checkConnection(ds, dbSchema, "Checking privileged connection failed")); 3.201 + unprivilegedDataSource.ifPresent((ds) -> checkConnection(ds, dbSchema, "Checking unprivileged connection failed")); 3.202 + } catch (NamingException | ClassCastException ex) { 3.203 + LOG.error("Cannot access JNDI resources.", ex); 3.204 + } 3.205 + 3.206 + sc.setAttribute(SC_ATTR_NAME, this); 3.207 + LOG.info("Database facade injected into ServletContext."); 3.208 + } 3.209 + 3.210 + @Override 3.211 + public void contextDestroyed(ServletContextEvent sce) { 3.212 + privilegedDataSource = unprivilegedDataSource = null; 3.213 + } 3.214 +}
4.1 --- a/src/java/log4j2.properties Tue Dec 26 19:45:31 2017 +0100 4.2 +++ b/src/java/log4j2.properties Sat Dec 30 20:35:23 2017 +0100 4.3 @@ -29,7 +29,7 @@ 4.4 appender.console.type = Console 4.5 appender.console.name = STDOUT 4.6 appender.console.layout.type = PatternLayout 4.7 -appender.console.layout.pattern = %d{ISO8601} %p - %M: %m %n 4.8 +appender.console.layout.pattern = %d{ISO8601} [%p] %m %n 4.9 4.10 rootLogger.appenderRef.stdout.ref = STDOUT 4.11
5.1 --- a/web/META-INF/context.xml Tue Dec 26 19:45:31 2017 +0100 5.2 +++ b/web/META-INF/context.xml Sat Dec 30 20:35:23 2017 +0100 5.3 @@ -1,2 +1,11 @@ 5.4 <?xml version="1.0" encoding="UTF-8"?> 5.5 -<Context path="/lightpit" /> 5.6 +<Context path="/lightpit"> 5.7 + <ResourceLink name="jdbc/lightpit/app" 5.8 + global="jdbc/lightpit/app" 5.9 + type="javax.sql.DataSource" /> 5.10 + 5.11 + <!-- Remove this link after installation and configuration --> 5.12 + <ResourceLink name="jdbc/lightpit/dbo" 5.13 + global="jdbc/lightpit/dbo" 5.14 + type="javax.sql.DataSource" /> 5.15 +</Context>