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
     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>

mercurial