diff --git a/core/membership/service.go b/core/membership/service.go index d3fa987a5..205312efe 100644 --- a/core/membership/service.go +++ b/core/membership/service.go @@ -509,13 +509,10 @@ func (s *Service) cascadeRemovePrincipal(ctx context.Context, org organization.O // clean up SpiceDB relations for _, g := range orgGroups { - if err := s.removeRelations(ctx, g.ID, schema.GroupNamespace, principalID, principalType); err != nil { + if err := s.removeGroupMemberRelation(ctx, g.ID, principalID, principalType); err != nil { return fmt.Errorf("remove group %s relations: %w", g.ID, err) } } - if err := s.removeRelations(ctx, orgID, schema.OrganizationNamespace, principalID, principalType); err != nil { - return fmt.Errorf("remove org relations: %w", err) - } // remove identity link for service users if principalType == schema.ServiceUserPrincipal { @@ -549,15 +546,15 @@ func (s *Service) removeCustomResourceAccess(ctx context.Context, principalID, p return nil } -// removeRelations deletes owner and member relations for a principal on a resource. -func (s *Service) removeRelations(ctx context.Context, resourceID, resourceType, principalID, principalType string) error { - obj := relation.Object{ID: resourceID, Namespace: resourceType} - sub := relation.Subject{ID: principalID, Namespace: principalType} - for _, name := range []string{schema.OwnerRelationName, schema.MemberRelationName} { - err := s.relationService.Delete(ctx, relation.Relation{Object: obj, Subject: sub, RelationName: name}) - if err != nil && !errors.Is(err, relation.ErrNotExist) { - return fmt.Errorf("delete relation %s: %w", name, err) - } +// removeGroupMemberRelation deletes the member relation for a principal on a group. +func (s *Service) removeGroupMemberRelation(ctx context.Context, groupID, principalID, principalType string) error { + err := s.relationService.Delete(ctx, relation.Relation{ + Object: relation.Object{ID: groupID, Namespace: schema.GroupNamespace}, + Subject: relation.Subject{ID: principalID, Namespace: principalType}, + RelationName: schema.MemberRelationName, + }) + if err != nil && !errors.Is(err, relation.ErrNotExist) { + return fmt.Errorf("delete relation %s: %w", schema.MemberRelationName, err) } return nil } @@ -1395,7 +1392,7 @@ func (s *Service) RemoveGroupMember(ctx context.Context, groupID, principalID, p } } - if err := s.removeRelations(ctx, groupID, schema.GroupNamespace, principalID, principalType); err != nil { + if err := s.removeGroupMemberRelation(ctx, groupID, principalID, principalType); err != nil { s.log.ErrorContext(ctx, "membership state inconsistent: group policies removed but relation cleanup failed, needs manual fix", "group_id", groupID, "principal_id", principalID, @@ -1410,7 +1407,7 @@ func (s *Service) RemoveGroupMember(ctx context.Context, groupID, principalID, p } // RemoveAllGroupMembers tears down membership for a group that is being -// destroyed: deletes every policy on the group and every owner/member +// destroyed: deletes every policy on the group and every member // relation per principal. No min-owner check — the group itself is going // away, so the invariant doesn't apply. Errors are joined; partial failures // are logged so a retry can complete the cleanup. @@ -1442,7 +1439,7 @@ func (s *Service) RemoveAllGroupMembers(ctx context.Context, groupID string) err if _, hadFailure := failed[key]; hadFailure { continue } - if relErr := s.removeRelations(ctx, groupID, schema.GroupNamespace, p.PrincipalID, p.PrincipalType); relErr != nil { + if relErr := s.removeGroupMemberRelation(ctx, groupID, p.PrincipalID, p.PrincipalType); relErr != nil { errs = errors.Join(errs, fmt.Errorf("remove relations for %s:%s: %w", p.PrincipalType, p.PrincipalID, relErr)) } } @@ -1540,8 +1537,8 @@ func (s *Service) linkGroupToOrg(ctx context.Context, groupID, orgID string) err return nil } -// unlinkGroupFromOrg removes both hierarchy relations between a group and its -// org. Used as best-effort cleanup when group-create wiring fails partway. +// unlinkGroupFromOrg removes the group#org identity link. Used as best-effort +// cleanup when group-create wiring fails partway. // relation.ErrNotExist is ignored; any other error is returned. func (s *Service) unlinkGroupFromOrg(ctx context.Context, groupID, orgID string) error { if err := s.relationService.Delete(ctx, relation.Relation{ @@ -1551,18 +1548,6 @@ func (s *Service) unlinkGroupFromOrg(ctx context.Context, groupID, orgID string) }); err != nil && !errors.Is(err, relation.ErrNotExist) { return err } - - if err := s.relationService.Delete(ctx, relation.Relation{ - Object: relation.Object{ID: orgID, Namespace: schema.OrganizationNamespace}, - Subject: relation.Subject{ - ID: groupID, - Namespace: schema.GroupNamespace, - SubRelationName: schema.MemberRelationName, - }, - RelationName: schema.MemberRelationName, - }); err != nil && !errors.Is(err, relation.ErrNotExist) { - return err - } return nil } diff --git a/core/membership/service_test.go b/core/membership/service_test.go index ee824d1f9..6a9eb44fa 100644 --- a/core/membership/service_test.go +++ b/core/membership/service_test.go @@ -734,7 +734,6 @@ func TestService_RemoveOrganizationMember(t *testing.T) { enabledOrg := organization.Organization{ID: orgID, Title: "Test Org"} - orgObj := relation.Object{ID: orgID, Namespace: schema.OrganizationNamespace} grpObj := relation.Object{ID: groupID, Namespace: schema.GroupNamespace} userSub := relation.Subject{ID: userID, Namespace: schema.UserPrincipal} @@ -828,8 +827,6 @@ func TestService_RemoveOrganizationMember(t *testing.T) { }, nil) d.policySvc.EXPECT().Delete(ctx, "org-p1").Return(nil) d.resSvc.EXPECT().RemovePrincipalAccess(ctx, userID, schema.UserPrincipal, []string{}).Return(nil) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.OwnerRelationName}).Return(relation.ErrNotExist) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.MemberRelationName}).Return(nil) d.auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, nil) }, wantErr: nil, @@ -853,9 +850,6 @@ func TestService_RemoveOrganizationMember(t *testing.T) { d.policySvc.EXPECT().Delete(ctx, "proj-p1").Return(nil) d.policySvc.EXPECT().Delete(ctx, "grp-p1").Return(nil) d.resSvc.EXPECT().RemovePrincipalAccess(ctx, userID, schema.UserPrincipal, []string{projectID}).Return(nil) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.OwnerRelationName}).Return(nil) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.MemberRelationName}).Return(relation.ErrNotExist) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: grpObj, Subject: userSub, RelationName: schema.OwnerRelationName}).Return(nil) d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: grpObj, Subject: userSub, RelationName: schema.MemberRelationName}).Return(relation.ErrNotExist) d.auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, nil) }, @@ -878,8 +872,6 @@ func TestService_RemoveOrganizationMember(t *testing.T) { }, nil) d.policySvc.EXPECT().Delete(ctx, "org-p1").Return(nil) d.resSvc.EXPECT().RemovePrincipalAccess(ctx, userID, schema.UserPrincipal, []string{}).Return(nil) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.OwnerRelationName}).Return(relation.ErrNotExist) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.MemberRelationName}).Return(nil) d.auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, nil) }, wantErr: nil, @@ -901,13 +893,13 @@ func TestService_RemoveOrganizationMember(t *testing.T) { wantErrContain: "delete sub-resource policy", }, { - name: "should return error if org relation removal fails after org policies deleted", + name: "should return error if group relation removal fails after org policies deleted", setup: func(d testDeps) { d.orgSvc.EXPECT().Get(ctx, orgID).Return(enabledOrg, nil) d.policySvc.EXPECT().List(ctx, policy.Filter{OrgID: orgID, PrincipalID: userID, PrincipalType: schema.UserPrincipal}).Return([]policy.Policy{{ID: "org-p1", RoleID: viewerRoleID}}, nil) d.roleSvc.EXPECT().Get(ctx, schema.RoleOrganizationOwner).Return(role.Role{ID: ownerRoleID, Name: schema.RoleOrganizationOwner}, nil) d.projSvc.EXPECT().List(ctx, project.Filter{OrgID: orgID}).Return([]project.Project{}, nil) - d.grpSvc.EXPECT().List(ctx, group.Filter{OrganizationID: orgID}).Return([]group.Group{}, nil) + d.grpSvc.EXPECT().List(ctx, group.Filter{OrganizationID: orgID}).Return([]group.Group{{ID: groupID}}, nil) d.policySvc.EXPECT().List(ctx, policy.Filter{PrincipalID: userID, PrincipalType: schema.UserPrincipal}).Return([]policy.Policy{ {ID: "org-p1", ResourceType: schema.OrganizationNamespace, ResourceID: orgID, RoleID: viewerRoleID}, }, nil) @@ -915,9 +907,9 @@ func TestService_RemoveOrganizationMember(t *testing.T) { d.policySvc.EXPECT().Delete(ctx, "org-p1").Return(nil) d.resSvc.EXPECT().RemovePrincipalAccess(ctx, userID, schema.UserPrincipal, []string{}).Return(nil) // then relation removal fails - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.OwnerRelationName}).Return(errors.New("spicedb down")) + d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: grpObj, Subject: userSub, RelationName: schema.MemberRelationName}).Return(errors.New("spicedb down")) }, - wantErrContain: "remove org relations", + wantErrContain: "relations", }, { name: "should remove custom-resource access across every project in the org", @@ -933,8 +925,6 @@ func TestService_RemoveOrganizationMember(t *testing.T) { }, nil) d.policySvc.EXPECT().Delete(ctx, "org-p1").Return(nil) d.resSvc.EXPECT().RemovePrincipalAccess(ctx, userID, schema.UserPrincipal, []string{projectID, secondProjectID}).Return(nil) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.OwnerRelationName}).Return(relation.ErrNotExist) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.MemberRelationName}).Return(nil) d.auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, nil) }, wantErr: nil, @@ -1018,8 +1008,6 @@ func TestService_ForceRemoveOrganizationMember(t *testing.T) { ownerRoleID := uuid.New().String() enabledOrg := organization.Organization{ID: orgID, Title: "Test Org"} - orgObj := relation.Object{ID: orgID, Namespace: schema.OrganizationNamespace} - userSub := relation.Subject{ID: userID, Namespace: schema.UserPrincipal} type testDeps struct { policySvc *mocks.PolicyService @@ -1051,8 +1039,6 @@ func TestService_ForceRemoveOrganizationMember(t *testing.T) { // plain delete, not DeleteWithMinRoleGuard d.policySvc.EXPECT().Delete(ctx, "org-p1").Return(nil) d.resSvc.EXPECT().RemovePrincipalAccess(ctx, userID, schema.UserPrincipal, []string{}).Return(nil) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.OwnerRelationName}).Return(nil) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.MemberRelationName}).Return(relation.ErrNotExist) d.auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, nil) }, }, @@ -1065,8 +1051,6 @@ func TestService_ForceRemoveOrganizationMember(t *testing.T) { d.grpSvc.EXPECT().List(ctx, group.Filter{OrganizationID: orgID}).Return([]group.Group{}, nil) d.policySvc.EXPECT().List(ctx, policy.Filter{PrincipalID: userID, PrincipalType: schema.UserPrincipal}).Return([]policy.Policy{}, nil) d.resSvc.EXPECT().RemovePrincipalAccess(ctx, userID, schema.UserPrincipal, []string{}).Return(nil) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.OwnerRelationName}).Return(relation.ErrNotExist) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.MemberRelationName}).Return(relation.ErrNotExist) d.auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, nil) }, }, @@ -1085,8 +1069,6 @@ func TestService_ForceRemoveOrganizationMember(t *testing.T) { }, nil) d.policySvc.EXPECT().Delete(ctx, "org-p1").Return(nil) d.resSvc.EXPECT().RemovePrincipalAccess(ctx, userID, schema.UserPrincipal, []string{}).Return(nil) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.OwnerRelationName}).Return(nil) - d.relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.MemberRelationName}).Return(relation.ErrNotExist) d.auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, nil) }, }, @@ -1137,9 +1119,6 @@ func TestService_RemoveOrganizationMember_WithoutResourceService(t *testing.T) { ownerRoleID := uuid.New().String() viewerRoleID := uuid.New().String() - orgObj := relation.Object{ID: orgID, Namespace: schema.OrganizationNamespace} - userSub := relation.Subject{ID: userID, Namespace: schema.UserPrincipal} - policySvc := mocks.NewPolicyService(t) relSvc := mocks.NewRelationService(t) roleSvc := mocks.NewRoleService(t) @@ -1157,8 +1136,6 @@ func TestService_RemoveOrganizationMember_WithoutResourceService(t *testing.T) { {ID: "org-p1", ResourceType: schema.OrganizationNamespace, ResourceID: orgID}, }, nil) policySvc.EXPECT().Delete(ctx, "org-p1").Return(nil) - relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.OwnerRelationName}).Return(relation.ErrNotExist) - relSvc.EXPECT().Delete(ctx, relation.Relation{Object: orgObj, Subject: userSub, RelationName: schema.MemberRelationName}).Return(nil) auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, nil) svc := membership.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)), policySvc, relSvc, roleSvc, orgSvc, mocks.NewUserService(t), projSvc, grpSvc, mocks.NewServiceuserService(t), auditRepo) @@ -2080,15 +2057,6 @@ func TestService_OnGroupCreated(t *testing.T) { Subject: relation.Subject{ID: orgID, Namespace: schema.OrganizationNamespace}, RelationName: schema.OrganizationRelationName, } - orgGroupMemberRelation := relation.Relation{ - Object: relation.Object{ID: orgID, Namespace: schema.OrganizationNamespace}, - Subject: relation.Subject{ - ID: groupID, - Namespace: schema.GroupNamespace, - SubRelationName: schema.MemberRelationName, - }, - RelationName: schema.MemberRelationName, - } creatorMemberRelation := relation.Relation{ Object: relation.Object{ID: groupID, Namespace: schema.GroupNamespace}, Subject: relation.Subject{ID: creatorID, Namespace: schema.UserPrincipal}, @@ -2150,9 +2118,8 @@ func TestService_OnGroupCreated(t *testing.T) { _ = mockRoleSvc _ = mockUserSvc - // rollback: delete the identity link plus any legacy org-member relation + // rollback: delete the identity link mockRelSvc.EXPECT().Delete(ctx, groupOrgRelation).Return(nil) - mockRelSvc.EXPECT().Delete(ctx, orgGroupMemberRelation).Return(relation.ErrNotExist) svc := membership.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)), mockPolicySvc, mockRelSvc, mockRoleSvc, mocks.NewOrgService(t), mockUserSvc, mocks.NewProjectService(t), mockGrpSvc, mocks.NewServiceuserService(t), mocks.NewAuditRecordRepository(t)) @@ -2232,7 +2199,7 @@ func TestService_RemoveGroupMember(t *testing.T) { wantErr: membership.ErrLastGroupOwnerRole, }, { - name: "should remove a member (non-owner) and delete both relations", + name: "should remove a member (non-owner) and delete the member relation", setup: func(policySvc *mocks.PolicyService, relSvc *mocks.RelationService, roleSvc *mocks.RoleService, grpSvc *mocks.GroupService, userSvc *mocks.UserService, auditRepo *mocks.AuditRecordRepository) { grpSvc.EXPECT().Get(ctx, groupID).Return(grp, nil) userSvc.EXPECT().GetByID(ctx, userID).Return(enabledUser, nil) @@ -2242,7 +2209,6 @@ func TestService_RemoveGroupMember(t *testing.T) { policySvc.EXPECT().Delete(ctx, "p1").Return(nil) obj := relation.Object{ID: groupID, Namespace: schema.GroupNamespace} sub := relation.Subject{ID: userID, Namespace: schema.UserPrincipal} - relSvc.EXPECT().Delete(ctx, relation.Relation{Object: obj, Subject: sub, RelationName: schema.OwnerRelationName}).Return(relation.ErrNotExist) relSvc.EXPECT().Delete(ctx, relation.Relation{Object: obj, Subject: sub, RelationName: schema.MemberRelationName}).Return(nil) auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, nil) }, @@ -2259,7 +2225,6 @@ func TestService_RemoveGroupMember(t *testing.T) { policySvc.EXPECT().DeleteWithMinRoleGuard(ctx, "p1", ownerRoleID).Return(nil) obj := relation.Object{ID: groupID, Namespace: schema.GroupNamespace} sub := relation.Subject{ID: userID, Namespace: schema.UserPrincipal} - relSvc.EXPECT().Delete(ctx, relation.Relation{Object: obj, Subject: sub, RelationName: schema.OwnerRelationName}).Return(nil) relSvc.EXPECT().Delete(ctx, relation.Relation{Object: obj, Subject: sub, RelationName: schema.MemberRelationName}).Return(relation.ErrNotExist) auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, nil) }, @@ -2325,9 +2290,7 @@ func TestService_RemoveAllGroupMembers(t *testing.T) { policySvc.EXPECT().Delete(ctx, "p1").Return(nil) policySvc.EXPECT().Delete(ctx, "p2").Return(nil) policySvc.EXPECT().Delete(ctx, "p3").Return(nil) - relSvc.EXPECT().Delete(ctx, relFor(schema.OwnerRelationName, userA)).Return(relation.ErrNotExist) relSvc.EXPECT().Delete(ctx, relFor(schema.MemberRelationName, userA)).Return(nil) - relSvc.EXPECT().Delete(ctx, relFor(schema.OwnerRelationName, userB)).Return(nil) relSvc.EXPECT().Delete(ctx, relFor(schema.MemberRelationName, userB)).Return(relation.ErrNotExist) svc := membership.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)), policySvc, relSvc, @@ -2348,7 +2311,6 @@ func TestService_RemoveAllGroupMembers(t *testing.T) { }, nil) policySvc.EXPECT().Delete(ctx, "p1").Return(errors.New("db down")) policySvc.EXPECT().Delete(ctx, "p2").Return(nil) - relSvc.EXPECT().Delete(ctx, relFor(schema.OwnerRelationName, userB)).Return(nil) relSvc.EXPECT().Delete(ctx, relFor(schema.MemberRelationName, userB)).Return(nil) svc := membership.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)), policySvc, relSvc, @@ -2382,21 +2344,12 @@ func TestService_OnGroupDeleted(t *testing.T) { }).Return([]policy.Policy{{ID: "principal-p1"}}, nil) policySvc.EXPECT().Delete(ctx, "principal-p1").Return(nil) - // unlinkGroupFromOrg: both hierarchy relations + // unlinkGroupFromOrg: the identity link relSvc.EXPECT().Delete(ctx, relation.Relation{ Object: relation.Object{ID: groupID, Namespace: schema.GroupNamespace}, Subject: relation.Subject{ID: orgID, Namespace: schema.OrganizationNamespace}, RelationName: schema.OrganizationRelationName, }).Return(nil) - relSvc.EXPECT().Delete(ctx, relation.Relation{ - Object: relation.Object{ID: orgID, Namespace: schema.OrganizationNamespace}, - Subject: relation.Subject{ - ID: groupID, - Namespace: schema.GroupNamespace, - SubRelationName: schema.MemberRelationName, - }, - RelationName: schema.MemberRelationName, - }).Return(nil) svc := membership.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)), policySvc, relSvc, mocks.NewRoleService(t), mocks.NewOrgService(t), mocks.NewUserService(t), diff --git a/internal/bootstrap/schema/base_schema.zed b/internal/bootstrap/schema/base_schema.zed index 59a14f549..0cb224d27 100644 --- a/internal/bootstrap/schema/base_schema.zed +++ b/internal/bootstrap/schema/base_schema.zed @@ -30,8 +30,6 @@ definition app/organization { relation platform: app/platform relation granted: app/rolebinding relation pat_granted: app/rolebinding - relation member: app/user | app/group#member | app/serviceuser - relation owner: app/user | app/serviceuser // permissions // org @@ -68,7 +66,6 @@ definition app/group { relation org: app/organization relation granted: app/rolebinding relation member: app/user - relation owner: app/user | app/serviceuser // permissions permission delete = org->group_delete + granted->app_group_administer + granted->app_group_delete diff --git a/internal/bootstrap/testdata/compiled_schema.zed b/internal/bootstrap/testdata/compiled_schema.zed index f8678346d..705a5bf4d 100644 --- a/internal/bootstrap/testdata/compiled_schema.zed +++ b/internal/bootstrap/testdata/compiled_schema.zed @@ -9,7 +9,6 @@ definition app/group { // relations relation org: app/organization - relation owner: app/user | app/serviceuser permission update = org->group_update + granted->app_group_administer + granted->app_group_update } @@ -45,8 +44,6 @@ definition app/organization { permission grouplist = platform->superuser + granted->app_organization_administer + granted->app_organization_grouplist permission invitationcreate = platform->superuser + granted->app_organization_administer + granted->app_organization_invitationcreate permission invitationlist = platform->superuser + granted->app_organization_administer + granted->app_organization_invitationlist - relation member: app/user | app/group#member | app/serviceuser - relation owner: app/user | app/serviceuser relation pat_granted: app/rolebinding // relations diff --git a/test/e2e/regression/api_test.go b/test/e2e/regression/api_test.go index eccd28145..df1952a23 100644 --- a/test/e2e/regression/api_test.go +++ b/test/e2e/regression/api_test.go @@ -1593,7 +1593,7 @@ func (s *APIRegressionTestSuite) TestUserAPI() { func (s *APIRegressionTestSuite) TestRelationAPI() { ctxOrgAdminAuth := testbench.ContextWithAuth(context.Background(), s.adminCookie) - s.Run("1. an owner relation alone gives no org access; an owner role policy does", func() { + s.Run("1. an owner relation cannot be created; an owner role policy grants access", func() { existingOrg, err := s.testBench.Client.GetOrganization(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.GetOrganizationRequest{ Id: "org-relation-1", })) @@ -1614,30 +1614,17 @@ func (s *APIRegressionTestSuite) TestRelationAPI() { s.Assert().NoError(err) s.Assert().Equal(1, len(orgUsersResp.Msg.GetUsers())) - // a raw owner relation grants nothing: permissions come from roles only + // the org has no owner relation in the schema anymore — writing one must fail _, err = s.testBench.Client.CreateRelation(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreateRelationRequest{Body: &frontierv1beta1.RelationRequestBody{ Object: schema.JoinNamespaceAndResourceID(schema.OrganizationNamespace, existingOrg.Msg.GetOrganization().GetId()), Subject: schema.JoinNamespaceAndResourceID(schema.UserPrincipal, createUserResp.Msg.GetUser().GetId()), - Relation: schema.OwnerRelationName, + Relation: "owner", }})) - s.Assert().NoError(err) + s.Assert().Error(err) relUserCookie, err := testbench.AuthenticateUser(context.Background(), s.testBench.Client, createUserResp.Msg.GetUser().GetEmail()) s.Require().NoError(err) ctxOrgUserAuth := testbench.ContextWithAuth(context.Background(), relUserCookie) - checkPermission, err := s.testBench.Client.CheckResourcePermission(ctxOrgUserAuth, connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ - Resource: schema.JoinNamespaceAndResourceID(schema.OrganizationNamespace, existingOrg.Msg.GetOrganization().GetId()), - Permission: schema.DeletePermission, - })) - s.Assert().NoError(err) - s.Assert().Equal(false, checkPermission.Msg.GetStatus()) - - checkReadPermission, err := s.testBench.Client.CheckResourcePermission(ctxOrgUserAuth, connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ - Resource: schema.JoinNamespaceAndResourceID(schema.OrganizationNamespace, existingOrg.Msg.GetOrganization().GetId()), - Permission: schema.GetPermission, - })) - s.Assert().NoError(err) - s.Assert().Equal(false, checkReadPermission.Msg.GetStatus()) // the owner role policy is what grants access _, err = s.testBench.Client.CreatePolicy(ctxOrgAdminAuth, connect.NewRequest(&frontierv1beta1.CreatePolicyRequest{ @@ -1649,7 +1636,7 @@ func (s *APIRegressionTestSuite) TestRelationAPI() { })) s.Assert().NoError(err) - checkPermission, err = s.testBench.Client.CheckResourcePermission(ctxOrgUserAuth, connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ + checkPermission, err := s.testBench.Client.CheckResourcePermission(ctxOrgUserAuth, connect.NewRequest(&frontierv1beta1.CheckResourcePermissionRequest{ Resource: schema.JoinNamespaceAndResourceID(schema.OrganizationNamespace, existingOrg.Msg.GetOrganization().GetId()), Permission: schema.DeletePermission, }))