-
Notifications
You must be signed in to change notification settings - Fork 88
ENG-2194: Add custom default policy configuration support #7323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Deployment failed with the following error: |
….com:ethyca/fides into ENG-2194-be-default-policy-configuration-api
Greptile OverviewGreptile SummaryThis PR adds support for custom per-action default policies via 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 Main issue to address before merge: the new Confidence Score: 4/5
Important Files Changed
|
There was a problem hiding this 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
|
@greptile please review |
There was a problem hiding this 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
|
@greptile please review |
There was a problem hiding this 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
src/fides/api/task/conditional_dependencies/privacy_request/convenience_fields.py
Show resolved
Hide resolved
src/fides/api/task/conditional_dependencies/policy_evaluation.py
Outdated
Show resolved
Hide resolved
|
@greptile please review |
There was a problem hiding this 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
src/fides/api/task/conditional_dependencies/policy_evaluation.py
Outdated
Show resolved
Hide resolved
|
@greptile please review |
There was a problem hiding this 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
|
@greptile please review |
There was a problem hiding this 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
| location_data = get_location_by_id(country_code) or location_groups.get( | ||
| country_code | ||
| ) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
| 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 {} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
@greptile please review and take comments into account |
There was a problem hiding this 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
| @field_validator("conditions", mode="before") | ||
| @classmethod | ||
| def extract_conditions(cls, v: Any) -> dict[str, Any]: | ||
| """Extract the condition tree from the ORM conditions relationship. |
There was a problem hiding this comment.
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.
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, thePolicyEvaluatornow:ApplicationConfig.api_set)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.
PolicyResponseincludes a newconditionsfield 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). TheMUTUALLY_EXCLUSIVE_FIELDSset now includeslocation_groupsandlocation_regulationsfor proper condition tiebreaking.Code Changes
src/fides/api/task/conditional_dependencies/policy_evaluation.py_get_configured_default_policy_key()method and updated_get_default_policy()to check custom defaults first; addedlocation_groupsandlocation_regulationstoMUTUALLY_EXCLUSIVE_FIELDSsrc/fides/api/schemas/policy.pyconditionsfield toPolicyResponsewithextract_conditionsmodel validator; modernized type hints (List→list,Dict→dict)src/fides/api/api/v1/endpoints/policy_endpoints.pyjoinedload(Policy.conditions)to policy list query for eager loadingsrc/fides/api/task/conditional_dependencies/privacy_request/convenience_fields.pylocation_groupslookup before country code fallback in convenience field resolutionsrc/fides/api/util/default_policy_config.pyDEFAULT_POLICY_CONFIG_KEYconstant shared between fides and fidesplustests/api/task/conditional_dependencies/test_policy_evaluation.pyTestCustomDefaultPolicytest class (5 tests); expandedtest_routes_by_locationandtest_ambiguous_tie_raises_errorto parameterize over all location field types (location, country, groups, regulations)tests/ops/api/v1/endpoints/privacy_request/test_privacy_request_endpoints.py"conditions": {}changelog/7323.yamlSteps to Confirm
Prerequisites: Create policies with conditions using the policy conditions endpoints from #3031.
Verify policy conditions are returned in the policy list using
GET /api/v1/policyExpected: Each policy includes a conditions field containing its condition tree ({} if no conditions).
Set a custom default policy using P
UT /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.
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).
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.
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.
Verify location regulation matching - Create a policy with a
location_regulations = gdprcondition, thenPOST /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.
Read the current default config using
GET /api/v1/plus/dsr/policy/defaultExpected: Returns access, erasure, and consent fields (each null for system default or a policy key for custom).
Pre-Merge Checklist
CHANGELOG.mdupdatedMade with Cursor