Skip to content

Conversation

@vairav
Copy link
Collaborator

@vairav vairav commented Nov 13, 2025

Summary

Add authorization infrastructure to support fine-grained access control for timeseries data:

  • Create AuthorizationContextHelper to parse x-cwms-auth-context header and extract user context (username, offices, roles, persona)
  • Create AuthorizationFilterHelper to generate JOOQ conditions for server-side filtering (embargo rules, time windows, office restrictions, data classification)
  • Integrate authorization filtering in TimeSeriesController with office access validation
  • Apply embargo and time window filters at DAO level in TimeSeriesDaoImpl using JOOQ conditions
  • All filtering happens at database level - no unauthorized data ever leaves the database

This implementation supports the authorization proxy pattern where policy decisions are made by OPA and enforcement happens in the CWMS Data API via the authorization context header.


Screenshots

N/A - Backend authorization infrastructure


Todos

  • AuthorizationContextHelper for header parsing
  • AuthorizationFilterHelper for JOOQ filter generation
  • Office-based filtering (allowed_offices constraint)
  • Embargo rules filtering (hide recent data by office)
  • Time window filtering (restrict historical data access)
  • Data classification filtering (classification levels)
  • Integration in TimeSeriesController
  • Integration in TimeSeriesDaoImpl
  • Logging for debugging and monitoring
  • Backward compatibility (method overloads)
  • Tests (unit tests for helpers, integration tests for DAO filtering)
  • Documentation (inline comments and usage examples)

Steps to Validate

  1. Start CWMS Data API with authorization proxy:

    # Ensure authorization proxy is running and sending x-cwms-auth-context header
    podman ps | grep authorizer-proxy
  2. Test office access validation:

    # Request timeseries for office user has access to (should succeed)
    curl http://localhost:3001/cwms-data/timeseries?name=TEST.Flow.Inst.1Hour.0.Rev&office=SWT \
      -H "Authorization: Bearer $TOKEN"
    
    # Request timeseries for office user doesn't have access to (should fail with 400)
    curl http://localhost:3001/cwms-data/timeseries?name=TEST.Flow.Inst.1Hour.0.Rev&office=SPK \
      -H "Authorization: Bearer $TOKEN"
  3. Test embargo filtering (requires OPA policy with embargo rules):

    # Recent data should be hidden for non-exempt users
    curl http://localhost:3001/cwms-data/timeseries?name=TEST.Flow.Inst.1Hour.0.Rev&office=SWT&begin=PT-24H \
      -H "Authorization: Bearer $TOKEN_NON_EXEMPT"
    
    # Recent data should be visible for embargo-exempt users (water managers)
    curl http://localhost:3001/cwms-data/timeseries?name=TEST.Flow.Inst.1Hour.0.Rev&office=SWT&begin=PT-24H \
      -H "Authorization: Bearer $TOKEN_WATER_MANAGER"
  4. Test time window filtering (requires OPA policy with time_window constraint):

    # Dam operator with 8-hour time window restriction
    curl http://localhost:3001/cwms-data/timeseries?name=TEST.Flow.Inst.1Hour.0.Rev&office=SPK&begin=PT-48H \
      -H "Authorization: Bearer $TOKEN_DAM_OPERATOR"
    # Should only return last 8 hours, not full 48 hours requested
  5. Check logs for filter application:

    # Controller logs
    podman logs data-api | grep "Authorization context"
    
    # DAO logs
    podman logs data-api | grep "Applied embargo filter"
    podman logs data-api | grep "Applied time window filter"
  6. Verify backward compatibility (requests without authorization header):

    # Should work without authorization header (no filtering applied)
    curl http://localhost:7001/cwms-data/timeseries?name=TEST.Flow.Inst.1Hour.0.Rev&office=SWT

Local Environment Notes

  • Backward compatible: Methods without authorization filters still work as before
  • Optional filtering: Authorization filters only applied when x-cwms-auth-context header is present
  • Performance: Filtering at database level using JOOQ conditions - no in-memory filtering
  • Logging: FINE-level logs for debugging authorization decisions
  • Integration: Designed to work with authorization proxy sending x-cwms-auth-context header

Impacted Areas in Application

  • TimeSeriesController (office access validation, authorization filter instantiation)
  • TimeSeriesDaoImpl (embargo and time window filtering in SQL queries)
  • Helper classes (new AuthorizationContextHelper and AuthorizationFilterHelper)
  • JOOQ queries (additional WHERE clause conditions for embargo and time window filtering)

Notes

  • This PR is part of the larger authorization infrastructure project
  • Works in conjunction with the authorization proxy from cwms-access-management repo
  • Tests will be added in follow-up commits in this same PR
  • Current implementation focuses on functionality and validation, where policy decisions come from CWMS Access Management

…lter 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.
…imeSeriesDaoImpl

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.
@vairav vairav changed the title feat: add authorization context helpers and server-side filtering for timeseries WIP: feat: Add authorization context helpers and server-side filtering for timeseries Nov 13, 2025
Copy link
Contributor

@MikeNeilson MikeNeilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall it seems reasonable.

The initial Context helper processing should probably go in ApiServer.java, here:

after the authenticator, and they like the authenticator itself, or what you see on line 360, set a context attributed with the appropriate created object.

return hasAuthContext;
}

public Condition getOfficeFilter(Field<String> officeField, String requestedOffice) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return DSL.noCondition instead of null.

It makes the downstream logic cleaner (no null checks) and we've found the behavior is better.

return officeField.in(allowedOffices);
}

public Condition getEmbargoFilter(Field<Timestamp> timestampField, Field<String> officeField) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, return noCondition instead of null

}

// Add office-specific embargo rules
if (embargoRulesNode.has("SPK")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would seem this and below should be one block that uses "requestedOffice" in the has and get methods.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a means in the CWMS schema to determine if a given office has embargoRules?

Or is this more of a hold-over?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we still need to sort out exactly how that gets determined and then converted into a policy.

List<Condition> conditions = new ArrayList<>();

Condition officeFilter = getOfficeFilter(officeField, requestedOffice);
if (officeFilter != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can remove null checks if methods all return noCondition

if (authFilter != null && authFilter.hasAuthorizationContext()) {
Field<String> officeField = valid.field("office_id", String.class);

Condition embargoFilter = authFilter.getEmbargoFilter(dataEntryDate, officeField);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as another comments, favor returning noCondition over returning null.

…ilters

* origin:
  Entity endpoint Controller and Integration test (#1497)
  Enhancements/blob clob query (#1483)
  Update treafik for latest docker. (#1493)
  Bugfix/cda 45 ts vertical datum (#1344)
  CDA-66: Updated TS identifier descriptor paging (#1481)
  add in missing expiration date to constant/seasonal levels (#1490)
  1351 implement cda gui code formatter (#1460)
  add missing back tic
  CWMS Data API documentation /timeseries GET endpoints. (#1476)
  CDA-60: Accept Header Formatting Documentation (#1463)
  The temp users set needs to be a LinkedHashSet, otherwise the last user in the list isn't always the last user and the paging doesn't work.
  Add static analysis unit test for Controller classes (#1362)
  Bugfix/incorrect parameter warning cda 58 (#1470)
  Test updates for latest schema and correct release schema image. (#1474)
  CDA-54 - Implements Entity DTO and Dao (#1482)
  Update npm pacakges (#1478)
  Correct required java version (#1462)
  CDA-40: Exception Handling Implementation Updates (#1358)
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants