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

[Backend] Privacy Notice Templates #3120

Closed
wants to merge 4 commits into from

Conversation

pattisdr
Copy link
Contributor

@pattisdr pattisdr commented Apr 21, 2023

Closes #2833

Code Changes

  • Add a new PrivacyNoticeTemplate table to store our out of the box templates. These are not the notices surfaced to the users.
  • Add an optional FK from PrivacyNoticeHistory and PrivacyNotice back to PrivacyNoticeTemplate, if the notice was, in fact, created from a template
  • Add a yaml file data/privacy_notices/privacy_notice_templates.yml for defining the out of the box notices. Give each template an id ahead of time so we can upsert.
  • Add some new regions
  • Extract the code in the existing endpoint to create privacy notices/update privacy notices to use for upserting templates creating privacy notices on startup.

Steps to Confirm

  • Bring up the server with nox -s dev
  • Issue an API call to get privacy notices GET {{host}}/privacy-notice
  • Make invalid changes to privacy_notice_templates.yml (such as a bad data use, or duplicate data uses, or an invalid consent mechanism). Stop the server and then restart. Verify you get validation errors in the logs but server still comes up.
  • Revert the bad updates. Update a yaml. Restart the server. Verify the template is updated but it's corresponding privacy notice is not.

Pre-Merge Checklist

Description Of Changes

Start fides with an initial set of privacy notices. Create a yaml file to define them and a separate table to store the templates. On startup, upsert the templates into the PrivacyNoticeTemplate table (the templates have ids already in the yaml). Then if the templates are new, create corresponding PrivacyNotice and PrivacyNoticeHistory records. Otherwise don't touch those tables.

Note that templates are not what's shown to users, it's the enabled notices in the PrivacyNotice table. But this action lets us give customers a starting set of notices out of the box.

Adds what templates I can from: https://ethyca.atlassian.net/wiki/spaces/PM/pages/2656108545/Privacy+Notice+Templates

…vacyNotice and PrivacyNoticeHistory back to those templates.

- Define a yaml with the default privacy notices and add a separate schema migration to populate all three tables
- Split out the code from the endpoint to create privacy notices to also use to create templates and initial notices
Comment on lines +5 to +32
regions:
- eu_be
- eu_bg
- eu_cz
- eu_dk
- eu_de
- eu_ee
- eu_ie
- eu_el
- eu_es
- eu_fr
- eu_hr
- eu_it
- eu_cy
- eu_lv
- eu_lt
- eu_lu
- eu_hu
- eu_mt
- eu_nl
- eu_at
- eu_pl
- eu_pt
- eu_ro
- eu_si
- eu_sk
- eu_fi
- eu_se
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently listing all the EU regions out individually, let's double check this is how we want to define this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

- eu_se
consent_mechanism: notice_only
data_uses:
- provide
Copy link
Contributor Author

@pattisdr pattisdr Apr 21, 2023

Choose a reason for hiding this comment

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

Our current validation doesn't allow data uses in the same hierarchy to be used on the same region, including if they are on the same notice.

This is defined in JIRA as being both provide and provide.service. I've added just provide here. Alternatively, we could change the validation, but I also think provide.service would be irrelevant if provide was also added here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

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

@pattisdr This sounds correct, and we will get the templated notices updated ASAP. One other question though: Can we still use two different child data uses from the same hierarchy in the same region? For example, could I create two notices for CA that each had one of these uses: advertising.first_party.personalized and advertising.third_party.personalized ?

Copy link
Contributor Author

@pattisdr pattisdr Apr 25, 2023

Choose a reason for hiding this comment

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

@mfbrown not currently no, an advertising.first_party.personalized for CA and an advertising.third_party.personalized for CA would be disallowed.It's pretty strict at the moment. I can look into allowing this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @mfbrown I've reticketed this here: advertising.third_party.personalized

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I was wrong, it does work currently, so I'm just adding more tests for this.

Comment on lines +64 to +77
op.create_foreign_key(
"privacynotice_origin_fkey",
"privacynotice",
"privacynoticetemplate",
["origin"],
["id"],
)
op.create_foreign_key(
"privacynoticehistory_origin_fkey",
"privacynoticehistory",
"privacynoticetemplate",
["origin"],
["id"],
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know if this really needs to be a FK to the template. The PrivacyNoticeTemplate table might be a temporary solution, but I think it's easier to add it up front and remove it later, then to handle the reverse.

Comment on lines 208 to 209
existing_notices = (
PrivacyNotice.query(db).filter(PrivacyNotice.disabled.is_(False)).all()
)

new_notices = [
PrivacyNotice(**privacy_notice.dict(exclude_unset=True))
for privacy_notice in privacy_notices
]
try:
check_conflicting_data_uses(new_notices, existing_notices)
except ValidationError as e:
raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY, detail=e.message)

return [
PrivacyNotice.create(
db=db, data=privacy_notice.dict(exclude_unset=True), check_name=False
)
for privacy_notice in privacy_notices
]
return create_privacy_notices_util(db, privacy_notices, PrivacyNotice) # type: ignore[return-value]
Copy link
Contributor Author

@pattisdr pattisdr Apr 21, 2023

Choose a reason for hiding this comment

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

I move all this into create_privacy_notices_util so I can share all this nice validation when populating the templates and then the notices from those templates. The edge cases are well-tested in the endpoint so I just add a light pass of tests on the new method.

- eu_se
consent_mechanism: opt_in
data_uses:
- advertising.third_party.personalized
Copy link
Contributor Author

@pattisdr pattisdr Apr 21, 2023

Choose a reason for hiding this comment

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

Data uses are a required item for creating a privacy notice so I've had to omit all privacy notices without them - only ~8 are added here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

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

Makes sense. These will take their permanent shape with the Fideslang 1.4 update.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mfbrown are you sure we shouldn't wait to release this until Fideslang 1.4?

@cypress
Copy link

cypress bot commented Apr 21, 2023

Passing run #1503 ↗︎

0 3 0 0 Flakiness 0
⚠️ You've recorded test results over your free plan limit.
Upgrade your plan to view test results.

Details:

Merge b822303 into d39da31...
Project: fides Commit: 07bc62e051 ℹ️
Status: Passed Duration: 00:36 💡
Started: Apr 21, 2023 10:06 PM Ended: Apr 21, 2023 10:06 PM

This comment has been generated by cypress-bot as a result of this project's GitHub integration settings.

PrivacyNotices and PrivacyNoticeHistories from there"""
sessionlocal = get_db_session(CONFIG)
with sessionlocal() as session:
load_default_notices(session, DEFAULT_PRIVACY_NOTICES_PATH)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is another example of something that is problematic in ops unit testing due to #2016

@codecov
Copy link

codecov bot commented Apr 21, 2023

Codecov Report

Patch coverage: 100.00% and project coverage change: +0.06 🎉

Comparison is base (860a334) 87.54% compared to head (b822303) 87.60%.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3120      +/-   ##
==========================================
+ Coverage   87.54%   87.60%   +0.06%     
==========================================
  Files         307      308       +1     
  Lines       17734    17809      +75     
  Branches     2288     2296       +8     
==========================================
+ Hits        15525    15602      +77     
+ Misses       1793     1792       -1     
+ Partials      416      415       -1     
Impacted Files Coverage Δ
src/fides/api/main.py 80.11% <100.00%> (+1.10%) ⬆️
...i/ops/api/v1/endpoints/privacy_notice_endpoints.py 100.00% <100.00%> (ø)
src/fides/api/ops/db/base.py 100.00% <100.00%> (ø)
src/fides/api/ops/models/privacy_notice.py 100.00% <100.00%> (ø)
src/fides/api/ops/util/consent_util.py 100.00% <100.00%> (ø)

... and 4 files with indirect coverage changes

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

data_uses:
- improve.system
enforcement_level: frontend
disabled: False
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we load in as disabled by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@pattisdr pattisdr marked this pull request as ready for review April 21, 2023 13:15
@pattisdr pattisdr changed the title [WIP] Out of the box Privacy Notices [Backend] Privacy Notice Templates Apr 21, 2023
@pattisdr pattisdr marked this pull request as draft April 21, 2023 13:38
…he-box templates upfront.

On startup, upsert these templates. If the id doesn't exist, create a new template. If an id does exist, update its contents. If the template is new, create a PrivacyNotice and PrivacyNoticeHistory.
@pattisdr pattisdr marked this pull request as ready for review April 21, 2023 23:46
@pattisdr
Copy link
Contributor Author

@adamsachs I've mostly repurposed your logic from creating and updating privacy notices to be able to upsert privacy notice templates and then create privacy notices from those templates on startup. Let me know what you think!

Comment on lines +122 to +139
def dry_update(self, *, data: dict[str, Any]) -> PrivacyNoticeBase:
"""
A utility method to get an updated object without saving it to the db.

This is used to see what an object update would look like, in memory,
without actually persisting the update to the db
"""
# Update our attributes with values in data
cloned_attributes = self.__dict__.copy()
for key, val in data.items():
cloned_attributes[key] = val

# remove protected fields from the cloned dict
cloned_attributes.pop("_sa_instance_state")

# create a new object with the updated attribute data to keep this
# ORM object (i.e., `self`) pristine
return self.__class__(**cloned_attributes)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved @adamsachs's dry update here to share between updating templates and notices.

Comment on lines +242 to +266
def prepare_privacy_notice_patches(
privacy_notice_updates: List[PrivacyNoticeWithId],
db: Session,
model: Union[Type[PrivacyNotice], Type[PrivacyNoticeTemplate]],
allow_create: bool = False,
ignore_disabled: bool = True,
) -> List[
Tuple[PrivacyNoticeWithId, Optional[Union[PrivacyNotice, PrivacyNoticeTemplate]]]
]:
"""
Prepares PrivacyNotice/Template creates and updates including performing data use
conflict validation on proposed changes.

Returns a list of tuples that have the PrivacyNotice update data (API schema) alongside
their associated existing PrivacyNotice db record that will be updated

:param privacy_notice_updates: List of privacy notice schemas with ids: appropriate
for editing PrivacyNotices or upserting PrivacyNoticeTemplates
:param db: Session
:param model: one of PrivacyNotice or PrivacyNoticeTemplate
:param allow_create: If True, this method can prepare data to be both created and updated. Otherwise,
this is just intended for updates and will fail if a record doesn't exist.
:param ignore_disabled: Should we skip checking disabled data uses?
:return:
"""
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adapted this method that @adamsachs already wrote for updating privacy notices. This can also handle upserting Privacy Notice templates, shared between both locations.

Comment on lines +220 to +239
def create_privacy_notices_util(
db: Session, privacy_notice_schemas: List[PrivacyNoticeCreation]
) -> List[PrivacyNotice]:
"""Performs validation before creating Privacy Notices and Privacy Notice History records"""
validate_notice_data_uses(privacy_notice_schemas, db) # type: ignore[arg-type]

existing_notices = PrivacyNotice.query(db).filter(PrivacyNotice.disabled.is_(False)).all() # type: ignore[attr-defined]

new_notices = [
PrivacyNotice(**privacy_notice.dict(exclude_unset=True))
for privacy_notice in privacy_notice_schemas
]
check_conflicting_data_uses(new_notices, existing_notices)

return [
PrivacyNotice.create(
db=db, data=privacy_notice.dict(exclude_unset=True), check_name=False
)
for privacy_notice in privacy_notice_schemas
]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved here from @adamsachs create privacy notices endpoint to share so we can add notices on startup as well.

Comment on lines +186 to +218
def validate_notice_data_uses(
privacy_notices: List[Union[PrivacyNoticeWithId, PrivacyNoticeCreation]],
db: Session,
) -> None:
"""
Ensures that all the provided `PrivacyNotice`s have valid data uses.
Raises a 422 HTTP exception if an unknown data use is found on any `PrivacyNotice`
"""
valid_data_uses = [data_use.fides_key for data_use in DataUse.query(db).all()]
try:
for privacy_notice in privacy_notices:
privacy_notice.validate_data_uses(valid_data_uses)
except ValueError as e:
raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))


def ensure_unique_ids(
privacy_notices: List[PrivacyNoticeWithId],
) -> None:
"""
Ensures that all the provided PrivacyNotices in the request body have unique ids
Raises a 422 HTTP exception if there is more than one PrivacyNotice with the same ID
"""
ids = set()
for privacy_notice in privacy_notices:
if privacy_notice.id not in ids:
ids.add(privacy_notice.id)
else:
raise HTTPException(
HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"More than one provided PrivacyNotice with ID {privacy_notice.id}.",
)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Likewise moved here from privacy notice endpoints -

@@ -0,0 +1,235 @@
privacy_notices:
- name: Essential
id: pri_51c85583-40ec-4257-95ef-db3eea860c52
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Felt like we needed to specify id's here so we can upsert these as needed in the future. I think this supports a future goal of being able to do a "fides push"-like command and update the existing privacy notice templates without having to release a new version of fides.

@pattisdr
Copy link
Contributor Author

@adamsachs No need to review this any longer, I'm moving this into blocked for now. The privacy notices data uses will change a lot with fideslang 1.4. Further I was only able to template a subset of privacy notices as several were missing.

I think this does lay out a way that we can add these templated privacy notices to the db and automatically populate starting privacy notices from there. Will need to get this updated again when we're ready to surface.

@pattisdr
Copy link
Contributor Author

I'm going to close this PR for now as it's going to get out of date quickly, but I'll borrow heavily from this when we're ready to add the privacy notices post- fideslang 1.4.

@pattisdr pattisdr closed this Apr 26, 2023
@NevilleS NevilleS deleted the fides_2833_default_privacy_notices branch May 16, 2024 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Backend Out of the Box Privacy Notices
2 participants