Skip to content

Conversation

@JadeCara
Copy link
Contributor

@JadeCara JadeCara commented Feb 5, 2026

Ticket

ENG-2194

Description Of Changes

Custom Default Policy Configuration

Add support for custom default policies per action type via ApplicationConfig. When no policy conditions match a privacy request, the PolicyEvaluator now:

  1. First checks for a custom default policy configured via the API (ApplicationConfig.api_set)
  2. Validates the custom default still exists and has rules for the action type
  3. Falls back to the system default if the custom default is missing or invalid

This enables administrators to override the system default policies (default_access_policy, default_erasure_policy, default_consent_policy) with organization-specific defaults.

Policy Conditions in API Responses

The policy list endpoint now eagerly loads and returns policy conditions in the response. PolicyResponse includes a new conditions field populated via a model validator that extracts the condition tree from the ORM relationship.

Location Groups & Regulations Support

Expanded location convenience field resolution to support location groups (e.g., "us", "ca" as groups, not just individual locations). The MUTUALLY_EXCLUSIVE_FIELDS set now includes location_groups and location_regulations for proper condition tiebreaking.

Code Changes

File Change
src/fides/api/task/conditional_dependencies/policy_evaluation.py Added _get_configured_default_policy_key() method and updated _get_default_policy() to check custom defaults first; added location_groups and location_regulations to MUTUALLY_EXCLUSIVE_FIELDS
src/fides/api/schemas/policy.py Added conditions field to PolicyResponse with extract_conditions model validator; modernized type hints (Listlist, Dictdict)
src/fides/api/api/v1/endpoints/policy_endpoints.py Added joinedload(Policy.conditions) to policy list query for eager loading
src/fides/api/task/conditional_dependencies/privacy_request/convenience_fields.py Added location_groups lookup before country code fallback in convenience field resolution
src/fides/api/util/default_policy_config.py New file with DEFAULT_POLICY_CONFIG_KEY constant shared between fides and fidesplus
tests/api/task/conditional_dependencies/test_policy_evaluation.py Added TestCustomDefaultPolicy test class (5 tests); expanded test_routes_by_location and test_ambiguous_tie_raises_error to parameterize over all location field types (location, country, groups, regulations)
tests/ops/api/v1/endpoints/privacy_request/test_privacy_request_endpoints.py Updated expected response assertions to include "conditions": {}
changelog/7323.yaml Added changelog entry

Steps to Confirm

Note: This PR provides the fides OSS infrastructure. End-to-end testing requires the companion fidesplus branch (ENG-2194-be-default-policy-configuration-api on fidesplus, PR #3050) which adds the API endpoints. Deploy both branches together to test.

Prerequisites: Create policies with conditions using the policy conditions endpoints from #3031.

  1. Verify policy conditions are returned in the policy list using GET /api/v1/policy
    Expected: Each policy includes a conditions field containing its condition tree ({} if no conditions).

  2. Set a custom default policy using PUT /api/v1/plus/dsr/policy/default
    { "action_type": "access", "policy_key": "my_custom_access_policy"}
    Expected: 200 response with updated config showing the custom policy key for access.

  3. Verify custom default is used when no conditions match using POST /api/v1/privacy-request
    [ { "identity": {"email": "test@example.com"}, "location": "XX", "action_type": "access" }]
    Expected: Request created using my_custom_access_policy (not default_access_policy).

  4. Reset to system default using PUT /api/v1/plus/dsr/policy/default
    { "action_type": "access", "policy_key": null}
    Expected: 200 response. Subsequent privacy requests with no matching conditions fall back to default_access_policy.

  5. Verify location group matching - Create a policy with a location_groups = eea condition, then POST /api/v1/privacy-request
    [ { "identity": {"email": "test@example.com"}, "location": "FR", "action_type": "access" }]
    Expected: France resolves to the eea group and matches the policy.

  6. Verify location regulation matching - Create a policy with a location_regulations = gdpr condition, then POST /api/v1/privacy-request
    [{"identity": {"email": "test@example.com"}, "location": "FR", "action_type": "access" }]
    Expected: France resolves to the gdpr regulation and matches the policy.

  7. Read the current default config using GET /api/v1/plus/dsr/policy/default
    Expected: Returns access, erasure, and consent fields (each null for system default or a policy key for custom).

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • No UX review needed
  • Followup issues:
    • No followup issues
  • Database migrations:
    • No migrations
  • Documentation:

Made with Cursor

Allow configuring custom default policies per action type via
ApplicationConfig. Falls back to system defaults if custom
default is missing or invalid.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link
Contributor

vercel bot commented Feb 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Feb 10, 2026 0:33am
fides-privacy-center Ignored Ignored Feb 10, 2026 0:33am

Request Review

@vercel
Copy link
Contributor

vercel bot commented Feb 9, 2026

Deployment failed with the following error:

Creating the Deployment Timed Out.

@JadeCara JadeCara marked this pull request as ready for review February 9, 2026 21:30
@JadeCara JadeCara requested review from a team as code owners February 9, 2026 21:30
Jade Wibbels added 2 commits February 9, 2026 14:31
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

This PR adds support for custom per-action default policies via ApplicationConfig.api_set, allowing PolicyEvaluator to prefer an administrator-configured default policy when no conditions match and to fall back to system-seeded defaults if the configured key is missing/invalid.

It also extends location convenience-field resolution to support location groups/regulations, updates specificity/tiebreaking to treat those fields as mutually exclusive with other location selectors, and updates the policy list endpoint to eager-load rules (including storage_destination) and conditions so policy responses can include a new conditions field derived from the Policy.conditions relationship.

Main issue to address before merge: the new PolicyResponse.conditions validator currently only handles dict and plain list inputs; if SQLAlchemy provides a non-list collection type, the API will silently return {} even when conditions exist, which breaks the intended API contract for GET /api/v1/policy.

Confidence Score: 4/5

  • Mostly safe to merge, but there is a correctness risk in how policy conditions are serialized to the API response.
  • Core behavior changes (custom default selection, location tiering, eager loading) are covered by tests and use defensive config parsing. The remaining concern is PolicyResponse.extract_conditions() relying on isinstance(v, list); if the ORM relationship is not a plain list type, conditions may incorrectly serialize as {} despite DB state, undermining the new API feature.
  • src/fides/api/schemas/policy.py

Important Files Changed

Filename Overview
changelog/7323.yaml Adds changelog entry for custom default policy configuration support.
src/fides/api/api/v1/endpoints/policy_endpoints.py Eager-loads policy rules/storage destinations and policy conditions for the list endpoint to avoid N+1 queries.
src/fides/api/schemas/policy.py Adds conditions field with a validator extracting condition trees; potential bug if ORM relationship isn't a plain list type, leading to {} even when conditions exist.
src/fides/api/task/conditional_dependencies/policy_evaluation.py Adds ApplicationConfig-based custom default policy lookup with defensive type checks; expands mutually exclusive location fields and tiering.
src/fides/api/task/conditional_dependencies/privacy_request/convenience_fields.py Extends location convenience resolution to support location groups and group fallback when extracting country codes.
src/fides/api/util/default_policy_config.py Introduces shared DEFAULT_POLICY_CONFIG_KEY constant for ApplicationConfig api_set default policies.
tests/api/task/conditional_dependencies/test_policy_evaluation.py Adds coverage for custom default policies and parameterizes location-based routing/tie-breaking across location/country/groups/regulations.
tests/ops/api/v1/endpoints/privacy_request/test_privacy_request_endpoints.py Updates privacy request endpoint expectations to include empty policy conditions field.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@JadeCara
Copy link
Contributor Author

JadeCara commented Feb 9, 2026

@greptile please review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@JadeCara
Copy link
Contributor Author

JadeCara commented Feb 9, 2026

@greptile please review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Jade Wibbels added 2 commits February 9, 2026 16:13
@JadeCara
Copy link
Contributor Author

JadeCara commented Feb 9, 2026

@greptile please review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@JadeCara
Copy link
Contributor Author

@greptile please review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@JadeCara
Copy link
Contributor Author

@greptile please review

@JadeCara JadeCara requested a review from johnewart February 10, 2026 00:37
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +156 to +158
location_data = get_location_by_id(country_code) or location_groups.get(
country_code
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong type returned

location_groups.get(country_code) can return a LocationGroup (not a Location). Immediately after, this code treats location_data like a Location (location_data.belongs_to, location_data.regulation, location_data.is_country), which will raise AttributeError when the lookup hits a group (e.g., when country_code is a group id like "us"). This will break privacy request evaluation for such inputs.

Also appears at src/fides/api/task/conditional_dependencies/privacy_request/convenience_fields.py:147-148 (group lookup assigned to location_data).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@greptile LocationGroup inherits from Location, so it has all the same attributes: belongs_to, regulation, and is_country. Accessing those on a LocationGroup instance will not raise AttributeError — it works identically to accessing them on a Location.

Comment on lines +163 to +179
if isinstance(v, list):
# Multiple rows per policy is prevented by the DB unique constraint
# `uq_policy_condition_policy_id` on policy_condition.policy_id
# (see PolicyCondition model and migration 6d5f70dd0ba5).
# We log-and-continue rather than raise so that read endpoints
# never 500 on unexpected DB state.
if len(v) > 1:
logger.warning(
"Policy has %d condition rows; expected at most 1. "
"This indicates data corruption — the unique constraint on "
"policy_id should prevent multiple condition rows. "
"Using the first row.",
len(v),
)
if hasattr(v[0], "condition_tree") and v[0].condition_tree:
return v[0].condition_tree
return {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Conditions silently truncated

When Policy.conditions has multiple rows, extract_conditions() logs a warning and then returns only v[0].condition_tree. If multiple PolicyCondition rows can exist in the DB (legacy/corruption/manual edits), this makes GET /api/v1/policy return a condition tree that can disagree with how policy evaluation actually works (which iterates over all PolicyCondition rows). This should be handled deterministically (e.g., enforce the 1-row invariant at read-time with a clear error payload, or serialize/merge all trees) rather than silently picking the first row.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@greptile There will only be 1 row - there is a unique constraint AND returns one or none.

@JadeCara
Copy link
Contributor Author

@greptile please review and take comments into account

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +148 to +151
@field_validator("conditions", mode="before")
@classmethod
def extract_conditions(cls, v: Any) -> dict[str, Any]:
"""Extract the condition tree from the ORM conditions relationship.
Copy link
Contributor

Choose a reason for hiding this comment

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

Conditions serialization mismatch

PolicyResponse.extract_conditions() only handles dict and list inputs; if the ORM relationship comes through as a SQLAlchemy InstrumentedList/other collection type (not a plain list), this will fall through to return {} even when conditions exist, making GET /api/v1/policy silently omit condition trees. Consider accepting generic sequences (e.g., collections.abc.Sequence) or explicitly handling SQLAlchemy collection types so non-empty relationships always serialize correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant