Skip to content

Commit

Permalink
Policy-aware traversal (#5689)
Browse files Browse the repository at this point in the history
  • Loading branch information
galvana authored Jan 29, 2025
1 parent 0f218f3 commit 1f440a2
Show file tree
Hide file tree
Showing 30 changed files with 825 additions and 133 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o
- Privacy request status tags colors have been updated [#5699](https://github.com/ethyca/fides/pull/5699)
- The privacy declarations for a system are now sorted alphabetically by name. [#5683](https://github.com/ethyca/fides/pull/5683)
- Upgraded GPP library to `3.1.5` and added support for all available state sections (ustx, usde, usia, etc.) [#5696](https://github.com/ethyca/fides/pull/5696)
- Updating DSR execution to allow collections to be unreachable when they don't contain policy-relevant data categories [#5689](https://github.com/ethyca/fides/pull/5689)

### Developer Experience
- Migrated radio buttons and groups to Ant Design [#5681](https://github.com/ethyca/fides/pull/5681)
Expand Down
1 change: 1 addition & 0 deletions clients/admin-ui/src/features/common/api.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const baseApi = createApi({
"Notification",
"Organization",
"Plus",
"Policies",
"Privacy Experience Configs",
"Experience Config Translations",
"Privacy Notices",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,13 +385,14 @@ export const datastoreConnectionApi = baseApi.injectEndpoints({
{
connection_key: string;
dataset_key: string;
input_data: Record<string, any>;
identities: Record<string, any>;
policy_key: string;
}
>({
query: (params) => ({
url: `${CONNECTION_ROUTE}/${params.connection_key}/dataset/${params.dataset_key}/test`,
method: "POST",
body: params.input_data,
body: { identities: params.identities, policy_key: params.policy_key },
}),
}),
getDatasetInputs: build.query<
Expand All @@ -406,12 +407,18 @@ export const datastoreConnectionApi = baseApi.injectEndpoints({
}),
getDatasetReachability: build.query<
{ reachable: boolean; details: string },
{ connectionKey: string; datasetKey: string }
{ connectionKey: string; datasetKey: string; policyKey?: string }
>({
query: ({ connectionKey, datasetKey }) => ({
url: `${CONNECTION_ROUTE}/${connectionKey}/dataset/${datasetKey}/reachability`,
method: "GET",
}),
query: ({ connectionKey, datasetKey, policyKey }) => {
const baseUrl = `${CONNECTION_ROUTE}/${connectionKey}/dataset/${datasetKey}/reachability`;
const queryString = policyKey ? `?policy_key=${policyKey}` : "";
const url = baseUrl + queryString;

return {
url,
method: "GET",
};
},
providesTags: () => ["Datastore Connection"],
}),
}),
Expand Down
14 changes: 14 additions & 0 deletions clients/admin-ui/src/features/policy/policy.slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { baseApi } from "~/features/common/api.slice";
import { Page_PolicyResponse_ } from "~/types/api";

// Policy API
const policyApi = baseApi.injectEndpoints({
endpoints: (build) => ({
getPolicies: build.query<Page_PolicyResponse_, void>({
query: () => ({ url: `/dsr/policy` }),
providesTags: () => ["Policies"],
}),
}),
});

export const { useGetPoliciesQuery } = policyApi;
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ import {
} from "~/features/datastore-connections";
import { Dataset } from "~/types/api";

import { selectCurrentDataset, setCurrentDataset } from "./dataset-test.slice";
import {
selectCurrentDataset,
selectCurrentPolicyKey,
setCurrentDataset,
setReachability,
} from "./dataset-test.slice";
import { removeNulls } from "./helpers";

interface EditorSectionProps {
Expand All @@ -40,6 +45,7 @@ const EditorSection = ({ connectionKey }: EditorSectionProps) => {

const [editorContent, setEditorContent] = useState<string>("");
const currentDataset = useAppSelector(selectCurrentDataset);
const currentPolicyKey = useAppSelector(selectCurrentPolicyKey);

const {
data: datasetConfigs,
Expand All @@ -54,12 +60,19 @@ const EditorSection = ({ connectionKey }: EditorSectionProps) => {
{
connectionKey,
datasetKey: currentDataset?.fides_key || "",
policyKey: currentPolicyKey,
},
{
skip: !connectionKey || !currentDataset?.fides_key,
skip: !connectionKey || !currentDataset?.fides_key || !currentPolicyKey,
},
);

useEffect(() => {
if (reachability) {
dispatch(setReachability(reachability.reachable));
}
}, [reachability, dispatch]);

const datasetOptions = useMemo(
() =>
(datasetConfigs?.items || []).map((item) => ({
Expand Down Expand Up @@ -90,10 +103,21 @@ const EditorSection = ({ connectionKey }: EditorSectionProps) => {
}, [currentDataset]);

useEffect(() => {
if (connectionKey && currentDataset?.fides_key) {
if (currentPolicyKey && currentDataset?.fides_key && connectionKey) {
refetchReachability();
}
}, [connectionKey, currentDataset, refetchReachability]);
}, [
currentPolicyKey,
currentDataset?.fides_key,
connectionKey,
refetchReachability,
]);

useEffect(() => {
if (reachability) {
dispatch(setReachability(reachability.reachable));
}
}, [reachability, dispatch]);

const handleDatasetChange = async (value: string) => {
const selectedConfig = datasetConfigs?.items.find(
Expand Down Expand Up @@ -142,6 +166,7 @@ const EditorSection = ({ connectionKey }: EditorSectionProps) => {
);
toast(successToastParams("Successfully modified dataset"));
await refetchDatasets();
await refetchReachability();
};

const handleRefresh = async () => {
Expand Down Expand Up @@ -224,30 +249,32 @@ const EditorSection = ({ connectionKey }: EditorSectionProps) => {
theme="light"
/>
</Stack>
<Stack
backgroundColor={reachability?.reachable ? "green.50" : "red.50"}
border="1px solid"
borderColor={reachability?.reachable ? "green.500" : "red.500"}
borderRadius="md"
p={2}
flexShrink={0}
mt={2}
>
<HStack alignItems="center">
<HStack flex="1">
{reachability?.reachable ? (
<GreenCheckCircleIcon />
) : (
<ErrorWarningIcon />
)}
<Text fontSize="sm" whiteSpace="pre-wrap">
{reachability?.reachable
? "Dataset is reachable"
: `Dataset is not reachable. ${reachability?.details}`}
</Text>
{reachability && (
<Stack
backgroundColor={reachability?.reachable ? "green.50" : "red.50"}
border="1px solid"
borderColor={reachability?.reachable ? "green.500" : "red.500"}
borderRadius="md"
p={2}
flexShrink={0}
mt={2}
>
<HStack alignItems="center">
<HStack flex="1">
{reachability?.reachable ? (
<GreenCheckCircleIcon />
) : (
<ErrorWarningIcon />
)}
<Text fontSize="sm" whiteSpace="pre-wrap">
{reachability?.reachable
? "Dataset is reachable"
: `Dataset is not reachable. ${reachability?.details}`}
</Text>
</HStack>
</HStack>
</HStack>
</Stack>
</Stack>
)}
</VStack>
);
};
Expand Down
54 changes: 49 additions & 5 deletions clients/admin-ui/src/features/test-datasets/TestRunnerSection.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
AntButton as Button,
AntSelect as Select,
Heading,
HStack,
Text,
Textarea,
useToast,
VStack,
} from "fidesui";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useSelector } from "react-redux";

import { useAppDispatch } from "~/app/hooks";
Expand All @@ -23,13 +24,17 @@ import { useGetFilteredResultsQuery } from "~/features/privacy-requests";
import { PrivacyRequestStatus } from "~/types/api";
import { isErrorResult } from "~/types/errors";

import { useGetPoliciesQuery } from "../policy/policy.slice";
import {
finishTest,
selectCurrentDataset,
selectCurrentPolicyKey,
selectIsReachable,
selectIsTestRunning,
selectPrivacyRequestId,
selectTestInputs,
selectTestResults,
setCurrentPolicyKey,
setPrivacyRequestId,
setTestInputs,
setTestResults,
Expand All @@ -46,8 +51,10 @@ const TestResultsSection = ({ connectionKey }: TestResultsSectionProps) => {
const [testDatasets] = useTestDatastoreConnectionDatasetsMutation();

const currentDataset = useSelector(selectCurrentDataset);
const isReachable = useSelector(selectIsReachable);
const testResults = useSelector(selectTestResults);
const testInputs = useSelector(selectTestInputs);
const currentPolicyKey = useSelector(selectCurrentPolicyKey);
const isTestRunning = useSelector(selectIsTestRunning);
const privacyRequestId = useSelector(selectPrivacyRequestId);

Expand Down Expand Up @@ -80,6 +87,20 @@ const TestResultsSection = ({ connectionKey }: TestResultsSectionProps) => {
},
);

const { data: policies } = useGetPoliciesQuery();
const policyOptions = useMemo(
() =>
(policies?.items || [])
.filter((policy) =>
policy.rules?.some((rule) => rule.action_type === "access"),
)
.map((item) => ({
value: item.key,
label: item.name,
})),
[policies?.items],
);

useEffect(() => {
const datasetKey = currentDataset?.fides_key;

Expand Down Expand Up @@ -155,8 +176,19 @@ const TestResultsSection = ({ connectionKey }: TestResultsSectionProps) => {
}
};

const handlePolicyChange = (policyKey: string) => {
dispatch(setCurrentPolicyKey(policyKey));
};

const currentPolicyKeyValue = useMemo(() => {
if (!currentPolicyKey || !policies?.items) {
return null;
}
return currentPolicyKey;
}, [currentPolicyKey, policies?.items]);

const handleTestRun = async () => {
if (!currentDataset?.fides_key) {
if (!currentDataset?.fides_key || !currentPolicyKey) {
return;
}

Expand All @@ -174,7 +206,8 @@ const TestResultsSection = ({ connectionKey }: TestResultsSectionProps) => {
const result = await testDatasets({
connection_key: connectionKey,
dataset_key: currentDataset.fides_key,
input_data: parsedInput,
identities: parsedInput,
policy_key: currentPolicyKey,
});

if (isErrorResult(result)) {
Expand Down Expand Up @@ -202,21 +235,32 @@ const TestResultsSection = ({ connectionKey }: TestResultsSectionProps) => {
justifyContent="space-between"
>
<HStack>
<Text>Test inputs (identities and references)</Text>
<Text>Test inputs</Text>
<ClipboardButton copyText={inputValue} />
</HStack>
<HStack>
<QuestionTooltip label="Run a test access request using the provided test input data" />
<Select
id="policy"
aria-label="Policy selector"
data-testid="policy-select"
placeholder="Select policy"
value={currentPolicyKeyValue}
options={policyOptions}
onChange={handlePolicyChange}
className="w-64"
/>
<Button
htmlType="submit"
size="small"
type="primary"
data-testid="run-btn"
onClick={handleTestRun}
loading={isTestRunning}
disabled={!currentPolicyKey || !isReachable}
>
Run
</Button>
<QuestionTooltip label="Run a test access request using the provided test input data and the selected access policy" />
</HStack>
</Heading>
<Textarea
Expand Down
15 changes: 15 additions & 0 deletions clients/admin-ui/src/features/test-datasets/dataset-test.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import { DatasetConfigSchema } from "~/types/api";
interface DatasetTestState {
privacyRequestId: string | null;
currentDataset: DatasetConfigSchema | null;
isReachable: boolean;
testInputs: Record<string, Record<string, any>>;
testResults: Record<string, string>;
isTestRunning: boolean;
currentPolicyKey?: string;
}

const initialState: DatasetTestState = {
privacyRequestId: null,
currentDataset: null,
isReachable: false,
testInputs: {},
testResults: {},
isTestRunning: false,
Expand Down Expand Up @@ -76,13 +79,19 @@ export const datasetTestSlice = createSlice({
[action.payload.datasetKey]: mergedValues,
};
},
setCurrentPolicyKey: (draftState, action: PayloadAction<string>) => {
draftState.currentPolicyKey = action.payload;
},
setCurrentDataset: (
draftState,
action: PayloadAction<DatasetConfigSchema | null>,
) => {
draftState.currentDataset = action.payload;
draftState.privacyRequestId = null;
},
setReachability: (draftState, action: PayloadAction<boolean>) => {
draftState.isReachable = action.payload;
},
setTestResults: (
draftState,
action: PayloadAction<{
Expand All @@ -103,7 +112,9 @@ export const {
setPrivacyRequestId,
finishTest,
setTestInputs,
setCurrentPolicyKey,
setCurrentDataset,
setReachability,
setTestResults,
} = datasetTestSlice.actions;

Expand All @@ -113,12 +124,16 @@ export const selectDatasetTestPrivacyRequestId = (state: RootState) =>
state.datasetTest.privacyRequestId;
export const selectCurrentDataset = (state: RootState) =>
state.datasetTest.currentDataset;
export const selectIsReachable = (state: RootState) =>
state.datasetTest.isReachable;
export const selectTestInputs = (state: RootState) => {
const { currentDataset } = state.datasetTest;
return currentDataset
? state.datasetTest.testInputs[currentDataset.fides_key] || {}
: {};
};
export const selectCurrentPolicyKey = (state: RootState) =>
state.datasetTest.currentPolicyKey;
export const selectTestResults = (state: RootState) => {
const { currentDataset } = state.datasetTest;
return currentDataset
Expand Down
Loading

0 comments on commit 1f440a2

Please sign in to comment.