Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.flogger.FluentLogger;
import cwms.cda.ApiServlet;
import cwms.cda.api.enums.UnitSystem;
import cwms.cda.api.errors.CdaError;
import cwms.cda.api.errors.ExceptionTraceSupport;
import cwms.cda.api.errors.NotFoundException;
import cwms.cda.data.dao.AuthDao;
import cwms.cda.data.dao.JooqDao;
import cwms.cda.data.dao.StoreRule;
import cwms.cda.data.dao.TimeSeriesDao;
Expand All @@ -32,6 +34,7 @@
import io.javalin.core.validation.JavalinValidation;
import io.javalin.core.validation.Validator;
import io.javalin.http.Context;
import io.javalin.http.HandlerType;
import io.javalin.plugin.openapi.annotations.HttpMethod;
import io.javalin.plugin.openapi.annotations.OpenApi;
import io.javalin.plugin.openapi.annotations.OpenApiContent;
Expand Down Expand Up @@ -197,7 +200,13 @@ public void create(@NotNull Context ctx) {
}

protected DSLContext getDslContext(Context ctx) {
return JooqDao.getDslContext(ctx);
String office = null;
if (ctx.handlerType() == HandlerType.GET
&& ctx.attribute(AuthDao.DATA_API_PRINCIPAL) != null) {
office = ctx.queryParamAsClass(OFFICE, String.class)
.getOrDefault(ctx.attribute(ApiServlet.OFFICE_ID));
}
return JooqDao.getDslContext(ctx, office);
}

@NotNull
Expand Down
42 changes: 31 additions & 11 deletions cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@
import cwms.cda.api.errors.FieldLengthExceededException;
import cwms.cda.api.errors.InvalidItemException;
import cwms.cda.api.errors.NotFoundException;
import cwms.cda.api.Controllers;
import cwms.cda.datasource.ConnectionPreparingDataSource;
import cwms.cda.datasource.DelegatingConnectionPreparer;
import cwms.cda.datasource.LrtsSessionPreparer;
import cwms.cda.datasource.SessionOfficePreparer;
import cwms.cda.helpers.DatabaseHelpers.SCHEMA_VERSION;
import cwms.cda.security.CwmsAuthException;
import io.javalin.http.BadRequestResponse;
Expand Down Expand Up @@ -139,15 +142,37 @@ protected JooqDao(DSLContext dsl) {
* @return A DSLContext for the current request.
*/
public static DSLContext getDslContext(Context ctx) {
return getDslContext(ctx, null);
}

/**
* Creates a DSL context whose checked-out connections use the supplied CWMS
* session office. Callers should only supply an office when the endpoint's
* database behavior requires it; setting it globally changes legacy access
* checks for unrelated endpoints.
*
* @param ctx The current request context.
* @param office The session office to prepare, or null to leave it unchanged.
* @return A DSLContext for the current request.
*/
public static DSLContext getDslContext(Context ctx, String office) {
DSLContext retVal;

final DataSource dataSource = ctx.attribute(ApiServlet.DATA_SOURCE);
final boolean isNewLRTS = ctx.header(ApiServlet.IS_NEW_LRTS) != null
&& Boolean.parseBoolean(ctx.header(ApiServlet.IS_NEW_LRTS));

// Snapshot client-info and the requested office up front so the per-checkout
// preparer lambdas don't capture the Javalin Context — async work (e.g. the
// total-count future in TimeSeriesDaoImpl) can outlive the request facade.
final String module = (ctx.handlerType() == HandlerType.BEFORE)
? "BEFORE-HANDLER" : ctx.endpointHandlerPath();
final String action = ctx.method();
final String clientId = ctx.url().replace(ctx.path(), "") + ctx.contextPath();
DelegatingConnectionPreparer preparer = new DelegatingConnectionPreparer(
connection -> setClientInfo(ctx, connection),
new cwms.cda.datasource.LrtsSessionPreparer(isNewLRTS));
connection -> setClientInfo(connection, module, action, clientId),
new LrtsSessionPreparer(isNewLRTS),
new SessionOfficePreparer(office));
DataSource wrappedDataSource = new ConnectionPreparingDataSource(preparer, dataSource);
retVal = DSL.using(wrappedDataSource, SQLDialect.ORACLE18C);

Expand All @@ -156,7 +181,6 @@ public static DSLContext getDslContext(Context ctx) {
return retVal;
}


protected static Timestamp buildTimestamp(Instant date) {
return date != null ? Timestamp.from(date) : null;
}
Expand All @@ -176,19 +200,15 @@ public static DSLContext getDslContext(Connection connection, String officeId) {
return dsl;
}

private static Connection setClientInfo(Context ctx, Connection connection) {
private static Connection setClientInfo(Connection connection, String module, String action, String clientId) {
try {
final String apiVersion = ApiServlet.getApiVersion();
connection.setClientInfo("OCSID.ECID",
ApiServlet.APPLICATION_TITLE + " "
+ apiVersion.substring(0, Math.min(ORACLE_ECID_MAX_LENGTH, apiVersion.length())));
if (ctx.handlerType() == HandlerType.BEFORE) {
connection.setClientInfo("OCSID.MODULE", "BEFORE-HANDLER");
} else {
connection.setClientInfo("OCSID.MODULE", ctx.endpointHandlerPath());
}
connection.setClientInfo("OCSID.ACTION", ctx.method());
connection.setClientInfo("OCSID.CLIENTID", ctx.url().replace(ctx.path(), "") + ctx.contextPath());
connection.setClientInfo("OCSID.MODULE", module);
connection.setClientInfo("OCSID.ACTION", action);
connection.setClientInfo("OCSID.CLIENTID", clientId);
} catch (SQLClientInfoException ex) {
logger.atFinest() // this is usually useless information
.withCause(ex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import fixtures.TestServletInputStream;
import io.javalin.core.util.Header;
import io.javalin.http.Context;
import io.javalin.http.ContextResolver;
import io.javalin.http.HandlerType;
import io.javalin.http.util.ContextUtil;
import io.javalin.plugin.json.JavalinJackson;
Expand All @@ -39,8 +40,15 @@ void bad_format_returns_501() throws Exception {
HashMap<String, Object> attributes = new HashMap<>();
attributes.put(ContextUtil.maxRequestSizeKey, Integer.MAX_VALUE);
attributes.put(JsonMapperKt.JSON_MAPPER_KEY, new JavalinJackson());
// JooqDao.getDslContext uses ctx.url() which delegates through ContextResolver
attributes.put("contextResolver", new ContextResolver());

when(request.getInputStream()).thenReturn(new TestServletInputStream(testBody));
// JooqDao.getDslContext snapshots client-info from the request for connection preparers
when(request.getMethod()).thenReturn("GET");
when(request.getRequestURI()).thenReturn("/cwms-data/clobs");
when(request.getRequestURL()).thenReturn(new StringBuffer("http://localhost:7000/cwms-data/clobs"));
when(request.getContextPath()).thenReturn("/cwms-data");

Context context = ContextUtil.init(request, response, "*", new HashMap<>(),
HandlerType.GET, attributes);
Expand Down
76 changes: 76 additions & 0 deletions cwms-data-api/src/test/java/cwms/cda/data/dao/JooqDaoTestIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cwms.cda.data.dao;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import cwms.cda.api.DataApiTestIT;
import cwms.cda.datasource.SessionOfficePreparer;
import fixtures.CwmsDataApiSetupCallback;
import java.sql.Connection;
import java.sql.SQLException;
import mil.army.usace.hec.test.database.CwmsDatabaseContainer;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag("integration")
public final class JooqDaoTestIT extends DataApiTestIT {

private static final String SESSION_OFFICE_QUERY =
"SELECT SYS_CONTEXT('CWMS_ENV','SESSION_OFFICE_ID') FROM dual";

/**
* Verifies that SessionOfficePreparer sets the Oracle session variable
* on a cold connection. This is the preparer added to the chain in
* JooqDao.getDslContext — without it, cold pooled connections fail
* intermittently with ORA-20047 when CWMS PL/SQL checks session office.
*
* Uses a raw JDBC connection (not the API pool) to guarantee cold state.
*/
@Test
void test_session_office_preparer_sets_office_on_cold_connection_success() throws SQLException {
CwmsDatabaseContainer<?> db = CwmsDataApiSetupCallback.getDatabaseLink();
String officeId = db.getOfficeId();
String webUser = CwmsDataApiSetupCallback.getWebUser();

db.connection(c -> {
assertNull(readSessionOffice(c),
"Precondition: fresh connection should have no session office");

new SessionOfficePreparer(officeId).prepare(c);

assertEquals(officeId.toUpperCase(), readSessionOffice(c).toUpperCase(),
"SessionOfficePreparer should set SYS_CONTEXT session office");
}, webUser);
}

/**
* Verifies that SessionOfficePreparer correctly overrides a stale session
* office from a previous request — the cross-office contamination case.
* Without the preparer in the checkout chain, a pooled connection warmed
* by one office retains that office for subsequent requests.
*/
@Test
void test_session_office_preparer_overrides_stale_office_success() throws SQLException {
CwmsDatabaseContainer<?> db = CwmsDataApiSetupCallback.getDatabaseLink();
String officeId = db.getOfficeId();
String webUser = CwmsDataApiSetupCallback.getWebUser();

db.connection(c -> {
new SessionOfficePreparer(officeId).prepare(c);
assertEquals(officeId.toUpperCase(), readSessionOffice(c).toUpperCase(),
"Session should be " + officeId + " after first prepare");

new SessionOfficePreparer("SPK").prepare(c);
assertEquals("SPK", readSessionOffice(c).toUpperCase(),
"Session should switch to SPK — stale " + officeId + " must be overridden");
}, webUser);
}

private static String readSessionOffice(Connection c) {
return DSL.using(c, SQLDialect.ORACLE18C)
.fetchOne(SESSION_OFFICE_QUERY)
.get(0, String.class);
}
}
Loading