From 142efb8f44d42f3ff30afd7b8af0e6904221d54b Mon Sep 17 00:00:00 2001 From: aman Date: Fri, 23 Oct 2026 22:03:31 +0530 Subject: [PATCH 1/2] test(e2e): org delete leaves no access and no live policy After DeleteOrganization, nobody who had access can get the org, its projects or a resource inside one, no live policy is left on them or names the deleted group or service user, and the rows stay marked deleted. --- test/e2e/regression/api_test.go | 184 ++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/test/e2e/regression/api_test.go b/test/e2e/regression/api_test.go index 62be22653..ca0fb4a0e 100644 --- a/test/e2e/regression/api_test.go +++ b/test/e2e/regression/api_test.go @@ -3925,6 +3925,190 @@ func (s *APIRegressionTestSuite) TestOrganizationDeleteAlsoDeletesDisabledProjec }) } +func (s *APIRegressionTestSuite) TestOrganizationDeleteLeavesNoAccessAndNoLivePolicy() { + ctxOrgAdminAuth := testbench.ContextWithAuth(context.Background(), s.adminCookie) + + createOrgResp, err := s.testBench.Client.CreateOrganization(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreateOrganizationRequest{ + Body: &frontierv1beta1.OrganizationRequestBody{Title: "org delete access", Name: "org-delete-access"}, + })) + s.Require().NoError(err) + orgID := createOrgResp.Msg.GetOrganization().GetId() + + createProject := func(name string) string { + resp, err := s.testBench.Client.CreateProject(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreateProjectRequest{ + Body: &frontierv1beta1.ProjectRequestBody{Name: name, OrgId: orgID}, + })) + s.Require().NoError(err) + return resp.Msg.GetProject().GetId() + } + projectID := createProject("org-delete-access-project") + disabledProjectID := createProject("org-delete-access-disabled-project") + + createOrderResp, err := s.testBench.Client.CreateProjectResource(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreateProjectResourceRequest{ + ProjectId: projectID, + Body: &frontierv1beta1.ResourceRequestBody{Name: "org-delete-access-order", Namespace: computeOrderNamespace}, + })) + s.Require().NoError(err) + orderID := createOrderResp.Msg.GetResource().GetId() + + createUser := func(name string) string { + resp, err := s.testBench.Client.CreateUser(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreateUserRequest{ + Body: &frontierv1beta1.UserRequestBody{Email: name + "@raystack.org", Name: name}, + })) + s.Require().NoError(err) + return resp.Msg.GetUser().GetId() + } + ownerID := createUser("org-delete-access-owner") + memberID := createUser("org-delete-access-member") + projectUserID := createUser("org-delete-access-project-user") + groupMemberID := createUser("org-delete-access-group-member") + orderViewerID := createUser("org-delete-access-order-viewer") + + addOrgMembersResp, err := s.testBench.AdminClient.AddOrganizationMembers(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.AddOrganizationMembersRequest{ + OrgId: orgID, + Members: []*frontierv1beta1.OrgMemberEntry{ + {UserId: ownerID, RoleId: s.orgOwnerRole}, + {UserId: memberID, RoleId: s.orgViewerRole}, + {UserId: groupMemberID, RoleId: s.orgViewerRole}, + }, + })) + requireAddOrgMembersSuccess(s.T(), addOrgMembersResp, err) + + createGroupResp, err := s.testBench.Client.CreateGroup(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreateGroupRequest{ + OrgId: orgID, + Body: &frontierv1beta1.GroupRequestBody{Name: "org-delete-access-group"}, + })) + s.Require().NoError(err) + groupID := createGroupResp.Msg.GetGroup().GetId() + _, err = s.testBench.Client.SetGroupMemberRole(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.SetGroupMemberRoleRequest{ + GroupId: groupID, + PrincipalId: groupMemberID, + PrincipalType: schema.UserPrincipal, + RoleId: s.groupMemberRole, + })) + s.Require().NoError(err) + + createServiceUserResp, err := s.testBench.Client.CreateServiceUser(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreateServiceUserRequest{ + OrgId: orgID, + })) + s.Require().NoError(err) + serviceUserID := createServiceUserResp.Msg.GetServiceuser().GetId() + + projectResource := schema.JoinNamespaceAndResourceID(schema.ProjectNamespace, projectID) + orderResource := schema.JoinNamespaceAndResourceID(computeOrderNamespace, orderID) + for _, p := range []struct{ role, resource, principal string }{ + {schema.RoleProjectViewer, projectResource, schema.JoinNamespaceAndResourceID(schema.UserPrincipal, projectUserID)}, + {schema.RoleProjectViewer, projectResource, schema.JoinNamespaceAndResourceID(schema.GroupPrincipal, groupID)}, + {schema.RoleProjectViewer, projectResource, schema.JoinNamespaceAndResourceID(schema.ServiceUserPrincipal, serviceUserID)}, + {computeViewerRoleName, orderResource, schema.JoinNamespaceAndResourceID(schema.UserPrincipal, orderViewerID)}, + } { + _, err := s.testBench.Client.CreatePolicy(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreatePolicyRequest{ + Body: &frontierv1beta1.PolicyRequestBody{RoleId: p.role, Resource: p.resource, Principal: p.principal}, + })) + s.Require().NoError(err) + } + + _, err = s.testBench.Client.DisableProject(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.DisableProjectRequest{Id: disabledProjectID})) + s.Require().NoError(err) + + objects := map[string]string{ + "org": schema.JoinNamespaceAndResourceID(schema.OrganizationNamespace, orgID), + "project": projectResource, + "disabled project": schema.JoinNamespaceAndResourceID(schema.ProjectNamespace, disabledProjectID), + "order": orderResource, + } + subjects := map[string]string{ + "org owner": schema.JoinNamespaceAndResourceID(schema.UserPrincipal, ownerID), + "org member": schema.JoinNamespaceAndResourceID(schema.UserPrincipal, memberID), + "project user": schema.JoinNamespaceAndResourceID(schema.UserPrincipal, projectUserID), + "group member": schema.JoinNamespaceAndResourceID(schema.UserPrincipal, groupMemberID), + "service user": schema.JoinNamespaceAndResourceID(schema.ServiceUserPrincipal, serviceUserID), + "order viewer": schema.JoinNamespaceAndResourceID(schema.UserPrincipal, orderViewerID), + } + canGet := func(subject, object string) bool { + resp, err := s.testBench.AdminClient.CheckFederatedResourcePermission(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CheckFederatedResourcePermissionRequest{ + Resource: object, + Permission: schema.GetPermission, + Subject: subject, + })) + s.Require().NoError(err) + return resp.Msg.GetStatus() + } + for subject, objectNames := range map[string][]string{ + "org owner": {"org", "project", "disabled project", "order"}, + "org member": {"org"}, + "project user": {"project"}, + "group member": {"org", "project"}, + "service user": {"project"}, + "order viewer": {"order"}, + } { + for _, object := range objectNames { + s.Require().Truef(canGet(subjects[subject], objects[object]), "%s on the %s before the delete", subject, object) + } + } + + livePoliciesOn := func(id string) int { + var n int + s.Require().NoError(s.dbc.GetContext(context.Background(), &n, + "SELECT count(*) FROM policies WHERE resource_id = $1 AND deleted_at IS NULL", id)) + return n + } + livePoliciesNaming := func(id string) int { + var n int + s.Require().NoError(s.dbc.GetContext(context.Background(), &n, + "SELECT count(*) FROM policies WHERE principal_id = $1 AND deleted_at IS NULL", id)) + return n + } + policyResources := map[string]string{ + "org": orgID, "project": projectID, "disabled project": disabledProjectID, "group": groupID, "order": orderID, + } + policyPrincipals := map[string]string{"group": groupID, "service user": serviceUserID} + for name, id := range policyResources { + s.Require().Positivef(livePoliciesOn(id), "live policies on the %s before the delete", name) + } + for name, id := range policyPrincipals { + s.Require().Positivef(livePoliciesNaming(id), "live policies naming the %s before the delete", name) + } + + _, err = s.testBench.Client.DeleteOrganization(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.DeleteOrganizationRequest{Id: orgID})) + s.Require().NoError(err) + + s.Run("nobody who had access can get the org, its projects or the order", func() { + for subject, subjectID := range subjects { + for object, objectID := range objects { + s.Assert().Falsef(canGet(subjectID, objectID), "%s on the %s after the delete", subject, object) + } + } + }) + + s.Run("no live policy is left on the org, its projects, its group or the order", func() { + for name, id := range policyResources { + s.Assert().Zerof(livePoliciesOn(id), "live policies on the %s after the delete", name) + } + }) + + s.Run("no live policy names the deleted group or service user", func() { + for name, id := range policyPrincipals { + s.Assert().Zerof(livePoliciesNaming(id), "live policies naming the %s after the delete", name) + } + }) + + s.Run("the org and everything removed with it stay in the database marked deleted", func() { + for _, row := range []struct{ name, query, id string }{ + {"org", "SELECT count(*) FROM organizations WHERE id = $1 AND deleted_at IS NOT NULL", orgID}, + {"project", "SELECT count(*) FROM projects WHERE id = $1 AND deleted_at IS NOT NULL", projectID}, + {"disabled project", "SELECT count(*) FROM projects WHERE id = $1 AND deleted_at IS NOT NULL", disabledProjectID}, + {"group", "SELECT count(*) FROM groups WHERE id = $1 AND deleted_at IS NOT NULL", groupID}, + {"service user", "SELECT count(*) FROM serviceusers WHERE id = $1 AND deleted_at IS NOT NULL", serviceUserID}, + {"order", "SELECT count(*) FROM resources WHERE id = $1 AND deleted_at IS NOT NULL", orderID}, + } { + var n int + s.Require().NoError(s.dbc.GetContext(context.Background(), &n, row.query, row.id)) + s.Assert().Equalf(1, n, "the %s row is kept and marked deleted", row.name) + } + }) +} + func (s *APIRegressionTestSuite) TestProjectDeleteKeepsTheRowAndRemovesAccess() { ctxOrgAdminAuth := testbench.ContextWithAuth(context.Background(), s.adminCookie) From 146838897fcf6a8a6207f2bf9bbffcc337aaca10 Mon Sep 17 00:00:00 2001 From: aman Date: Tue, 27 Oct 2026 15:35:20 +0530 Subject: [PATCH 2/2] test(e2e): org delete check covers the group and its relations The access check now includes the group, and the group's relations are listed before and after the delete. No relation may be left on the group, which also catches member tuples left behind when only the member policies are removed. --- test/e2e/regression/api_test.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/e2e/regression/api_test.go b/test/e2e/regression/api_test.go index ca0fb4a0e..85eb7b10e 100644 --- a/test/e2e/regression/api_test.go +++ b/test/e2e/regression/api_test.go @@ -4015,6 +4015,7 @@ func (s *APIRegressionTestSuite) TestOrganizationDeleteLeavesNoAccessAndNoLivePo "org": schema.JoinNamespaceAndResourceID(schema.OrganizationNamespace, orgID), "project": projectResource, "disabled project": schema.JoinNamespaceAndResourceID(schema.ProjectNamespace, disabledProjectID), + "group": schema.JoinNamespaceAndResourceID(schema.GroupNamespace, groupID), "order": orderResource, } subjects := map[string]string{ @@ -4035,10 +4036,10 @@ func (s *APIRegressionTestSuite) TestOrganizationDeleteLeavesNoAccessAndNoLivePo return resp.Msg.GetStatus() } for subject, objectNames := range map[string][]string{ - "org owner": {"org", "project", "disabled project", "order"}, + "org owner": {"org", "project", "disabled project", "group", "order"}, "org member": {"org"}, "project user": {"project"}, - "group member": {"org", "project"}, + "group member": {"org", "project", "group"}, "service user": {"project"}, "order viewer": {"order"}, } { @@ -4047,6 +4048,15 @@ func (s *APIRegressionTestSuite) TestOrganizationDeleteLeavesNoAccessAndNoLivePo } } + groupRelations := func() []*frontierv1beta1.Relation { + resp, err := s.testBench.AdminClient.ListRelations(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.ListRelationsRequest{Object: objects["group"]})) + s.Require().NoError(err) + return resp.Msg.GetRelations() + } + s.Require().Truef(slices.ContainsFunc(groupRelations(), func(r *frontierv1beta1.Relation) bool { + return r.GetRelation() == schema.MemberRelationName && r.GetSubject() == subjects["group member"] + }), "the group member's %s relation on the group before the delete", schema.MemberRelationName) + livePoliciesOn := func(id string) int { var n int s.Require().NoError(s.dbc.GetContext(context.Background(), &n, @@ -4073,7 +4083,7 @@ func (s *APIRegressionTestSuite) TestOrganizationDeleteLeavesNoAccessAndNoLivePo _, err = s.testBench.Client.DeleteOrganization(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.DeleteOrganizationRequest{Id: orgID})) s.Require().NoError(err) - s.Run("nobody who had access can get the org, its projects or the order", func() { + s.Run("nobody who had access can get the org, its projects, its group or the order", func() { for subject, subjectID := range subjects { for object, objectID := range objects { s.Assert().Falsef(canGet(subjectID, objectID), "%s on the %s after the delete", subject, object) @@ -4093,6 +4103,10 @@ func (s *APIRegressionTestSuite) TestOrganizationDeleteLeavesNoAccessAndNoLivePo } }) + s.Run("no relation is left on the deleted group", func() { + s.Assert().Empty(groupRelations()) + }) + s.Run("the org and everything removed with it stay in the database marked deleted", func() { for _, row := range []struct{ name, query, id string }{ {"org", "SELECT count(*) FROM organizations WHERE id = $1 AND deleted_at IS NOT NULL", orgID},