From ad2564bb0ce6894281163462725d501b50a2aeae Mon Sep 17 00:00:00 2001 From: Vairav Laxman Date: Thu, 13 Nov 2025 07:58:12 -0500 Subject: [PATCH 1/3] feat: add authorization helper classes for header parsing and JOOQ filter generation Add AuthorizationContextHelper and AuthorizationFilterHelper to support fine-grained authorization for CWMS Data API: - AuthorizationContextHelper parses x-cwms-auth-context header and extracts user context (username, offices, roles, persona) - AuthorizationFilterHelper generates JOOQ conditions for server-side filtering: - Office-based filtering (allowed_offices constraint) - Embargo rules (hide recent data by office) - Time window restrictions (limit historical data access) - Data classification filtering (classification levels) Both helpers follow CWMS codebase style with minimal comments and clear naming. All filtering logic generates type-safe JOOQ conditions for database-level enforcement. This infrastructure supports the authorization proxy pattern where OPA makes policy decisions and CWMS Data API enforces them via the authorization context header. --- .../helpers/AuthorizationContextHelper.java | 159 +++++++++++ .../helpers/AuthorizationFilterHelper.java | 248 ++++++++++++++++++ 2 files changed, 407 insertions(+) create mode 100644 cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationContextHelper.java create mode 100644 cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationFilterHelper.java diff --git a/cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationContextHelper.java b/cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationContextHelper.java new file mode 100644 index 000000000..72af4331d --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationContextHelper.java @@ -0,0 +1,159 @@ +package cwms.cda.helpers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.javalin.http.Context; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class AuthorizationContextHelper { + private static final Logger LOGGER = Logger.getLogger(AuthorizationContextHelper.class.getName()); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String AUTH_CONTEXT_HEADER = "x-cwms-auth-context"; + + private final Map authContext; + private final Map userContext; + private final Map constraints; + + public AuthorizationContextHelper(Context ctx) { + this.authContext = parseAuthContextHeader(ctx); + this.userContext = extractUserContext(authContext); + this.constraints = extractConstraints(authContext); + } + + private Map parseAuthContextHeader(Context ctx) { + String headerValue = ctx.header(AUTH_CONTEXT_HEADER); + if (headerValue == null || headerValue.isEmpty()) { + LOGGER.log(Level.FINE, "No authorization context header found"); + return Collections.emptyMap(); + } + + try { + return OBJECT_MAPPER.readValue(headerValue, Map.class); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to parse authorization context header", e); + return Collections.emptyMap(); + } + } + + @SuppressWarnings("unchecked") + private Map extractUserContext(Map authContext) { + if (authContext.containsKey("user")) { + return (Map) authContext.get("user"); + } + return Collections.emptyMap(); + } + + @SuppressWarnings("unchecked") + private Map extractConstraints(Map authContext) { + if (authContext.containsKey("constraints")) { + return (Map) authContext.get("constraints"); + } + return Collections.emptyMap(); + } + + public String getUserId() { + return (String) userContext.getOrDefault("id", null); + } + + public String getUsername() { + return (String) userContext.getOrDefault("username", null); + } + + public String getEmail() { + return (String) userContext.getOrDefault("email", null); + } + + @SuppressWarnings("unchecked") + public List getRoles() { + Object roles = userContext.get("roles"); + if (roles instanceof List) { + return (List) roles; + } + return Collections.emptyList(); + } + + @SuppressWarnings("unchecked") + public List getOffices() { + Object offices = userContext.get("offices"); + if (offices instanceof List) { + return (List) offices; + } + return Collections.emptyList(); + } + + public String getPrimaryOffice() { + return (String) userContext.getOrDefault("primary_office", null); + } + + public String getPersona() { + return (String) userContext.getOrDefault("persona", null); + } + + public String getRegion() { + return (String) userContext.getOrDefault("region", null); + } + + public String getAllowedOfficesConstraint() { + return (String) constraints.getOrDefault("allowed_offices", null); + } + + public boolean isEmbargoExempt() { + Object exempt = constraints.get("embargo_exempt"); + return exempt != null && (boolean) exempt; + } + + public String getTimezone() { + return (String) constraints.getOrDefault("timezone", null); + } + + public boolean hasRole(String role) { + return getRoles().contains(role); + } + + public boolean hasOfficeAccess(String office) { + if (office == null) { + return false; + } + List userOffices = getOffices(); + return userOffices.contains(office); + } + + public String buildOfficeFilter() { + String allowedOffices = getAllowedOfficesConstraint(); + if (allowedOffices != null && !allowedOffices.isEmpty()) { + if ("*".equals(allowedOffices)) { + return null; + } + return allowedOffices; + } + + List offices = getOffices(); + if (offices.isEmpty()) { + return null; + } + + return String.join(",", offices); + } + + public boolean isAuthorizationHeaderPresent() { + return !authContext.isEmpty(); + } + + public Map getFullContext() { + return Collections.unmodifiableMap(authContext); + } + + @Override + public String toString() { + return "AuthorizationContextHelper{" + + "userId='" + getUserId() + '\'' + + ", username='" + getUsername() + '\'' + + ", offices=" + getOffices() + + ", roles=" + getRoles() + + ", persona='" + getPersona() + '\'' + + '}'; + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationFilterHelper.java b/cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationFilterHelper.java new file mode 100644 index 000000000..536190848 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationFilterHelper.java @@ -0,0 +1,248 @@ +package cwms.cda.helpers; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.impl.DSL; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Applies authorization filtering constraints from x-cwms-auth-context header to database queries. + * Generates JOOQ conditions for office filtering, embargo rules, time windows, and data classification. + */ +public class AuthorizationFilterHelper { + + private static final Logger logger = Logger.getLogger(AuthorizationFilterHelper.class.getName()); + private static final ObjectMapper mapper = new ObjectMapper(); + + private final JsonNode constraints; + private final boolean hasAuthContext; + + public AuthorizationFilterHelper(io.javalin.http.Context ctx) { + JsonNode constraintsNode = null; + boolean hasContext = false; + + try { + String authHeader = ctx.header("x-cwms-auth-context"); + if (authHeader != null && !authHeader.isEmpty()) { + JsonNode authContext = mapper.readTree(authHeader); + constraintsNode = authContext.get("constraints"); + hasContext = true; + + logger.log(Level.FINE, "Authorization context loaded with constraints: {0}", + constraintsNode != null ? constraintsNode.toString() : "none"); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to parse authorization context", e); + } + + this.constraints = constraintsNode; + this.hasAuthContext = hasContext; + } + + public AuthorizationFilterHelper(JsonNode constraints) { + this.constraints = constraints; + this.hasAuthContext = constraints != null; + } + + public boolean hasAuthorizationContext() { + return hasAuthContext; + } + + public Condition getOfficeFilter(Field officeField, String requestedOffice) { + if (constraints == null || !constraints.has("allowed_offices")) { + return null; + } + + JsonNode allowedOfficesNode = constraints.get("allowed_offices"); + List allowedOffices = new ArrayList<>(); + + if (allowedOfficesNode.isArray()) { + for (JsonNode office : allowedOfficesNode) { + allowedOffices.add(office.asText()); + } + } + + // System admins can access all offices + if (allowedOffices.contains("*")) { + logger.log(Level.FINE, "User has access to all offices"); + return null; + } + + if (allowedOffices.isEmpty()) { + logger.log(Level.WARNING, "User has no allowed offices - denying all access"); + return DSL.falseCondition(); + } + + // User requested a specific office + if (requestedOffice != null && !requestedOffice.isEmpty()) { + if (!allowedOffices.contains(requestedOffice)) { + logger.log(Level.WARNING, "User not authorized for office: {0}", requestedOffice); + return DSL.falseCondition(); + } + return officeField.eq(requestedOffice); + } + + // Filter to user's allowed offices + logger.log(Level.FINE, "Filtering to allowed offices: {0}", allowedOffices); + return officeField.in(allowedOffices); + } + + public Condition getEmbargoFilter(Field timestampField, Field officeField) { + if (constraints == null) { + return null; + } + + // Check if user is exempt from embargo + boolean embargoExempt = constraints.has("embargo_exempt") && + constraints.get("embargo_exempt").asBoolean(); + + if (embargoExempt) { + logger.log(Level.FINE, "User is exempt from embargo rules"); + return null; + } + + // Get embargo rules + JsonNode embargoRulesNode = constraints.get("embargo_rules"); + if (embargoRulesNode == null || embargoRulesNode.isNull()) { + logger.log(Level.FINE, "No embargo rules present"); + return null; + } + + // Build office-specific embargo condition + // For each office, calculate: data_timestamp + embargo_hours < current_time + Condition embargoCondition = null; + Timestamp currentTime = Timestamp.from(Instant.now()); + + if (embargoRulesNode.has("default")) { + int defaultHours = embargoRulesNode.get("default").asInt(); + // Default case: timestamp must be older than embargo period + Timestamp defaultCutoff = Timestamp.from(Instant.now().minus(defaultHours, ChronoUnit.HOURS)); + embargoCondition = timestampField.lessThan(defaultCutoff); + + logger.log(Level.FINE, "Applying default embargo: {0} hours (data before {1})", + new Object[]{defaultHours, defaultCutoff}); + } + + // Add office-specific embargo rules + if (embargoRulesNode.has("SPK")) { + int spkHours = embargoRulesNode.get("SPK").asInt(); + Timestamp spkCutoff = Timestamp.from(Instant.now().minus(spkHours, ChronoUnit.HOURS)); + Condition spkCondition = officeField.eq("SPK").and(timestampField.lessThan(spkCutoff)); + + embargoCondition = embargoCondition != null + ? DSL.or(spkCondition, embargoCondition) + : spkCondition; + } + + if (embargoRulesNode.has("SWT")) { + int swtHours = embargoRulesNode.get("SWT").asInt(); + Timestamp swtCutoff = Timestamp.from(Instant.now().minus(swtHours, ChronoUnit.HOURS)); + Condition swtCondition = officeField.eq("SWT").and(timestampField.lessThan(swtCutoff)); + + embargoCondition = embargoCondition != null + ? DSL.or(swtCondition, embargoCondition) + : swtCondition; + } + + return embargoCondition; + } + + public Condition getTimeWindowFilter(Field timestampField, Timestamp userRequestedBeginTime) { + if (constraints == null || !constraints.has("time_window")) { + return null; + } + + JsonNode timeWindowNode = constraints.get("time_window"); + if (timeWindowNode.isNull() || !timeWindowNode.has("restrict_hours")) { + return null; + } + + int restrictHours = timeWindowNode.get("restrict_hours").asInt(); + Timestamp cutoffTime = Timestamp.from(Instant.now().minus(restrictHours, ChronoUnit.HOURS)); + + logger.log(Level.INFO, "Applying time window restriction: {0} hours (data after {1})", + new Object[]{restrictHours, cutoffTime}); + + // Override user's requested time if it's outside the allowed window + if (userRequestedBeginTime == null || userRequestedBeginTime.before(cutoffTime)) { + return timestampField.greaterOrEqual(cutoffTime); + } + + // User's request is within allowed window + return timestampField.greaterOrEqual(userRequestedBeginTime); + } + + public Condition getClassificationFilter(Field classificationField) { + if (constraints == null || !constraints.has("data_classification")) { + return null; + } + + JsonNode classificationNode = constraints.get("data_classification"); + List allowedClassifications = new ArrayList<>(); + + if (classificationNode.isArray()) { + for (JsonNode classification : classificationNode) { + allowedClassifications.add(classification.asText()); + } + } + + if (allowedClassifications.isEmpty()) { + logger.log(Level.WARNING, "No allowed classifications - denying all access"); + return DSL.falseCondition(); + } + + logger.log(Level.FINE, "Filtering to allowed classifications: {0}", allowedClassifications); + return DSL.or( + classificationField.in(allowedClassifications), + classificationField.isNull() // Allow data with no classification set + ); + } + + public Condition getAllFilters( + Field officeField, + Field timestampField, + Field classificationField, + String requestedOffice, + Timestamp userRequestedBeginTime) { + + List conditions = new ArrayList<>(); + + Condition officeFilter = getOfficeFilter(officeField, requestedOffice); + if (officeFilter != null) { + conditions.add(officeFilter); + } + + Condition embargoFilter = getEmbargoFilter(timestampField, officeField); + if (embargoFilter != null) { + conditions.add(embargoFilter); + } + + Condition timeWindowFilter = getTimeWindowFilter(timestampField, userRequestedBeginTime); + if (timeWindowFilter != null) { + conditions.add(timeWindowFilter); + } + + if (classificationField != null) { + Condition classificationFilter = getClassificationFilter(classificationField); + if (classificationFilter != null) { + conditions.add(classificationFilter); + } + } + + if (conditions.isEmpty()) { + return null; + } + + return DSL.and(conditions); + } +} From a0702b1fafc8d79b42401424f9891fd0e3db6667 Mon Sep 17 00:00:00 2001 From: Vairav Laxman Date: Thu, 13 Nov 2025 07:58:18 -0500 Subject: [PATCH 2/3] feat: integrate authorization filtering in TimeSeriesController and TimeSeriesDaoImpl Integrate authorization helpers into timeseries data retrieval: Controller changes (TimeSeriesController): - Create AuthorizationContextHelper and AuthorizationFilterHelper from request context - Validate office access before querying data (early validation) - Pass AuthorizationFilterHelper to DAO for server-side filtering - Log authorization context for debugging DAO changes (TimeSeriesDaoImpl): - Add method overload accepting AuthorizationFilterHelper parameter - Apply embargo filter to hide recent data based on office-specific rules - Apply time window filter to restrict historical data access - Combine authorization filters with existing filter conditions using JOOQ - All filtering happens at database level via SQL WHERE clauses Backward compatible: Existing methods without authorization filters continue to work unchanged. Authorization filtering only applied when x-cwms-auth-context header is present. --- .../cwms/cda/api/TimeSeriesController.java | 40 ++++++++++++++++++- .../cwms/cda/data/dao/TimeSeriesDaoImpl.java | 23 +++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesController.java index 6b906f44c..52651288f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesController.java @@ -18,6 +18,8 @@ import cwms.cda.data.dto.TimeSeries; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import cwms.cda.helpers.AuthorizationContextHelper; +import cwms.cda.helpers.AuthorizationFilterHelper; import cwms.cda.helpers.DateUtils; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; @@ -389,6 +391,14 @@ public void getAll(@NotNull Context ctx) { try (final Timer.Context ignored = markAndTime(GET_ALL)) { DSLContext dsl = getDslContext(ctx); + AuthorizationContextHelper authHelper = new AuthorizationContextHelper(ctx); + AuthorizationFilterHelper authFilter = new AuthorizationFilterHelper(ctx); + + if (authHelper.isAuthorizationHeaderPresent()) { + logger.log(Level.INFO, "Authorization context - User: {0}, Offices: {1}, Roles: {2}", + new Object[]{authHelper.getUsername(), authHelper.getOffices(), authHelper.getRoles()}); + } + TimeSeriesDao dao = getTimeSeriesDao(dsl); String format = ctx.queryParamAsClass(FORMAT, String.class).getOrDefault(""); String names = requiredParam(ctx, NAME); @@ -440,6 +450,14 @@ public void getAll(@NotNull Context ctx) { } String office = requiredParam(ctx, OFFICE); + + if (authHelper.isAuthorizationHeaderPresent() && !authHelper.hasOfficeAccess(office)) { + String errorMsg = String.format("User %s does not have access to office %s. Allowed offices: %s", + authHelper.getUsername(), office, authHelper.getOffices()); + logger.log(Level.WARNING, errorMsg); + throw new IllegalArgumentException(errorMsg); + } + TimeSeriesRequestParameters requestParameters = new TimeSeriesRequestParameters.Builder() .withNames(names) .withOffice(office) @@ -450,7 +468,14 @@ public void getAll(@NotNull Context ctx) { .withShouldTrim(trim.getOrDefault(true)) .withIncludeEntryDate(includeEntryDate) .build(); - TimeSeries ts = dao.getTimeseries(cursor, pageSize, requestParameters); + + TimeSeries ts; + if (dao instanceof TimeSeriesDaoImpl && authFilter.hasAuthorizationContext()) { + ts = ((TimeSeriesDaoImpl) dao).getRequestedTimeSeries(cursor, pageSize, requestParameters, null, authFilter); + logger.log(Level.FINE, "Applied authorization filtering at DAO level"); + } else { + ts = dao.getTimeseries(cursor, pageSize, requestParameters); + } results = Formats.format(contentType, ts); @@ -475,6 +500,19 @@ public void getAll(@NotNull Context ctx) { } String office = ctx.queryParam(OFFICE); + + if (authHelper.isAuthorizationHeaderPresent()) { + if (office == null || office.isEmpty()) { + office = authHelper.buildOfficeFilter(); + logger.log(Level.INFO, "No office specified, applying user office filter: {0}", office); + } else if (!authHelper.hasOfficeAccess(office)) { + String errorMsg = String.format("User %s does not have access to office %s. Allowed offices: %s", + authHelper.getUsername(), office, authHelper.getOffices()); + logger.log(Level.WARNING, errorMsg); + throw new IllegalArgumentException(errorMsg); + } + } + results = dao.getTimeseries(format, names, office, units, datum, beginZdt, endZdt, tz); ctx.status(HttpServletResponse.SC_OK); ctx.result(results); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java index 097c1a83a..349cc7e1c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java @@ -39,6 +39,7 @@ import cwms.cda.data.dto.filteredtimeseries.FilteredTimeSeries; import cwms.cda.formatters.FormattingException; import cwms.cda.formatters.xml.XMLv1; +import cwms.cda.helpers.AuthorizationFilterHelper; import cwms.cda.helpers.DateUtils; import java.math.BigDecimal; import java.math.BigInteger; @@ -243,6 +244,11 @@ public FilteredTimeSeries getTimeseries(String page, int pageSize, TimeSeriesReq protected TimeSeries getRequestedTimeSeries(String page, int pageSize, @NotNull TimeSeriesRequestParameters requestParameters, @Nullable FilteredTimeSeriesParameters fp) { + return getRequestedTimeSeries(page, pageSize, requestParameters, fp, null); + } + + protected TimeSeries getRequestedTimeSeries(String page, int pageSize, @NotNull TimeSeriesRequestParameters requestParameters, + @Nullable FilteredTimeSeriesParameters fp, @Nullable AuthorizationFilterHelper authFilter) { String names = requestParameters.getNames(); String office = requestParameters.getOffice(); @@ -374,6 +380,23 @@ protected TimeSeries getRequestedTimeSeries(String page, int pageSize, @NotNull filterConditions = getFilterCondition(fp, resolver); } + if (authFilter != null && authFilter.hasAuthorizationContext()) { + Field officeField = valid.field("office_id", String.class); + + Condition embargoFilter = authFilter.getEmbargoFilter(dataEntryDate, officeField); + if (embargoFilter != null) { + filterConditions = filterConditions.and(embargoFilter); + logger.log(Level.FINE, "Applied embargo filter to timeseries query"); + } + + Condition timeWindowFilter = authFilter.getTimeWindowFilter(dataEntryDate, + Timestamp.from(beginTime.toInstant())); + if (timeWindowFilter != null) { + filterConditions = filterConditions.and(timeWindowFilter); + logger.log(Level.FINE, "Applied time window filter to timeseries query"); + } + } + Field totalField; if (total != null) { totalField = DSL.val(total).as("TOTAL"); From b544347727a3231ed7b218e67666a012332b5f2a Mon Sep 17 00:00:00 2001 From: Vairav Laxman Date: Mon, 8 Dec 2025 11:02:09 -0500 Subject: [PATCH 3/3] Return DSL.noCondition() instead of null from filter methods - AuthorizationFilterHelper methods now return noCondition() for cleaner downstream logic without null checks - Refactored getEmbargoFilter to use requestedOffice parameter instead of hardcoded office-specific blocks - Simplified getAllFilters by removing null checks - Updated TimeSeriesDaoImpl to pass office parameter and remove null checks --- .../cwms/cda/data/dao/TimeSeriesDaoImpl.java | 14 +-- .../helpers/AuthorizationFilterHelper.java | 91 +++++-------------- 2 files changed, 28 insertions(+), 77 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java index 175f4dea8..7caa91f93 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java @@ -387,18 +387,14 @@ protected TimeSeries getRequestedTimeSeries(String page, int pageSize, @NotNull if (authFilter != null && authFilter.hasAuthorizationContext()) { Field officeField = valid.field("office_id", String.class); - Condition embargoFilter = authFilter.getEmbargoFilter(dataEntryDate, officeField); - if (embargoFilter != null) { - filterConditions = filterConditions.and(embargoFilter); - logger.log(Level.FINE, "Applied embargo filter to timeseries query"); - } + Condition embargoFilter = authFilter.getEmbargoFilter(dataEntryDate, officeField, office); + filterConditions = filterConditions.and(embargoFilter); + logger.log(Level.FINE, "Applied embargo filter to timeseries query"); Condition timeWindowFilter = authFilter.getTimeWindowFilter(dataEntryDate, Timestamp.from(beginTime.toInstant())); - if (timeWindowFilter != null) { - filterConditions = filterConditions.and(timeWindowFilter); - logger.log(Level.FINE, "Applied time window filter to timeseries query"); - } + filterConditions = filterConditions.and(timeWindowFilter); + logger.log(Level.FINE, "Applied time window filter to timeseries query"); } Field totalField; diff --git a/cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationFilterHelper.java b/cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationFilterHelper.java index 536190848..76a6995fb 100644 --- a/cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationFilterHelper.java +++ b/cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationFilterHelper.java @@ -11,7 +11,6 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -60,7 +59,7 @@ public boolean hasAuthorizationContext() { public Condition getOfficeFilter(Field officeField, String requestedOffice) { if (constraints == null || !constraints.has("allowed_offices")) { - return null; + return DSL.noCondition(); } JsonNode allowedOfficesNode = constraints.get("allowed_offices"); @@ -72,10 +71,9 @@ public Condition getOfficeFilter(Field officeField, String requestedOffi } } - // System admins can access all offices if (allowedOffices.contains("*")) { logger.log(Level.FINE, "User has access to all offices"); - return null; + return DSL.noCondition(); } if (allowedOffices.isEmpty()) { @@ -97,74 +95,52 @@ public Condition getOfficeFilter(Field officeField, String requestedOffi return officeField.in(allowedOffices); } - public Condition getEmbargoFilter(Field timestampField, Field officeField) { + public Condition getEmbargoFilter(Field timestampField, Field officeField, String requestedOffice) { if (constraints == null) { - return null; + return DSL.noCondition(); } - // Check if user is exempt from embargo boolean embargoExempt = constraints.has("embargo_exempt") && constraints.get("embargo_exempt").asBoolean(); if (embargoExempt) { logger.log(Level.FINE, "User is exempt from embargo rules"); - return null; + return DSL.noCondition(); } - // Get embargo rules JsonNode embargoRulesNode = constraints.get("embargo_rules"); if (embargoRulesNode == null || embargoRulesNode.isNull()) { logger.log(Level.FINE, "No embargo rules present"); - return null; + return DSL.noCondition(); } - // Build office-specific embargo condition - // For each office, calculate: data_timestamp + embargo_hours < current_time - Condition embargoCondition = null; - Timestamp currentTime = Timestamp.from(Instant.now()); + if (requestedOffice != null && embargoRulesNode.has(requestedOffice)) { + int embargoHours = embargoRulesNode.get(requestedOffice).asInt(); + Timestamp cutoff = Timestamp.from(Instant.now().minus(embargoHours, ChronoUnit.HOURS)); + logger.log(Level.FINE, "Applying {0} embargo: {1} hours (data before {2})", + new Object[]{requestedOffice, embargoHours, cutoff}); + return timestampField.lessThan(cutoff); + } if (embargoRulesNode.has("default")) { int defaultHours = embargoRulesNode.get("default").asInt(); - // Default case: timestamp must be older than embargo period Timestamp defaultCutoff = Timestamp.from(Instant.now().minus(defaultHours, ChronoUnit.HOURS)); - embargoCondition = timestampField.lessThan(defaultCutoff); - logger.log(Level.FINE, "Applying default embargo: {0} hours (data before {1})", new Object[]{defaultHours, defaultCutoff}); + return timestampField.lessThan(defaultCutoff); } - // Add office-specific embargo rules - if (embargoRulesNode.has("SPK")) { - int spkHours = embargoRulesNode.get("SPK").asInt(); - Timestamp spkCutoff = Timestamp.from(Instant.now().minus(spkHours, ChronoUnit.HOURS)); - Condition spkCondition = officeField.eq("SPK").and(timestampField.lessThan(spkCutoff)); - - embargoCondition = embargoCondition != null - ? DSL.or(spkCondition, embargoCondition) - : spkCondition; - } - - if (embargoRulesNode.has("SWT")) { - int swtHours = embargoRulesNode.get("SWT").asInt(); - Timestamp swtCutoff = Timestamp.from(Instant.now().minus(swtHours, ChronoUnit.HOURS)); - Condition swtCondition = officeField.eq("SWT").and(timestampField.lessThan(swtCutoff)); - - embargoCondition = embargoCondition != null - ? DSL.or(swtCondition, embargoCondition) - : swtCondition; - } - - return embargoCondition; + return DSL.noCondition(); } public Condition getTimeWindowFilter(Field timestampField, Timestamp userRequestedBeginTime) { if (constraints == null || !constraints.has("time_window")) { - return null; + return DSL.noCondition(); } JsonNode timeWindowNode = constraints.get("time_window"); if (timeWindowNode.isNull() || !timeWindowNode.has("restrict_hours")) { - return null; + return DSL.noCondition(); } int restrictHours = timeWindowNode.get("restrict_hours").asInt(); @@ -184,7 +160,7 @@ public Condition getTimeWindowFilter(Field timestampField, Timestamp public Condition getClassificationFilter(Field classificationField) { if (constraints == null || !constraints.has("data_classification")) { - return null; + return DSL.noCondition(); } JsonNode classificationNode = constraints.get("data_classification"); @@ -215,34 +191,13 @@ public Condition getAllFilters( String requestedOffice, Timestamp userRequestedBeginTime) { - List conditions = new ArrayList<>(); - Condition officeFilter = getOfficeFilter(officeField, requestedOffice); - if (officeFilter != null) { - conditions.add(officeFilter); - } - - Condition embargoFilter = getEmbargoFilter(timestampField, officeField); - if (embargoFilter != null) { - conditions.add(embargoFilter); - } - + Condition embargoFilter = getEmbargoFilter(timestampField, officeField, requestedOffice); Condition timeWindowFilter = getTimeWindowFilter(timestampField, userRequestedBeginTime); - if (timeWindowFilter != null) { - conditions.add(timeWindowFilter); - } - - if (classificationField != null) { - Condition classificationFilter = getClassificationFilter(classificationField); - if (classificationFilter != null) { - conditions.add(classificationFilter); - } - } - - if (conditions.isEmpty()) { - return null; - } + Condition classificationFilter = classificationField != null + ? getClassificationFilter(classificationField) + : DSL.noCondition(); - return DSL.and(conditions); + return DSL.and(officeFilter, embargoFilter, timeWindowFilter, classificationFilter); } }