Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle decoding problems with passed in fides_string #4350

Merged
merged 12 commits into from
Oct 31, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The types of changes are:
- Bug where not all system forms would appear to save when used with Compass [#4347](https://github.com/ethyca/fides/pull/4347)
- Restrict TCF Privacy Experience Config if TCF is disabled [#4348](https://github.com/ethyca/fides/pull/4348)
- Handle invalid `fides_string` when passed in as an override [#4350](https://github.com/ethyca/fides/pull/4350)
- Removes overflow styling for embedded modal in Fides.js [#4345](https://github.com/ethyca/fides/pull/4345)

### Changed
- Derive cookie storage info, privacy policy and legitimate interest disclosure URLs, and data retention data from the data map instead of directly from gvl.json [#4286](https://github.com/ethyca/fides/pull/4286)
Expand Down
98 changes: 77 additions & 21 deletions clients/fides-js/src/fides-tcf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
import type { Fides } from "./lib/initialize";
import { dispatchFidesEvent } from "./lib/events";
import {
buildTcfEntitiesFromCookie,
debugLog,
experienceIsValid,
FidesCookie,
Expand Down Expand Up @@ -121,13 +122,43 @@ const getInitialPreference = (
return tcfObject.default_preference ?? UserConsentPreference.OPT_OUT;
};

const updateCookie = async (
oldCookie: FidesCookie,
experience: PrivacyExperience
): Promise<FidesCookie> => {
// ignore server-side prefs if either user has no prefs to override, or TC str override is set
const updateCookieAndExperience = async ({
cookie,
experience,
debug = false,
hasFidesStringOverride,
isExperienceClientSideFetched,
}: {
cookie: FidesCookie;
experience: PrivacyExperience;
debug?: boolean;
hasFidesStringOverride: boolean;
isExperienceClientSideFetched: boolean;
}): Promise<{
cookie: FidesCookie;
experience: Partial<PrivacyExperience>;
}> => {
// If string override exists, the cookie is already okay, but the experience may not be.
allisonking marked this conversation as resolved.
Show resolved Hide resolved
if (hasFidesStringOverride) {
// If we didn't get the experience until the client side fetch, we need to update the fetched
// experience based on the cookie here.
if (isExperienceClientSideFetched) {
allisonking marked this conversation as resolved.
Show resolved Hide resolved
debugLog(
debug,
"Overriding preferences from client-side fetched experience with cookie fides_string consent",
cookie.fides_string
);
const tcfEntities = buildTcfEntitiesFromCookie(experience, cookie);
return { cookie, experience: tcfEntities };
}
// If it's not client side fetched, we don't update anything since the cookie has already
// been updated earlier.
return { cookie, experience };
}

// ignore server-side prefs if user has no prefs to override
allisonking marked this conversation as resolved.
Show resolved Hide resolved
if (!hasSavedTcfPreferences(experience)) {
return { ...oldCookie, fides_string: "" };
return { cookie: { ...cookie, fides_string: "" }, experience };
}

const tcSavePrefs: TcfSavePreferences = {};
Expand Down Expand Up @@ -165,7 +196,38 @@ const updateCookie = async (
tcStringPreferences: enabledIds,
});
const tcfConsent = transformTcfPreferencesToCookieKeys(tcSavePrefs);
return { ...oldCookie, fides_string: fidesString, tcf_consent: tcfConsent };
return {
cookie: { ...cookie, fides_string: fidesString, tcf_consent: tcfConsent },
experience,
};
};

/**
* If a fidesString is explicitly passed in, we override the associated cookie props, which are then
* used to override associated props in the experience.
*/
const updateFidesCookieFromString = (
allisonking marked this conversation as resolved.
Show resolved Hide resolved
cookie: FidesCookie,
fidesString: string,
debug: boolean
): { cookie: FidesCookie; success: boolean } => {
debugLog(
debug,
"Explicit fidesString detected. Proceeding to override all TCF preferences with given fidesString"
);
try {
const cookieKeys = transformFidesStringToCookieKeys(fidesString, debug);
return {
cookie: { ...cookie, tcf_consent: cookieKeys, fides_string: fidesString },
success: true,
};
} catch (error) {
debugLog(
debug,
`Could not decode tcString from ${fidesString}, it may be invalid. ${error}`
);
return { cookie, success: false };
}
};

/**
Expand All @@ -177,24 +239,17 @@ const init = async (config: FidesConfig) => {
// eslint-disable-next-line no-param-reassign
config.options = { ...config.options, ...overrideOptions };
const cookie = getInitialCookie(config);
let hasFidesStringOverride = !!config.options.fidesString;
if (config.options.fidesString) {
// If a fidesString is explicitly passed in, we override the associated cookie props, which are then used to
// override associated props in experience
debugLog(
config.options.debug,
"Explicit fidesString detected. Proceeding to override all TCF preferences with given fidesString"
);
const { cookieKeys, success } = transformFidesStringToCookieKeys(
const { cookie: updatedCookie, success } = updateFidesCookieFromString(
cookie,
config.options.fidesString,
config.options.debug
);
if (!success) {
// Treat an unsuccessful transform as if no fidesString were passed in.
// eslint-disable-next-line no-param-reassign
config.options.fidesString = null;
if (success) {
Object.assign(cookie, updatedCookie);
} else {
cookie.tcf_consent = cookieKeys;
cookie.fides_string = config.options.fidesString;
hasFidesStringOverride = false;
allisonking marked this conversation as resolved.
Show resolved Hide resolved
}
} else if (
tcfConsentCookieObjHasSomeConsentSet(cookie.tcf_consent) &&
Expand Down Expand Up @@ -228,7 +283,8 @@ const init = async (config: FidesConfig) => {
cookie,
experience,
renderOverlay,
updateCookie,
updateCookieAndExperience: (props) =>
allisonking marked this conversation as resolved.
Show resolved Hide resolved
updateCookieAndExperience({ ...props, hasFidesStringOverride }),
});
Object.assign(_Fides, updatedFides);

Expand Down
10 changes: 7 additions & 3 deletions clients/fides-js/src/fides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ const updateCookie = async (
oldCookie: FidesCookie,
experience: PrivacyExperience,
debug?: boolean
): Promise<FidesCookie> => {
): Promise<{ cookie: FidesCookie; experience: PrivacyExperience }> => {
const context = getConsentContext();
const consent = buildCookieConsentForExperiences(
experience,
context,
!!debug
);
return { ...oldCookie, consent };
return { cookie: { ...oldCookie, consent }, experience };
};

/**
Expand All @@ -122,7 +122,11 @@ const init = async (config: FidesConfig) => {
cookie,
experience,
renderOverlay,
updateCookie,
updateCookieAndExperience: ({
cookie: oldCookie,
experience: effectiveExperience,
debug,
}) => updateCookie(oldCookie, effectiveExperience, debug),
});
Object.assign(_Fides, updatedFides);

Expand Down
63 changes: 28 additions & 35 deletions clients/fides-js/src/lib/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { meta } from "../integrations/meta";
import { shopify } from "../integrations/shopify";
import { getConsentContext } from "./consent-context";
import {
buildTcfEntitiesFromCookie,
CookieIdentity,
CookieKeyConsent,
CookieMeta,
Expand Down Expand Up @@ -259,15 +258,28 @@ export const initialize = async ({
experience,
geolocation,
renderOverlay,
updateCookie,
updateCookieAndExperience,
}: {
cookie: FidesCookie;
renderOverlay: (props: OverlayProps, parent: ContainerNode) => void;
updateCookie: (
oldCookie: FidesCookie,
experience: PrivacyExperience,
debug?: boolean
) => Promise<FidesCookie>;
/**
* Once we for sure have a valid experience, this is another chance to update values
* before the overlay renders.
*/
updateCookieAndExperience: ({
cookie,
experience,
debug,
isExperienceClientSideFetched,
}: {
cookie: FidesCookie;
experience: PrivacyExperience;
debug?: boolean;
isExperienceClientSideFetched: boolean;
}) => Promise<{
cookie: FidesCookie;
experience: Partial<PrivacyExperience>;
}>;
} & FidesConfig): Promise<Partial<Fides>> => {
let shouldInitOverlay: boolean = options.isOverlayEnabled;
let effectiveExperience = experience;
Expand Down Expand Up @@ -312,34 +324,15 @@ export const initialize = async ({
isPrivacyExperience(effectiveExperience) &&
experienceIsValid(effectiveExperience, options)
) {
if (options.fidesString) {
if (fetchedClientSideExperience) {
// if tc str was explicitly passed in, we need to override the client-side-fetched experience with consent from the cookie
// we don't update cookie because it already has been overridden by the injected fidesString
debugLog(
options.debug,
"Overriding preferences from client-side fetched experience with cookie fides_string consent",
cookie.fides_string
);
const tcfEntities = buildTcfEntitiesFromCookie(
effectiveExperience,
cookie
);
Object.assign(effectiveExperience, tcfEntities);
}
} else {
const updatedCookie = await updateCookie(
cookie,
effectiveExperience,
options.debug
);
debugLog(
options.debug,
"Updated cookie based on experience",
updatedCookie
);
Object.assign(cookie, updatedCookie);
}
const updated = await updateCookieAndExperience({
cookie,
experience: effectiveExperience,
debug: options.debug,
isExperienceClientSideFetched: fetchedClientSideExperience,
});
debugLog(options.debug, "Updated cookie and experience", updated);
Object.assign(cookie, updated.cookie);
Object.assign(effectiveExperience, updated.experience);
if (shouldInitOverlay) {
await initOverlay({
experience: effectiveExperience,
Expand Down
64 changes: 29 additions & 35 deletions clients/fides-js/src/lib/tcf/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,38 @@ import { decodeFidesString, idsFromAcString } from "./fidesString";
export const transformFidesStringToCookieKeys = (
fidesString: string,
debug: boolean
): { cookieKeys: TcfCookieConsent; success: boolean } => {
): TcfCookieConsent => {
const { tc: tcString, ac: acString } = decodeFidesString(fidesString);
const tcModel: TCModel = TCString.decode(tcString);

const cookieKeys: TcfCookieConsent = {};
try {
const tcModel: TCModel = TCString.decode(tcString);
// map tc model key to cookie key
TCF_KEY_MAP.forEach(({ tcfModelKey, cookieKey }) => {
if (tcfModelKey) {
const items: TcfCookieKeyConsent = {};
(tcModel[tcfModelKey] as Vector).forEach((consented, id) => {
items[id] = consented;
});
cookieKeys[cookieKey] = items;
}
});

// Set AC consents, which will only be on vendor_consents
const acIds = idsFromAcString(acString, debug);
acIds.forEach((acId) => {
if (!cookieKeys.vendor_consent_preferences) {
cookieKeys.vendor_consent_preferences = { [acId]: true };
} else {
cookieKeys.vendor_consent_preferences[acId] = true;
}
});
debugLog(
debug,
`Generated cookie.tcf_consent from explicit fidesString.`,
cookieKeys
);
return { cookieKeys, success: true };
} catch (error) {
debugLog(
debug,
`Could not decode tcString ${tcString}, it may be invalid. ${error}`
);
return { cookieKeys, success: false };
}
// map tc model key to cookie key
TCF_KEY_MAP.forEach(({ tcfModelKey, cookieKey }) => {
if (tcfModelKey) {
const items: TcfCookieKeyConsent = {};
(tcModel[tcfModelKey] as Vector).forEach((consented, id) => {
items[id] = consented;
});
cookieKeys[cookieKey] = items;
}
});

// Set AC consents, which will only be on vendor_consents
const acIds = idsFromAcString(acString, debug);
acIds.forEach((acId) => {
if (!cookieKeys.vendor_consent_preferences) {
cookieKeys.vendor_consent_preferences = { [acId]: true };
} else {
cookieKeys.vendor_consent_preferences[acId] = true;
}
});
debugLog(
debug,
`Generated cookie.tcf_consent from explicit fidesString.`,
cookieKeys
);
return cookieKeys;
};

export const generateFidesStringFromCookieTcfConsent = async (
Expand Down
Loading