From ecf82c5d704400cb11077891ebbe54e9c10bd099 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Thu, 5 Dec 2024 16:17:12 -0700 Subject: [PATCH 1/5] Embedded CMP Layer 2 example page (#5564) --- CHANGELOG.md | 3 +- clients/sample-app/README.md | 7 ++ .../sample-app/src/pages/embedded-consent.tsx | 99 +++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 clients/sample-app/src/pages/embedded-consent.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a29e3703b..67b323801c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ The types of changes are: ## [Unreleased](https://github.com/ethyca/fides/compare/2.51.0...main) - +### Added +- New page in the Cookie House sample app to demonstrate the use of embedding the FidesJS SDK on the page [#5564](https://github.com/ethyca/fides/pull/5564) diff --git a/clients/sample-app/README.md b/clients/sample-app/README.md index 9142c14617..73d161ae94 100644 --- a/clients/sample-app/README.md +++ b/clients/sample-app/README.md @@ -31,6 +31,13 @@ This will automatically bring up a Docker Compose project to create a sample app Once running successfully, open http://localhost:3000 to see the Cookie House! +Note: If you are already running a database on port 5432 locally, you can override the default port by setting the `FIDES_SAMPLE_APP__DATABASE_PORT` environment variable and ALSO changing the **host** port number in the `docker-compose.yml` file. For example: + +```yaml +ports: + - "5433:5432" +``` + ## Pre-commit Before committing any changes, run the following: diff --git a/clients/sample-app/src/pages/embedded-consent.tsx b/clients/sample-app/src/pages/embedded-consent.tsx new file mode 100644 index 0000000000..8700b8ce3e --- /dev/null +++ b/clients/sample-app/src/pages/embedded-consent.tsx @@ -0,0 +1,99 @@ +import { GetServerSideProps } from "next"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import Script from "next/script"; + +interface Props { + gtmContainerId: string | null; + privacyCenterUrl: string; +} + +// Regex to ensure the provided GTM container ID appears valid (e.g. "GTM-ABCD123") +// NOTE: this also protects against XSS since this ID is added to a script template +const VALID_GTM_REGEX = /^[0-9a-zA-Z-]+$/; + +/** + * Pass the following server-side ENV variables to the page: + * - FIDES_SAMPLE_APP__GOOGLE_TAG_MANAGER_CONTAINER_ID: configure a GTM container, e.g. "GTM-ABCD123" + * - FIDES_SAMPLE_APP__PRIVACY_CENTER_URL: configure Privacy Center URL, e.g. "http://localhost:3001" + */ +export const getServerSideProps: GetServerSideProps = async () => { + // Check for a valid FIDES_SAMPLE_APP__GOOGLE_TAG_MANAGER_CONTAINER_ID + let gtmContainerId = null; + if ( + process.env.FIDES_SAMPLE_APP__GOOGLE_TAG_MANAGER_CONTAINER_ID?.match( + VALID_GTM_REGEX, + ) + ) { + gtmContainerId = + process.env.FIDES_SAMPLE_APP__GOOGLE_TAG_MANAGER_CONTAINER_ID; + } + + // Check for a valid FIDES_SAMPLE_APP__PRIVACY_CENTER_URL + const privacyCenterUrl = + process.env.FIDES_SAMPLE_APP__PRIVACY_CENTER_URL || "http://localhost:3001"; + + // Pass the server-side props to the page + return { props: { gtmContainerId, privacyCenterUrl } }; +}; + +const IndexPage = ({ gtmContainerId, privacyCenterUrl }: Props) => { + // Load the fides.js script from the Fides Privacy Center, assumed to be + // running at http://localhost:3001 + const fidesScriptTagUrl = new URL(`${privacyCenterUrl}/fides.js`); + const router = useRouter(); + // eslint-disable-next-line @typescript-eslint/naming-convention + const { geolocation, property_id } = router.query; + + // If `geolocation=` or `property_id` query params exists, pass those along to the fides.js fetch + if (geolocation && typeof geolocation === "string") { + fidesScriptTagUrl.searchParams.append("geolocation", geolocation); + } + if (typeof property_id === "string") { + fidesScriptTagUrl.searchParams.append("property_id", property_id); + } + + return ( + <> + + Cookie House + {/* Require FidesJS to "embed" it's UI onto the page, instead of as an overlay over the itself. (see https://ethyca.com/docs/dev-docs/js/reference/interfaces/FidesOptions#fides_embed) */} + + {/* Allow the embedded consent modal to fill the screen */} + + + {/** + Insert the fides.js script and run the GTM integration once ready + DEFER: using "beforeInteractive" here triggers a lint warning from NextJS + as it should only be used in the _document.tsx file. This still works and + ensures that fides.js fires earlier than other scripts, but isn't a best + practice. + */} + + ) : null} +
+ + ); +}; + +export default IndexPage; From ca8e8470f38218fd95d448e1cf643e1e9a777b1d Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Thu, 5 Dec 2024 15:26:49 -0800 Subject: [PATCH 2/5] Removing unnecessary integration-external mark (#5565) --- tests/ops/api/v1/endpoints/test_dataset_test_endpoints.py | 2 +- .../ops/api/v1/endpoints/test_privacy_request_endpoints.py | 2 +- tests/ops/service/dataset/test_dataset_service.py | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/ops/api/v1/endpoints/test_dataset_test_endpoints.py b/tests/ops/api/v1/endpoints/test_dataset_test_endpoints.py index c5056aec61..bbb2154766 100644 --- a/tests/ops/api/v1/endpoints/test_dataset_test_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_dataset_test_endpoints.py @@ -194,7 +194,7 @@ def test_dataset_reachability( assert set(response.json().keys()) == {"reachable", "details"} -@pytest.mark.integration_external +@pytest.mark.integration @pytest.mark.integration_postgres class TestDatasetTest: @pytest.fixture(scope="function") diff --git a/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py b/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py index 5ddbcd8f60..21007687ec 100644 --- a/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py @@ -8364,7 +8364,7 @@ def test_get_access_results_contributor_but_disabled( assert response.status_code == 403 -@pytest.mark.integration_external +@pytest.mark.integration @pytest.mark.integration_postgres class TestPrivacyRequestFilteredResults: @pytest.fixture(scope="function") diff --git a/tests/ops/service/dataset/test_dataset_service.py b/tests/ops/service/dataset/test_dataset_service.py index c1e56f8d01..ca433671cf 100644 --- a/tests/ops/service/dataset/test_dataset_service.py +++ b/tests/ops/service/dataset/test_dataset_service.py @@ -111,9 +111,13 @@ def test_get_identities_and_references( assert required_identities == expected_required_identities -@pytest.mark.integration_external +@pytest.mark.integration @pytest.mark.integration_postgres class TestRunTestAccessRequest: + """ + Run test requests against the postgres_example database + """ + @pytest.mark.usefixtures("postgres_integration_db") def test_run_test_access_request( self, From bd7bd4bfcc2f4a4a3bf72514c63621ef2025c0d2 Mon Sep 17 00:00:00 2001 From: Andres Torres Date: Fri, 6 Dec 2024 10:08:17 -0600 Subject: [PATCH 3/5] Hj-298 - Fix tests/ctl/core/test_api.py::TestSystemList::test_vendor_deleted_systems test (#5570) --- tests/ctl/core/test_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/ctl/core/test_api.py b/tests/ctl/core/test_api.py index bfca14dd84..1a47e32b80 100644 --- a/tests/ctl/core/test_api.py +++ b/tests/ctl/core/test_api.py @@ -1669,9 +1669,9 @@ def test_list_with_pagination_and_multiple_filters_2( "vendor_deleted_date, expected_systems_count, show_deleted", [ (datetime.now() - timedelta(days=1), 1, True), - (datetime.now() - timedelta(days=1), 0, None), - (datetime.now() + timedelta(days=1), 1, None), - (None, 1, None), + (datetime.now() - timedelta(days=1), 0, False), + (datetime.now() + timedelta(days=1), 1, False), + (None, 1, False), ], ) def test_vendor_deleted_systems( @@ -1691,13 +1691,13 @@ def test_vendor_deleted_systems( url=test_config.cli.server_url, headers=test_config.user.auth_header, resource_type="system", - query_params={"show_deleted": True} if show_deleted else {}, + query_params={"show_deleted": show_deleted, "size": 50}, ) assert result.status_code == 200 result_json = result.json() - assert len(result_json) == expected_systems_count + assert len(result_json["items"]) == expected_systems_count @pytest.mark.unit From 3f0c29639d8d94a706f57122951f4b4b9f993e31 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Fri, 6 Dec 2024 09:31:13 -0800 Subject: [PATCH 4/5] Escape datetime and ObjectId values in test privacy results (#5567) --- .../v1/endpoints/privacy_request_endpoints.py | 9 +++- .../test_privacy_request_endpoints.py | 49 +++++++++++++++++-- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/fides/api/api/v1/endpoints/privacy_request_endpoints.py b/src/fides/api/api/v1/endpoints/privacy_request_endpoints.py index c8dc17a273..84cb453a63 100644 --- a/src/fides/api/api/v1/endpoints/privacy_request_endpoints.py +++ b/src/fides/api/api/v1/endpoints/privacy_request_endpoints.py @@ -2,6 +2,7 @@ import csv import io +import json from collections import defaultdict from datetime import datetime from typing import ( @@ -148,6 +149,7 @@ from fides.api.util.enums import ColumnSort from fides.api.util.fuzzy_search_utils import get_decrypted_identities_automaton from fides.api.util.logger import Pii +from fides.api.util.storage_util import storage_json_encoder from fides.common.api.scope_registry import ( PRIVACY_REQUEST_CALLBACK_RESUME, PRIVACY_REQUEST_CREATE, @@ -2657,8 +2659,13 @@ def get_test_privacy_request_results( ) privacy_request.save(db=db) + # Escape datetime and ObjectId values + raw_data = privacy_request.get_raw_access_results() + escaped_json = json.dumps(raw_data, indent=2, default=storage_json_encoder) + escaped_data = json.loads(escaped_json) + return { "privacy_request_id": privacy_request.id, "status": privacy_request.status, - "results": privacy_request.get_raw_access_results(), + "results": escaped_data, } diff --git a/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py b/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py index 21007687ec..aaf64d4e7b 100644 --- a/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_privacy_request_endpoints.py @@ -8365,7 +8365,6 @@ def test_get_access_results_contributor_but_disabled( @pytest.mark.integration -@pytest.mark.integration_postgres class TestPrivacyRequestFilteredResults: @pytest.fixture(scope="function") def default_access_policy(self, db) -> None: @@ -8427,15 +8426,18 @@ def test_filtered_results_with_roles( ) assert response.status_code == expected_status + @pytest.mark.integration_postgres @pytest.mark.usefixtures("default_access_policy", "postgres_integration_db") - def test_filtered_results( + def test_filtered_results_postgres( self, connection_config, - dataset_config, + postgres_example_test_dataset_config, api_client: TestClient, generate_auth_header, ) -> None: - dataset_url = get_connection_dataset_url(connection_config, dataset_config) + dataset_url = get_connection_dataset_url( + connection_config, postgres_example_test_dataset_config + ) auth_header = generate_auth_header(scopes=[DATASET_TEST]) response = api_client.post( dataset_url + "/test", @@ -8459,3 +8461,42 @@ def test_filtered_results( "status", "results", } + + @pytest.mark.integration_mongo + @pytest.mark.usefixtures("default_access_policy") + def test_filtered_results_mongo( + self, + mongo_connection_config, + mongo_dataset_config, + api_client: TestClient, + generate_auth_header, + ) -> None: + dataset_url = get_connection_dataset_url( + mongo_connection_config, mongo_dataset_config + ) + auth_header = generate_auth_header(scopes=[DATASET_TEST]) + response = api_client.post( + dataset_url + "/test", + headers=auth_header, + json={ + "email": "employee-1@example.com", + "postgres_example_test_dataset:customer:id": 1, + }, + ) + assert response.status_code == HTTP_200_OK + + privacy_request_id = response.json()["privacy_request_id"] + url = V1_URL_PREFIX + PRIVACY_REQUEST_FILTERED_RESULTS.format( + privacy_request_id=privacy_request_id + ) + auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_READ_ACCESS_RESULTS]) + response = api_client.get( + url, + headers=auth_header, + ) + assert response.status_code == HTTP_200_OK + assert set(response.json().keys()) == { + "privacy_request_id", + "status", + "results", + } From 05cf5efa6ebbf7a63586ad255036e4011ba02769 Mon Sep 17 00:00:00 2001 From: jpople Date: Fri, 6 Dec 2024 13:03:36 -0600 Subject: [PATCH 5/5] Fix Fides brand link position on small screens (#5572) --- CHANGELOG.md | 1 + .../privacy-center/components/BrandLink.tsx | 40 ++++++++++++------ clients/privacy-center/components/Layout.tsx | 15 +------ .../consent/ConfigDrivenConsent.tsx | 2 + .../notice-driven/NoticeDrivenConsent.tsx | 6 ++- clients/privacy-center/pages/index.tsx | 42 ++++++++++++------- 6 files changed, 65 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b323801c..db499e3ed4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ The types of changes are: - Updating dataset PUT to allow deleting all datasets [#5524](https://github.com/ethyca/fides/pull/5524) - Adds support for fides_key generation when parent_key is provided in Taxonomy create endpoints [#5542](https://github.com/ethyca/fides/pull/5542) - An integration will no longer re-enable after saving the connection form [#5555](https://github.com/ethyca/fides/pull/5555) +- Fixed positioning of Fides brand link in privacy center [#5572](https://github.com/ethyca/fides/pull/5572) ### Removed - Removed unnecessary debug logging from the load_file config helper [#5544](https://github.com/ethyca/fides/pull/5544) diff --git a/clients/privacy-center/components/BrandLink.tsx b/clients/privacy-center/components/BrandLink.tsx index c069ed57f6..15976ceb06 100644 --- a/clients/privacy-center/components/BrandLink.tsx +++ b/clients/privacy-center/components/BrandLink.tsx @@ -1,16 +1,32 @@ import { EthycaLogo, Link, LinkProps } from "fidesui"; -const BrandLink = (props: LinkProps) => ( - - Powered by - -); +import { useSettings } from "~/features/common/settings.slice"; + +const BrandLink = ({ + position = "absolute", + right = 6, + ...props +}: LinkProps) => { + const { SHOW_BRAND_LINK } = useSettings(); + + if (!SHOW_BRAND_LINK) { + return null; + } + + return ( + + Powered by + + ); +}; export default BrandLink; diff --git a/clients/privacy-center/components/Layout.tsx b/clients/privacy-center/components/Layout.tsx index 9565fc7f20..e1e73a498b 100644 --- a/clients/privacy-center/components/Layout.tsx +++ b/clients/privacy-center/components/Layout.tsx @@ -2,16 +2,13 @@ import { Flex } from "fidesui"; import Head from "next/head"; import React, { ReactNode } from "react"; -import BrandLink from "~/components/BrandLink"; import Logo from "~/components/Logo"; import { useConfig } from "~/features/common/config.slice"; -import { useSettings } from "~/features/common/settings.slice"; import { useStyles } from "~/features/common/styles.slice"; const Layout = ({ children }: { children: ReactNode }) => { const config = useConfig(); const styles = useStyles(); - const { SHOW_BRAND_LINK } = useSettings(); return ( <> @@ -32,17 +29,7 @@ const Layout = ({ children }: { children: ReactNode }) => { -
- {children} - {SHOW_BRAND_LINK && ( - - )} -
+
{children}
); }; diff --git a/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx b/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx index f59e7bb4e3..3808605783 100644 --- a/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx +++ b/clients/privacy-center/components/consent/ConfigDrivenConsent.tsx @@ -14,6 +14,7 @@ import { useAppDispatch, useAppSelector } from "~/app/hooks"; import { inspectForBrowserIdentities } from "~/common/browser-identities"; import { useLocalStorage } from "~/common/hooks"; import { ErrorToastOptions, SuccessToastOptions } from "~/common/toast-options"; +import BrandLink from "~/components/BrandLink"; import { useConfig } from "~/features/common/config.slice"; import { changeConsent, @@ -209,6 +210,7 @@ const ConfigDrivenConsent = ({ cancelLabel="Cancel" saveLabel="Save" /> + ); }; diff --git a/clients/privacy-center/components/consent/notice-driven/NoticeDrivenConsent.tsx b/clients/privacy-center/components/consent/notice-driven/NoticeDrivenConsent.tsx index d8765f587c..bd24228af2 100644 --- a/clients/privacy-center/components/consent/notice-driven/NoticeDrivenConsent.tsx +++ b/clients/privacy-center/components/consent/notice-driven/NoticeDrivenConsent.tsx @@ -25,6 +25,7 @@ import { inspectForBrowserIdentities } from "~/common/browser-identities"; import { useLocalStorage } from "~/common/hooks"; import useI18n from "~/common/hooks/useI18n"; import { ErrorToastOptions, SuccessToastOptions } from "~/common/toast-options"; +import BrandLink from "~/components/BrandLink"; import { useProperty } from "~/features/common/property.slice"; import { selectPrivacyExperience, @@ -400,7 +401,10 @@ const NoticeDrivenConsent = ({ base64Cookie }: { base64Cookie: boolean }) => { onCancel={handleCancel} justifyContent="center" /> - + + + + ); }; diff --git a/clients/privacy-center/pages/index.tsx b/clients/privacy-center/pages/index.tsx index 287a0b8f91..e7c21b22fa 100644 --- a/clients/privacy-center/pages/index.tsx +++ b/clients/privacy-center/pages/index.tsx @@ -13,6 +13,7 @@ import React, { useEffect, useState } from "react"; import { useAppDispatch, useAppSelector } from "~/app/hooks"; import { ConfigErrorToastOptions } from "~/common/toast-options"; +import BrandLink from "~/components/BrandLink"; import ConsentCard from "~/components/consent/ConsentCard"; import { ConsentRequestModal, @@ -25,7 +26,10 @@ import { } from "~/components/modals/privacy-request-modal/PrivacyRequestModal"; import PrivacyCard from "~/components/PrivacyCard"; import { useConfig } from "~/features/common/config.slice"; -import { selectIsNoticeDriven } from "~/features/common/settings.slice"; +import { + selectIsNoticeDriven, + useSettings, +} from "~/features/common/settings.slice"; import { clearLocation, selectPrivacyExperience, @@ -68,6 +72,10 @@ const Home: NextPage = () => { let isConsentModalOpen = isConsentModalOpenConst; const getIdVerificationConfigQuery = useGetIdVerificationConfigQuery(); + const { SHOW_BRAND_LINK } = useSettings(); + const showPrivacyPolicyLink = + !!config.privacy_policy_url && !!config.privacy_policy_url_text; + // Subscribe to experiences just to see if there are any notices. // The subscription automatically handles skipping if overlay is not enabled useSubscribeToPrivacyExperienceQuery(); @@ -214,19 +222,25 @@ const Home: NextPage = () => { {paragraph} ))} - {config.privacy_policy_url && config.privacy_policy_url_text ? ( - - {config.privacy_policy_url_text} - - ) : null} + + {(SHOW_BRAND_LINK || showPrivacyPolicyLink) && ( + + {showPrivacyPolicyLink && ( + + {config.privacy_policy_url_text} + + )} + + + )}