Feature/dpav 3019 product subscription with policy-set terms - #75
Merged
Merged
Conversation
- Deleted `ProductService`, `ProductServiceImpl`, and associated test classes (`ProductServiceImplTest`, `ProductRepositoryTest`). - Updated `ProductControllerTest` to use `ProductDiscoveryService` for discovery scenarios. - Cleaned up redundant methods and DTOs, reducing unnecessary maintenance overhead. - Updated `Product` entity to include a description field for more detailed search and discovery.
…andling - Migrated `ProductDiscoveryServiceImpl` to a modular design, introducing search planning, criteria validation, repository execution, and assembly components. - Enhanced policy enforcement with delegated searches through the `ProductQueryPlanner` and enriched output via the `DiscoveredProductAssembler`. - Updated OPA fallback and dispatch rules for more descriptive policy denials with specific refusal reasons. - Improved policy documentation to reflect updated request handling, configuration options, and contract-based search execution flows.
…andling - Migrated `ProductDiscoveryServiceImpl` to a modular design, introducing search planning, criteria validation, repository execution, and assembly components. - Enhanced policy enforcement with delegated searches through the `ProductQueryPlanner` and enriched output via the `DiscoveredProductAssembler`. - Updated OPA fallback and dispatch rules for more descriptive policy denials with specific refusal reasons. - Improved policy documentation to reflect updated request handling, configuration options, and contract-based search execution flows.
…andling - Migrated `ProductDiscoveryServiceImpl` to a modular design, introducing search planning, criteria validation, repository execution, and assembly components. - Enhanced policy enforcement with delegated searches through the `ProductQueryPlanner` and enriched output via the `DiscoveredProductAssembler`. - Updated OPA fallback and dispatch rules for more descriptive policy denials with specific refusal reasons. - Improved policy documentation to reflect updated request handling, configuration options, and contract-based search execution flows.
- Introduced test specification documentation for API manual testing, including objectives, preconditions, test data, and edge cases. - Updated OPA policy documentation and sample stories to clarify product `view` and `discover` rules. - Detailed the unification of the discovery and view rules to ensure consistent policy behavior and prevent rule divergence. - Revised product attribute definitions, population risk tags, and coverage jurisdiction logic with new data samples and validation information.
- Introduced test specification documentation for API manual testing, including objectives, preconditions, test data, and edge cases. - Updated OPA policy documentation and sample stories to clarify product `view` and `discover` rules. - Detailed the unification of the discovery and view rules to ensure consistent policy behavior and prevent rule divergence. - Revised product attribute definitions, population risk tags, and coverage jurisdiction logic with new data samples and validation information.
…arity - Extracted column selection logic in `ProductSearchQueryBuilder` into a dedicated method for improved readability and reusability. - Refactored test assertions for better readability by introducing intermediate variables for views and comparisons. - Clarified SQL concatenation handling in `ProductDiscoveryRepository` to address injection risk review notes. - Enhanced `ProductSearchCriteriaFactory` by separating filter and sort key extraction into modular private methods for better maintainability. - Updated relevant unit tests to align with new method structures and improve test comprehension.
…arity - Extracted column selection logic in `ProductSearchQueryBuilder` into a dedicated method for improved readability and reusability. - Refactored test assertions for better readability by introducing intermediate variables for views and comparisons. - Clarified SQL concatenation handling in `ProductDiscoveryRepository` to address injection risk review notes. - Enhanced `ProductSearchCriteriaFactory` by separating filter and sort key extraction into modular private methods for better maintainability. - Updated relevant unit tests to align with new method structures and improve test comprehension.
…d reuse - Moved static SQL query strings in `ProductDiscoveryRepository` to centralized fields for reuse and improved maintainability. - Updated query methods to use new query fields, reducing redundancy and enhancing readability. - Refined generic handling and improved method signatures in `ProductQueryPlanner` for better type safety and modularity. - Adjusted JavaDoc comments and formatting for consistency in `ProductSearchCriteriaFactory`.
…nced policy enforcement - Introduced `ProductSubscriptionService` for processing subscription requests with direct policy decision integration. - Added validation and exception handling logic for subscription edge cases (e.g., ambiguous consumers, duplicates, missing product). - Updated `ProductController` to utilize `ProductSubscriptionService` and pass policy decisions consistently. - Enhanced `PolicyTarget` to support resource loading, enabling more comprehensive decision-making. - Revised unit tests to cover new subscription scenarios, response formatting, and error conditions comprehensively.
…ndling - Implemented consumer default logic with `is_default` flag, enabling streamlined subscription scenarios. - Added SQL migration to introduce `is_default` column and enforce one default consumer per organization. - Created `ProductSubscriptionServiceImpl` with robust consumer resolution, policy-based validation, and exception handling. - Introduced unit tests to ensure edge case coverage and enforce new subscription logic. - Added `PolicyResourceIdExtractor` and loader unit tests to support flexible policy decision enforcement.
Contributor
✅ OSS Checks PassedAll tracked OSS checks passed in this run.
Results from commit 029a4da, view the full job summary ♻️ This comment has been updated with latest results. |
…ndling - Implemented consumer default logic with `is_default` flag, enabling streamlined subscription scenarios. - Added SQL migration to introduce `is_default` column and enforce one default consumer per organization. - Created `ProductSubscriptionServiceImpl` with robust consumer resolution, policy-based validation, and exception handling. - Introduced unit tests to ensure edge case coverage and enforce new subscription logic. - Added `PolicyResourceIdExtractor` and loader unit tests to support flexible policy decision enforcement.
# Conflicts: # docker/opa/policies/product/subscribe.rego # docs/AUTHENTICATION_REQUIREMENTS.md # docs/DATABASE_SCHEMA.md # docs/POLICY_ENFORCEMENT.md # docs/tests/products/README.md # src/main/java/uk/gov/dbt/ndtp/ia/node/management/controller/v1/ProductController.java # src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/handlers/GlobalExceptionHandler.java # src/main/resources/db/samples/V20260918140000__flag_sensitive_organisation_attributes.sql # src/test/java/uk/gov/dbt/ndtp/ia/node/management/controller/v1/ProductControllerTest.java # src/test/java/uk/gov/dbt/ndtp/ia/node/management/web/policy/PolicyAnnotationValidatorTest.java
|
nikan-negaresh-informed
enabled auto-merge (squash)
September 24, 2026 10:45
jenniferbrown94
approved these changes
Sep 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



feat(subscription): product subscription with policy-set terms
Sensitive Credential Checks
Motivation and Context
DPAV-3018 gave a federator the ability to find a product and read one. It could not yet
take one. This adds the third endpoint,
POST /api/v1/product/subscribe, which records a grantof a product to one of the calling organisation's consumers.
Subscription is the first product action that writes, and the first whose policy decision is
about terms rather than visibility. That makes two things new here:
thing and let the row filter answer per product in SQL; neither reads an entity in Rego. How
long a subscription may run, though, depends on what the data is — so the product named in the
body has to be loaded and sent to the PDP. The mechanism for that (
@Policy(loadResource = true)and the loader/extractor pair) is introduced in this PR.
rule from the caller's purpose and the product's attributes. No validity arithmetic lives in
Java.
Description
The endpoint
POST /api/v1/product/subscribe, rolemanagement-node:product_subscribe, annotated@Policy(resource = "product", action = "subscribe", details = ProductSubscriptionPolicyDecisionDetails.class, loadResource = true).The handler receives the decision and passes it to the service rather than letting the service ask
for a second one — the terms applied are the ones the request was allowed on.
ProductSubscriptionServiceImplanswers four questions in order, each able to end the request:which consumer takes the subscription, does the product exist, does the grant already exist, and
how long is it valid for. Only the last needs the decision.
Choosing the consumer
consumerIdis optional, because most organisations run one consumer or nominate a default;naming one is for organisations that run several.
consumerIdgivenis_default400, asking forconsumer_id400Step 1's ownership check is the one that matters for security: without it a consumer id would be a
way to subscribe somebody else's consumer.
Schema
V20260922100000consumer.is_default—NOT NULL DEFAULT FALSE, plus a partial unique index on(org_id) WHERE is_default = TRUEV20260922110000(sample)ENVa default consumer, so both resolution paths are testable without editing the databaseV20260922120000The index is partial on purpose: an organisation has at most one default and may have none,
while any number of non-default consumers stays legal. A plain unique index on
org_idwould haveallowed only one consumer per organisation. The column is
is_defaultrather thandefaultbecause
DEFAULTis a reserved word in SQL — an unquoted reference would be a syntax error ratherthan a missing-column error.
V20260922120000is a fix, not a feature: the trigger function resolved its table namesunqualified, so PL/pgSQL resolved them against the caller's
search_path, and a sessionwithout the schema on its path could not delete from any owning table at all. Dropping it leaves a
consequence that is documented rather than fixed here —
entity_idis polymorphic with no foreignkey, so nothing now soft-deletes an entity's attribute values when the entity is removed.
Whatever deletes one of those entities must do it in the same transaction. Recorded in
docs/DATABASE_SCHEMA.md, andPolicyAttributeValueSoftDeleteTriggerTestis updated to assert theabsence rather than the behaviour.
Status codes, and the two that are easy to confuse
200400403(role)product_subscribe— noreasons403(policy)reasons404409403is "you may not";409is "that is already done".SubscriptionRejectedExceptioncarriesa
ReasonthatGlobalExceptionHandlermaps to the status, so the mapping is in one place.Uniqueness is per product and consumer (
uq_product_consumer_pair): one organisation may holdthe same product on several of its consumers — that is how it feeds the same data to more than one
system — while the same consumer cannot hold it twice. The duplicate is checked before insert so
the caller gets a message naming what already exists rather than a constraint violation.
The rule:
policies.product.subscribe, 2.0.0 → 4.0.03.0.0 —
validity_days, the term worked out from the product as well as the caller.max_validity_daysis the ceiling the caller's purpose allows and says nothing about the data.validity_daysis the grant actually made: the lower of that ceiling and what the product permits.The last line is the deliberate one: an unclassified product is not evidence that it is safe to
hold for a year. The product can only shorten a grant, never extend one — a caller entitled to
a year of a validated, anonymised feed is not entitled to a year of a provisional, directly
identifiable one.
4.0.0 — the jurisdiction rule. An organisation whose
jurisdictionsinclude Wales may notsubscribe to a product whose type is
topic, whatever else it is entitled to:The
code:subjectform is the convention the product rules already use for a parameterisedrefusal: the code before the colon is the stable key audit and logs match on, and the sentence
after it reaches the caller as written. The wording, including the spelling
Juristiction, isas agreed — a reviewer "correcting" it silently breaks the tests that assert on it.
It is enforced entirely in Rego; no Java change was needed. The product's type already reaches
the PDP as
input.resource.fields.type(fromproduct_type.name), and any reason not prefixeddispatch.orpolicy.passes throughPolicyDecision.callerReasons()to the caller.Three properties to review it against, all intended:
just the same if the caller covers Wales;
ENV(England andWales) is caught;
HEGandBCCare not;ENVis refused its own topic products.Because
V20251013135858backfilled every product existing at that point to typetopic, eight ofthe seventeen sample products are topics, so this puts about half the sample catalogue out of
ENV's reach. That is the rule working, and it is why several scenarios in the test specificationmoved onto
fileproducts.Scope: the rule is on the subscribe rule alone. Discover and view are unchanged —
ENVcanstill find and read topic products, it just cannot subscribe to them.
Documentation
docs/tests/products/subscribe.md— new, the test requirement specification for the manualtesting team: §3 covers subscription generally (S1–S16), §4 the jurisdiction rule (W1–W9), and
§4.1 is a product → type table, needed because the type appears in no API response.
docs/POLICY_ENFORCEMENT.md— Loading the entity a decision is about, the subscribe refusaltable, and the
loadResourcecolumn.docs/DATABASE_SCHEMA.md—consumer.is_defaultand the trigger removal with its consequence.docs/AUTHENTICATION_REQUIREMENTS.md,docker/opa/policy_sample_stories.md,docs/tests/products/README.md— updated to match.How Has This Been Tested?
Automated — all green
./mvnw test./mvnw spotless:checkopa test /popa check --strict /popa fmt --diff /pOPA commands run against the host policy tree, since the image has no shell:
Tests follow the repo convention: plain JUnit 5 with
@ExtendWith(MockitoExtension.class), no@SpringBootTestand no Testcontainers.New and changed tests in this delta
Java
ProductSubscriptionServiceImplTest— new, 15 tests: every consumer-resolution path, theownership check, the duplicate grant, and that the validity written is the one policy decided.
ProductPolicyResourceLoaderTest— new, 5 tests: that an unknown or non-numeric id loadsnothing rather than failing the decision.
ProductControllerTest,PolicyInputFactoryTest,PolicyAnnotationValidatorTest,PolicyDecisionSerializationTest— extended forloadResource,resource.fieldsand the newresponse shape.
PolicyAttributeValueSoftDeleteTriggerTest— now asserts the trigger's absence.Rego —
subscribe_test.regogoes from 10 to 24 cases. Six covervalidity_days(eachproduct ceiling, the no-attributes case, and that the product can only shorten). Eight cover the
jurisdiction rule: a Welsh organisation refused a topic product; the same caller allowed the same
product as a
file; non-Welsh organisations allowed topic products; the caller's own topic productstill refused; a product with no type recorded allowed — which is what catches a rule written
as not file rather than is topic; both reasons reported together when a schedule type is also
wrong; the terms still returned with the refusal; and a non-string type not caught.
dispatch_test.regofollows the version bump.Manual verification against a running PDP
OPA restarted (
docker compose restart opaindocker/opa) and each case queried directly at/v1/data/dispatch/decision. Provenance readspolicies.product.subscribe/4.0.0, confirming thenew module answered rather than a cached rule:
ENV(England, Wales)topicENVfilevalidity_days30ENVvalidity_days90HEG(England, Scotland)topicENV+ an impermissible schedule typetopicHow this affects other areas
403; expected, nota defect. With
application.opa.enabled=falseno decision is taken at all, and the service fallsback to a deliberately short 30-day validity — a grant nobody authorised should expire soon. Note
that such a grant is in force and is indistinguishable from one policy allowed, which is the
reason not to run with policy off anywhere that matters.
(
PolicyInput,PolicyInputFactory,PolicyResource,PolicyTarget,Policy,PolicyEnforcementInterceptor) are additive:loadResourcedefaults tofalse, so an endpointthat does not ask for an entity gets exactly what it got before.
organisationclaim must be thedatabase key (
ENV), not the client id (FEDERATOR_ENV), or no organisation row matches,attributes come back empty, and every product rule refuses with
organisation.missing.Screenshots (if appropriate):
n/a — no UI in this repository.
Checklist:
feature/DPAV-3018this is the subscription work alone. If retargeted atdevelopthis isno longer true, since it would then carry DPAV-3018's discovery and view work with it.
cases; full suite 823 Java tests and 130 Rego tests, all passing.