Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o
- https://github.com/ethyca/fides/labels/high-risk: to indicate that a change is a "high-risk" change that could potentially lead to unanticipated regressions or degradations
- https://github.com/ethyca/fides/labels/db-migration: to indicate that a given change includes a DB migration

## [Unreleased](https://github.com/ethyca/fides/compare/2.74.1..main)
## [Unreleased](https://github.com/ethyca/fides/compare/2.74.2..main)



## [2.74.2](https://github.com/ethyca/fides/compare/2.74.1..2.74.2)

### Fixed
- Fixed the IdentityValue schema so it uses Multivalue instead of string [#6964](https://github.com/ethyca/fides/pull/6964)

## [2.74.1](https://github.com/ethyca/fides/compare/2.74.0..2.74.1)

Expand Down
19 changes: 17 additions & 2 deletions src/fides/api/schemas/privacy_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
from fides.api.schemas.base_class import FidesSchema
from fides.api.schemas.policy import ActionType, CurrentStep
from fides.api.schemas.policy import PolicyResponse as PolicySchema
from fides.api.schemas.redis_cache import CustomPrivacyRequestField, Identity
from fides.api.schemas.redis_cache import (
CustomPrivacyRequestField,
Identity,
MultiValue,
)
from fides.api.schemas.user import PrivacyRequestUser
from fides.api.util.collection_util import Row
from fides.api.util.encryption.aes_gcm_encryption_scheme import verify_encryption_key
Expand Down Expand Up @@ -311,8 +315,19 @@ class PrivacyRequestStatus(str, EnumType):


class IdentityValue(BaseModel):
"""Represents an identity value with a label in API responses.

The value field accepts MultiValue types which match what LabeledIdentity supports:
- int
- str
- List[Union[int, str]]

This allows the schema to accept list values that were previously causing
validation errors.
"""

label: str
value: Optional[str] = None
value: Optional[MultiValue] = None


class PrivacyRequestResponse(FidesSchema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,14 @@
MessagingServiceType,
RequestReceiptBodyParams,
RequestReviewDenyBodyParams,
SubjectIdentityVerificationBodyParams,
)
from fides.api.schemas.policy import ActionType, CurrentStep, PolicyResponse
from fides.api.schemas.privacy_request import PrivacyRequestSource, PrivacyRequestStatus
from fides.api.schemas.privacy_request import (
IdentityValue,
PrivacyRequestResponse,
PrivacyRequestSource,
PrivacyRequestStatus,
)
from fides.api.schemas.redis_cache import (
CustomPrivacyRequestField,
Identity,
Expand Down Expand Up @@ -1210,6 +1214,195 @@ def test_get_privacy_requests_with_identity(
assert resp["items"][0]["id"] == succeeded_privacy_request.id
assert resp["items"][0].get("identity") is None

@pytest.mark.parametrize(
"custom_identities,expected_identity_values",
[
# Test case 1: List of integers
(
{
"regi_id": LabeledIdentity(label="Regi ID", value=[12345678]),
},
{
"regi_id": {"label": "Regi ID", "value": [12345678]},
},
),
# Test case 2: List of strings
(
{
"agent_id": LabeledIdentity(
label="Agent ID", value=["one", "two", "three"]
),
},
{
"agent_id": {"label": "Agent ID", "value": ["one", "two", "three"]},
},
),
# Test case 3: Mixed list
(
{
"user_id": LabeledIdentity(
label="User ID", value=[12345678, "one", "two", "three"]
),
},
{
"user_id": {
"label": "User ID",
"value": [12345678, "one", "two", "three"],
},
},
),
# Test case 4: All three cases together
(
{
"regi_id": LabeledIdentity(label="Regi ID", value=[12345678]),
"agent_id": LabeledIdentity(
label="Agent ID", value=["one", "two", "three"]
),
"user_id": LabeledIdentity(
label="User ID", value=[12345678, "one", "two", "three"]
),
},
{
"regi_id": {"label": "Regi ID", "value": [12345678]},
"agent_id": {"label": "Agent ID", "value": ["one", "two", "three"]},
"user_id": {
"label": "User ID",
"value": [12345678, "one", "two", "three"],
},
},
),
# Test case 5: Single integer in list
(
{
"customer_id": LabeledIdentity(label="Customer ID", value=[999]),
},
{
"customer_id": {"label": "Customer ID", "value": [999]},
},
),
# Test case 6: Empty list
(
{
"empty_list": LabeledIdentity(label="Empty List", value=[]),
},
{
"empty_list": {"label": "Empty List", "value": []},
},
),
# Test case 7: String value (not a list)
(
{
"customer_name": LabeledIdentity(
label="Customer Name", value="John Doe"
),
},
{
"customer_name": {"label": "Customer Name", "value": "John Doe"},
},
),
# Test case 8: Integer value (not a list)
(
{
"account_number": LabeledIdentity(
label="Account Number", value=98765
),
},
{
"account_number": {"label": "Account Number", "value": 98765},
},
),
# Test case 9: Mixed types - string, int, and list
(
{
"name": LabeledIdentity(label="Name", value="Jane Smith"),
"id": LabeledIdentity(label="ID", value=456789),
"tags": LabeledIdentity(label="Tags", value=["tag1", "tag2"]),
},
{
"name": {"label": "Name", "value": "Jane Smith"},
"id": {"label": "ID", "value": 456789},
"tags": {"label": "Tags", "value": ["tag1", "tag2"]},
},
),
],
)
def test_get_privacy_requests_with_custom_identities(
self,
api_client: TestClient,
url,
generate_auth_header,
db,
policy,
custom_identities,
expected_identity_values,
):
"""Test that privacy requests with custom identities containing various value types
can be retrieved and validated correctly.

This test would have caught the validation error where IdentityValue.value
was too restrictive and couldn't accept list values like [12345678] or
['one', 'two', 'three'].

The test is parametrized to cover:
- List values
- String values
- Integer values
- Mixed types (string, int, list)

Note: LabeledIdentity only accepts MultiValue types (int, str, or list of int/str)
"""
# Create a privacy request
privacy_request = PrivacyRequest.create(
db=db,
data={
"status": PrivacyRequestStatus.pending,
"policy_id": policy.id,
"client_id": policy.client_id,
},
)

# Persist identity with custom fields that have various value types
identity_dict = {"email": "[email protected]", **custom_identities}
privacy_request.persist_identity(db=db, identity=Identity(**identity_dict))

auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_READ])
response = api_client.get(
url + f"?include_identities=true", headers=auth_header
)
assert response.status_code == 200
resp = response.json()

# Verify the response validates correctly
assert len(resp["items"]) == 1
assert resp["items"][0]["id"] == privacy_request.id

# Verify the identity field is present and contains the list values
identity = resp["items"][0]["identity"]
assert identity is not None

# Verify standard identity field
assert identity["email"] == {
"label": "Email",
"value": "[email protected]",
}

# Verify custom identity fields with various value types
for field_name, expected_value in expected_identity_values.items():
assert identity[field_name] == expected_value

validated_response = PrivacyRequestResponse(**resp["items"][0])
assert validated_response.identity is not None

# Verify each custom identity field can be accessed and has the correct value
for field_name, expected_value in expected_identity_values.items():
assert field_name in validated_response.identity
identity_value = validated_response.identity[field_name]
assert isinstance(identity_value, IdentityValue)
assert identity_value.value == expected_value["value"]
assert identity_value.label == expected_value["label"]

privacy_request.delete(db)

def test_get_privacy_requests_with_custom_fields(
self,
api_client: TestClient,
Expand Down
Loading