Feature/3754 timebound access revoke#3920
Conversation
The backend split input ports into a link plus time-bound access
requests (ADR-0021) and now reports an InputPortStatus (pending,
approved, denied, expired) alongside a renewal status. This adapts
the frontend and seed data to that model.
- Add a Status column to the consumer and producer input-port
tables with a tag for renewal pending, renewal declined, or
expiring soon, plus an Expiry date column (blank while
pending/denied, 'Permanent Access' when open-ended, else the date).
- Migrate the input-port display path from DecisionStatus to
InputPortStatus so expired grants render correctly.
- Add an id to the user input-port response so a pending action can
be matched back to its output-port row.
- Seed all seven access states for John Scientist from both the
consumer and producer perspective.
…test fail, rename fixes
- Fix renewal_status to require a prior grant, not just a non-
approved latest request, so first-time pending/denied requests
no longer show a false renewal tag.
- Split RenewalTag into RenewalTag (renewal-status only) and
IsExpiringSoonTag per review comment.
- Move access-duration-type lookup onto OutputPort and restore the
PERMANENT short-circuit; it must never require a DB row, since
access_durations rows aren't guaranteed to exist for every type.
- Add a check constraint enforcing days is set iff time_bound.
- Seed an 'expiring soon' input port so the new tag is testable.
- Add a renewal_status test suite and auto-provision the matching
AccessDuration row from OutputPortFactory so existing tests don't
need updating
…urable in settinsg
Renewing access previously reused the "request new access" endpoint and service method (existing links were silently updated instead of rejected), which was flagged in review as conflating two distinct actions. Split AbstractDataProductService into add/renew methods and add a dedicated renew endpoint on both the data products and explorations routers. Surface it as a Renew button in the input-port table for links that are expired or expiring soon.
Color was overloaded across the status badge and renewal/expiry tags, making the real status hard to scan. Now only the status badge carries color (expired is red, requested is grey); renewal and expiring-soon indicators are neutral icon+text instead of colored tags. 'Expiring soon' moved to the Expiry date column, and the renewal tag is hidden when status is already 'Requested' since that's redundant for end users.
Producers could only end a consumer's access by hard-deleting the input port link, and denying a request doubled as a way to pull already-approved access, which didn't reflect what actually happened. Consumers had the same ambiguity: cancelling a pending request and giving up approved access were both the same hard delete. Introduce revoke (ends an approved grant, available to either side) and cancel (withdraws a still-pending request, consumer-only) as first-class, non-destructive actions. Revoke sets revoked_at / revoked_by_id on the approved request without touching its decision; cancel moves the request's decision to a new CANCELLED value on a dedicated InputPortRequestDecision enum, kept separate from the shared DecisionStatus used by other approval flows. Deny is now scoped to pending requests only. recompute_status and current_request were reworked so a pending request never hides a link's real prior outcome (Expired/Revoked/ Denied) on its own - only a first-ever request, or a fresh re-ask after a plain denial, still reports Pending, since nothing else would otherwise signal that a decision is awaited. The UI only ever exposes revoke and cancel. The original hard-delete routes (producer-side /remove, consumer-side DELETE) stay alive in the backend as a deliberate two-way door, unreferenced by any button, in case fully removing an input port ever turns out to be genuinely needed. Updates ADR-0021 and adds test coverage for the full status/renewal display matrix on both backend and frontend.
There was a problem hiding this comment.
Pull request overview
Adds first-class, non-destructive “revoke” (end an approved grant) and “cancel” (withdraw a pending request) actions for Input Port access, updating backend state handling (revocation tracking + request decision enum), regenerating clients/SDK/CLI, and aligning the UI to expose revoke/cancel/renew actions with updated status display.
Changes:
- Backend: introduce revoke/cancel/renew endpoints, add revocation tracking to
input_port_requests, and separate request decision enum from shared approval enums. - Frontend: update input-port tables/actions, status labeling/badges, and add tests for access status and action button behavior.
- Regenerated clients: update OpenAPI-derived frontend services, Python SDK models/apis, and Go CLI validators/params/ops.
Reviewed changes
Copilot reviewed 65 out of 78 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| sdk/sdk/api_client/models/revoke_output_port_as_input_port_request.py | Adds SDK request model for producer-side revoke endpoint. |
| sdk/sdk/api_client/models/revoke_input_port_for_exploration_response.py | Adds SDK response model for exploration revoke. |
| sdk/sdk/api_client/models/revoke_input_port_for_data_product_response.py | Adds SDK response model for data product revoke. |
| sdk/sdk/api_client/models/renew_input_port_for_exploration_response.py | Adds SDK response model for exploration renew. |
| sdk/sdk/api_client/models/renew_input_port_for_data_product_response.py | Adds SDK response model for data product renew. |
| sdk/sdk/api_client/models/input_port_status.py | Extends SDK status enum with revoked/cancelled. |
| sdk/sdk/api_client/models/input_port_request.py | Switches decision enum, changes valid_until to date, adds revoke metadata fields. |
| sdk/sdk/api_client/models/input_port_request_decision.py | Introduces dedicated SDK enum for input port request decisions. |
| sdk/sdk/api_client/models/input_port_request_base.py | Mirrors InputPortRequest base changes (decision enum/date/revoke fields). |
| sdk/sdk/api_client/models/cancel_input_port_for_exploration_response.py | Adds SDK response model for exploration cancel. |
| sdk/sdk/api_client/models/cancel_input_port_for_data_product_response.py | Adds SDK response model for data product cancel. |
| sdk/sdk/api_client/models/init.py | Exports newly added SDK models. |
| sdk/sdk/api_client/api/explorations/revoke_input_port_for_exploration.py | Adds SDK API call for exploration revoke endpoint. |
| sdk/sdk/api_client/api/explorations/renew_input_port_for_exploration.py | Adds SDK API call for exploration renew endpoint. |
| sdk/sdk/api_client/api/explorations/remove_input_port_for_exploration.py | Updates naming in generated docs to “For Exploration”. |
| sdk/sdk/api_client/api/explorations/cancel_input_port_for_exploration.py | Adds SDK API call for exploration cancel endpoint. |
| sdk/sdk/api_client/api/data_products/revoke_input_port_for_data_product.py | Adds SDK API call for data product revoke endpoint. |
| sdk/sdk/api_client/api/data_products/renew_input_port_for_data_product.py | Adds SDK API call for data product renew endpoint. |
| sdk/sdk/api_client/api/data_products/remove_input_port_for_data_product.py | Updates naming in generated docs to “For Data Product”. |
| sdk/sdk/api_client/api/data_products/cancel_input_port_for_data_product.py | Adds SDK API call for data product cancel endpoint. |
| sdk/sdk/api_client/api/data_products_output_ports_input_ports/revoke_output_port_as_input_port.py | Adds SDK API call for producer-side revoke. |
| frontend/src/utils/status.helper.ts | Adds labels/badge mappings for revoked/cancelled and request decision union typing. |
| frontend/src/tests/mocks/explorations.ts | Updates mocks to use InputPortRequestDecision. |
| frontend/src/tests/mocks/dataProducts.ts | Updates mocks to use InputPortRequestDecision. |
| frontend/src/tests/components/input-port/access-status.test.tsx | Adds unit tests for expiring/renewal display and expiry date rendering. |
| frontend/src/tests/components/abstract-data-products/input-port-table/input-port-action-button.test.tsx | Adds unit tests for renew/cancel/revoke button visibility logic. |
| frontend/src/store/api/services/tags/explorationTags.ts | Adds tag invalidation for exploration renew/revoke/cancel mutations. |
| frontend/src/store/api/services/tags/dataProductTags.ts | Adds tag invalidation for data product revoke/cancel/renew mutations. |
| frontend/src/store/api/services/tags/dataProductsOutputPortsInputPortsTags.ts | Renames remove→revoke tags wiring for producer-side revoke mutation. |
| frontend/src/store/api/services/generated/usersApi.ts | Regenerates request decision type + adds revoke metadata on request objects. |
| frontend/src/store/api/services/generated/explorationsApi.ts | Regenerates to add renew/revoke/cancel exploration mutations and enums. |
| frontend/src/store/api/services/generated/dataProductsOutputPortsInputPortsApi.ts | Regenerates to add producer-side revoke mutation and enums. |
| frontend/src/store/api/services/generated/dataProductsApi.ts | Regenerates to add data product renew/revoke/cancel mutations and enums. |
| frontend/src/pages/product-studio/components/requests/request-utils.ts | Widens decision typing to include InputPortRequestDecision. |
| frontend/src/pages/product-studio/components/my-requests-tab/use-table-columns.tsx | Adjusts decision renderer typing for mixed decision enums. |
| frontend/src/pages/exploration/components/exploration-input-ports.tsx | Wires exploration UI to cancel/revoke/renew endpoints. |
| frontend/src/pages/dataset/components/dataset-tabs/consumers-tab/components/consumers-table/revoke-access-modal.tsx | Removes decision-note revoke modal (revoke now confirmation-only). |
| frontend/src/pages/dataset/components/dataset-tabs/consumers-tab/components/consumers-table/consumers-table.component.tsx | Switches producer UI from hard-delete/deny-as-revoke to proper revoke endpoint. |
| frontend/src/pages/dataset/components/dataset-tabs/consumers-tab/components/consumers-table/consumers-table-columns.tsx | Updates producer table actions/status layout and adds revoke confirmation. |
| frontend/src/pages/data-product/components/data-product-tabs/input-ports/input-port-tab.tsx | Wires data product UI to cancel/revoke/renew endpoints. |
| frontend/src/components/input-port/access-status.tsx | Updates renewal/expiring display and exports helpers for reuse/testing. |
| frontend/src/components/abstract-data-products/input-port-tab/input-port-tab.tsx | Updates tab props to pass cancel/revoke/renew handlers. |
| frontend/src/components/abstract-data-products/input-port-tab/components/input-port-table/input-port-table.component.tsx | Threads new handler props into table column builder. |
| frontend/src/components/abstract-data-products/input-port-tab/components/input-port-table/input-port-table-columns.tsx | Uses new action button component + passes validity/renewal state. |
| frontend/src/components/abstract-data-products/input-port-tab/components/input-port-table/input-port-action-button.component.tsx | New unified cancel/revoke/renew button component with confirmation prompts. |
| frontend/src/components/abstract-data-products/input-port-tab/components/input-port-table/input-port-action-botton.component.tsx | Removes old “remove/cancel” mixed-action button component. |
| frontend/public/locales/en/translation.json | Adds/removes i18n keys for revoke/cancel/renew UI messaging. |
| docs/adr/0021-input-ports-and-input-port-requests.md | Updates ADR for revoke/cancel semantics and expanded status/decision model. |
| cli/golang/pkg/api/oas_validators_gen.go | Regenerates CLI validators for new enums/fields. |
| cli/golang/pkg/api/oas_request_encoders_gen.go | Regenerates encoder for revoke request body. |
| cli/golang/pkg/api/oas_parameters_gen.go | Regenerates operation parameter structs for new endpoints. |
| cli/golang/pkg/api/oas_operations_gen.go | Regenerates operation name constants for new endpoints. |
| cli/golang/pkg/api/oas_interfaces_gen.go | Regenerates response interfaces for new endpoints. |
| backend/tests/factories/input_port.py | Updates factories for new decision enum + revoked metadata on requests. |
| backend/tests/factories/input_port_request.py | Updates factory decision type and decided_by logic. |
| backend/tests/app/explorations/test_router.py | Adds tests for exploration renew/revoke/cancel + hard-delete behavior. |
| backend/tests/app/data_products/output_ports/input_ports/test_service.py | Adds/updates service tests for deny restrictions and revoke behavior. |
| backend/tests/app/data_products/output_ports/input_ports/test_router.py | Adds/updates router tests for renew/revoke/cancel and hard-delete behavior. |
| backend/tests/app/abstract_data_product/test_request_input_ports.py | Splits “request” vs “renew” behavior and adds revoke/cancel tests. |
| backend/tests/app/abstract_data_product/input_ports/test_model.py | Expands model tests for recompute/current_request semantics (revoked/cancelled). |
| backend/app/explorations/schema_response.py | Adds typed responses for exploration renew/revoke/cancel endpoints. |
| backend/app/explorations/router.py | Implements exploration renew/revoke/cancel endpoints with ownership checks. |
| backend/app/events/enums.py | Adds event types for revoked/cancelled link events. |
| backend/app/database/alembic/versions/2026_07_22_1200-c3d8a1f56e42_add_revoke_tracking_to_input_port_requests.py | Adds DB columns + FK for revoked_at/revoked_by_id. |
| backend/app/data_products/schema_response.py | Adds typed responses for data product renew/revoke/cancel endpoints. |
| backend/app/data_products/router.py | Adds consumer-side renew/revoke/cancel routes and centralizes request notifications/events. |
| backend/app/data_products/output_ports/input_ports/service.py | Scopes deny to pending-only and adds revoke service method. |
| backend/app/data_products/output_ports/input_ports/schema.py | Updates schema types for date-based valid_until + revoke metadata + new decision enum. |
| backend/app/data_products/output_ports/input_ports/schema_request.py | Adds schema for producer-side revoke request body. |
| backend/app/data_products/output_ports/input_ports/router.py | Adds producer-side revoke endpoint and adjusts remove/deny logic to match new semantics. |
| backend/app/abstract_data_product/service.py | Refactors request vs renew flows; implements revoke/cancel operations. |
| backend/app/abstract_data_product/input_ports/model.py | Implements new recompute/current_request logic; adds revoke tracking fields/relations. |
| backend/app/abstract_data_product/input_ports/enums.py | Adds REVOKED/CANCELLED link status and dedicated request decision enum. |
Files not reviewed (9)
- cli/golang/pkg/api/oas_client_gen.go: Generated file
- cli/golang/pkg/api/oas_interfaces_gen.go: Generated file
- cli/golang/pkg/api/oas_json_gen.go: Generated file
- cli/golang/pkg/api/oas_operations_gen.go: Generated file
- cli/golang/pkg/api/oas_parameters_gen.go: Generated file
- cli/golang/pkg/api/oas_request_encoders_gen.go: Generated file
- cli/golang/pkg/api/oas_response_decoders_gen.go: Generated file
- cli/golang/pkg/api/oas_schemas_gen.go: Generated file
- cli/golang/pkg/api/oas_validators_gen.go: Generated file
Comments suppressed due to low confidence (1)
backend/app/data_products/router.py:514
- This notification guard uses input_port.status == APPROVED, which is true even when a renewal request is pending on top of an active grant. That can produce an "approved" notification even though the newly created request is still pending.
for input_port, event_id in zip(input_ports, event_ids):
if input_port.status == InputPortStatus.APPROVED:
NotificationService(db).create_data_product_notifications(
data_product_id=input_port.consuming_abstract_data_product_id,
event_id=event_id,
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The 'My Requests' history showed 'Available' for input port access that had already been revoked, because the request's decision stays APPROVED forever (a historical fact) while revocation is tracked separately via revoked_at. Status helpers now check revoked_at before falling back to the decision label. Also removes an unreachable status assignment in approve_request (immediately overwritten by recompute_status() in both callers), an orphaned pre-ADR-0021 schema pair still importing the shared DecisionStatus enum, and a input_port_count filter that compared against DecisionStatus instead of InputPortStatus.
stijn-janssens
left a comment
There was a problem hiding this comment.
Overall really nice PR, i think the eventing is the only major remark I have. The rest are smaller comments
Add DATA_PRODUCT_DATASET_LINK_REVOKED/CANCELLED to the frontend event types and history helper (backend enum had them, frontend didn't). Drop console.error calls from input port actions since nothing else in the app logs to console. Replace the ad hoc 'cancelled' string literal with a proper InputPortRequestDecision enum, mirroring the existing DecisionStatus wrapper, and type the input port status helpers with InputPortStatus instead of a loose string.
#3754
Add revoke/cancel actions for input port access
Producers could only end a consumer's access by hard-deleting the
input port link, and denying a request doubled as a way to pull
already-approved access, which didn't reflect what actually happened.
Consumers had the same ambiguity: cancelling a pending request and
giving up approved access were both the same hard delete.
Introduce revoke (ends an approved grant, available to either side)
and cancel (withdraws a still-pending request, consumer-only) as
first-class, non-destructive actions. Revoke sets revoked_at /
revoked_by_id on the approved request without touching its decision;
cancel moves the request's decision to a new CANCELLED value on a
dedicated InputPortRequestDecision enum, kept separate from the
shared DecisionStatus used by other approval flows. Deny is now
scoped to pending requests only.
recompute_status and current_request were reworked so a pending
request never hides a link's real prior outcome (Expired/Revoked/
Denied) on its own - only a first-ever request, or a fresh re-ask
after a plain denial, still reports Pending, since nothing else
would otherwise signal that a decision is awaited.
The UI only ever exposes revoke and cancel. The original hard-delete
routes (producer-side /remove, consumer-side DELETE) stay alive in
the backend as a deliberate two-way door, unreferenced by any button,
in case fully removing an input port ever turns out to be genuinely
needed.
Input Port table view (consumer side)

Consumers table view (producer side)
