Skip to content

ENG-3098: PBAC management UI — data purposes, consumers, query log config#7700

Open
galvana wants to merge 18 commits intomainfrom
feat/pbac-ui-management
Open

ENG-3098: PBAC management UI — data purposes, consumers, query log config#7700
galvana wants to merge 18 commits intomainfrom
feat/pbac-ui-management

Conversation

@galvana
Copy link
Contributor

@galvana galvana commented Mar 19, 2026

Ticket ENG-3098

Dependency: Requires fidesplus#3247 for the backend PBAC API endpoints

Description Of Changes

Add the admin UI for managing purpose-based access control (PBAC) entities. Three feature areas:

Data purposes management (/data-purposes)

  • List page with Ant Table, search, pagination
  • Add/edit pages using Ant Design v5 Form (Form.useForm, Form.Item)
  • Form mirrors privacy declaration fields: data use (taxonomy select), data categories, data subjects, legal basis, retention period, special category legal basis, features
  • Delete confirmation modal with scope-based access control

Data consumers management (/data-consumers)

  • List page with Ant Table, search, pagination
  • Add/edit pages with purpose assignment via multi-select
  • Consumer types: service, application, group, user
  • Delete confirmation modal with scope-based access control

Query log config (integration detail tab)

  • Settings toggle panel (not CRUD table) — enable/disable with poll interval
  • Inline Test connection and Poll now action buttons
  • Registered for BigQuery and test_datastore (mock) integration types
  • test_datastore connections always pass connection test (no secrets needed)

All pages gated behind alphaPurposeBasedAccessControl feature flag and requiresPlus.

Code Changes

  • features/data-purposes/ — 8 files: RTK slice, table, form, delete modal, actions cell, constants, barrel
  • features/data-consumers/ — 8 files: RTK slice, table, form, delete modal, actions cell, constants, barrel
  • features/integrations/configure-query-log/ — 6 files: RTK slice, tab, table hook, modal, actions cell, constants
  • pages/data-purposes/ — 3 pages: list, add, edit
  • pages/data-consumers/ — 3 pages: list, add, edit
  • features/common/nav/routes.ts — 8 new route constants
  • features/common/nav/nav-config.tsx — 2 nav items under Core Configuration
  • features/common/api.slice.ts — 3 cache tags
  • types/api/models/ScopeRegistryEnum.ts — 12 scope enums
  • types/api/models/IntegrationFeature.ts — QUERY_LOGGING enum
  • features/integrations/add-integration/allIntegrationTypes.tsx — test_datastore type info
  • features/integrations/integration-type-info/bigqueryInfo.tsx — QUERY_LOGGING feature
  • features/integrations/hooks/useFeatureBasedTabs.tsx — Query logging tab
  • src/fides/service/connection/connection_service.py — test types always pass connection test

Steps to Confirm

  1. Start dev: nox -s "dev(slim)" -- fides-pkg fides-admin-ui
  2. Enable alphaPurposeBasedAccessControl feature flag
  3. Navigate to Core Configuration > Data purposes — create, edit, delete a purpose
  4. Navigate to Core Configuration > Data consumers — create with purpose assignment, edit, delete
  5. Navigate to Integrations > create a test_datastore integration > Query logging tab > enable, test, poll
  6. Confirm lint/typecheck: cd clients/admin-ui && npm run lint && npm run typecheck

Pre-Merge Checklist

  • All CI Pipelines Succeeded
  • New features have been verified on (and/or added to) Demo Environment using nox -s dev -- demo
  • Documentation:
    • if there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
  • Issue Requirements are Met
  • Optional: Follow-Up Issues Created
  • Update CHANGELOG.md

Adrian Galvan and others added 5 commits March 16, 2026 16:46
Update fideslang dependency to use the feat/add-data-purposes-to-dataset-models
branch which adds data_purposes at dataset, collection, field, and sub-field
levels.

Dependency: ethyca/fideslang#39

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rect-reference error

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add admin UI for managing purpose-based access control (PBAC) entities:

Data purposes & data consumers management pages:
- List pages with Ant Table, search, pagination under Core Configuration
- Add/edit pages using Ant Design v5 Form (Form.useForm, Form.Item)
- Data purpose form mirrors privacy declaration fields (data use, categories,
  subjects, legal basis, retention, special category, features)
- Data consumer form includes purpose assignment via multi-select
- Delete confirmation modals with scope-based access control
- RTK Query slices with cache invalidation (DataPurpose, DataConsumer tags)
- Nav registration gated behind alphaPurposeBasedAccessControl flag

Query log config integration tab:
- Settings toggle panel on integration detail page (not CRUD table)
- Enable/disable switch with poll interval selector
- Inline Test connection and Poll now action buttons
- RTK Query slice for query log config CRUD + test + poll endpoints
- Tab registered for BigQuery and test_datastore (mock) integration types
- test_datastore connections always pass connection test (no secrets needed)

Infrastructure:
- 12 new OAuth scope enums (DATA_PURPOSE_*, DATA_CONSUMER_*, QUERY_LOG_SOURCE_*)
- 3 new cache tags (DataPurpose, DataConsumer, QueryLogConfig)
- IntegrationFeature.QUERY_LOGGING enum value
- test_datastore integration type info with QUERY_LOGGING feature
- Connection test always succeeds for test_datastore/test_website types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Contributor

vercel bot commented Mar 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fides-plus-nightly Error Error Mar 25, 2026 7:23pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
fides-privacy-center Ignored Ignored Mar 25, 2026 7:23pm

Request Review

Adds a new "Seed Data" page under the Developer nav (dev-only) that
lets users select and trigger seed scenarios via the seed API. Includes
RTK Query slice with status polling and cache tag invalidation mapped
per seed task. Currently supports the PBAC scenario.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Toggling the query log switch off now sends PUT {enabled: false}
instead of DELETE, preserving the config and its watermark so
re-enabling resumes from where it left off.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The form was requesting size=500 but the API enforces max 100,
causing a validation error and an empty dropdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove seed data files from this PR so they can be submitted
as a standalone PR on top. Files moved:
- features/seed-data/SeedDataPanel.tsx
- features/seed-data/seed-data.slice.ts
- pages/poc/seed-data.tsx
- nav-config + routes entries for /poc/seed-data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@galvana galvana changed the title feat: PBAC UI — data purposes, data consumers, and query log config ENG-3098: PBAC management UI — data purposes, consumers, query log config Mar 23, 2026
@galvana galvana marked this pull request as ready for review March 23, 2026 23:22
@galvana galvana requested review from a team as code owners March 23, 2026 23:22
@galvana galvana requested review from johnewart and removed request for a team March 23, 2026 23:22
@galvana galvana requested review from kruulik, lucanovera and thabofletcher and removed request for johnewart and kruulik March 23, 2026 23:22
@galvana galvana changed the base branch from feat/dataset-data-purposes to main March 23, 2026 23:23
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR adds the admin UI for three PBAC (purpose-based access control) feature areas: data purposes management, data consumers management, and a query log config tab on integration detail pages. All are gated behind the alphaPurposeBasedAccessControl feature flag and requiresPlus. The RTK slices, forms, tables, and pages are well-structured and follow existing codebase conventions.

Issues found:

  • Raw HTML buttons in ConfigureQueryLogModal — The cancel/submit buttons use <button className="ant-btn ant-btn-default"> instead of the Button component from fidesui. This bypasses the component library's theming and will silently break on Ant Design CSS class name changes.
  • Sub-hour poll intervals in productionconstants.ts exposes ONE_MINUTE (60s) and FIVE_MINUTES (300s) as selectable options, and the default is 5 minutes. Per team convention, production poll intervals should be in hours, not seconds or minutes. These values should be removed or gated before merging.
  • Hardcoded size: 100 in DataConsumerForm — The data purposes multi-select silently truncates to 100 items. Any purpose beyond that threshold will be invisible to the consumer assignment UI.
  • Duplicate hook instantiation — Both pages/data-consumers/index.tsx and pages/data-purposes/index.tsx call the respective table hook at the page level just to check for errors, while the child table component also instantiates the same hook independently. As the user modifies search/pagination, the two instances diverge and the page-level error check becomes stale.
  • test_website connection bypassconnection_service.py short-circuits both test_datastore and test_website connection types, but the PR description only mentions test_datastore. The test_website bypass may be unintentional.
  • PR size — 38 files and 3,226 line additions exceed the team guideline of 15 files / 500 lines per PR.

Confidence Score: 3/5

  • Not safe to merge as-is — sub-hour poll intervals and raw HTML buttons are production-quality concerns that should be resolved before merge.
  • The core data structures, RTK slices, and form logic are solid. However, there are three concrete issues that need fixing before merge: sub-hour poll intervals violate the team's production polling convention (and the rule explicitly says "development values should be updated before merging"), raw HTML buttons in ConfigureQueryLogModal will silently break on Ant Design upgrades, and the hardcoded size: 100 limit on purpose fetching creates a data truncation bug at scale. The duplicate hook pattern and test_website bypass are secondary concerns but add to the overall signal.
  • configure-query-log/constants.ts, configure-query-log/ConfigureQueryLogModal.tsx, data-consumers/DataConsumerForm.tsx, and src/fides/service/connection/connection_service.py need the most attention.

Important Files Changed

Filename Overview
clients/admin-ui/src/features/integrations/configure-query-log/ConfigureQueryLogModal.tsx New modal for configuring query log settings — uses raw HTML <button> elements with manually applied Ant Design CSS class names instead of the Button component from fidesui; this is fragile and will break on Ant Design version upgrades.
clients/admin-ui/src/features/data-consumers/DataConsumerForm.tsx New Ant Design form for data consumer CRUD — hardcodes size: 100 when fetching data purposes for the multi-select, which will silently truncate options once the purpose count exceeds that limit.
clients/admin-ui/src/features/integrations/configure-query-log/constants.ts Defines PollInterval enum and select options — includes sub-hour intervals (1min, 5min, 15min) that violate the team rule requiring production poll intervals to be in hours; these appear to be dev values that should be removed or guarded before merging.
clients/admin-ui/src/features/integrations/configure-query-log/QueryLogConfigTab.tsx New settings toggle tab for enabling/disabling query logging — uses a magic number 300 instead of PollInterval.FIVE_MINUTES for the default poll interval; otherwise the create/update/disable logic is clean.
clients/admin-ui/src/pages/data-consumers/index.tsx Data consumers list page — calls useDataConsumersTable() at the page level solely to check for errors, while DataConsumersTable also instantiates the same hook independently; creates two diverging state machines as the user interacts with search/pagination.
clients/admin-ui/src/pages/data-purposes/index.tsx Data purposes list page — same duplicate hook instantiation pattern as data-consumers/index.tsx; page-level error check becomes stale once user modifies search/pagination in the child table component.
src/fides/service/connection/connection_service.py Adds short-circuit for test connection types — bypasses the real connector path for both test_datastore and test_website, but the PR description only mentions test_datastore; the test_website bypass may be an unintentional side effect.
clients/admin-ui/src/features/data-purposes/data-purpose.slice.ts New RTK Query slice for data purposes CRUD — all mutations include invalidatesTags, delete also invalidates DataConsumer to keep cross-resource data fresh; well-structured.
clients/admin-ui/src/features/data-consumers/data-consumer.slice.ts New RTK Query slice for data consumers CRUD including purpose assignment endpoint — all mutations correctly invalidate DataConsumer cache tag.
clients/admin-ui/src/features/integrations/configure-query-log/query-log-config.slice.ts New RTK Query slice for query log config — CRUD plus test and poll trigger endpoints; testQueryLogConnection intentionally omits invalidatesTags (read-only operation), all mutations correctly invalidate QueryLogConfig.
clients/admin-ui/src/features/data-purposes/DataPurposeForm.tsx New Ant Design form for data purpose CRUD — mirrors privacy declaration fields, uses taxonomy hooks for dynamic options, auto-generates fides_key from name with proper validation pattern.
clients/admin-ui/src/features/common/nav/routes.ts Adds 6 new route constants for data purposes and data consumers — consistent naming with existing route patterns.

Reviews (1): Last reviewed commit: "chore: move seed data UI to separate bra..." | Re-trigger Greptile

Comment on lines +172 to +191
</Flex>

<div>
<Typography.Text strong className="mb-1 block">
Poll interval
</Typography.Text>
<Select
value={values.poll_interval_seconds}
onChange={(value) =>
setFieldValue("poll_interval_seconds", value)
}
options={POLL_INTERVAL_OPTIONS}
aria-label="Poll interval"
className="w-full"
data-testid="query-log-config-poll-select"
/>
</div>

<Flex justify="flex-end" gap={8} className="mt-2">
<button
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Raw HTML buttons instead of component library Button

The cancel and submit buttons are implemented as raw <button> elements with manually applied Ant Design CSS class names (ant-btn ant-btn-default, ant-btn ant-btn-primary). This is fragile and bypasses the component library's event handling, theming system, and accessibility features. If Ant Design updates its CSS class names (which happens across major versions), these buttons will break silently.

Use the Button component from fidesui instead:

Suggested change
</Flex>
<div>
<Typography.Text strong className="mb-1 block">
Poll interval
</Typography.Text>
<Select
value={values.poll_interval_seconds}
onChange={(value) =>
setFieldValue("poll_interval_seconds", value)
}
options={POLL_INTERVAL_OPTIONS}
aria-label="Poll interval"
className="w-full"
data-testid="query-log-config-poll-select"
/>
</div>
<Flex justify="flex-end" gap={8} className="mt-2">
<button
<Flex justify="flex-end" gap={8} className="mt-2">
<Button
type="default"
onClick={onClose}
>
Cancel
</Button>
<Button
type="primary"
onClick={submitForm}
disabled={isSubmitting || !values.name}
loading={isSubmitting}
>
{isEditing ? "Update" : "Create"}
</Button>
</Flex>

Comment on lines +34 to +35

const purposeOptions = useMemo(
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Hardcoded purpose list truncation

The purposes query is capped at size: 100. If there are more than 100 data purposes in the system, the select will silently show an incomplete list and the user will be unable to assign purposes beyond that threshold. Since the purpose count is unbounded, this will break purpose assignment at scale.

Consider fetching all pages or implementing a debounced search-as-you-type select (e.g., Ant Design's Select with showSearch and a server-side filter query) to avoid loading all items at once:

Suggested change
const purposeOptions = useMemo(
const { data: purposesData, isLoading: purposesLoading } =
useGetAllDataPurposesQuery({ size: 200 });

Or, better, use a paginated/searchable select that queries the API on user input rather than loading everything upfront.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Bumped to 200 as a safety margin. In practice, data purposes are a bounded set, but the extra headroom is sensible.

Comment on lines +1 to +8
export enum PollInterval {
ONE_MINUTE = 60,
FIVE_MINUTES = 300,
FIFTEEN_MINUTES = 900,
ONE_HOUR = 3600,
SIX_HOURS = 21600,
TWENTY_FOUR_HOURS = 86400,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Sub-hour poll intervals should not be available in production

The available poll intervals include ONE_MINUTE (60s) and FIVE_MINUTES (300s). Per the team's polling convention, production poll intervals for async operations should be set in hours, not seconds or minutes, since results can take extended periods to arrive. Short intervals create unnecessary load and are considered development-only values that must be updated before merging.

Consider removing the sub-hour options or guarding them behind a dev-mode flag:

Suggested change
export enum PollInterval {
ONE_MINUTE = 60,
FIVE_MINUTES = 300,
FIFTEEN_MINUTES = 900,
ONE_HOUR = 3600,
SIX_HOURS = 21600,
TWENTY_FOUR_HOURS = 86400,
}
export enum PollInterval {
ONE_HOUR = 3600,
SIX_HOURS = 21600,
TWELVE_HOURS = 43200,
TWENTY_FOUR_HOURS = 86400,
}

Rule Used: Polling intervals for async operations should be s... (source)

Learnt From
ethyca/fides#6566

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are configurable poll intervals for how often the system polls the database for new query log entries, not UI polling intervals for async operations. Sub-hour intervals are intentional here since query log freshness is time-sensitive for access control monitoring.

Comment on lines +22 to +25
useUpdateQueryLogConfigMutation,
} from "./query-log-config.slice";

interface FormValues {
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Magic number instead of named enum constant

The default poll_interval_seconds value of 300 is hardcoded rather than referencing the PollInterval enum defined in constants.ts. This makes the intent unclear and creates a maintenance hazard if the enum value changes.

Suggested change
useUpdateQueryLogConfigMutation,
} from "./query-log-config.slice";
interface FormValues {
const DEFAULT_VALUES: FormValues = {
enabled: false,
poll_interval_seconds: PollInterval.FIVE_MINUTES,
};

Also requires importing PollInterval at the top of the file.

Rule Used: Define magic strings as named variables with expla... (source)

Learnt From
ethyca/fidesplus#2515

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, replaced magic number with PollInterval.FIVE_MINUTES.

import useDataConsumersTable from "~/features/data-consumers/useDataConsumersTable";

const DataConsumersPage: NextPage = () => {
const { error } = useDataConsumersTable();
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Duplicate hook instantiation creates independent state machines

DataConsumersPage calls useDataConsumersTable() only to check for an error, but DataConsumersTable (rendered below) also calls useDataConsumersTable() internally. This creates two independent hook instances — each with its own useTableState and useGetAllDataConsumersQuery subscription. As the user interacts with the search box, the page-level hook's search state will diverge from the table's, and the error check will no longer reflect the current table query's error state.

The same issue exists in pages/data-purposes/index.tsx (line 12).

A cleaner approach is to let DataConsumersTable own the error state and surface it via its own empty/error state handling, or lift the hook to the page and pass its output as props to the table component.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced the duplicate table hook with a direct RTK Query call using default params for the top-level error guard. Fixed in both data-consumers and data-purposes pages.

Comment on lines +251 to +261
# Test connection types always succeed (no real connector to test)
if connection_config.connection_type in (
ConnectionType.test_datastore,
ConnectionType.test_website,
):
connection_config.update_test_status(
test_status=ConnectionTestStatus.succeeded, db=self.db
)
return TestStatusMessage(
msg=msg,
test_status=ConnectionTestStatus.succeeded,
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 test_website connection type also bypassed

The PR description states "test_datastore connections always pass connection test (no secrets needed)", but the implementation also short-circuits ConnectionType.test_website. If test_website was previously exercised through a real connector path (even a trivial one), this changes its behavior unexpectedly. Please confirm that bypassing the connection test for test_website is intentional and document this in the comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Both test_datastore and test_website are synthetic connection types used for development/testing with no real connector behind them. Updated the comment to be more explicit.

Copy link
Contributor

@kruulik kruulik left a comment

Choose a reason for hiding this comment

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

Couple comments about dead code, but maybe that's just going to be used later?

);
router.push(DATA_CONSUMERS_ROUTE);
} catch (error) {
message.error(getErrorMessage(error as any));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
message.error(getErrorMessage(error as any));
message.error(getErrorMessage(error as RTKErrorResult["error"]));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, updated to use the proper type.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this used anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Confirmed unused, removed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Possibly dead code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Confirmed unused, removed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Only used within configure-query-log

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only imported by useQueryLogConfigTable which is itself unused. Removed both.

@galvana
Copy link
Contributor Author

galvana commented Mar 25, 2026

Good catch on the dead code! All three files (ConfigureQueryLogModal, useQueryLogConfigTable, QueryLogConfigActionsCell) are confirmed unused. Removed them.

- Remove dead code: ConfigureQueryLogModal, useQueryLogConfigTable, QueryLogConfigActionsCell
- Replace `as any` with RTKErrorResult["error"] in DeleteDataConsumerModal
- Bump purpose query size from 100 to 200 in DataConsumerForm
- Replace magic number 300 with PollInterval.FIVE_MINUTES in QueryLogConfigTab
- Fix duplicate hook instantiation in data-consumers and data-purposes pages
- Clarify comment on test connection type bypass in connection_service

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@galvana galvana requested a review from kruulik March 25, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants