diff --git a/core/invitation/service.go b/core/invitation/service.go index 936f0c620..c3dafcdb5 100644 --- a/core/invitation/service.go +++ b/core/invitation/service.go @@ -247,12 +247,15 @@ func (s Service) createRelations(ctx context.Context, invitationID uuid.UUID, or } func (s Service) Delete(ctx context.Context, id uuid.UUID) error { + // Remove every relation anchored on the invitation object, not just the org + // one. createRelations writes both a user (app/invitation:#user) and an + // org (app/invitation:#org) tuple; filtering by org alone leaked the user + // tuple on every accept/expire/delete. Omitting RelationName matches both. err := s.relationService.Delete(ctx, relation.Relation{ Object: relation.Object{ ID: id.String(), Namespace: schema.InvitationNamespace, }, - RelationName: schema.OrganizationRelationName, }) if err != nil { return fmt.Errorf("failed to delete relation for invitation: %w", err) diff --git a/test/e2e/regression/api_test.go b/test/e2e/regression/api_test.go index eca5df037..f45e7ca8c 100644 --- a/test/e2e/regression/api_test.go +++ b/test/e2e/regression/api_test.go @@ -2319,6 +2319,14 @@ func (s *APIRegressionTestSuite) TestInvitationAPI() { })) s.Assert().Error(err) + // no SpiceDB relation should linger on the invitation object after accept: + // both the #user and #org tuples must be gone, not just #org (gap #1661.3) + leftoverInviteRelations, err := s.testBench.AdminClient.ListRelations(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.ListRelationsRequest{ + Object: schema.JoinNamespaceAndResourceID(schema.InvitationNamespace, createdInvite.GetId()), + })) + s.Assert().NoError(err) + s.Assert().Empty(leftoverInviteRelations.Msg.GetRelations()) + // should be part of group listGroupUsersAfterAccept, err := s.testBench.Client.ListGroupUsers(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.ListGroupUsersRequest{ Id: createGroupResp.Msg.GetGroup().GetId(),