ethyca-fides 2.68.1b0__py2.py3-none-any.whl → 2.68.1b2__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ethyca-fides might be problematic. Click here for more details.
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/METADATA +1 -1
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/RECORD +128 -118
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/3baf42d251a6_add_generic_taxonomy_models.py +239 -0
- fides/api/api/deps.py +2 -0
- fides/api/api/v1/endpoints/generic_overrides.py +64 -167
- fides/api/db/base.py +6 -0
- fides/api/db/ctl_session.py +3 -0
- fides/api/db/session.py +2 -1
- fides/api/models/privacy_request/privacy_request.py +15 -0
- fides/api/models/taxonomy.py +275 -0
- fides/api/schemas/application_config.py +2 -1
- fides/api/schemas/privacy_center_config.py +15 -0
- fides/api/service/deps.py +5 -0
- fides/api/service/privacy_request/request_service.py +6 -1
- fides/api/task/conditional_dependencies/evaluator.py +192 -45
- fides/api/task/conditional_dependencies/logging_utils.py +196 -0
- fides/api/task/conditional_dependencies/operators.py +8 -2
- fides/api/task/conditional_dependencies/schemas.py +25 -1
- fides/api/task/graph_task.py +9 -2
- fides/api/task/manual/manual_task_conditional_evaluation.py +193 -0
- fides/api/task/manual/manual_task_graph_task.py +224 -119
- fides/api/task/manual/manual_task_utils.py +0 -4
- fides/api/tasks/__init__.py +1 -0
- fides/api/util/connection_type.py +68 -33
- fides/config/database_settings.py +10 -1
- fides/data/sample_project/docker-compose.yml +3 -3
- fides/service/taxonomy/__init__.py +0 -0
- fides/service/taxonomy/handlers/__init__.py +11 -0
- fides/service/taxonomy/handlers/base.py +42 -0
- fides/service/taxonomy/handlers/legacy_handler.py +95 -0
- fides/service/taxonomy/taxonomy_service.py +261 -0
- fides/service/taxonomy/utils.py +160 -0
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-65723cd4b8fc36ac.js → _app-2c10f6b217b7978b.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-58827eb86516931f.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-766e57bcf38b5b1e.js → [id]-4e286a1e501a0c73.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-709bcb0bc6a5382d.js +1 -0
- fides/ui-build/static/admin/_next/static/css/a72179b1754aadd3.css +1 -0
- fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → qvk5eMANVfwYkdURE7fgG}/_buildManifest.js +1 -1
- fides/ui-build/static/admin/add-systems/manual.html +1 -1
- fides/ui-build/static/admin/add-systems/multiple.html +1 -1
- fides/ui-build/static/admin/add-systems.html +1 -1
- fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
- fides/ui-build/static/admin/consent/configure.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
- fides/ui-build/static/admin/consent/properties.html +1 -1
- fides/ui-build/static/admin/consent/reporting.html +1 -1
- fides/ui-build/static/admin/consent.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
- fides/ui-build/static/admin/data-catalog.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
- fides/ui-build/static/admin/data-discovery/activity.html +1 -1
- fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/detection.html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
- fides/ui-build/static/admin/datamap.html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
- fides/ui-build/static/admin/dataset/new.html +1 -1
- fides/ui-build/static/admin/dataset.html +1 -1
- fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
- fides/ui-build/static/admin/datastore-connection/new.html +1 -1
- fides/ui-build/static/admin/datastore-connection.html +1 -1
- fides/ui-build/static/admin/index.html +1 -1
- fides/ui-build/static/admin/integrations/[id].html +1 -1
- fides/ui-build/static/admin/integrations.html +1 -1
- fides/ui-build/static/admin/lib/fides-preview.js +1 -1
- fides/ui-build/static/admin/lib/fides-tcf.js +2 -2
- fides/ui-build/static/admin/lib/fides.js +1 -1
- fides/ui-build/static/admin/login/[provider].html +1 -1
- fides/ui-build/static/admin/login.html +1 -1
- fides/ui-build/static/admin/messaging/[id].html +1 -1
- fides/ui-build/static/admin/messaging/add-template.html +1 -1
- fides/ui-build/static/admin/messaging.html +1 -1
- fides/ui-build/static/admin/poc/ant-components.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
- fides/ui-build/static/admin/poc/forms.html +1 -1
- fides/ui-build/static/admin/poc/table-migration.html +1 -1
- fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/settings/about/alpha.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/regulations.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id].html +1 -1
- fides/ui-build/static/admin/systems.html +1 -1
- fides/ui-build/static/admin/taxonomy.html +1 -1
- fides/ui-build/static/admin/user-management/new.html +1 -1
- fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
- fides/ui-build/static/admin/user-management.html +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-53a763e49ce34a74.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-f43a988542813110.js +0 -1
- fides/ui-build/static/admin/_next/static/css/e1628f15dd5f019b.css +0 -1
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → qvk5eMANVfwYkdURE7fgG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# pylint: disable=protected-access
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Dict, List, Optional, Type
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import (
|
|
8
|
+
BOOLEAN,
|
|
9
|
+
Column,
|
|
10
|
+
ForeignKey,
|
|
11
|
+
ForeignKeyConstraint,
|
|
12
|
+
String,
|
|
13
|
+
Text,
|
|
14
|
+
UniqueConstraint,
|
|
15
|
+
)
|
|
16
|
+
from sqlalchemy.ext.associationproxy import association_proxy
|
|
17
|
+
from sqlalchemy.ext.declarative import declared_attr
|
|
18
|
+
from sqlalchemy.orm import RelationshipProperty, Session, relationship
|
|
19
|
+
|
|
20
|
+
from fides.api.common_exceptions import ValidationError
|
|
21
|
+
from fides.api.db.base_class import Base
|
|
22
|
+
from fides.api.models.sql_models import FidesBase # type: ignore[attr-defined]
|
|
23
|
+
|
|
24
|
+
LEGACY_TAXONOMIES = {"data_categories", "data_uses", "data_subjects"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TargetType(str, Enum):
|
|
28
|
+
"""Enumeration of target types that taxonomies can apply to."""
|
|
29
|
+
|
|
30
|
+
SYSTEM = "system"
|
|
31
|
+
PRIVACY_DECLARATION = "privacy_declaration"
|
|
32
|
+
TAXONOMY = "taxonomy" # For taxonomy-to-taxonomy relationships
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Taxonomy(Base, FidesBase):
|
|
36
|
+
"""The SQL model for taxonomy resources.
|
|
37
|
+
|
|
38
|
+
This is a generic taxonomy model that can be used to create any taxonomy.
|
|
39
|
+
For now we seed the database with the legacy taxonomies (data_category, data use, data subject)
|
|
40
|
+
so that these legacy taxonomy types can be used for allowed usage relationships.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Overriding the id definition from Base so we don't treat this as the primary key
|
|
44
|
+
id = Column(
|
|
45
|
+
String(255),
|
|
46
|
+
nullable=False,
|
|
47
|
+
index=False,
|
|
48
|
+
unique=True,
|
|
49
|
+
default=FidesBase.generate_uuid,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# The fides_key is inherited from FidesBase and acts as the primary key
|
|
53
|
+
|
|
54
|
+
# This is private to encourage the use of applies_to (see comment below)
|
|
55
|
+
_allowed_usages: RelationshipProperty[List[TaxonomyAllowedUsage]] = relationship(
|
|
56
|
+
"TaxonomyAllowedUsage",
|
|
57
|
+
back_populates="source_taxonomy",
|
|
58
|
+
cascade="all, delete-orphan",
|
|
59
|
+
lazy="selectin",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Association proxy to simplify access to target_type values
|
|
63
|
+
# This allows getting a list of strings while the actual storage
|
|
64
|
+
# is handled through the TaxonomyAllowedUsage model
|
|
65
|
+
# Updates should be done through the create/update methods
|
|
66
|
+
applies_to: List[str] = association_proxy(
|
|
67
|
+
"_allowed_usages",
|
|
68
|
+
"target_type",
|
|
69
|
+
# Allow setting via strings, the relationship backref will set FK
|
|
70
|
+
creator=lambda target_type: TaxonomyAllowedUsage(target_type=target_type),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def create(
|
|
75
|
+
cls: Type["Taxonomy"],
|
|
76
|
+
db: Session,
|
|
77
|
+
*,
|
|
78
|
+
data: Dict[str, Any],
|
|
79
|
+
check_name: bool = True,
|
|
80
|
+
) -> "Taxonomy":
|
|
81
|
+
"""Create a new Taxonomy with proper handling of applies_to."""
|
|
82
|
+
# Disallow creating taxonomies that represent legacy types
|
|
83
|
+
fides_key = data.get("fides_key")
|
|
84
|
+
if fides_key in LEGACY_TAXONOMIES:
|
|
85
|
+
raise ValidationError(
|
|
86
|
+
f"Cannot create taxonomy '{fides_key}'. This is a taxonomy managed by the system."
|
|
87
|
+
)
|
|
88
|
+
applies_to = data.pop("applies_to", [])
|
|
89
|
+
|
|
90
|
+
# Create the taxonomy
|
|
91
|
+
taxonomy: Taxonomy = super().create(db=db, data=data, check_name=check_name)
|
|
92
|
+
|
|
93
|
+
# Reconcile allowed usages if applies_to was provided
|
|
94
|
+
if applies_to:
|
|
95
|
+
taxonomy._reconcile_allowed_usages(db, applies_to)
|
|
96
|
+
|
|
97
|
+
return cls.persist_obj(db, taxonomy)
|
|
98
|
+
|
|
99
|
+
def update(self, db: Session, *, data: Dict[str, Any]) -> "Taxonomy":
|
|
100
|
+
"""Update a Taxonomy with proper handling of applies_to."""
|
|
101
|
+
applies_to = data.pop("applies_to", None)
|
|
102
|
+
|
|
103
|
+
# Update the base fields
|
|
104
|
+
super().update(db=db, data=data)
|
|
105
|
+
|
|
106
|
+
# If applies_to was provided, reconcile allowed usages
|
|
107
|
+
if applies_to is not None:
|
|
108
|
+
self._reconcile_allowed_usages(db, applies_to)
|
|
109
|
+
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
def save(self, db: Session) -> "Taxonomy":
|
|
113
|
+
"""Override save to reconcile any direct `applies_to` edits before persisting.
|
|
114
|
+
|
|
115
|
+
This allows callers to mutate `applies_to` via the association proxy and then call save.
|
|
116
|
+
"""
|
|
117
|
+
# Ensure no duplicate target types and reconcile relationship objects to the current values
|
|
118
|
+
self._reconcile_allowed_usages(db, list(self.applies_to))
|
|
119
|
+
return super().save(db) # type: ignore[return-value]
|
|
120
|
+
|
|
121
|
+
def _reconcile_allowed_usages(self, db: Session, applies_to: List[str]) -> None:
|
|
122
|
+
"""Ensure `_allowed_usages` matches the provided `applies_to` list.
|
|
123
|
+
|
|
124
|
+
- Deletes usages not in the provided list
|
|
125
|
+
- Creates usages missing from the relationship
|
|
126
|
+
- Deduplicates by target_type
|
|
127
|
+
"""
|
|
128
|
+
existing_usages = {usage.target_type: usage for usage in self._allowed_usages}
|
|
129
|
+
desired_types = set(applies_to)
|
|
130
|
+
|
|
131
|
+
# Delete usages that should no longer exist
|
|
132
|
+
for target_type in set(existing_usages.keys()) - desired_types:
|
|
133
|
+
# Remove from relationship; delete-orphan cascade will handle DB delete
|
|
134
|
+
self._allowed_usages.remove(existing_usages[target_type])
|
|
135
|
+
|
|
136
|
+
# Add missing usages
|
|
137
|
+
for target_type in desired_types - set(existing_usages.keys()):
|
|
138
|
+
self._allowed_usages.append(TaxonomyAllowedUsage(target_type=target_type))
|
|
139
|
+
|
|
140
|
+
# Deduplicate any accidental duplicates in-memory
|
|
141
|
+
seen: set[str] = set()
|
|
142
|
+
for usage in list(self._allowed_usages):
|
|
143
|
+
if usage.target_type in seen:
|
|
144
|
+
# Remove duplicate from relationship; delete-orphan will handle DB
|
|
145
|
+
self._allowed_usages.remove(usage)
|
|
146
|
+
else:
|
|
147
|
+
seen.add(usage.target_type)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TaxonomyAllowedUsage(Base):
|
|
151
|
+
"""
|
|
152
|
+
The SQL model for taxonomy allowed usage.
|
|
153
|
+
Defines what types of targets a taxonomy can be applied to.
|
|
154
|
+
|
|
155
|
+
target_type can be either:
|
|
156
|
+
- A generic type: "system", "privacy_declaration", "taxonomy"
|
|
157
|
+
- A taxonomy key: "data_categories", "data_uses", etc.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
@declared_attr
|
|
161
|
+
def __tablename__(self) -> str:
|
|
162
|
+
return "taxonomy_allowed_usage"
|
|
163
|
+
|
|
164
|
+
id = Column(
|
|
165
|
+
String(255),
|
|
166
|
+
nullable=False,
|
|
167
|
+
index=False,
|
|
168
|
+
unique=True,
|
|
169
|
+
default=FidesBase.generate_uuid,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
source_taxonomy: RelationshipProperty[Taxonomy] = relationship(
|
|
173
|
+
"Taxonomy",
|
|
174
|
+
back_populates="_allowed_usages",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
source_taxonomy_key: Column[str] = Column(
|
|
178
|
+
String,
|
|
179
|
+
ForeignKey("taxonomy.fides_key", ondelete="CASCADE"),
|
|
180
|
+
primary_key=True,
|
|
181
|
+
)
|
|
182
|
+
target_type: Column[str] = Column(
|
|
183
|
+
String, primary_key=True
|
|
184
|
+
) # Can be "system", "dataset", OR a taxonomy key like "data_categories"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TaxonomyElement(Base, FidesBase):
|
|
188
|
+
"""
|
|
189
|
+
The SQL model for taxonomy elements.
|
|
190
|
+
|
|
191
|
+
This is a generic taxonomy element model that can be used to create any taxonomy element.
|
|
192
|
+
|
|
193
|
+
As of now the legacy taxonomy elements still exist in their own tables (ctl_data_categories, ctl_data_uses, ctl_data_subjects),
|
|
194
|
+
but we can migrate them to this model in the future if needed.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
@declared_attr
|
|
198
|
+
def __tablename__(self) -> str:
|
|
199
|
+
return "taxonomy_element"
|
|
200
|
+
|
|
201
|
+
id = Column(
|
|
202
|
+
String(255),
|
|
203
|
+
nullable=False,
|
|
204
|
+
index=False,
|
|
205
|
+
unique=True,
|
|
206
|
+
default=FidesBase.generate_uuid,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Which taxonomy this element belongs to
|
|
210
|
+
taxonomy_type = Column(
|
|
211
|
+
String,
|
|
212
|
+
ForeignKey("taxonomy.fides_key", ondelete="CASCADE"),
|
|
213
|
+
nullable=False,
|
|
214
|
+
index=True,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
parent_key = Column(
|
|
218
|
+
Text, ForeignKey("taxonomy_element.fides_key", ondelete="RESTRICT"), index=True
|
|
219
|
+
)
|
|
220
|
+
active = Column(BOOLEAN, default=True, nullable=False, index=True)
|
|
221
|
+
|
|
222
|
+
children: RelationshipProperty[List[TaxonomyElement]] = relationship(
|
|
223
|
+
"TaxonomyElement",
|
|
224
|
+
back_populates="parent",
|
|
225
|
+
cascade="save-update, merge, refresh-expire", # intentionally do not cascade deletes
|
|
226
|
+
passive_deletes="all",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
parent: RelationshipProperty[Optional[TaxonomyElement]] = relationship(
|
|
230
|
+
"TaxonomyElement",
|
|
231
|
+
back_populates="children",
|
|
232
|
+
remote_side="TaxonomyElement.fides_key",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class TaxonomyUsage(Base):
|
|
237
|
+
"""
|
|
238
|
+
The SQL model for taxonomy usage.
|
|
239
|
+
Tracks the application of taxonomy elements to other taxonomy elements.
|
|
240
|
+
|
|
241
|
+
Example: Applying a "high" tag (from sensitivity taxonomy) to "user.contact.email" (from data_categories taxonomy).
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
@declared_attr
|
|
245
|
+
def __tablename__(self) -> str:
|
|
246
|
+
return "taxonomy_usage"
|
|
247
|
+
|
|
248
|
+
# The taxonomy element being applied (e.g., risk)
|
|
249
|
+
source_element_key = Column(String, nullable=False, index=True)
|
|
250
|
+
|
|
251
|
+
# The taxonomy element it's being applied to (e.g., a data category)
|
|
252
|
+
target_element_key = Column(String, nullable=False, index=True)
|
|
253
|
+
|
|
254
|
+
# Denormalized taxonomy types for validation and performance
|
|
255
|
+
source_taxonomy = Column(String, nullable=False, index=True)
|
|
256
|
+
target_taxonomy = Column(String, nullable=False, index=True)
|
|
257
|
+
|
|
258
|
+
__table_args__ = (
|
|
259
|
+
# Validate that this type of usage is allowed
|
|
260
|
+
ForeignKeyConstraint(
|
|
261
|
+
["source_taxonomy", "target_taxonomy"],
|
|
262
|
+
[
|
|
263
|
+
"taxonomy_allowed_usage.source_taxonomy_key",
|
|
264
|
+
"taxonomy_allowed_usage.target_type",
|
|
265
|
+
],
|
|
266
|
+
ondelete="RESTRICT",
|
|
267
|
+
name="fk_taxonomy_usage_allowed",
|
|
268
|
+
),
|
|
269
|
+
# Prevent duplicate applications
|
|
270
|
+
UniqueConstraint(
|
|
271
|
+
"source_element_key",
|
|
272
|
+
"target_element_key",
|
|
273
|
+
name="uq_taxonomy_usage",
|
|
274
|
+
),
|
|
275
|
+
)
|
|
@@ -73,7 +73,8 @@ class ExecutionApplicationConfig(FidesSchema):
|
|
|
73
73
|
memory_watchdog_enabled: Optional[bool] = None
|
|
74
74
|
sql_dry_run: Optional[SqlDryRunMode] = None
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
# Allow deprecated / unknown fields (e.g. “safe_mode”) to pass through
|
|
77
|
+
model_config = ConfigDict(use_enum_values=True, extra="ignore")
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
class AdminUIConfig(FidesSchema):
|
|
@@ -12,10 +12,22 @@ class CustomIdentity(FidesSchema):
|
|
|
12
12
|
label: str
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
class LocationIdentityField(FidesSchema):
|
|
16
|
+
"""Location field configuration that extends the useful parts of CustomPrivacyRequestField"""
|
|
17
|
+
|
|
18
|
+
label: str
|
|
19
|
+
required: Optional[bool] = True
|
|
20
|
+
default_value: Optional[str] = None
|
|
21
|
+
query_param_key: Optional[str] = None
|
|
22
|
+
ip_geolocation_hint: Optional[bool] = False
|
|
23
|
+
# Note: We intentionally omit 'hidden' field as it doesn't make sense for location identity input
|
|
24
|
+
|
|
25
|
+
|
|
15
26
|
class IdentityInputs(FidesSchema):
|
|
16
27
|
name: Optional[RequiredType] = None
|
|
17
28
|
email: Optional[RequiredType] = None
|
|
18
29
|
phone: Optional[RequiredType] = None
|
|
30
|
+
location: Optional[Union[RequiredType, LocationIdentityField]] = None
|
|
19
31
|
model_config = ConfigDict(extra="allow")
|
|
20
32
|
|
|
21
33
|
def __init__(self, **data: Any):
|
|
@@ -30,6 +42,9 @@ class IdentityInputs(FidesSchema):
|
|
|
30
42
|
f'Custom identity "{field}" must be an instance of CustomIdentity '
|
|
31
43
|
'(e.g. {"label": "Field label"})'
|
|
32
44
|
)
|
|
45
|
+
elif field == "location" and isinstance(value, dict):
|
|
46
|
+
# Handle location field as LocationIdentityField
|
|
47
|
+
data[field] = LocationIdentityField(**value)
|
|
33
48
|
super().__init__(**data)
|
|
34
49
|
|
|
35
50
|
|
fides/api/service/deps.py
CHANGED
|
@@ -8,6 +8,7 @@ from fides.service.dataset.dataset_config_service import DatasetConfigService
|
|
|
8
8
|
from fides.service.dataset.dataset_service import DatasetService
|
|
9
9
|
from fides.service.messaging.messaging_service import MessagingService
|
|
10
10
|
from fides.service.privacy_request.privacy_request_service import PrivacyRequestService
|
|
11
|
+
from fides.service.taxonomy.taxonomy_service import TaxonomyService
|
|
11
12
|
from fides.service.user.user_service import UserService
|
|
12
13
|
|
|
13
14
|
|
|
@@ -41,3 +42,7 @@ def get_user_service(
|
|
|
41
42
|
config_proxy: ConfigProxy = Depends(get_config_proxy),
|
|
42
43
|
) -> UserService:
|
|
43
44
|
return UserService(db, config, config_proxy)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_taxonomy_service(db: Session = Depends(get_db)) -> TaxonomyService:
|
|
48
|
+
return TaxonomyService(db)
|
|
@@ -191,7 +191,11 @@ def poll_for_exited_privacy_request_tasks(self: DatabaseTask) -> Set[str]:
|
|
|
191
191
|
db.query(PrivacyRequest)
|
|
192
192
|
.filter(
|
|
193
193
|
PrivacyRequest.status.in_(
|
|
194
|
-
[
|
|
194
|
+
[
|
|
195
|
+
PrivacyRequestStatus.in_processing,
|
|
196
|
+
PrivacyRequestStatus.approved,
|
|
197
|
+
PrivacyRequestStatus.requires_input,
|
|
198
|
+
]
|
|
195
199
|
)
|
|
196
200
|
)
|
|
197
201
|
# Only look at Privacy Requests that haven't been deleted
|
|
@@ -546,6 +550,7 @@ def requeue_interrupted_tasks(self: DatabaseTask) -> None:
|
|
|
546
550
|
[
|
|
547
551
|
PrivacyRequestStatus.in_processing,
|
|
548
552
|
PrivacyRequestStatus.approved,
|
|
553
|
+
PrivacyRequestStatus.requires_input,
|
|
549
554
|
]
|
|
550
555
|
)
|
|
551
556
|
)
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
from typing import Any, Union
|
|
1
|
+
from typing import Any, Optional, Union
|
|
2
2
|
|
|
3
3
|
from loguru import logger
|
|
4
4
|
from sqlalchemy.orm import Session
|
|
5
5
|
|
|
6
6
|
from fides.api.graph.config import FieldPath
|
|
7
|
-
from fides.api.task.conditional_dependencies.operators import
|
|
7
|
+
from fides.api.task.conditional_dependencies.operators import (
|
|
8
|
+
LOGICAL_OPERATORS,
|
|
9
|
+
OPERATOR_METHODS,
|
|
10
|
+
)
|
|
8
11
|
from fides.api.task.conditional_dependencies.schemas import (
|
|
9
12
|
Condition,
|
|
13
|
+
ConditionEvaluationResult,
|
|
10
14
|
ConditionGroup,
|
|
11
15
|
ConditionLeaf,
|
|
12
|
-
|
|
16
|
+
EvaluationResult,
|
|
17
|
+
GroupEvaluationResult,
|
|
13
18
|
Operator,
|
|
14
19
|
)
|
|
15
20
|
|
|
@@ -19,84 +24,226 @@ class ConditionEvaluationError(Exception):
|
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
class ConditionEvaluator:
|
|
22
|
-
"""Evaluates nested conditions
|
|
27
|
+
"""Evaluates nested conditions and returns a boolean result and a detailed evaluation report"""
|
|
23
28
|
|
|
24
29
|
def __init__(self, db: Session):
|
|
25
30
|
self.db = db
|
|
26
31
|
|
|
27
|
-
def evaluate_rule(
|
|
28
|
-
|
|
32
|
+
def evaluate_rule(
|
|
33
|
+
self, rule: Condition, data: Union[dict, Any]
|
|
34
|
+
) -> EvaluationResult:
|
|
35
|
+
"""Evaluate a nested condition rule against input data and return detailed results
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
rule: The condition rule to evaluate
|
|
39
|
+
data: The data to evaluate the condition against
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
evaluation report: A detailed report of the evaluation
|
|
43
|
+
- The field address of the condition
|
|
44
|
+
- The operator used in the condition
|
|
45
|
+
- The expected value of the condition
|
|
46
|
+
- The actual value of the condition
|
|
47
|
+
- The result of the condition evaluation
|
|
48
|
+
- A message describing the condition evaluation
|
|
49
|
+
"""
|
|
29
50
|
if isinstance(rule, ConditionLeaf):
|
|
30
|
-
|
|
51
|
+
leaf_result = self._evaluate_leaf_condition(rule, data)
|
|
52
|
+
return leaf_result
|
|
31
53
|
# ConditionGroup
|
|
32
|
-
|
|
54
|
+
group_result = self._evaluate_group_condition(rule, data)
|
|
55
|
+
return group_result
|
|
33
56
|
|
|
34
57
|
def _evaluate_leaf_condition(
|
|
35
58
|
self, condition: ConditionLeaf, data: Union[dict, Any]
|
|
36
|
-
) ->
|
|
37
|
-
"""Evaluate a leaf condition against input data
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
) -> ConditionEvaluationResult:
|
|
60
|
+
"""Evaluate a leaf condition against input data
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
condition: The leaf condition to evaluate
|
|
64
|
+
data: The data to evaluate the condition against
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
A detailed evaluation report for the leaf condition
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ConditionEvaluationError: If there is an issue applying the operator or if an unexpected error occurs.
|
|
71
|
+
"""
|
|
72
|
+
# Handle both colon-separated and dot-separated field addresses
|
|
73
|
+
if ":" in condition.field_address:
|
|
74
|
+
# Full field address like "dataset:collection:field" - split on colons
|
|
75
|
+
keys = condition.field_address.split(":")
|
|
76
|
+
else:
|
|
77
|
+
# Relative field path like "field.subfield" - split on dots
|
|
78
|
+
keys = condition.field_address.split(".")
|
|
79
|
+
|
|
80
|
+
data_value = self._get_nested_value(data, keys)
|
|
81
|
+
|
|
82
|
+
# Apply operator and get result
|
|
83
|
+
try:
|
|
84
|
+
result = self._apply_operator(
|
|
85
|
+
data_value, condition.operator, condition.value
|
|
86
|
+
)
|
|
87
|
+
message = f"Condition '{condition.field_address} {condition.operator} {condition.value}' evaluated to {result}"
|
|
88
|
+
except ConditionEvaluationError as e:
|
|
89
|
+
logger.error(
|
|
90
|
+
f"Unexpected error evaluating condition '{condition.field_address} {condition.operator} {condition.value}': {str(e)}"
|
|
91
|
+
)
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
return ConditionEvaluationResult(
|
|
95
|
+
field_address=condition.field_address,
|
|
96
|
+
operator=condition.operator,
|
|
97
|
+
expected_value=condition.value,
|
|
98
|
+
actual_value=data_value,
|
|
99
|
+
result=result,
|
|
100
|
+
message=message,
|
|
101
|
+
)
|
|
41
102
|
|
|
42
103
|
def _evaluate_group_condition(
|
|
43
104
|
self, group: ConditionGroup, data: Union[dict, Any]
|
|
44
|
-
) ->
|
|
45
|
-
"""Evaluate a group condition against input data
|
|
105
|
+
) -> GroupEvaluationResult:
|
|
106
|
+
"""Evaluate a group condition against input data
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
group: The group condition to evaluate
|
|
110
|
+
data: The data to evaluate the condition against
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
A detailed evaluation report for the group condition
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ConditionEvaluationError: If there is an issue evaluating the group condition (e.g., from evaluate_rule calls)
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
operator_func = LOGICAL_OPERATORS[group.logical_operator]
|
|
120
|
+
except KeyError as e:
|
|
121
|
+
raise ConditionEvaluationError(
|
|
122
|
+
f"Unknown logical operator: {group.logical_operator}"
|
|
123
|
+
) from e
|
|
124
|
+
|
|
46
125
|
results = [
|
|
47
126
|
self.evaluate_rule(condition, data) for condition in group.conditions
|
|
48
127
|
]
|
|
128
|
+
group_result = operator_func([r.result for r in results])
|
|
129
|
+
|
|
130
|
+
return GroupEvaluationResult(
|
|
131
|
+
logical_operator=group.logical_operator,
|
|
132
|
+
condition_results=results,
|
|
133
|
+
result=group_result,
|
|
134
|
+
)
|
|
49
135
|
|
|
50
|
-
|
|
51
|
-
|
|
136
|
+
def _get_nested_value_from_fides_reference_structure(
|
|
137
|
+
self, data: Any, keys: list[str]
|
|
138
|
+
) -> Optional[Any]:
|
|
139
|
+
"""Get nested value from Fides reference structure
|
|
52
140
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
141
|
+
Args:
|
|
142
|
+
data: The Fides reference structure to get the nested value from
|
|
143
|
+
keys: The keys to for the specific nested value in the data
|
|
56
144
|
|
|
57
|
-
|
|
145
|
+
Returns:
|
|
146
|
+
The nested value from the data or None if not a Fides reference structure
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
AttributeError: If the data does not have a get_field_value method
|
|
150
|
+
ValueError: If the keys are not valid for the Fides reference structure
|
|
151
|
+
"""
|
|
152
|
+
if hasattr(data, "get_field_value"):
|
|
153
|
+
try:
|
|
154
|
+
field_path = FieldPath(*keys) if len(keys) > 1 else FieldPath(keys[0])
|
|
155
|
+
return data.get_field_value(field_path)
|
|
156
|
+
except (AttributeError, ValueError):
|
|
157
|
+
logger.debug(
|
|
158
|
+
f"Fides reference structure does not have a get_field_value method: {data}"
|
|
159
|
+
)
|
|
160
|
+
raise
|
|
161
|
+
raise ConditionEvaluationError(
|
|
162
|
+
f"Data does not have a get_field_value method: {data}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _get_nested_value_from_dict(self, data: dict, keys: list[str]) -> Optional[Any]:
|
|
166
|
+
"""Get nested value from dictionary. This is the fallback and will return None if the key is not found.
|
|
167
|
+
When the data is missing the None value will work with exists/not_exists operations and correctly evaluate to False
|
|
168
|
+
for other operations like eq, not_eq, etc.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
data: The dictionary to get the nested value from
|
|
172
|
+
keys: The keys to for the specific nested value in the data
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
The nested value from the data
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
KeyError: If the keys are not valid for the dictionary
|
|
179
|
+
"""
|
|
180
|
+
current: Any = data
|
|
181
|
+
for key in keys:
|
|
182
|
+
if not isinstance(current, dict):
|
|
183
|
+
return None
|
|
184
|
+
current = current.get(key)
|
|
185
|
+
if current is None:
|
|
186
|
+
return None
|
|
187
|
+
return current
|
|
58
188
|
|
|
59
189
|
def _get_nested_value(self, data: Union[dict, Any], keys: list[str]) -> Any:
|
|
60
|
-
"""Get nested value from data using dot notation
|
|
190
|
+
"""Get nested value from data using dot notation or colon notation
|
|
61
191
|
|
|
62
192
|
Supports both simple dictionary access and Fides reference structures:
|
|
63
193
|
- Simple dict: data["user"]["name"]
|
|
64
194
|
- Fides FieldAddress: data.get_field_value(FieldAddress("dataset", "collection", "field_address"))
|
|
65
195
|
- Fides Collection: data.get_field_value(FieldPath("field_address", "subfield"))
|
|
196
|
+
|
|
197
|
+
Also supports full field addresses with dataset:collection:field format
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
data: The data to get the nested value from
|
|
201
|
+
keys: The keys to for the specific nested value in the data
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
The nested value from the data
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
KeyError: If the keys are not valid for the dictionary
|
|
66
208
|
"""
|
|
67
209
|
if not keys:
|
|
68
210
|
return data
|
|
69
211
|
|
|
70
|
-
current = data
|
|
71
|
-
|
|
72
212
|
# Try Fides reference structures first
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
except (AttributeError, ValueError):
|
|
78
|
-
pass
|
|
79
|
-
|
|
80
|
-
# Fall back to dictionary access
|
|
81
|
-
for key in keys:
|
|
82
|
-
if not isinstance(current, dict):
|
|
83
|
-
current = current.get(key, {}) if hasattr(current, "get") else None
|
|
84
|
-
else:
|
|
85
|
-
current = current.get(key, {})
|
|
213
|
+
try:
|
|
214
|
+
return self._get_nested_value_from_fides_reference_structure(data, keys)
|
|
215
|
+
except (AttributeError, ValueError, ConditionEvaluationError):
|
|
216
|
+
pass
|
|
86
217
|
|
|
87
|
-
|
|
218
|
+
# Fall back to dictionary access for all path types
|
|
219
|
+
return self._get_nested_value_from_dict(data, keys)
|
|
88
220
|
|
|
89
221
|
def _apply_operator(
|
|
90
222
|
self, data_value: Any, operator: Operator, user_input_value: Any
|
|
91
223
|
) -> bool:
|
|
92
|
-
"""Apply operator to actual and expected values
|
|
224
|
+
"""Apply operator to actual and expected values
|
|
225
|
+
The operator is validated in the ConditionLeaf and ConditionGroup schemas,
|
|
226
|
+
so we don't need to validate it here.
|
|
93
227
|
|
|
228
|
+
Args:
|
|
229
|
+
data_value: The actual value to evaluate
|
|
230
|
+
operator: The operator to apply
|
|
231
|
+
user_input_value: The expected value to evaluate against
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The result of the operator applied to the actual and expected values
|
|
235
|
+
"""
|
|
94
236
|
# Get the method for the operator and execute it
|
|
95
|
-
operator_method = operator_methods.get(operator)
|
|
96
|
-
if operator_method is None:
|
|
97
|
-
logger.warning(f"Unknown operator: {operator}")
|
|
98
|
-
raise ConditionEvaluationError(f"Unknown operator: {operator}")
|
|
99
237
|
try:
|
|
238
|
+
operator_method = OPERATOR_METHODS[operator]
|
|
100
239
|
return operator_method(data_value, user_input_value)
|
|
101
|
-
except
|
|
102
|
-
|
|
240
|
+
except KeyError as e:
|
|
241
|
+
# Unknown operator
|
|
242
|
+
logger.error(f"Unknown operator: {operator}")
|
|
243
|
+
raise ConditionEvaluationError(f"Unknown operator: {operator}") from e
|
|
244
|
+
except Exception as e:
|
|
245
|
+
# Log unexpected errors but still raise them
|
|
246
|
+
logger.error(f"Unexpected error in operator {operator}: {e}")
|
|
247
|
+
raise ConditionEvaluationError(
|
|
248
|
+
f"Unexpected error evaluating condition: {e}"
|
|
249
|
+
) from e
|