ethyca-fides 2.58.2b2__py2.py3-none-any.whl → 2.58.2b5__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.
- {ethyca_fides-2.58.2b2.dist-info → ethyca_fides-2.58.2b5.dist-info}/METADATA +20 -11
- {ethyca_fides-2.58.2b2.dist-info → ethyca_fides-2.58.2b5.dist-info}/RECORD +118 -115
- {ethyca_fides-2.58.2b2.dist-info → ethyca_fides-2.58.2b5.dist-info}/WHEEL +1 -1
- {ethyca_fides-2.58.2b2.dist-info → ethyca_fides-2.58.2b5.dist-info}/entry_points.txt +0 -1
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/9288f729cac4_add_tcf_configuration_fk_to_experience_.py +62 -0
- fides/api/alembic/migrations/versions/99c603c1b8f9_add_password_login_enabled_and_totp_secret_to_fidesuser.py +45 -0
- fides/api/api/v1/endpoints/user_endpoints.py +8 -12
- fides/api/models/detection_discovery.py +31 -0
- fides/api/models/fides_user.py +26 -9
- fides/api/models/fides_user_invite.py +2 -0
- fides/api/models/privacy_experience.py +26 -0
- fides/api/models/tcf_publisher_restrictions.py +209 -48
- fides/api/schemas/user.py +5 -1
- fides/api/service/deps.py +9 -0
- fides/api/util/collection_util.py +48 -9
- fides/cli/commands/pull.py +77 -13
- fides/core/api.py +2 -1
- fides/core/pull.py +38 -7
- fides/service/user/__init__.py +0 -0
- fides/service/user/user_service.py +140 -0
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/{F-2Pz9ByzGwcvQtVLstwR → _o6WH0hDzNEhnUJyvLex7}/_buildManifest.js +1 -1
- fides/ui-build/static/admin/_next/static/chunks/1376-87058e04584cff20.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/4121-4d5273d7a354994d.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/{4450-6a8aa0d7358ac26f.js → 4450-9c3086ccb55c66aa.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/6315-24a0483ee1cab6cc.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/{9046-8a5fdd335a76d224.js → 9046-a69fa8f99c414570.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-4a5be35cd8f832c0.js → _app-0c1548ca3b158123.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-17d1525551d8904f.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/activity-7d2cb947eee11262.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-9ef6b422ae7bc2a8.js → [id]-b75ab4ee677f118d.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/messaging-26407674949bcbc4.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/messaging-28d4bdf060ec8cb2.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/storage-208e49ef43361d6f.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/settings/consent-ff1985f72d50ef47.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/settings/domains-5a0b10ec955097d4.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/user-management/{new-f8bca2e322ddf252.js → new-082c3156175f9267.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/user-management/profile/[id]-af83245e9373a064.js +1 -0
- 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/ant-poc.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-tcf.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/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.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/api/service/user/fides_user_service.py +0 -128
- fides/ui-build/static/admin/_next/static/chunks/1150-2642cd9cdc8a52f6.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/1376-03e7f50e708b7589.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/6315-1adb10a8b98b4a13.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-719949074f10bd6e.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/activity-4892603e743cd6ab.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/messaging-1e60754abec1ee6b.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/messaging-5e2687ab5ab10275.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/storage-2914aade73dcaecc.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/settings/consent-3ac1e5d3de5dd4a7.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/settings/domains-24cba38685dc872c.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/user-management/profile/[id]-c0378fd1a26a71da.js +0 -1
- {ethyca_fides-2.58.2b2.dist-info → ethyca_fides-2.58.2b5.dist-info/licenses}/LICENSE +0 -0
- {ethyca_fides-2.58.2b2.dist-info → ethyca_fides-2.58.2b5.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{F-2Pz9ByzGwcvQtVLstwR → _o6WH0hDzNEhnUJyvLex7}/_ssgManifest.js +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{1817-f82105a9608bba1a.js → 1817-48e1c9d3504e18f0.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/{6954-baa1d873abfe8b77.js → 6954-ec5276bb464d42b2.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{privacy-requests-b0f801d66e79a31a.js → privacy-requests-fd81714d811db7b3.js} +0 -0
- /fides/ui-build/static/admin/_next/static/chunks/pages/{user-management-3ca3c687e72d1364.js → user-management-a1db56f1cbfba373.js} +0 -0
@@ -1,5 +1,5 @@
|
|
1
1
|
from enum import Enum
|
2
|
-
from typing import Any, Dict, List, Optional
|
2
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
3
3
|
|
4
4
|
from pydantic import BaseModel, Field, ValidationError, model_validator
|
5
5
|
from sqlalchemy import Column
|
@@ -8,10 +8,13 @@ from sqlalchemy import ForeignKey, Index, Integer, String, insert, select, updat
|
|
8
8
|
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
9
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
10
10
|
from sqlalchemy.ext.declarative import declared_attr
|
11
|
-
from sqlalchemy.orm import Session
|
11
|
+
from sqlalchemy.orm import Session, relationship
|
12
12
|
|
13
13
|
from fides.api.db.base_class import Base
|
14
14
|
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from fides.api.models.privacy_experience import PrivacyExperienceConfig
|
17
|
+
|
15
18
|
|
16
19
|
class TCFRestrictionType(str, Enum):
|
17
20
|
"""Enum for TCF restriction types"""
|
@@ -46,14 +49,14 @@ class RangeEntry(BaseModel):
|
|
46
49
|
@model_validator(mode="after")
|
47
50
|
def validate_vendor_range(self) -> "RangeEntry":
|
48
51
|
"""Validates that end_vendor_id is greater than start_vendor_id if present."""
|
49
|
-
if
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
raise ValueError("end_vendor_id must be greater than start_vendor_id")
|
52
|
+
if self.end_vendor_id is not None and self.end_vendor_id < self.start_vendor_id:
|
53
|
+
raise ValueError(
|
54
|
+
"end_vendor_id must be greater than or equal to start_vendor_id"
|
55
|
+
)
|
54
56
|
return self
|
55
57
|
|
56
|
-
|
58
|
+
@property
|
59
|
+
def effective_end_vendor_id(self) -> int:
|
57
60
|
"""Get the effective end of the range."""
|
58
61
|
return self.end_vendor_id or self.start_vendor_id
|
59
62
|
|
@@ -68,7 +71,7 @@ class RangeEntry(BaseModel):
|
|
68
71
|
second = other if self.start_vendor_id <= other.start_vendor_id else self
|
69
72
|
|
70
73
|
# If first range's end is >= second range's start, they overlap
|
71
|
-
return first.
|
74
|
+
return first.effective_end_vendor_id >= second.start_vendor_id
|
72
75
|
|
73
76
|
|
74
77
|
class TCFConfiguration(Base):
|
@@ -82,6 +85,12 @@ class TCFConfiguration(Base):
|
|
82
85
|
|
83
86
|
name = Column(String, nullable=False, index=True, unique=True)
|
84
87
|
|
88
|
+
privacy_experience_configs = relationship(
|
89
|
+
"PrivacyExperienceConfig",
|
90
|
+
back_populates="tcf_configuration",
|
91
|
+
lazy="selectin",
|
92
|
+
)
|
93
|
+
|
85
94
|
|
86
95
|
class TCFPublisherRestriction(Base):
|
87
96
|
"""
|
@@ -134,9 +143,20 @@ class TCFPublisherRestriction(Base):
|
|
134
143
|
) -> None:
|
135
144
|
"""
|
136
145
|
Validates that if vendor_restriction is restrict_all_vendors, then the entries list is empty.
|
146
|
+
If vendor_restriction is not restrict_all_vendors, then there must be at least one entry.
|
137
147
|
"""
|
138
148
|
if vendor_restriction == TCFVendorRestriction.restrict_all_vendors and entries:
|
139
|
-
raise ValueError(
|
149
|
+
raise ValueError(
|
150
|
+
"A restrict_all_vendors restriction cannot have any range entries"
|
151
|
+
)
|
152
|
+
|
153
|
+
if (
|
154
|
+
vendor_restriction != TCFVendorRestriction.restrict_all_vendors
|
155
|
+
and not entries
|
156
|
+
):
|
157
|
+
raise ValueError(
|
158
|
+
f"A {vendor_restriction} restriction must have at least one range entry"
|
159
|
+
)
|
140
160
|
|
141
161
|
@staticmethod
|
142
162
|
def check_for_overlaps(entries: List[RangeEntry]) -> None:
|
@@ -186,17 +206,19 @@ class TCFPublisherRestriction(Base):
|
|
186
206
|
return data
|
187
207
|
|
188
208
|
@classmethod
|
189
|
-
async def
|
209
|
+
async def check_for_restriction_conflicts(
|
190
210
|
cls,
|
211
|
+
*,
|
191
212
|
async_db: AsyncSession,
|
192
213
|
configuration_id: str,
|
193
214
|
purpose_id: int,
|
194
|
-
|
215
|
+
restriction_type: TCFRestrictionType,
|
216
|
+
vendor_restriction: TCFVendorRestriction,
|
217
|
+
range_entries: Optional[list[RangeEntry]] = None,
|
218
|
+
restriction_id: Optional[str] = None,
|
195
219
|
) -> None:
|
196
220
|
"""
|
197
|
-
|
198
|
-
in the given configuration.
|
199
|
-
Raises a ValueError if any vendor ranges overlap.
|
221
|
+
Checks that the new restriction data does not conflict with any existing restrictions for the purpose.
|
200
222
|
"""
|
201
223
|
# First, get all the restrictions for the purpose in the given configuration
|
202
224
|
query = (
|
@@ -204,28 +226,145 @@ class TCFPublisherRestriction(Base):
|
|
204
226
|
.where(cls.tcf_configuration_id == configuration_id)
|
205
227
|
.where(cls.purpose_id == purpose_id)
|
206
228
|
)
|
207
|
-
|
208
|
-
restrictions
|
209
|
-
|
210
|
-
|
211
|
-
# so that the new_data restrictions don't overlap with themselves
|
212
|
-
if "id" in new_data:
|
213
|
-
existing_entries = [
|
214
|
-
entry
|
215
|
-
for r in restrictions
|
216
|
-
for entry in r.range_entries
|
217
|
-
if r.id != new_data["id"]
|
218
|
-
]
|
219
|
-
else:
|
220
|
-
existing_entries = [
|
221
|
-
entry for r in restrictions for entry in r.range_entries
|
222
|
-
]
|
229
|
+
# If we're updating an existing restriction, exclude it from the list of
|
230
|
+
# restrictions so it doesn't conflict with itself
|
231
|
+
if restriction_id:
|
232
|
+
query = query.where(cls.id != restriction_id)
|
223
233
|
|
224
|
-
|
225
|
-
|
226
|
-
|
234
|
+
restrictions_result = await async_db.execute(query)
|
235
|
+
relevant_restrictions = restrictions_result.scalars().all()
|
236
|
+
|
237
|
+
if any(
|
238
|
+
restriction.vendor_restriction == TCFVendorRestriction.restrict_all_vendors
|
239
|
+
for restriction in relevant_restrictions
|
240
|
+
):
|
241
|
+
raise ValueError(
|
242
|
+
f"Invalid restriction for purpose {purpose_id}: a restrict_all_vendors restriction exists for this purpose."
|
243
|
+
)
|
244
|
+
|
245
|
+
# If we're creating a restrict_all_vendors restriction,
|
246
|
+
# then there should be no other restrictions for this purpose
|
247
|
+
if vendor_restriction == TCFVendorRestriction.restrict_all_vendors:
|
248
|
+
if relevant_restrictions:
|
249
|
+
raise ValueError(
|
250
|
+
f"Invalid restrict_all_vendors restriction for purpose {purpose_id}: other restrictions already exist for this purpose."
|
251
|
+
)
|
252
|
+
|
253
|
+
return None
|
254
|
+
|
255
|
+
# If we already have a restriction for that restriction type,
|
256
|
+
# then we raise an error
|
257
|
+
if any(
|
258
|
+
restriction.restriction_type == restriction_type
|
259
|
+
for restriction in relevant_restrictions
|
260
|
+
):
|
261
|
+
raise ValueError(
|
262
|
+
f"Invalid {restriction_type} restriction for purpose {purpose_id}: a restriction of this type already exists for this purpose."
|
263
|
+
)
|
264
|
+
|
265
|
+
# We now need to check for vendor overlaps between all the restrictions.
|
266
|
+
# To achieve this, we need to transform allowlist-style restrictions into
|
267
|
+
# actual restriction ranges (rather than "allow" ranges).
|
268
|
+
new_range_entries = (
|
269
|
+
cls.transform_allowlist_restriction(range_entries or [])
|
270
|
+
if vendor_restriction == TCFVendorRestriction.allow_specific_vendors
|
271
|
+
else (range_entries or [])
|
227
272
|
)
|
228
273
|
|
274
|
+
existing_range_entries = []
|
275
|
+
for restriction in relevant_restrictions:
|
276
|
+
range_entries = [
|
277
|
+
RangeEntry.model_validate(entry) for entry in restriction.range_entries
|
278
|
+
]
|
279
|
+
transformed_range_entries = (
|
280
|
+
cls.transform_allowlist_restriction(range_entries)
|
281
|
+
if restriction.vendor_restriction
|
282
|
+
== TCFVendorRestriction.allow_specific_vendors
|
283
|
+
else range_entries
|
284
|
+
)
|
285
|
+
existing_range_entries.extend(transformed_range_entries)
|
286
|
+
|
287
|
+
all_entries = [*existing_range_entries, *new_range_entries]
|
288
|
+
cls.check_for_overlaps(all_entries)
|
289
|
+
|
290
|
+
@classmethod
|
291
|
+
def transform_allowlist_restriction(
|
292
|
+
cls, range_entries: list[RangeEntry]
|
293
|
+
) -> list[RangeEntry]:
|
294
|
+
"""
|
295
|
+
Transform allowlist-style restrictions into restriction ranges.
|
296
|
+
E.g if you have an "allow specific vendors" restriction with the range_entries
|
297
|
+
[
|
298
|
+
{start_vendor_id: 5, end_vendor_id: 10},
|
299
|
+
{start_vendor_id: 25, end_vendor_id: 40},
|
300
|
+
{start_vendor_id: 123 },
|
301
|
+
{start_vendor_id: 345, end_vendor_id: 380},
|
302
|
+
],
|
303
|
+
the transformed restriction ranges would be:
|
304
|
+
[
|
305
|
+
{start_vendor_id: 1, end_vendor_id: 4},
|
306
|
+
{start_vendor_id: 11, end_vendor_id: 24},
|
307
|
+
{start_vendor_id: 41, end_vendor_id: 122},
|
308
|
+
{start_vendor_id: 124, end_vendor_id: MAX_GVL_ID},
|
309
|
+
]
|
310
|
+
"""
|
311
|
+
MAX_GVL_ID = 9999 # TODO: get this from the TCF spec
|
312
|
+
|
313
|
+
# First, we need to sort the range_entries by start_vendor_id
|
314
|
+
sorted_range_entries = sorted(range_entries, key=lambda x: x.start_vendor_id)
|
315
|
+
|
316
|
+
# Now, we need to transform the allowlist-style restrictions into
|
317
|
+
# actual restriction ranges.
|
318
|
+
transformed_range_entries: list[RangeEntry] = []
|
319
|
+
|
320
|
+
total_entries = len(sorted_range_entries)
|
321
|
+
|
322
|
+
# This shouldn't happen, but just in case
|
323
|
+
if total_entries == 0:
|
324
|
+
raise ValueError(
|
325
|
+
"No range entries found for allow_specific_vendors restriction"
|
326
|
+
)
|
327
|
+
|
328
|
+
# If the first range entry starts at a number greater than 1,
|
329
|
+
# we need to add a transformed range entry from 1 up to the entry's start
|
330
|
+
if sorted_range_entries[0].start_vendor_id > 1:
|
331
|
+
transformed_range_entries.append(
|
332
|
+
RangeEntry(
|
333
|
+
start_vendor_id=1,
|
334
|
+
end_vendor_id=sorted_range_entries[0].start_vendor_id - 1,
|
335
|
+
)
|
336
|
+
)
|
337
|
+
|
338
|
+
# Iterate through the sorted range_entries and transform them into restriction ranges
|
339
|
+
for idx, range_entry in enumerate(sorted_range_entries):
|
340
|
+
# For all but the last range entry, we add an entry that corresponds to the numbers
|
341
|
+
# between the end of the current range entry and the start of the next range entry
|
342
|
+
if idx < total_entries - 1:
|
343
|
+
# Only create a range entry if there's actually a gap between the ranges
|
344
|
+
next_start = sorted_range_entries[idx + 1].start_vendor_id
|
345
|
+
current_end = range_entry.effective_end_vendor_id
|
346
|
+
if next_start > current_end + 1:
|
347
|
+
transformed_range_entries.append(
|
348
|
+
RangeEntry(
|
349
|
+
start_vendor_id=current_end + 1,
|
350
|
+
end_vendor_id=next_start - 1,
|
351
|
+
)
|
352
|
+
)
|
353
|
+
|
354
|
+
# If the last range entry ends at a number less than MAX_GVL_ID,
|
355
|
+
# we need to add a transformed range entry from the last entry's end to MAX_GVL_ID
|
356
|
+
if sorted_range_entries[-1].effective_end_vendor_id < MAX_GVL_ID:
|
357
|
+
transformed_range_entries.append(
|
358
|
+
RangeEntry(
|
359
|
+
start_vendor_id=sorted_range_entries[-1].effective_end_vendor_id
|
360
|
+
+ 1,
|
361
|
+
end_vendor_id=MAX_GVL_ID,
|
362
|
+
)
|
363
|
+
)
|
364
|
+
|
365
|
+
# Return the transformed restriction entries
|
366
|
+
return transformed_range_entries
|
367
|
+
|
229
368
|
@classmethod
|
230
369
|
def create(
|
231
370
|
cls,
|
@@ -249,6 +388,17 @@ class TCFPublisherRestriction(Base):
|
|
249
388
|
|
250
389
|
data = cls.validate_publisher_restriction_data(data)
|
251
390
|
|
391
|
+
await cls.check_for_restriction_conflicts(
|
392
|
+
async_db=async_db,
|
393
|
+
configuration_id=data["tcf_configuration_id"],
|
394
|
+
purpose_id=data["purpose_id"],
|
395
|
+
restriction_type=TCFRestrictionType(data["restriction_type"]),
|
396
|
+
vendor_restriction=TCFVendorRestriction(data["vendor_restriction"]),
|
397
|
+
range_entries=[
|
398
|
+
RangeEntry.model_validate(entry) for entry in data["range_entries"]
|
399
|
+
],
|
400
|
+
)
|
401
|
+
|
252
402
|
values = {
|
253
403
|
"tcf_configuration_id": data["tcf_configuration_id"],
|
254
404
|
"purpose_id": data["purpose_id"],
|
@@ -257,14 +407,6 @@ class TCFPublisherRestriction(Base):
|
|
257
407
|
"range_entries": data["range_entries"],
|
258
408
|
}
|
259
409
|
|
260
|
-
# Validate that the new vendor ranges do not overlap with any existing vendor ranges for the purpose
|
261
|
-
await cls.validate_vendor_overlaps_for_purpose(
|
262
|
-
async_db=async_db,
|
263
|
-
configuration_id=data["tcf_configuration_id"],
|
264
|
-
purpose_id=data["purpose_id"],
|
265
|
-
new_data=values,
|
266
|
-
)
|
267
|
-
|
268
410
|
# Insert the new restriction
|
269
411
|
insert_stmt = insert(cls).values(values) # type: ignore[arg-type]
|
270
412
|
result = await async_db.execute(insert_stmt)
|
@@ -280,22 +422,41 @@ class TCFPublisherRestriction(Base):
|
|
280
422
|
Update a TCFPublisherRestriction with the data.
|
281
423
|
Validates the data and checks for vendor overlaps.
|
282
424
|
"""
|
283
|
-
#
|
284
|
-
|
425
|
+
# Create a new dict merging the existing data and the updated data
|
426
|
+
updated_data = {
|
427
|
+
"id": self.id,
|
428
|
+
"tcf_configuration_id": self.tcf_configuration_id,
|
429
|
+
"purpose_id": self.purpose_id,
|
430
|
+
"restriction_type": self.restriction_type,
|
431
|
+
"vendor_restriction": self.vendor_restriction,
|
432
|
+
"range_entries": self.range_entries,
|
433
|
+
**data,
|
434
|
+
}
|
285
435
|
|
286
|
-
#
|
287
|
-
|
436
|
+
# First validate the data on its own
|
437
|
+
data = self.validate_publisher_restriction_data(updated_data)
|
438
|
+
|
439
|
+
# Then check for conflicts
|
440
|
+
await self.check_for_restriction_conflicts(
|
288
441
|
async_db=async_db,
|
289
442
|
configuration_id=self.tcf_configuration_id,
|
290
443
|
purpose_id=self.purpose_id,
|
291
|
-
|
444
|
+
restriction_type=TCFRestrictionType(data["restriction_type"]),
|
445
|
+
vendor_restriction=TCFVendorRestriction(data["vendor_restriction"]),
|
446
|
+
range_entries=[
|
447
|
+
RangeEntry.model_validate(entry) for entry in data["range_entries"]
|
448
|
+
],
|
449
|
+
restriction_id=self.id,
|
292
450
|
)
|
293
451
|
|
452
|
+
# Remove the id from the updated
|
453
|
+
updated_data.pop("id")
|
454
|
+
|
294
455
|
# Finally, make the update
|
295
456
|
update_query = (
|
296
457
|
update(TCFPublisherRestriction) # type: ignore[arg-type]
|
297
458
|
.where(TCFPublisherRestriction.id == self.id)
|
298
|
-
.values(**
|
459
|
+
.values(**updated_data)
|
299
460
|
)
|
300
461
|
await async_db.execute(update_query)
|
301
462
|
await async_db.commit()
|
fides/api/schemas/user.py
CHANGED
@@ -3,7 +3,7 @@ from datetime import datetime
|
|
3
3
|
from enum import Enum
|
4
4
|
from typing import Optional
|
5
5
|
|
6
|
-
from pydantic import EmailStr, field_validator
|
6
|
+
from pydantic import ConfigDict, EmailStr, field_validator
|
7
7
|
|
8
8
|
from fides.api.cryptography.cryptographic_util import decode_password
|
9
9
|
from fides.api.schemas.base_class import FidesSchema
|
@@ -27,6 +27,8 @@ class UserCreate(FidesSchema):
|
|
27
27
|
last_name: Optional[str] = None
|
28
28
|
disabled: bool = False
|
29
29
|
|
30
|
+
model_config = ConfigDict(extra="ignore")
|
31
|
+
|
30
32
|
@field_validator("username")
|
31
33
|
@classmethod
|
32
34
|
def validate_username(cls, username: str) -> str:
|
@@ -140,6 +142,8 @@ class UserUpdate(FidesSchema):
|
|
140
142
|
first_name: Optional[str] = None
|
141
143
|
last_name: Optional[str] = None
|
142
144
|
|
145
|
+
model_config = ConfigDict(extra="ignore")
|
146
|
+
|
143
147
|
|
144
148
|
class DisabledReason(Enum):
|
145
149
|
"""Reasons for why a user is disabled"""
|
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.user.user_service import UserService
|
11
12
|
|
12
13
|
|
13
14
|
def get_messaging_service(
|
@@ -32,3 +33,11 @@ def get_dataset_service(db: Session = Depends(get_db)) -> DatasetService:
|
|
32
33
|
|
33
34
|
def get_dataset_config_service(db: Session = Depends(get_db)) -> DatasetConfigService:
|
34
35
|
return DatasetConfigService(db)
|
36
|
+
|
37
|
+
|
38
|
+
def get_user_service(
|
39
|
+
db: Session = Depends(get_db),
|
40
|
+
config: FidesConfig = Depends(get_config),
|
41
|
+
config_proxy: ConfigProxy = Depends(get_config_proxy),
|
42
|
+
) -> UserService:
|
43
|
+
return UserService(db, config, config_proxy)
|
@@ -122,6 +122,7 @@ def extract_key_for_address(
|
|
122
122
|
return f"{dataset}:{collection}"
|
123
123
|
|
124
124
|
|
125
|
+
# pylint: disable=too-many-branches
|
125
126
|
def unflatten_dict(flat_dict: Dict[str, Any], separator: str = ".") -> Dict[str, Any]:
|
126
127
|
"""
|
127
128
|
Converts a dictionary of paths/values into a nested dictionary
|
@@ -149,17 +150,29 @@ def unflatten_dict(flat_dict: Dict[str, Any], separator: str = ".") -> Dict[str,
|
|
149
150
|
for i, current_key in enumerate(keys[:-1]):
|
150
151
|
next_key = keys[i + 1]
|
151
152
|
if next_key.isdigit():
|
152
|
-
target
|
153
|
+
if isinstance(target, dict): # Only call setdefault on dictionaries
|
154
|
+
target = target.setdefault(current_key, [])
|
155
|
+
elif isinstance(
|
156
|
+
target, list
|
157
|
+
): # If target is a list, handle differently
|
158
|
+
idx = int(current_key)
|
159
|
+
while len(target) <= idx:
|
160
|
+
target.append([]) # Add a list since next_key is a digit
|
161
|
+
target = target[idx]
|
153
162
|
else:
|
154
163
|
if isinstance(target, dict):
|
155
164
|
target = target.setdefault(current_key, {})
|
156
165
|
elif isinstance(target, list):
|
157
|
-
|
166
|
+
idx = int(current_key)
|
167
|
+
while len(target) <= idx:
|
158
168
|
target.append({})
|
159
|
-
target = target[
|
169
|
+
target = target[idx]
|
160
170
|
try:
|
161
171
|
if isinstance(target, list):
|
162
|
-
|
172
|
+
idx = int(keys[-1]) if keys[-1].isdigit() else len(target)
|
173
|
+
while len(target) <= idx:
|
174
|
+
target.append(None)
|
175
|
+
target[idx] = value
|
163
176
|
else:
|
164
177
|
# If the value is a dictionary, add its components to the queue for processing
|
165
178
|
if isinstance(value, dict):
|
@@ -176,10 +189,12 @@ def unflatten_dict(flat_dict: Dict[str, Any], separator: str = ".") -> Dict[str,
|
|
176
189
|
return output
|
177
190
|
|
178
191
|
|
192
|
+
# pylint: disable=too-many-branches
|
179
193
|
def flatten_dict(data: Any, prefix: str = "", separator: str = ".") -> Dict[str, Any]:
|
180
194
|
"""
|
181
195
|
Recursively flatten a dictionary or list into a flat dictionary with dot-notation keys.
|
182
196
|
Handles nested dictionaries and arrays with proper indices.
|
197
|
+
Preserves empty lists and dictionaries.
|
183
198
|
|
184
199
|
example:
|
185
200
|
|
@@ -191,7 +206,9 @@ def flatten_dict(data: Any, prefix: str = "", separator: str = ".") -> Dict[str,
|
|
191
206
|
"D": [
|
192
207
|
{"E": "3"},
|
193
208
|
{"E": "4"}
|
194
|
-
]
|
209
|
+
],
|
210
|
+
"E": [],
|
211
|
+
"F": {}
|
195
212
|
}
|
196
213
|
|
197
214
|
becomes
|
@@ -200,7 +217,9 @@ def flatten_dict(data: Any, prefix: str = "", separator: str = ".") -> Dict[str,
|
|
200
217
|
"A.B": "1",
|
201
218
|
"A.C": "2",
|
202
219
|
"D.0.E": "3",
|
203
|
-
"D.1.E": "4"
|
220
|
+
"D.1.E": "4",
|
221
|
+
"E": [],
|
222
|
+
"F": {}
|
204
223
|
}
|
205
224
|
|
206
225
|
Args:
|
@@ -211,20 +230,40 @@ def flatten_dict(data: Any, prefix: str = "", separator: str = ".") -> Dict[str,
|
|
211
230
|
Returns:
|
212
231
|
A flattened dictionary with dot-notation keys
|
213
232
|
"""
|
214
|
-
items = {}
|
233
|
+
items: Dict[str, Any] = {}
|
215
234
|
|
216
235
|
if isinstance(data, dict):
|
236
|
+
# Handle top-level empty dictionary case
|
237
|
+
if not data and not prefix:
|
238
|
+
return {}
|
239
|
+
|
240
|
+
# If the dictionary is empty but has a prefix, store it as is
|
241
|
+
if not data:
|
242
|
+
items[prefix] = {}
|
243
|
+
return items
|
244
|
+
|
217
245
|
for k, v in data.items():
|
218
246
|
new_key = f"{prefix}{separator}{k}" if prefix else k
|
219
247
|
if isinstance(v, (dict, list)):
|
220
|
-
|
248
|
+
if not v: # Handle empty dict or list
|
249
|
+
items[new_key] = v
|
250
|
+
else:
|
251
|
+
items.update(flatten_dict(v, new_key, separator))
|
221
252
|
else:
|
222
253
|
items[new_key] = v
|
223
254
|
elif isinstance(data, list):
|
255
|
+
# If the list is empty, store it as is
|
256
|
+
if not data:
|
257
|
+
items[prefix] = []
|
258
|
+
return items
|
259
|
+
|
224
260
|
for i, v in enumerate(data):
|
225
261
|
new_key = f"{prefix}{separator}{i}"
|
226
262
|
if isinstance(v, (dict, list)):
|
227
|
-
|
263
|
+
if not v: # Handle empty dict or list
|
264
|
+
items[new_key] = v
|
265
|
+
else:
|
266
|
+
items.update(flatten_dict(v, new_key, separator))
|
228
267
|
else:
|
229
268
|
items[new_key] = v
|
230
269
|
else:
|
fides/cli/commands/pull.py
CHANGED
@@ -5,9 +5,11 @@ from click_default_group import DefaultGroup
|
|
5
5
|
|
6
6
|
from fides.cli.options import fides_key_argument, manifests_dir_argument
|
7
7
|
from fides.cli.utils import with_analytics, with_server_health_check
|
8
|
-
from fides.common.utils import echo_red
|
8
|
+
from fides.common.utils import echo_green, echo_red
|
9
9
|
from fides.core import parse as _parse
|
10
10
|
from fides.core import pull as _pull
|
11
|
+
from fides.core.api_helpers import list_server_resources
|
12
|
+
from fides.core.pull import remove_nulls, write_manifest_file
|
11
13
|
from fides.core.utils import git_is_dirty
|
12
14
|
|
13
15
|
|
@@ -60,26 +62,88 @@ def pull_all(
|
|
60
62
|
|
61
63
|
@pull.command(name="dataset") # type: ignore
|
62
64
|
@click.pass_context
|
63
|
-
@
|
65
|
+
@click.argument("fides_key", required=False)
|
64
66
|
@manifests_dir_argument
|
65
|
-
|
67
|
+
@click.option(
|
68
|
+
"--all-resources",
|
69
|
+
"-a",
|
70
|
+
is_flag=True,
|
71
|
+
default=False,
|
72
|
+
help="Pull all datasets from the server.",
|
73
|
+
)
|
74
|
+
@click.option(
|
75
|
+
"--separate-files",
|
76
|
+
is_flag=True,
|
77
|
+
default=False,
|
78
|
+
help="Write each dataset to a separate file named after its fides_key.",
|
79
|
+
)
|
80
|
+
@with_analytics
|
81
|
+
@with_server_health_check
|
82
|
+
def pull_dataset(
|
66
83
|
ctx: click.Context,
|
67
|
-
fides_key: str,
|
84
|
+
fides_key: Optional[str],
|
68
85
|
manifests_dir: str,
|
86
|
+
all_resources: bool,
|
87
|
+
separate_files: bool,
|
69
88
|
) -> None:
|
70
89
|
"""
|
71
|
-
Retrieve
|
90
|
+
Retrieve datasets from the server and update the local manifest files.
|
91
|
+
|
92
|
+
If FIDES_KEY is provided, only that dataset will be pulled.
|
93
|
+
If --all-resources is specified, all datasets will be pulled.
|
94
|
+
If --separate-files is specified, each dataset will be written to a separate file.
|
72
95
|
"""
|
96
|
+
if not fides_key and not all_resources:
|
97
|
+
echo_red("Error: Either FIDES_KEY or --all-resources must be specified.")
|
98
|
+
raise SystemExit(1)
|
73
99
|
|
74
100
|
config = ctx.obj["CONFIG"]
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
101
|
+
|
102
|
+
# Check for unstaged git changes before proceeding
|
103
|
+
if git_is_dirty(manifests_dir):
|
104
|
+
echo_red(
|
105
|
+
f"There are unstaged changes in your manifest directory: '{manifests_dir}' \nAborting pull!"
|
106
|
+
)
|
107
|
+
raise SystemExit(1)
|
108
|
+
|
109
|
+
if fides_key:
|
110
|
+
_pull.pull(
|
111
|
+
url=config.cli.server_url,
|
112
|
+
manifests_dir=manifests_dir,
|
113
|
+
headers=config.user.auth_header,
|
114
|
+
fides_key=fides_key,
|
115
|
+
resource_type="dataset",
|
116
|
+
all_resources_file=None,
|
117
|
+
)
|
118
|
+
elif all_resources:
|
119
|
+
# Get all available datasets from server
|
120
|
+
datasets = list_server_resources(
|
121
|
+
url=config.cli.server_url,
|
122
|
+
headers=config.user.auth_header,
|
123
|
+
resource_type="dataset",
|
124
|
+
exclude_keys=[],
|
125
|
+
)
|
126
|
+
|
127
|
+
if not datasets:
|
128
|
+
echo_red("No datasets found on the server.")
|
129
|
+
return
|
130
|
+
|
131
|
+
# Remove null values
|
132
|
+
datasets = [remove_nulls(dataset) for dataset in datasets]
|
133
|
+
|
134
|
+
if separate_files:
|
135
|
+
# Write each dataset to a separate file
|
136
|
+
for dataset in datasets:
|
137
|
+
if "fides_key" in dataset:
|
138
|
+
fides_key = dataset["fides_key"]
|
139
|
+
manifest_path = f"{manifests_dir.rstrip('/')}/{fides_key}.yml"
|
140
|
+
write_manifest_file(manifest_path, {"dataset": [dataset]})
|
141
|
+
else:
|
142
|
+
# Write all datasets to a single file
|
143
|
+
all_datasets_file = f"{manifests_dir.rstrip('/')}/datasets.yml"
|
144
|
+
write_manifest_file(all_datasets_file, {"dataset": datasets})
|
145
|
+
|
146
|
+
echo_green("Pull complete.")
|
83
147
|
|
84
148
|
|
85
149
|
@pull.command(name="system") # type: ignore
|
fides/core/api.py
CHANGED
@@ -4,7 +4,8 @@ from typing import Dict, List, Optional, Union
|
|
4
4
|
|
5
5
|
import requests
|
6
6
|
|
7
|
-
from fides.api.util.endpoint_utils
|
7
|
+
# Not using the constant value from fides.api.util.endpoint_utils to reduce the startup time for the CLI
|
8
|
+
API_PREFIX = "/api/v1"
|
8
9
|
|
9
10
|
|
10
11
|
def generate_resource_url(
|