From b42ec2fa294ed1b079666cc8bcbbfd7e0f893ae0 Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Mon, 17 Nov 2025 16:15:16 -0800 Subject: [PATCH 01/26] add delegating data source - initial commit --- .../odcsapi/dao/OpenDcsDatabaseFactory.java | 16 ++- .../dao/datasource/ConnectionPreparer.java | 11 ++ .../ConnectionPreparingDataSource.java | 63 ++++++++++ .../DelegatingConnectionPreparer.java | 38 ++++++ .../dao/datasource/DelegatingDataSource.java | 110 ++++++++++++++++++ .../dao/datasource/DirectUserPreparer.java | 34 ++++++ .../dao/datasource/SessionOfficePreparer.java | 40 +++++++ .../datasource/SessionTimeZonePreparer.java | 29 +++++ .../odcsapi/res/ContextPropertySetup.java | 1 - .../odcsapi/sec/AuthorizationCheck.java | 8 +- .../basicauth/OpenTsdbAuthorizationDAO.java | 3 +- .../sec/cwms/CwmsAuthorizationDAO.java | 9 +- 12 files changed, 350 insertions(+), 12 deletions(-) create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/ConnectionPreparer.java create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/ConnectionPreparingDataSource.java create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingConnectionPreparer.java create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingDataSource.java create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DirectUserPreparer.java create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/SessionOfficePreparer.java create mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/SessionTimeZonePreparer.java diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/OpenDcsDatabaseFactory.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/OpenDcsDatabaseFactory.java index 78345ead..aac5bb9a 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/OpenDcsDatabaseFactory.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/OpenDcsDatabaseFactory.java @@ -17,6 +17,8 @@ import java.sql.Connection; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import javax.sql.DataSource; import decodes.cwms.CwmsDatabaseProvider; @@ -26,6 +28,12 @@ import opendcs.opentsdb.OpenTsdbProvider; import org.opendcs.database.DatabaseService; import org.opendcs.database.api.OpenDcsDatabase; +import org.opendcs.odcsapi.dao.datasource.ConnectionPreparer; +import org.opendcs.odcsapi.dao.datasource.ConnectionPreparingDataSource; +import org.opendcs.odcsapi.dao.datasource.DelegatingConnectionPreparer; +import org.opendcs.odcsapi.dao.datasource.DirectUserPreparer; +import org.opendcs.odcsapi.dao.datasource.SessionOfficePreparer; +import org.opendcs.odcsapi.dao.datasource.SessionTimeZonePreparer; import org.opendcs.odcsapi.hydrojson.DbInterface; import org.opendcs.spi.database.DatabaseProvider; import org.slf4j.Logger; @@ -41,8 +49,14 @@ private OpenDcsDatabaseFactory() throw new AssertionError("Utility class"); } - public static synchronized OpenDcsDatabase createDb(DataSource dataSource) + public static synchronized OpenDcsDatabase createDb(DataSource dataSource, String organization, String user) { + List preparers = new ArrayList<>(); + preparers.add(new SessionTimeZonePreparer()); + preparers.add(new SessionOfficePreparer(organization)); + preparers.add(new DirectUserPreparer(user)); + + DataSource wrappedDataSource = new ConnectionPreparingDataSource(new DelegatingConnectionPreparer(preparers), dataSource); if(database != null) { return database; diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/ConnectionPreparer.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/ConnectionPreparer.java new file mode 100644 index 00000000..c0fb33b6 --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/ConnectionPreparer.java @@ -0,0 +1,11 @@ +package org.opendcs.odcsapi.dao.datasource; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.opendcs.odcsapi.dao.DbException; + +public interface ConnectionPreparer +{ + Connection prepare(Connection connection) throws SQLException; +} diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/ConnectionPreparingDataSource.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/ConnectionPreparingDataSource.java new file mode 100644 index 00000000..5e6bc1e0 --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/ConnectionPreparingDataSource.java @@ -0,0 +1,63 @@ +package org.opendcs.odcsapi.dao.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; + +import org.opendcs.odcsapi.dao.DbException; + +public class ConnectionPreparingDataSource extends DelegatingDataSource +{ + + private ConnectionPreparer preparer; + + public ConnectionPreparingDataSource(ConnectionPreparer preparer, DataSource targetDataSource) + { + super(targetDataSource); + this.preparer = preparer; + } + + @Override + public Connection getConnection() throws SQLException + { + Connection connection = getDelegate().getConnection(); + + try + { + return getPreparer().prepare(connection); + } + catch(Exception e) + { + try + { + // If there was some problem preparing the connection + // we close the connection in order to return it to + // the pool it probably came from. + connection.close(); + } + catch(SQLException ex) + { + e.addSuppressed(ex); + } + throw e; + } + } + + /** + * @return the preparer + */ + public ConnectionPreparer getPreparer() + { + return preparer; + } + + /** + * @param preparer the preparer to set + */ + public void setPreparer(ConnectionPreparer preparer) + { + this.preparer = preparer; + } + + +} \ No newline at end of file diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingConnectionPreparer.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingConnectionPreparer.java new file mode 100644 index 00000000..89feab72 --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingConnectionPreparer.java @@ -0,0 +1,38 @@ +package org.opendcs.odcsapi.dao.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DelegatingConnectionPreparer implements ConnectionPreparer +{ + + private static final Logger logger = LoggerFactory.getLogger(DelegatingConnectionPreparer.class); + private final List delegates = new ArrayList<>(); + + public DelegatingConnectionPreparer(List preparers) + { + if(preparers != null) + { + delegates.addAll(preparers); + } + } + + @Override + public Connection prepare(Connection connection) throws SQLException + { + Connection retval = connection; + for(ConnectionPreparer delegate : delegates) + { + logger.atTrace().log(delegate.getClass().getName()); + retval = delegate.prepare(retval); + } + + return retval; + } + +} diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingDataSource.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingDataSource.java new file mode 100644 index 00000000..c21b9b57 --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingDataSource.java @@ -0,0 +1,110 @@ +package org.opendcs.odcsapi.dao.datasource; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Logger; +import javax.sql.DataSource; + + +/** + * This class is a wrapper around a DataSource that delegates all calls to the + * wrapped DataSource. It is intended to be extended by classes that need to + * override DataSource methods. + */ +public class DelegatingDataSource implements DataSource +{ + + private DataSource delegate; + + /** + * Create a new DelegatingDataSource. + * + * @param delegate the target DataSource + */ + public DelegatingDataSource(DataSource delegate) + { + setDelegate(delegate); + } + + + /** + * Set the target DataSource that this DataSource should delegate to. + */ + public void setDelegate(DataSource delegate) + { + this.delegate = delegate; + } + + /** + * Return the target DataSource that this DataSource should delegate to. + */ + + public DataSource getDelegate() + { + return this.delegate; + } + + + @Override + public Connection getConnection() throws SQLException + { + return getDelegate().getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException + { + return getDelegate().getConnection(username, password); + } + + @Override + public PrintWriter getLogWriter() throws SQLException + { + return getDelegate().getLogWriter(); + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException + { + getDelegate().setLogWriter(out); + } + + @Override + public int getLoginTimeout() throws SQLException + { + return getDelegate().getLoginTimeout(); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException + { + getDelegate().setLoginTimeout(seconds); + } + + + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class iface) throws SQLException + { + if(iface.isInstance(this)) + { + return (T) this; + } + return getDelegate().unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException + { + return (iface.isInstance(this) || getDelegate().isWrapperFor(iface)); + } + + + @Override + public Logger getParentLogger() + { + return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + } + +} \ No newline at end of file diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DirectUserPreparer.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DirectUserPreparer.java new file mode 100644 index 00000000..a953e99b --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DirectUserPreparer.java @@ -0,0 +1,34 @@ +package org.opendcs.odcsapi.dao.datasource; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.opendcs.odcsapi.dao.DbException; + + +public class DirectUserPreparer implements ConnectionPreparer +{ + private final String user; + + public DirectUserPreparer(String user) + { + this.user = user; + } + + @Override + public Connection prepare(Connection conn) throws SQLException + { + if(user != null) + { + String sql = "begin cwms_env.set_session_user_direct(upper(?)); end;"; + try(PreparedStatement setApiUser = conn.prepareStatement(sql)) + { + setApiUser.setString(1, user); + setApiUser.execute(); + } + } + + return conn; + } +} diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/SessionOfficePreparer.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/SessionOfficePreparer.java new file mode 100644 index 00000000..18453ef1 --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/SessionOfficePreparer.java @@ -0,0 +1,40 @@ +package org.opendcs.odcsapi.dao.datasource; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SessionOfficePreparer implements ConnectionPreparer +{ + private static final Logger logger = LoggerFactory.getLogger(SessionOfficePreparer.class); + + private final String office; + + public SessionOfficePreparer(String office) + { + this.office = office; + } + + @Override + public Connection prepare(Connection conn) throws SQLException + { + if(office != null && !office.isBlank()) + { + String sql = "BEGIN cwms_ccp_vpd.set_ccp_session_ctx(cwms_util.get_office_code(:1), 2, :2 ); END;"; + try(PreparedStatement setApiUser = conn.prepareStatement(sql)) + { + setApiUser.setString(1, office); + setApiUser.setString(2, office); + setApiUser.execute(); + } + } + else + { + logger.atDebug().log("Office is null or empty."); + } + return conn; + } +} diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/SessionTimeZonePreparer.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/SessionTimeZonePreparer.java new file mode 100644 index 00000000..8c1fda4f --- /dev/null +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/SessionTimeZonePreparer.java @@ -0,0 +1,29 @@ +package org.opendcs.odcsapi.dao.datasource; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import javax.annotation.Nullable; + +public final class SessionTimeZonePreparer implements ConnectionPreparer +{ + + private static void setSessionTimeZoneUtc(Connection connection) throws SQLException + { + String sql = "alter session set time_zone = 'UTC'"; + try(CallableStatement statement = connection.prepareCall(sql)) + { + statement.execute(); + } + } + + @Override + public Connection prepare(Connection conn) throws SQLException + { + setSessionTimeZoneUtc(conn); + return conn; + } + +} \ No newline at end of file diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/ContextPropertySetup.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/ContextPropertySetup.java index eaf4b559..88d001b4 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/ContextPropertySetup.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/ContextPropertySetup.java @@ -31,7 +31,6 @@ public void contextInitialized(ServletContextEvent sce) { ServletContext servletContext = sce.getServletContext(); //Move this information to the database. https://github.com/opendcs/rest_api/issues/191 - initProp(servletContext, "opendcs.rest.api.cwms.office", "CwmsOfficeId", "OPENDCS_DB_OFFICE"); initProp(servletContext, "opendcs.rest.api.authorization.type", "opendcs.rest.api.authorization.type", "OPENDCS_AUTHORIZATION_TYPE"); initProp(servletContext, "opendcs.rest.api.authorization.expiration.duration", "opendcs.rest.api.authorization.expiration.duration", "OPENDCS_AUTHORIZATION_DURATION"); initProp(servletContext, "opendcs.rest.api.authorization.jwt.jwkset.url", "opendcs.rest.api.authorization.jwt.jwkset.url", "OPENDCS_AUTHORIZATION_JWK_SET_URL"); diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java index 66edf5f1..60135811 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java @@ -54,13 +54,13 @@ protected final ApiAuthorizationDAI getAuthDao(ServletContext servletContext) TimeSeriesDb timeSeriesDb = db.getLegacyDatabase(TimeSeriesDb.class) .orElseThrow(() -> new UnsupportedOperationException("Endpoint is unsupported by the OpenDCS REST API.")); //Need to figure out a better way to extend the toolkit API to be able to add dao's within the REST API - if(timeSeriesDb instanceof CwmsTimeSeriesDb) + if(timeSeriesDb instanceof CwmsTimeSeriesDb cwms) { - return new CwmsAuthorizationDAO(timeSeriesDb); + return new CwmsAuthorizationDAO(cwms); } - else if(timeSeriesDb instanceof OpenTsdb) + else if(timeSeriesDb instanceof OpenTsdb opentsdb) { - return new OpenTsdbAuthorizationDAO(timeSeriesDb); + return new OpenTsdbAuthorizationDAO(opentsdb); } throw new UnsupportedOperationException("Endpoint is unsupported by the OpenDCS REST API."); } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/OpenTsdbAuthorizationDAO.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/OpenTsdbAuthorizationDAO.java index f9ad10cc..9712f817 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/OpenTsdbAuthorizationDAO.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/OpenTsdbAuthorizationDAO.java @@ -23,6 +23,7 @@ import opendcs.dao.DaoBase; import opendcs.dao.DatabaseConnectionOwner; +import opendcs.opentsdb.OpenTsdb; import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; import org.opendcs.odcsapi.dao.DbException; import org.opendcs.odcsapi.sec.OpenDcsApiRoles; @@ -33,7 +34,7 @@ public final class OpenTsdbAuthorizationDAO extends DaoBase implements ApiAuthor { private static final Logger log = OpenDcsLoggerFactory.getLogger(); - public OpenTsdbAuthorizationDAO(DatabaseConnectionOwner tsdb) + public OpenTsdbAuthorizationDAO(OpenTsdb tsdb) { super(tsdb, "AuthorizationDAO"); } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/CwmsAuthorizationDAO.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/CwmsAuthorizationDAO.java index 9a93fd53..eaf8d679 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/CwmsAuthorizationDAO.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/CwmsAuthorizationDAO.java @@ -21,20 +21,19 @@ import java.util.EnumSet; import java.util.Set; +import decodes.cwms.CwmsTimeSeriesDb; import opendcs.dao.DaoBase; -import opendcs.dao.DatabaseConnectionOwner; import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; import org.opendcs.odcsapi.dao.DbException; -import org.opendcs.odcsapi.hydrojson.DbInterface; import org.opendcs.odcsapi.sec.OpenDcsApiRoles; -import org.slf4j.Logger; import org.opendcs.utils.logging.OpenDcsLoggerFactory; +import org.slf4j.Logger; public final class CwmsAuthorizationDAO extends DaoBase implements ApiAuthorizationDAI { private static final Logger log = OpenDcsLoggerFactory.getLogger(); - public CwmsAuthorizationDAO(DatabaseConnectionOwner tsdb) + public CwmsAuthorizationDAO(CwmsTimeSeriesDb tsdb) { super(tsdb, "AuthorizationDAO"); } @@ -56,7 +55,7 @@ public Set getRoles(String username) throws DbException " upper(?) " + " end " + " and is_member = 'T'"; - String cwmsOfficeId = DbInterface.decodesProperties.getProperty("CwmsOfficeId"); + String cwmsOfficeId = ((CwmsTimeSeriesDb) db).getDbOfficeId(); try { withConnection(c -> From ae88c842515cf0c5626fa5530ee3d1fb95f0988a Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Mon, 24 Nov 2025 12:46:46 -0800 Subject: [PATCH 02/26] parameterize orgainzation (office) id from the client to the REST API added to the existing username/password login though it'll just get replaced by https://github.com/opendcs/rest_api/pull/574 once this is done, will update to allow setting the organizationid header need to add to the settings options a place where we can change the office id without having to logout also need to populate swagger with a default organization header --- README.md | 2 - docker-compose.yaml | 1 - docker_files/tomcat/conf/context.xml | 1 - gradle.properties.example | 1 - .../odcsapi/fixtures/TomcatServer.java | 32 +---- .../org/opendcs/odcsapi/res/it/BaseIT.java | 2 - .../test/resources/tomcat/conf/context.xml | 1 - opendcs-rest-api/build.gradle | 18 +-- .../odcsapi/dao/ApiAuthorizationDAI.java | 4 +- .../odcsapi/dao/OpenDcsDatabaseFactory.java | 31 ++--- .../DelegatingConnectionPreparer.java | 1 - .../opendcs/odcsapi/res/OpenDcsResource.java | 26 +++- .../odcsapi/sec/AuthorizationCheck.java | 67 --------- .../opendcs/odcsapi/sec/SecurityFilter.java | 1 - .../odcsapi/sec/basicauth/BasicAuthCheck.java | 84 ----------- .../sec/basicauth/BasicAuthResource.java | 96 ++++++++----- .../basicauth/OpenTsdbAuthorizationDAO.java | 47 +++---- .../sec/cwms/CwmsAuthorizationDAO.java | 57 ++++---- .../odcsapi/sec/openid/OidcAuthCheck.java | 130 ------------------ .../main/webapp/WEB-INF/app_pages/login.jsp | 3 + .../main/webapp/WEB-INF/common/top-bar.jspf | 2 + .../src/main/webapp/resources/js/layout.js | 5 + .../src/main/webapp/resources/js/login.js | 12 +- .../src/main/webapp/resources/js/main.js | 7 +- settings.gradle | 2 +- 25 files changed, 185 insertions(+), 448 deletions(-) delete mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java delete mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthCheck.java delete mode 100644 opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java diff --git a/README.md b/README.md index b1678904..56b80c8e 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,6 @@ docker run --network database_net \ -e DB_URL=jdbc:postgres:db:5324/dcs \ -e DB_USERNAME=dcs_app \ -e DB_PASSWORD=dcs_app_password \ - -e DB_CONNECTION_INIT="select 1" \ -e DB_VALIDATION_QUERY="select 1" \ -e DB_MAX_CONNECTIONS=10 \ -e DB_MAX_IDLE=5 \ @@ -127,7 +126,6 @@ docker run --network database_net \ -e DB_URL=jdbc:oracle:thin:@//cwmsdb:1521/CWMSTEST \ -e DB_USERNAME=ccp_app \ -e DB_PASSWORD=ccp \ - -e DB_CONNECTION_INIT="BEGIN cwms_ccp_vpd.set_ccp_session_ctx(null, null, 'SPK' ); END;" \ -e DB_VALIDATION_QUERY="select 1 from dual" \ -e DB_MAX_CONNECTIONS=10 \ -e DB_MAX_IDLE=5 \ diff --git a/docker-compose.yaml b/docker-compose.yaml index ccfbeddd..c11f9de1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -44,7 +44,6 @@ services: - DB_DRIVER_CLASS=org.postgresql.Driver - DB_USERNAME=app - DB_PASSWORD=app_pass - - DB_CONNECTION_INIT=SELECT 1 - DB_VALIDATION_QUERY=SELECT 1 - DB_URL=jdbc:postgresql://db:5432/dcs - DB_MAX_CONNECTIONS=10 diff --git a/docker_files/tomcat/conf/context.xml b/docker_files/tomcat/conf/context.xml index 94c5a780..7dda5326 100644 --- a/docker_files/tomcat/conf/context.xml +++ b/docker_files/tomcat/conf/context.xml @@ -22,7 +22,6 @@ username="${DB_USERNAME}" password="${DB_PASSWORD}" driverClassName="${DB_DRIVER_CLASS}" - initSQL="${DB_CONNECTION_INIT}" logAbandoned="true" url="${DB_URL}" logValidationErrors="true" diff --git a/gradle.properties.example b/gradle.properties.example index 2f741ea1..1bd573fc 100644 --- a/gradle.properties.example +++ b/gradle.properties.example @@ -3,7 +3,6 @@ #DCSTOOL_USERDIR= ###Point to existing Postgres OpenTS database -#postgresdb.url= #opendcs.db.username= #opendcs.db.password= diff --git a/opendcs-integration-test/src/main/java/org/opendcs/odcsapi/fixtures/TomcatServer.java b/opendcs-integration-test/src/main/java/org/opendcs/odcsapi/fixtures/TomcatServer.java index 21725aa7..6592b70b 100644 --- a/opendcs-integration-test/src/main/java/org/opendcs/odcsapi/fixtures/TomcatServer.java +++ b/opendcs-integration-test/src/main/java/org/opendcs/odcsapi/fixtures/TomcatServer.java @@ -57,10 +57,8 @@ public final class TomcatServer implements AutoCloseable { private static final Logger log = OpenDcsLoggerFactory.getLogger(); - public static final String DB_OFFICE = "DB_OFFICE"; public static final String DB_DRIVER_CLASS = "DB_DRIVER_CLASS"; public static final String DB_DATASOURCE_CLASS = "DB_DATASOURCE_CLASS"; - public static final String DB_CONNECTION_INIT = "DB_CONNECTION_INIT"; public static final String DB_VALIDATION_QUERY = "DB_VALIDATION_QUERY"; public static final String DB_URL = "DB_URL"; public static final String DB_USERNAME = "DB_USERNAME"; @@ -97,11 +95,6 @@ public TomcatServer(String baseDir, int port, String restWar, String guiWar) thr restApiContext.setReloadable(true); restApiContext.setPrivileged(true); sessionManager = restApiContext::getManager; - if(System.getProperty(DB_OFFICE) != null) - { - restApiContext.removeParameter("opendcs.rest.api.cwms.office"); - restApiContext.addParameter("opendcs.rest.api.cwms.office", System.getProperty(DB_OFFICE)); - } StandardContext guiContext = (StandardContext) tomcatInstance.addWebapp("", guiWar); guiContext.setDelegate(true); @@ -200,10 +193,6 @@ private static void setupDbForBypass(DbType dbType) { System.setProperty(DB_DRIVER_CLASS, "oracle.jdbc.driver.OracleDriver"); System.setProperty(DB_DATASOURCE_CLASS, DataSourceFactory.class.getName()); - String dbOffice = System.getProperty("testcontainer.cwms.bypass.office.id"); - String initScript = String.format("BEGIN cwms_ccp_vpd.set_ccp_session_ctx(cwms_util.get_office_code('%s'), 2, '%s' ); END;", dbOffice, dbOffice); - System.setProperty(DB_OFFICE, dbOffice); - System.setProperty(DB_CONNECTION_INIT, initScript); System.setProperty(DB_VALIDATION_QUERY, "SELECT 1 FROM dual"); System.setProperty(DB_URL, System.getProperty("testcontainer.cwms.bypass.url")); System.setProperty(DB_USERNAME, System.getProperty("testcontainer.cwms.bypass.office.eroc") + "WEBTEST"); @@ -214,7 +203,6 @@ private static void setupDbForBypass(DbType dbType) String validationQuery = "SELECT 1"; System.setProperty(DB_DRIVER_CLASS, "org.postgresql.Driver"); System.setProperty(DB_DATASOURCE_CLASS, DataSourceFactory.class.getName()); - System.setProperty(DB_CONNECTION_INIT, validationQuery); System.setProperty(DB_VALIDATION_QUERY, validationQuery); System.setProperty(DB_URL, System.getProperty("testcontainer.opentsdb.bypass.url")); System.setProperty(DB_USERNAME, System.getProperty("testcontainer.opentsdb.bypass.username")); @@ -289,9 +277,6 @@ private static void startDbContainer(Configuration config, DbType dbType) throws { System.setProperty(DB_DRIVER_CLASS, "oracle.jdbc.driver.OracleDriver"); System.setProperty(DB_DATASOURCE_CLASS, "org.apache.tomcat.jdbc.pool.DataSourceFactory"); - String dbOffice = System.getProperty(DB_OFFICE); - String initScript = String.format("BEGIN cwms_ccp_vpd.set_ccp_session_ctx(cwms_util.get_office_code('%s'), 2, '%s' ); END;", dbOffice, dbOffice); - System.setProperty(DB_CONNECTION_INIT, initScript); System.setProperty(DB_VALIDATION_QUERY, "SELECT 1 FROM dual"); } else @@ -299,7 +284,6 @@ private static void startDbContainer(Configuration config, DbType dbType) throws String validationQuery = "SELECT 1"; System.setProperty(DB_DRIVER_CLASS, "org.postgresql.Driver"); System.setProperty(DB_DATASOURCE_CLASS, "org.apache.tomcat.jdbc.pool.DataSourceFactory"); - System.setProperty(DB_CONNECTION_INIT, validationQuery); System.setProperty(DB_VALIDATION_QUERY, validationQuery); } setupClientUser(dbType); @@ -312,28 +296,14 @@ private static void setupClientUser(DbType dbType) { // I have no idea why this is suddenly required but it was also affecting operations in // runtime test environments where the required entries weren't present. - String unlockUser = "begin cwms_sec.unlock_user(?,?); end;"; String userPermissions = "begin execute immediate 'grant web_user to ' || ?; end;"; - String dbOffice = System.getProperty(DB_OFFICE); - String setWebUserPermissions = "begin\n" + - " cwms_sec.add_user_to_group(?, 'CWMS User Admins',?) ;\n" + - " commit;\n" + - "end;"; try(Connection connection = DriverManager.getConnection(System.getProperty(DB_URL), "CWMS_20", System.getProperty(DB_PASSWORD)); - PreparedStatement unlockUserStmt = connection.prepareStatement(unlockUser); - PreparedStatement userPermissionsStmt = connection.prepareStatement(userPermissions); - PreparedStatement setWebUserPermissionsStmt = connection.prepareStatement(setWebUserPermissions)) + PreparedStatement userPermissionsStmt = connection.prepareStatement(userPermissions)) { final String username = System.getProperty(DB_USERNAME); - unlockUserStmt.setString(1, username); - unlockUserStmt.setString(2, dbOffice); - unlockUserStmt.executeQuery(); userPermissionsStmt.setString(1, username); userPermissionsStmt.executeQuery(); - setWebUserPermissionsStmt.setString(1, username); - setWebUserPermissionsStmt.setString(2, dbOffice); - setWebUserPermissionsStmt.executeQuery(); } catch(SQLException ex) { diff --git a/opendcs-integration-test/src/test/java/org/opendcs/odcsapi/res/it/BaseIT.java b/opendcs-integration-test/src/test/java/org/opendcs/odcsapi/res/it/BaseIT.java index 34a49d0e..18359b7e 100644 --- a/opendcs-integration-test/src/test/java/org/opendcs/odcsapi/res/it/BaseIT.java +++ b/opendcs-integration-test/src/test/java/org/opendcs/odcsapi/res/it/BaseIT.java @@ -47,7 +47,6 @@ import org.opendcs.odcsapi.fixtures.DatabaseSetupExtension; import org.opendcs.odcsapi.fixtures.DbType; import org.opendcs.odcsapi.fixtures.TomcatServer; -import org.opendcs.odcsapi.hydrojson.DbInterface; import org.opendcs.odcsapi.res.ObjectMapperContextResolver; import org.opendcs.odcsapi.sec.OpenDcsApiRoles; import org.opendcs.odcsapi.sec.OpenDcsPrincipal; @@ -56,7 +55,6 @@ import static io.restassured.RestAssured.given; import static java.util.stream.Collectors.joining; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; class BaseIT { diff --git a/opendcs-integration-test/src/test/resources/tomcat/conf/context.xml b/opendcs-integration-test/src/test/resources/tomcat/conf/context.xml index 8e0b7901..95c7bc7b 100644 --- a/opendcs-integration-test/src/test/resources/tomcat/conf/context.xml +++ b/opendcs-integration-test/src/test/resources/tomcat/conf/context.xml @@ -22,7 +22,6 @@ username="${DB_USERNAME}" password="${DB_PASSWORD}" driverClassName="${DB_DRIVER_CLASS}" - initSQL="${DB_CONNECTION_INIT}" logAbandoned="true" url="${DB_URL}" logValidationErrors="true" diff --git a/opendcs-rest-api/build.gradle b/opendcs-rest-api/build.gradle index 1bfee291..e63b91f9 100644 --- a/opendcs-rest-api/build.gradle +++ b/opendcs-rest-api/build.gradle @@ -113,14 +113,16 @@ sonarqube { // task to generate OpenAPI JSON file -resolve { - classpath = sourceSets.main.runtimeClasspath - outputFileName = 'opendcs-openapi' - outputFormat = providers.gradleProperty('outputFormat').getOrElse('JSON') - prettyPrint = 'TRUE' - resourcePackages = ['org.opendcs.odcsapi.res', 'org.opendcs.odcsapi.beans', 'org.opendcs.odcsapi.sec'] - outputDir = file('build/swagger/') -} +//resolve { +// doLast { +// classpath = sourceSets.main.runtimeClasspath +// outputFileName = 'opendcs-openapi' +// outputFormat = providers.gradleProperty('outputFormat').getOrElse('JSON') +// prettyPrint = 'TRUE' +// resourcePackages = ['org.opendcs.odcsapi.res', 'org.opendcs.odcsapi.beans', 'org.opendcs.odcsapi.sec'] +// outputDir = file('build/swagger/') +// } +//} tasks.register('generateOpenAPI') { group = "documentation" diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/ApiAuthorizationDAI.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/ApiAuthorizationDAI.java index afdfb811..57534ffa 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/ApiAuthorizationDAI.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/ApiAuthorizationDAI.java @@ -20,7 +20,7 @@ import org.opendcs.database.api.OpenDcsDao; import org.opendcs.odcsapi.sec.OpenDcsApiRoles; -public interface ApiAuthorizationDAI extends OpenDcsDao, AutoCloseable +public interface ApiAuthorizationDAI extends OpenDcsDao { - Set getRoles(String username) throws DbException; + Set getRoles(String username, String organizationId) throws DbException; } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/OpenDcsDatabaseFactory.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/OpenDcsDatabaseFactory.java index aac5bb9a..4d529f0b 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/OpenDcsDatabaseFactory.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/OpenDcsDatabaseFactory.java @@ -15,17 +15,13 @@ package org.opendcs.odcsapi.dao; -import java.sql.Connection; -import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Properties; import javax.sql.DataSource; -import decodes.cwms.CwmsDatabaseProvider; import decodes.db.DatabaseException; -import decodes.sql.OracleSequenceKeyGenerator; import decodes.util.DecodesSettings; -import opendcs.opentsdb.OpenTsdbProvider; import org.opendcs.database.DatabaseService; import org.opendcs.database.api.OpenDcsDatabase; import org.opendcs.odcsapi.dao.datasource.ConnectionPreparer; @@ -34,15 +30,9 @@ import org.opendcs.odcsapi.dao.datasource.DirectUserPreparer; import org.opendcs.odcsapi.dao.datasource.SessionOfficePreparer; import org.opendcs.odcsapi.dao.datasource.SessionTimeZonePreparer; -import org.opendcs.odcsapi.hydrojson.DbInterface; -import org.opendcs.spi.database.DatabaseProvider; -import org.slf4j.Logger; -import org.opendcs.utils.logging.OpenDcsLoggerFactory; public final class OpenDcsDatabaseFactory { - private static final Logger log = OpenDcsLoggerFactory.getLogger(); - private static OpenDcsDatabase database; private OpenDcsDatabaseFactory() { @@ -54,25 +44,26 @@ public static synchronized OpenDcsDatabase createDb(DataSource dataSource, Strin List preparers = new ArrayList<>(); preparers.add(new SessionTimeZonePreparer()); preparers.add(new SessionOfficePreparer(organization)); - preparers.add(new DirectUserPreparer(user)); + if(user != null) + { + preparers.add(new DirectUserPreparer(user)); + } DataSource wrappedDataSource = new ConnectionPreparingDataSource(new DelegatingConnectionPreparer(preparers), dataSource); - if(database != null) + if(dataSource == null) { - return database; + throw new IllegalStateException("No data source defined in context.xml"); } try { - if(dataSource == null) - { - throw new IllegalStateException("No data source defined in context.xml"); - } - database = DatabaseService.getDatabaseFor(dataSource); + Properties properties = new Properties(); + properties.put("CwmsOfficeId", organization); + OpenDcsDatabase retval = DatabaseService.getDatabaseFor(wrappedDataSource, properties); + return retval; } catch(DatabaseException ex) { throw new IllegalStateException("Error establishing database instance through data source.", ex); } - return database; } } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingConnectionPreparer.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingConnectionPreparer.java index 89feab72..e9f80583 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingConnectionPreparer.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/dao/datasource/DelegatingConnectionPreparer.java @@ -31,7 +31,6 @@ public Connection prepare(Connection connection) throws SQLException logger.atTrace().log(delegate.getClass().getName()); retval = delegate.prepare(retval); } - return retval; } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java index f1eff93a..74d990ff 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/res/OpenDcsResource.java @@ -15,8 +15,10 @@ package org.opendcs.odcsapi.res; +import java.security.Principal; import javax.servlet.ServletContext; import javax.sql.DataSource; +import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.core.Context; import decodes.db.Database; @@ -41,16 +43,36 @@ public class OpenDcsResource { private static final String UNSUPPORTED_OPERATION_MESSAGE = "Endpoint is unsupported by the OpenDCS REST API."; + @Context + private ContainerRequestContext request; + @Context protected ServletContext context; protected final synchronized OpenDcsDatabase createDb() + { + DataSource dataSource = getDataSource(); + Principal userPrincipal = request.getSecurityContext().getUserPrincipal(); + String clientId = null; + if(userPrincipal != null) + { + clientId = userPrincipal.getName(); + } + String organization = request.getHeaders().getFirst("X-ORGANIZATION-ID"); + return OpenDcsDatabaseFactory.createDb(dataSource, organization, clientId); + } + + protected final DataSource getDataSource() { DataSource dataSource = (DataSource) context.getAttribute(DATA_SOURCE_ATTRIBUTE_KEY); - return OpenDcsDatabaseFactory.createDb(dataSource); + if(dataSource == null) + { + throw new IllegalStateException("DataSource not found in ServletContext."); + } + return dataSource; } - protected DatabaseIO getLegacyDatabase() + protected final DatabaseIO getLegacyDatabase() { return createDb().getLegacyDatabase(Database.class) .map(Database::getDbIo) diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java deleted file mode 100644 index 60135811..00000000 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/AuthorizationCheck.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2025 OpenDCS Consortium and its Contributors - * - * Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.opendcs.odcsapi.sec; - -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import javax.sql.DataSource; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.core.SecurityContext; - -import decodes.cwms.CwmsTimeSeriesDb; -import decodes.tsdb.TimeSeriesDb; -import opendcs.opentsdb.OpenTsdb; -import org.opendcs.database.api.OpenDcsDatabase; -import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; -import org.opendcs.odcsapi.dao.OpenDcsDatabaseFactory; -import org.opendcs.odcsapi.sec.basicauth.OpenTsdbAuthorizationDAO; -import org.opendcs.odcsapi.sec.cwms.CwmsAuthorizationDAO; - -import static org.opendcs.odcsapi.res.DataSourceContextCreator.DATA_SOURCE_ATTRIBUTE_KEY; - -public abstract class AuthorizationCheck -{ - - /** - * Authorizes the current session returning the SecurityContext that will check user roles. - * - * @param requestContext context for the current session. - * @param httpServletRequest context for the current request. - */ - public abstract SecurityContext authorize(ContainerRequestContext requestContext, - HttpServletRequest httpServletRequest, ServletContext servletContext); - - public abstract boolean supports(String type, ContainerRequestContext requestContext, ServletContext servletContext); - - - protected final ApiAuthorizationDAI getAuthDao(ServletContext servletContext) - { - DataSource dataSource = (DataSource) servletContext.getAttribute(DATA_SOURCE_ATTRIBUTE_KEY); - OpenDcsDatabase db = OpenDcsDatabaseFactory.createDb(dataSource); - TimeSeriesDb timeSeriesDb = db.getLegacyDatabase(TimeSeriesDb.class) - .orElseThrow(() -> new UnsupportedOperationException("Endpoint is unsupported by the OpenDCS REST API.")); - //Need to figure out a better way to extend the toolkit API to be able to add dao's within the REST API - if(timeSeriesDb instanceof CwmsTimeSeriesDb cwms) - { - return new CwmsAuthorizationDAO(cwms); - } - else if(timeSeriesDb instanceof OpenTsdb opentsdb) - { - return new OpenTsdbAuthorizationDAO(opentsdb); - } - throw new UnsupportedOperationException("Endpoint is unsupported by the OpenDCS REST API."); - } -} diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/SecurityFilter.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/SecurityFilter.java index 22e01979..7f86b0c9 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/SecurityFilter.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/SecurityFilter.java @@ -35,7 +35,6 @@ import javax.ws.rs.ext.Provider; import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; -import org.opendcs.odcsapi.hydrojson.DbInterface; import org.slf4j.Logger; import org.opendcs.utils.logging.OpenDcsLoggerFactory; diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthCheck.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthCheck.java deleted file mode 100644 index c456c050..00000000 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthCheck.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2025 OpenDCS Consortium and its Contributors - * - * Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.opendcs.odcsapi.sec.basicauth; - -import java.util.Set; -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; -import javax.sql.DataSource; -import javax.ws.rs.NotAuthorizedException; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.core.SecurityContext; - -import com.google.auto.service.AutoService; -import decodes.tsdb.TimeSeriesDb; -import org.opendcs.database.api.OpenDcsDatabase; -import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; -import org.opendcs.odcsapi.dao.OpenDcsDatabaseFactory; -import org.opendcs.odcsapi.sec.AuthorizationCheck; -import org.opendcs.odcsapi.sec.OpenDcsApiRoles; -import org.opendcs.odcsapi.sec.OpenDcsPrincipal; -import org.opendcs.odcsapi.sec.OpenDcsSecurityContext; - -import static org.opendcs.odcsapi.res.DataSourceContextCreator.DATA_SOURCE_ATTRIBUTE_KEY; - -@AutoService(AuthorizationCheck.class) -public final class BasicAuthCheck extends AuthorizationCheck -{ - - @Override - public OpenDcsSecurityContext authorize(ContainerRequestContext requestContext, HttpServletRequest httpServletRequest, ServletContext servletContext) - { - HttpSession session = httpServletRequest.getSession(false); - if(session == null) - { - throw new NotAuthorizedException("User has not yet established a session."); - } - Object attribute = session.getAttribute(OpenDcsPrincipal.USER_PRINCIPAL_SESSION_ATTRIBUTE); - if(!(attribute instanceof OpenDcsPrincipal)) - { - throw new NotAuthorizedException("User has not established an authenticated session."); - } - String username = ((OpenDcsPrincipal) attribute).getName(); - Set roles = getUserRoles(username, servletContext); - OpenDcsPrincipal principal = new OpenDcsPrincipal(username, roles); - return new OpenDcsSecurityContext(principal, - httpServletRequest.isSecure(), SecurityContext.BASIC_AUTH); - } - - @Override - public boolean supports(String type, ContainerRequestContext ignored, ServletContext servletContext) - { - DataSource dataSource = (DataSource) servletContext.getAttribute(DATA_SOURCE_ATTRIBUTE_KEY); - OpenDcsDatabase db = OpenDcsDatabaseFactory.createDb(dataSource); - TimeSeriesDb timeSeriesDb = db.getLegacyDatabase(TimeSeriesDb.class) - .orElseThrow(() -> new UnsupportedOperationException("Endpoint is unsupported by the OpenDCS REST API.")); - return "basic".equals(type) && timeSeriesDb.isOpenTSDB(); - } - - private Set getUserRoles(String username, ServletContext servletContext) - { - try(ApiAuthorizationDAI dao = getAuthDao(servletContext)) - { - return dao.getRoles(username); - } - catch(Exception ex) - { - throw new IllegalStateException("Unable to query the database for user authorization", ex); - } - } -} diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java index 56ef0096..d08a8741 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/BasicAuthResource.java @@ -16,22 +16,13 @@ package org.opendcs.odcsapi.sec.basicauth; import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.Base64; +import java.util.Properties; import java.util.Set; import javax.annotation.security.RolesAllowed; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.StringToClassMapItem; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import io.swagger.v3.oas.annotations.responses.ApiResponse; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -41,22 +32,29 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; -import javax.ws.rs.ServerErrorException; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import decodes.tsdb.TimeSeriesDb; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.StringToClassMapItem; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import org.opendcs.odcsapi.beans.Status; import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; import org.opendcs.odcsapi.errorhandling.WebAppException; import org.opendcs.odcsapi.res.OpenDcsResource; import org.opendcs.odcsapi.sec.OpenDcsApiRoles; import org.opendcs.odcsapi.sec.OpenDcsPrincipal; +import org.opendcs.odcsapi.sec.cwms.CwmsAuthorizationDAO; import org.opendcs.odcsapi.util.ApiConstants; -import org.slf4j.Logger; import org.opendcs.utils.logging.OpenDcsLoggerFactory; +import org.slf4j.Logger; import static org.opendcs.odcsapi.res.DataSourceContextCreator.DATA_SOURCE_ATTRIBUTE_KEY; @@ -80,14 +78,17 @@ public final class BasicAuthResource extends OpenDcsResource @RolesAllowed({ApiConstants.ODCS_API_GUEST}) @Operation( summary = "The ‘credentials’ POST method is used to obtain a new token", - description = "The user name and password provided must be a valid login for the underlying database. \n" - + "Also, that user must be assigned either of the roles OTSDB_ADMIN or OTSDB_MGR.\n" - + "--- \n\n\n" - + "Starting in **API Version 0.0.3**, authentication credentials (username and password) " - + "may be passed as shown above in the POST body. \n" - + "They may also be passed in a GET call to the 'credentials' method, " - + "(e.g. '*http://localhost:8080/odcsapi/credentials*') containing an HTTP Authentication Basic " - + "header in the form 'username:password'. \n\nThe returned data to the GET call will be empty.", + description = """ + The user name and password provided must be a valid login for the underlying database. + Also, that user must be assigned either of the roles OTSDB_ADMIN or OTSDB_MGR. + --- + Starting in **API Version 0.0.3**, authentication credentials (username and password) \ + may be passed as shown above in the POST body. + They may also be passed in a GET call to the 'credentials' method, \ + (e.g. '*http://localhost:8080/odcsapi/credentials*') containing an HTTP Authentication Basic \ + header in the form 'username:password'. + + The returned data to the GET call will be empty.""", requestBody = @RequestBody( description = "Login Credentials", required = true, @@ -133,12 +134,6 @@ public final class BasicAuthResource extends OpenDcsResource ) public Response postCredentials(Credentials credentials) throws WebAppException { - TimeSeriesDb db = getLegacyTimeseriesDB(); - if(!db.isOpenTSDB()) - { - throw new ServerErrorException("Basic Auth is not supported", Response.Status.NOT_IMPLEMENTED); - } - //If credentials are null, Authorization header will be checked. if(credentials != null) { @@ -148,7 +143,8 @@ public Response postCredentials(Credentials credentials) throws WebAppException String authorizationHeader = httpHeaders.getHeaderString(HttpHeaders.AUTHORIZATION); credentials = getCredentials(credentials, authorizationHeader); validateDbCredentials(credentials); - Set roles = getUserRoles(credentials.getUsername()); + String organizationId = httpHeaders.getHeaderString("X-ORGANIZATION-ID"); + Set roles = getUserRoles(credentials.getUsername(), organizationId); OpenDcsPrincipal principal = new OpenDcsPrincipal(credentials.getUsername(), roles); HttpSession oldSession = request.getSession(false); if(oldSession != null) @@ -249,8 +245,8 @@ private void validateDbCredentials(Credentials creds) throws WebAppException Intentional unused connection. Makes a new db connection using passed credentials This validates the username & password and will throw SQLException if user/pw is not valid. */ - //noinspection EmptyTryBlock DataSource dataSource = (DataSource) context.getAttribute(DATA_SOURCE_ATTRIBUTE_KEY); + //noinspection EmptyTryBlock try (Connection ignored = dataSource.getConnection(creds.getUsername(), creds.getPassword())) {// NOSONAR @@ -263,11 +259,12 @@ private void validateDbCredentials(Credentials creds) throws WebAppException } } - private Set getUserRoles(String username) + private Set getUserRoles(String username, String organizationId) { - try(ApiAuthorizationDAI dao = getAuthDao()) + try { - return dao.getRoles(username); + ApiAuthorizationDAI dao = getAuthDao(); + return dao.getRoles(username, organizationId); } catch(Exception ex) { @@ -277,12 +274,37 @@ private Set getUserRoles(String username) private ApiAuthorizationDAI getAuthDao() { - TimeSeriesDb timeSeriesDb = getLegacyTimeseriesDB(); + DataSource dataSource = getDataSource(); + String databaseType = getDatabaseType(dataSource); // Username+Password login only supported by OpenTSDB - if(timeSeriesDb.isOpenTSDB()) + if("opentsdb".equalsIgnoreCase(databaseType)) + { + return new OpenTsdbAuthorizationDAO(dataSource); + } + else if("cwms".equalsIgnoreCase(databaseType)) { - return new OpenTsdbAuthorizationDAO(timeSeriesDb); + return new CwmsAuthorizationDAO(dataSource); } throw new UnsupportedOperationException("Endpoint is unsupported by the OpenDCS REST API."); } + + private static String getDatabaseType(DataSource dataSource) + { + String databaseType = ""; + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement("select prop_value from tsdb_property WHERE prop_name = 'editDatabaseType'"); + ResultSet rs = stmt.executeQuery()) + { + Properties props = new Properties(); + if (rs.next()) + { + databaseType = rs.getString("prop_value"); + } + } + catch (SQLException ex) + { + throw new IllegalStateException("editDatabaseType not set in tsdb_property table. Cannot determine the type of database.", ex); + } + return databaseType; + } } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/OpenTsdbAuthorizationDAO.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/OpenTsdbAuthorizationDAO.java index 9712f817..f123e170 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/OpenTsdbAuthorizationDAO.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/basicauth/OpenTsdbAuthorizationDAO.java @@ -15,12 +15,15 @@ package org.opendcs.odcsapi.sec.basicauth; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.EnumSet; import java.util.Set; +import javax.sql.DataSource; + import opendcs.dao.DaoBase; import opendcs.dao.DatabaseConnectionOwner; import opendcs.opentsdb.OpenTsdb; @@ -30,17 +33,18 @@ import org.slf4j.Logger; import org.opendcs.utils.logging.OpenDcsLoggerFactory; -public final class OpenTsdbAuthorizationDAO extends DaoBase implements ApiAuthorizationDAI +public final class OpenTsdbAuthorizationDAO implements ApiAuthorizationDAI { private static final Logger log = OpenDcsLoggerFactory.getLogger(); + private final DataSource dataSource; - public OpenTsdbAuthorizationDAO(OpenTsdb tsdb) + public OpenTsdbAuthorizationDAO(DataSource dataSource) { - super(tsdb, "AuthorizationDAO"); + this.dataSource = dataSource; } @Override - public Set getRoles(String username) throws DbException + public Set getRoles(String username, String unused) throws DbException { Set roles = EnumSet.noneOf(OpenDcsApiRoles.class); roles.add(OpenDcsApiRoles.ODCS_API_GUEST); @@ -48,37 +52,34 @@ public Set getRoles(String username) throws DbException String q = "select pm.roleid, pr.rolname from pg_auth_members pm, pg_roles pr" + " where pm.member = (select oid from pg_roles where upper(rolname) = upper(?))" + " and pm.roleid = pr.oid"; - try + try(Connection c = dataSource.getConnection()) { - withConnection(c -> + try(PreparedStatement statement = c.prepareStatement(q)) { - try(PreparedStatement statement = c.prepareStatement(q)) + statement.setString(1, username); + try(ResultSet rs = statement.executeQuery()) { - statement.setString(1, username); - try(ResultSet rs = statement.executeQuery()) + while(rs.next()) { - while(rs.next()) + int roleid = rs.getInt(1); + String role = rs.getString(2); + log.info("User '{}' has role {}={}", username, roleid, role); + if("OTSDB_ADMIN".equalsIgnoreCase(role)) + { + roles.add(OpenDcsApiRoles.ODCS_API_ADMIN); + } + if("OTSDB_MGR".equalsIgnoreCase(role)) { - int roleid = rs.getInt(1); - String role = rs.getString(2); - log.info("User '{}' has role {}={}", username, roleid, role); - if("OTSDB_ADMIN".equalsIgnoreCase(role)) - { - roles.add(OpenDcsApiRoles.ODCS_API_ADMIN); - } - if("OTSDB_MGR".equalsIgnoreCase(role)) - { - roles.add(OpenDcsApiRoles.ODCS_API_USER); - } + roles.add(OpenDcsApiRoles.ODCS_API_USER); } } } - }); + } return roles; } catch(SQLException ex) { - throw new DbException(module, ex, "Unable to determine user roles in the database."); + throw new DbException("Unable to determine user roles in the database.", ex); } } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/CwmsAuthorizationDAO.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/CwmsAuthorizationDAO.java index eaf8d679..aea39711 100644 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/CwmsAuthorizationDAO.java +++ b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/cwms/CwmsAuthorizationDAO.java @@ -15,31 +15,33 @@ package org.opendcs.odcsapi.sec.cwms; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.EnumSet; import java.util.Set; -import decodes.cwms.CwmsTimeSeriesDb; -import opendcs.dao.DaoBase; +import javax.sql.DataSource; + import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; import org.opendcs.odcsapi.dao.DbException; import org.opendcs.odcsapi.sec.OpenDcsApiRoles; import org.opendcs.utils.logging.OpenDcsLoggerFactory; import org.slf4j.Logger; -public final class CwmsAuthorizationDAO extends DaoBase implements ApiAuthorizationDAI +public final class CwmsAuthorizationDAO implements ApiAuthorizationDAI { private static final Logger log = OpenDcsLoggerFactory.getLogger(); + private final DataSource dataSource; - public CwmsAuthorizationDAO(CwmsTimeSeriesDb tsdb) + public CwmsAuthorizationDAO(DataSource dataSource) { - super(tsdb, "AuthorizationDAO"); + this.dataSource = dataSource; } @Override - public Set getRoles(String username) throws DbException + public Set getRoles(String username, String organizationId) throws DbException { Set roles = EnumSet.noneOf(OpenDcsApiRoles.class); roles.add(OpenDcsApiRoles.ODCS_API_GUEST); @@ -55,42 +57,37 @@ public Set getRoles(String username) throws DbException " upper(?) " + " end " + " and is_member = 'T'"; - String cwmsOfficeId = ((CwmsTimeSeriesDb) db).getDbOfficeId(); - try + try(Connection c = dataSource.getConnection()) { - withConnection(c -> + try(PreparedStatement statement = c.prepareStatement(q)) { - try(PreparedStatement statement = c.prepareStatement(q)) + statement.setString(1, organizationId); + statement.setString(2, username); + statement.setString(3, username); + statement.setString(4, username); + statement.setString(5, username); + try(ResultSet rs = statement.executeQuery()) { - statement.setString(1, cwmsOfficeId); - statement.setString(2, username); - statement.setString(3, username); - statement.setString(4, username); - statement.setString(5, username); - try(ResultSet rs = statement.executeQuery()) + while(rs.next()) { - while(rs.next()) + String role = rs.getString(1); + log.info("User '{}' has role {}", username, role); + if("CCP Mgr".equalsIgnoreCase(role)) + { + roles.add(OpenDcsApiRoles.ODCS_API_ADMIN); + } + if("CCP Proc".equalsIgnoreCase(role)) { - String role = rs.getString(1); - log.info("User '{}' has role {}", username, role); - if("CCP Mgr".equalsIgnoreCase(role)) - { - roles.add(OpenDcsApiRoles.ODCS_API_ADMIN); - } - if("CCP Proc".equalsIgnoreCase(role)) - { - roles.add(OpenDcsApiRoles.ODCS_API_USER); - } + roles.add(OpenDcsApiRoles.ODCS_API_USER); } } } - }); + } return roles; } catch(SQLException ex) { - throw new DbException("Unable to determine user roles for user: " + username - + " and office: " + cwmsOfficeId, ex); + throw new DbException("Unable to determine user roles for user: " + username + " and office: " + organizationId, ex); } } } diff --git a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java b/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java deleted file mode 100644 index 66d0c698..00000000 --- a/opendcs-rest-api/src/main/java/org/opendcs/odcsapi/sec/openid/OidcAuthCheck.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2025 OpenDCS Consortium and its Contributors - * - * Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.opendcs.odcsapi.sec.openid; - -import java.net.MalformedURLException; -import java.net.URL; -import java.text.ParseException; -import java.util.Set; -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.NotAuthorizedException; -import javax.ws.rs.ServerErrorException; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.core.Response; - -import com.google.auto.service.AutoService; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.jwk.source.JWKSourceBuilder; -import com.nimbusds.jose.proc.BadJOSEException; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jwt.JWTClaimsSet; -import org.opendcs.odcsapi.dao.ApiAuthorizationDAI; -import org.opendcs.odcsapi.hydrojson.DbInterface; -import org.opendcs.odcsapi.sec.AuthorizationCheck; -import org.opendcs.odcsapi.sec.OpenDcsApiRoles; -import org.opendcs.odcsapi.sec.OpenDcsPrincipal; -import org.opendcs.odcsapi.sec.OpenDcsSecurityContext; -import org.slf4j.Logger; -import org.opendcs.utils.logging.OpenDcsLoggerFactory; - -@AutoService(AuthorizationCheck.class) -public final class OidcAuthCheck extends AuthorizationCheck -{ - - private static final Logger log = OpenDcsLoggerFactory.getLogger(); - static final String AUTHORIZATION_HEADER = "Authorization"; - static final String BEARER_PREFIX = "Bearer "; - private final JWKSource keySource; - - public OidcAuthCheck() - { - this.keySource = setupJwkSource(); - } - - //Used by unit tests - OidcAuthCheck(JWKSource keySource) - { - this.keySource = keySource; - } - - private static JWKSource setupJwkSource() - { - JWKSource keySource = null; - String property = "opendcs.rest.api.authorization.jwt.jwkset.url"; - try - { - String jwkSetUrl = DbInterface.decodesProperties.getProperty(property); - if(jwkSetUrl == null) - { - log.warn("Property: {} not set. OpenID Authorization is disabled.", property); - } - else - { - keySource = JWKSourceBuilder - .create(new URL(jwkSetUrl)) - .retrying(true) - .build(); - } - } - catch(MalformedURLException ex) - { - log.atWarn().setCause(ex).log("Property: {} is invalid. OpenID Authorization is disabled.", property); - } - return keySource; - } - - @Override - public OpenDcsSecurityContext authorize(ContainerRequestContext requestContext, HttpServletRequest httpServletRequest, ServletContext servletContext) - { - String authorizationHeader = requestContext.getHeaderString(AUTHORIZATION_HEADER); - try - { - String token = authorizationHeader.substring(BEARER_PREFIX.length()); - JWTClaimsSet claimsSet = JwtVerifier.getInstance().getClaimsSet(keySource, token); - String username = claimsSet.getStringClaim("preferred_username"); - OpenDcsPrincipal principal = createPrincipalFromSubject(username, servletContext); - return new OpenDcsSecurityContext(principal, httpServletRequest.isSecure(), BEARER_PREFIX); - } - catch(ParseException | JOSEException | BadJOSEException ex) - { - log.atWarn().setCause(ex).log("Token processing error."); - throw new NotAuthorizedException("Invalid JWT."); - } - } - - private OpenDcsPrincipal createPrincipalFromSubject(String subject, ServletContext servletContext) - { - try(ApiAuthorizationDAI authorizationDao = getAuthDao(servletContext)) - { - Set roles = authorizationDao.getRoles(subject); - return new OpenDcsPrincipal(subject, roles); - } - catch(Exception ex) - { - throw new ServerErrorException("Error accessing database to determine user roles", - Response.Status.INTERNAL_SERVER_ERROR, ex); - } - } - - @Override - public boolean supports(String type, ContainerRequestContext requestContext, ServletContext ignored) - { - String authorizationHeader = requestContext.getHeaderString(AUTHORIZATION_HEADER); - return keySource != null && "openid".equals(type) && authorizationHeader != null && authorizationHeader.startsWith(BEARER_PREFIX); - } -} diff --git a/opendcs-web-client/src/main/webapp/WEB-INF/app_pages/login.jsp b/opendcs-web-client/src/main/webapp/WEB-INF/app_pages/login.jsp index fb64b0a1..f44bc54b 100644 --- a/opendcs-web-client/src/main/webapp/WEB-INF/app_pages/login.jsp +++ b/opendcs-web-client/src/main/webapp/WEB-INF/app_pages/login.jsp @@ -59,6 +59,9 @@

+

+ +

<% } %> diff --git a/opendcs-web-client/src/main/webapp/WEB-INF/common/top-bar.jspf b/opendcs-web-client/src/main/webapp/WEB-INF/common/top-bar.jspf index 5e56e1c3..f90a582b 100644 --- a/opendcs-web-client/src/main/webapp/WEB-INF/common/top-bar.jspf +++ b/opendcs-web-client/src/main/webapp/WEB-INF/common/top-bar.jspf @@ -80,6 +80,7 @@ diff --git a/opendcs-web-client/src/main/webapp/resources/js/modals/organizations.js b/opendcs-web-client/src/main/webapp/resources/js/modals/organizations.js new file mode 100644 index 00000000..f533a8c9 --- /dev/null +++ b/opendcs-web-client/src/main/webapp/resources/js/modals/organizations.js @@ -0,0 +1,68 @@ +document.addEventListener("DOMContentLoaded", function(event) { + console.log("organizations js loaded."); + document.getElementById('organization_select') + .addEventListener('click', () => {openOrgModal(); }); + document.getElementById("modal_organizations") + .addEventListener('click', (e) => { + if (e.target.classList.contains('modal-backdrop')) { + closeOrgModal(); + } + }); + + document.getElementById("modal_organizations_ok_button") + .addEventListener('click', () => { + const selectedId = document.getElementById('id_organization').value; + + if (!selectedId) { + alert('Please select an organization first.'); + return; + } + localStorage.setItem("organizationId", selectedId); + closeOrgModal(); + window.location.reload(); + }); +}); + +function openOrgModal() { + const myModal = getOrgModal(); + myModal.show(); + const $orgSelect = $('#id_organization'); + $.ajax({ + url: `${window.API_URL}/organizations`, + type: "GET", + dataType: "json", + success: function (data) { + data.forEach(function (org) { + $('