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] Add Cookies and Surface with Privacy Notices #3572

Merged
merged 28 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
22cd8aa
Initial commit - add a Cookies table with FK's to PrivacyDeclaration …
pattisdr Jun 14, 2023
bb4282d
Add optional path and domain to Cookies and allow upsert_cookies to u…
pattisdr Jun 15, 2023
29e2a2d
Surface relevant cookies on privacy notices by data use.
pattisdr Jun 15, 2023
f4a6936
Update fideslang version which removes cookies from System request, a…
pattisdr Jun 15, 2023
f5cca57
Remove the index from path and domain.
pattisdr Jun 15, 2023
1e8247c
Merge branch 'main' into fides_3478_system_cookies
pattisdr Jun 15, 2023
c510d1d
Merge main:
pattisdr Jun 16, 2023
561290e
Try to make tests more predictable.
pattisdr Jun 16, 2023
df5a462
Update changelog.
pattisdr Jun 16, 2023
a5b9125
Merge branch 'main' into fides_3478_system_cookies
pattisdr Jun 19, 2023
04ce44a
Add missing fixture.
pattisdr Jun 19, 2023
399ec19
Merge main, conflicts:
pattisdr Jun 20, 2023
9911bf2
Bump fides lang commit to see if organization relationship key findin…
pattisdr Jun 20, 2023
ba64204
Make history tests more reliable - there's no guarantee that these ar…
pattisdr Jun 20, 2023
47010a9
Bump fideslang commit
pattisdr Jun 20, 2023
3ee9f3a
fix: bump fideslang version for testing
ThomasLaPiana Jun 21, 2023
e134836
Merge branch 'main' into fides_3478_system_cookies
ThomasLaPiana Jun 21, 2023
805393e
fix: pin pydantic to a new version supported by fideslang
ThomasLaPiana Jun 21, 2023
02ac533
Merge main, conflicts:
pattisdr Jun 21, 2023
f597094
Try sorting declarations for repeatability in tests.
pattisdr Jun 21, 2023
c8493db
Data use cookie field (#3571)
allisonking Jun 21, 2023
2720914
`fides-js` and privacy center cookie enforcement (#3569)
allisonking Jun 21, 2023
a879831
More attempts to improve reliability of cookie tests
pattisdr Jun 21, 2023
16d9355
Fix new mypy errors.
pattisdr Jun 21, 2023
9e0bb35
Bump fideslang to 1.4.2.
pattisdr Jun 21, 2023
d9988ef
Merge branch 'main' into fides_3478_system_cookies
pattisdr Jun 21, 2023
478a951
Merge branch 'main' into fides_3478_system_cookies
pattisdr Jun 21, 2023
d283aab
Classmethod placement was preventing validator from running.
pattisdr Jun 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .fides/db_dataset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2240,4 +2240,41 @@ dataset:
description: 'The name of the organization this Fides deployment belongs to'
data_categories:
- user.workplace
data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified
- name: cookies
description: 'Fides Generated Description for Table: cookies'
data_categories: []
data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified
fields:
- name: created_at
data_categories:
- system.operations
data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified
- name: domain
data_categories:
- system.operations
data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified
- name: id
data_categories:
- system.operations
data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified
- name: name
data_categories:
- system.operations
data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified
- name: path
data_categories:
- system.operations
data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified
- name: privacy_declaration_id
data_categories:
- system.operations
data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified
- name: system_id
data_categories:
- system.operations
data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified
- name: updated_at
data_categories:
- system.operations
data_qualifier: aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified
139 changes: 139 additions & 0 deletions docs/fides/docs/development/postman/Fides.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -4922,6 +4922,145 @@
}
]
},
{
"name": "Systems",
"item": [
{
"name": "Get System",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{client_token}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{host}}/system/",
"host": [
"{{host}}"
],
"path": [
"system",
""
]
}
},
"response": []
},
{
"name": "Create System",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{client_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"data_responsibility_title\":\"Processor\",\n \"description\":\"Collect data about our users for marketing.\",\n \"egress\":[\n {\n \"fides_key\":\"demo_analytics_system\",\n \"type\":\"system\",\n \"data_categories\":null\n }\n ],\n \"fides_key\":\"test_system\",\n \"ingress\":null,\n \"name\":\"Test system\",\n \"organization_fides_key\":\"default_organization\",\n \"privacy_declarations\":[\n {\n \"name\":\"Collect data for marketing\",\n \"data_categories\":[\n \"user.device.cookie_id\"\n ],\n \"data_use\":\"personalize\",\n \"data_qualifier\":\"aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified\",\n \"data_subjects\":[\n \"customer\"\n ],\n \"dataset_references\":null,\n \"egress\":null,\n \"ingress\":null,\n \"cookies\":[\n {\n \"name\":\"test_cookie\",\n \"path\":\"/\"\n }\n ]\n }\n ],\n \"system_dependencies\":[\n \"demo_analytics_system\"\n ],\n \"system_type\":\"Service\",\n \"tags\":null,\n \"third_country_transfers\":null,\n \"administrating_department\":\"Marketing\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{host}}/system",
"host": [
"{{host}}"
],
"path": [
"system"
]
}
},
"response": []
},
{
"name": "Update System",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{client_token}}",
"type": "string"
}
]
},
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"data_responsibility_title\":\"Processor\",\n \"description\":\"Collect data about our users for marketing.\",\n \"egress\":[\n {\n \"fides_key\":\"demo_analytics_system\",\n \"type\":\"system\",\n \"data_categories\":null\n }\n ],\n \"fides_key\":\"test_system\",\n \"ingress\":null,\n \"name\":\"Test system\",\n \"organization_fides_key\":\"default_organization\",\n \"privacy_declarations\":[\n {\n \"name\":\"Collect data for marketing\",\n \"data_categories\":[\n \"user.device.cookie_id\"\n ],\n \"data_use\":\"marketing.advertising\",\n \"data_qualifier\":\"aggregated.anonymized.unlinked_pseudonymized.pseudonymized.identified\",\n \"data_subjects\":[\n \"customer\"\n ],\n \"dataset_references\":null,\n \"egress\":null,\n \"ingress\":null,\n \"cookies\":[\n {\n \"name\":\"another_cookie\",\n \"path\":\"/\"\n }\n ]\n }\n ],\n \"system_dependencies\":[\n \"demo_analytics_system\"\n ],\n \"system_type\":\"Service\",\n \"tags\":null,\n \"third_country_transfers\":null,\n \"administrating_department\":\"Marketing\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{host}}/system?",
"host": [
"{{host}}"
],
"path": [
"system"
],
"query": [
{
"key": "",
"value": null
}
]
}
},
"response": []
},
{
"name": "Delete System",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{client_token}}",
"type": "string"
}
]
},
"method": "DELETE",
"header": [],
"url": {
"raw": "{{host}}/system/test_system",
"host": [
"{{host}}"
],
"path": [
"system",
"test_system"
]
}
},
"response": []
}
]
},
{
"name": "Roles",
"item": [
Expand Down
11 changes: 2 additions & 9 deletions noxfiles/test_docker_nox.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import pytest

from docker_nox import (
generate_multiplatform_buildx_command,
get_buildx_commands,
)
from constants_nox import (
DEV_TAG_SUFFIX,
PRERELEASE_TAG_SUFFIX,
RC_TAG_SUFFIX,
)
from constants_nox import DEV_TAG_SUFFIX, PRERELEASE_TAG_SUFFIX, RC_TAG_SUFFIX
from docker_nox import generate_multiplatform_buildx_command, get_buildx_commands


class TestGenerateMultiplatformBuilxCommand:
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ expandvars==0.9.0
fastapi[all]==0.89.1
fastapi-caching[redis]==0.3.0
fastapi-pagination[sqlalchemy]~= 0.10.0
fideslang==1.4.1
fideslang @ git+https://github.com/ethyca/fideslang.git@6e93ab4ea7d35713201d48b3314b8d25df0815cb
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
fideslog==1.2.10
firebase-admin==5.3.0
GitPython==3.1.31
Expand Down
2 changes: 1 addition & 1 deletion src/fides/api/ctl/database/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ async def get_resource(
raise_not_found: bool = True,
) -> Base:
"""
Get a resource from the databse by its FidesKey.
Get a resource from the database by its FidesKey.

Returns a SQLAlchemy model of that resource.
"""
Expand Down
82 changes: 77 additions & 5 deletions src/fides/api/ctl/database/system.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
"""
Functions for interacting with System objects in the database.
"""
from typing import Dict, List, Tuple
from typing import Dict, List, Optional, Tuple

from fastapi import HTTPException
from fideslang.models import Cookies as CookieSchema
from fideslang.models import System as SystemSchema
from loguru import logger as log
from sqlalchemy import and_, delete, insert, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from starlette.status import HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND

from fides.api.ctl.database.crud import create_resource, get_resource, update_resource
from fides.api.ctl.sql_models import ( # type: ignore[attr-defined]
Cookies,
DataUse,
PrivacyDeclaration,
System,
Expand Down Expand Up @@ -112,23 +115,88 @@ async def upsert_privacy_declarations(
for privacy_declaration in resource.privacy_declarations:
# prepare our 'payload' for either create or update
data = privacy_declaration.dict()
cookies: List[Dict] = data.pop("cookies", None)
data["system_id"] = system.id # include FK back to system

# if we find matching declaration, remove it from our map
if existing_declaration := existing_declarations.pop(
if declaration := existing_declarations.pop(
privacy_declaration_logical_id(privacy_declaration), None
):
# and update existing declaration *in place*
existing_declaration.update(db, data=data)
declaration.update(db, data=data)
else:
# otherwise, create a new declaration record
PrivacyDeclaration.create(db, data=data)
declaration = PrivacyDeclaration.create(db, data=data)

# Upsert cookies for the given privacy declaration
await upsert_cookies(db, cookies, declaration, system)

# delete any existing privacy declarations that have not been "matched" in the request
for existing_declarations in existing_declarations.values():
await db.delete(existing_declarations)


async def upsert_cookies(
async_session: AsyncSession,
cookies: Optional[List[Dict]], # CookieSchema
privacy_declaration: PrivacyDeclaration,
system: System,
) -> None:
"""Upsert cookies for the given privacy declaration: retrieve cookies by name/system/privacy declaration
Remove any existing cookies that aren't specified here.
"""
cookie_list: List[CookieSchema] = cookies or []
for cookie_data in cookie_list:
# Check if cookie exists for this name/system/privacy declaration
result = await async_session.execute(
select(Cookies).where(
and_(
Cookies.name == cookie_data["name"],
Cookies.system_id == system.id,
Cookies.privacy_declaration_id == privacy_declaration.id,
)
)
)
row: Optional[Cookies] = result.scalars().first()
if row:
await async_session.execute(
update(Cookies).where(Cookies.id == row.id).values(cookie_data)
)

else:
await async_session.execute(
insert(Cookies).values(
{
"name": cookie_data.get("name"),
"path": cookie_data.get("path"),
"domain": cookie_data.get("domain"),
"privacy_declaration_id": privacy_declaration.id,
"system_id": system.id,
}
)
)

# Select cookies which are currently on the privacy declaration but not included in this request
delete_result = await async_session.execute(
select(Cookies).where(
and_(
Cookies.name.notin_([cookie["name"] for cookie in cookie_list]),
Cookies.system_id == system.id,
Cookies.privacy_declaration_id == privacy_declaration.id,
)
)
)

# Remove those cookies altogether
await async_session.execute(
delete(Cookies).where(
Cookies.id.in_(
[cookie.id for cookie in delete_result.scalars().unique().all()]
)
)
)


async def update_system(resource: SystemSchema, db: AsyncSession) -> Dict:
"""Helper function to share core system update logic for wrapping endpoint functions"""
system: System = await get_resource(
Expand Down Expand Up @@ -184,9 +252,13 @@ async def create_system(
for privacy_declaration in privacy_declarations:
data = privacy_declaration.dict()
data["system_id"] = created_system.id # add FK back to system
PrivacyDeclaration.create(
cookies: List[Dict] = data.pop("cookies", [])
privacy_declaration = PrivacyDeclaration.create(
db, data=data
) # create the associated PrivacyDeclaration
await upsert_cookies(
db, cookies, privacy_declaration, created_system
) # Create the associated cookies
except Exception as e:
log.error(
f"Error adding privacy declarations, reverting system creation: {str(privacy_declaration_exception)}"
Expand Down
Loading