adds DatabaseFacade

Sat, 30 Dec 2017 20:35:23 +0100

author
Mike Becker <universe@uap-core.de>
date
Sat, 30 Dec 2017 20:35:23 +0100
changeset 16
4e0998805276
parent 15
bb594abac796
child 17
d1036b776eee

adds DatabaseFacade

setup/postgres/psql_create_database.sql file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/Constants.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/DatabaseFacade.java file | annotate | diff | comparison | revisions
src/java/log4j2.properties file | annotate | diff | comparison | revisions
web/META-INF/context.xml file | annotate | diff | comparison | revisions
--- 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>

mercurial