IFC-1258: Fix relationship mutation constraints#9270
Conversation
There was a problem hiding this comment.
1 issue found across 3 files
Confidence score: 2/5
- There is a high-confidence, high-severity logic gap in
backend/infrahub/graphql/mutations/relationship.py: cardinality-one validation only checks pre-existing peers, so a single request can still create multiple new peers. - This can violate IFC-1258 and lead to concrete data-integrity/regression risk in relationship mutations, so this is not just a minor edge case.
- Pay close attention to
backend/infrahub/graphql/mutations/relationship.py- enforce cardinality checks against newly added peers within the same request.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="backend/infrahub/graphql/mutations/relationship.py">
<violation number="1" location="backend/infrahub/graphql/mutations/relationship.py:498">
P1: Cardinality-one validation is incomplete: it checks only pre-existing peers, so one request can still add multiple new peers and violate IFC-1258.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
| if existing_peers: | ||
| raise ValidationError( | ||
| {"name": f"'{relationship_name}' is a cardinality-one relationship and already has a peer"} |
There was a problem hiding this comment.
P1: Cardinality-one validation is incomplete: it checks only pre-existing peers, so one request can still add multiple new peers and violate IFC-1258.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/infrahub/graphql/mutations/relationship.py, line 498:
<comment>Cardinality-one validation is incomplete: it checks only pre-existing peers, so one request can still add multiple new peers and violate IFC-1258.</comment>
<file context>
@@ -492,6 +490,30 @@ async def _collect_current_peers(
+ rel_schema = source_node.get_schema().get_relationship(name=relationship_name)
+ if rel_schema.cardinality == RelationshipCardinality.ONE:
+ existing_peers = await _collect_current_peers(info=info, data=data, source_node=source_node)
+ if existing_peers:
+ raise ValidationError(
+ {"name": f"'{relationship_name}' is a cardinality-one relationship and already has a peer"}
</file context>
| if existing_peers: | |
| raise ValidationError( | |
| {"name": f"'{relationship_name}' is a cardinality-one relationship and already has a peer"} | |
| if existing_peers or len(data.get("nodes") or []) > 1: | |
| raise ValidationError( | |
| {"name": f"'{relationship_name}' is a cardinality-one relationship and already has a peer"} | |
| ) |
| await _validate_permissions(info=info, source_node=source, peers=nodes) | ||
| await _validate_peer_types(info=info, data=data, source_node=source, peers=nodes) | ||
| await _validate_peer_parents(info=info, data=data, source_node=source, peers=nodes) | ||
| await _validate_cardinality_add(info=info, data=data, source_node=source) |
There was a problem hiding this comment.
looks like this could be called after the call to _collect_current_peers on line 101/102 and it could receive the rel_schema and existing_peers as arguments to avoid calling _collect_current_peers twice
| @classmethod | ||
| @retry_db_transaction(name="relationship_add") | ||
| async def mutate( | ||
| async def mutate( # noqa: PLR0915 |
There was a problem hiding this comment.
is it possible to add this an an exception for this file in pyproject.toml somewhere so that it is included in our general list of "things to clean up?"
| nodes = await _validate_peers(info=info, data=data) | ||
| await _validate_permissions(info=info, source_node=source, peers=nodes) | ||
| await _validate_peer_types(info=info, data=data, source_node=source, peers=nodes) | ||
| await _validate_optional_remove(info=info, data=data, source_node=source) |
There was a problem hiding this comment.
same idea here to avoid multiple calls to _collect_current_peers
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
ajtmccarty
left a comment
There was a problem hiding this comment.
I think the robot might have a point with its comment. now that the cardinality-many check is removed, it actually might be possible to try to add multiple peers to a cardinality-one relationship. this might require a new test
| """ % ( | ||
| person_jack_main.id, | ||
| tag_red_main.id, | ||
| ) |
There was a problem hiding this comment.
we generally try to do this with graphql variables now
|
|
||
| rel_schema = source.get_schema().get_relationship(name=relationship_name) | ||
| if rel_schema.cardinality != RelationshipCardinality.MANY: | ||
| raise ValidationError({"name": f"'{relationship_name}' must be a relationship of cardinality Many"}) |
There was a problem hiding this comment.
this makes it look like the first bug this PR is fixing should have already been blocked
RelationshipAdd allowed adding multiple peers to a cardinality-one relationship, silently violating the schema.
| assert source2_final.id == profile.id | ||
|
|
||
|
|
||
| async def test_relationship_add_cardinality_one_rejected( |
There was a problem hiding this comment.
this looks like it is already tested above in test_relationship_wrong_name.
I'd say that this test should cover successfully adding a card-one relationship via graphql and then failing to add a second
| assert primary_tag.id == tag_blue_main.id | ||
|
|
||
|
|
||
| async def test_relationship_remove_mandatory_rejected( |
There was a problem hiding this comment.
this test and the one below look like they can be combined into a class to share fixtures and reduce loading time
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
| }) { | ||
| ok | ||
| } | ||
| } |
There was a problem hiding this comment.
optional. I think you can refactor this so that this query is defined in one place and uses graphql variables so that it does not have to be generated within the tests multiple times. it would make these tests easier to read
| @@ -0,0 +1 @@ | |||
| RelationshipAdd now rejects additions that would violate a cardinality-one constraint. RelationshipRemove now rejects removals that would leave a mandatory (optional=False) relationship without any peer. | |||
There was a problem hiding this comment.
the mutation already rejected cardinality-one constraint violations b/c it rejected any request to change a cardinality-one relationship. I think the fix is 1) that it now allows updating a cardinality-one relationship and 2) that it respects a mandatory constraint on a cardinality many relationship
| variable_values={"id": person_jack_main.id, "node_id": tag_red_main.id}, | ||
| ) | ||
| assert result.errors | ||
| assert result.errors[0].message == "'primary_tag' is a cardinality-one relationship and already has a peer at name" |
There was a problem hiding this comment.
this error looks wrong, specifically the at name piece at the end. does the error need to be raised in a different way to apply to the primary_tag field?
Why
RelationshipAddandRelationshipRemovemutations did not enforce two schema constraints:RelationshipAddallowed adding multiple peers to a cardinality-one relationship, silently violating the schema.RelationshipRemoveallowed removing the last peer from anoptional=Falserelationship, leaving the node in an invalid state.Closes #5794
How to test
Impact & rollout
RelationshipAdd, or to remove the last peer from anoptional=Falserelationship viaRelationshipRemove, will now receive aValidationError. These operations were already schema violations — this PR makes the API consistent with the schema definition.get_peers()DB query perRelationshipAddon cardinality-one relationships; one perRelationshipRemoveonoptional=Falserelationships. Negligible overhead.Checklist
backend/changelog/5794.fixed.md)Summary by cubic
Enforces schema constraints for GraphQL relationship mutations to fix IFC-1258. Supports cardinality-one relationships: the first add is allowed; violations return
ValidationError.RelationshipAddrejects adding a second peer to a cardinality-one relationship.RelationshipRemoverejects removing the last peer from anoptional=Falserelationship; partial removals are allowed.Written for commit 0895d2e. Summary will update on new commits. Review in cubic