diff --git a/components/gitpod-db/src/team-db.spec.db.ts b/components/gitpod-db/src/team-db.spec.db.ts index 704dcbee89f395..b920527df18df2 100644 --- a/components/gitpod-db/src/team-db.spec.db.ts +++ b/components/gitpod-db/src/team-db.spec.db.ts @@ -62,6 +62,57 @@ import { DBIdentity } from './typeorm/entity/db-identity'; expect(members[0].primaryEmail).to.eq('tom@example.com'); } + @test(timeout(15000)) + public async findTeamWhenUserIsSoleOwner() { + const user = await this.userDb.newUser(); + user.identities.push({ authProviderId: 'GitHub', authId: '2345', authName: 'Nana', primaryEmail: 'nana@example.com' }); + await this.userDb.storeUser(user); + + const ownTeam = await this.db.createTeam(user.id, 'My Own Team'); + + const teams = await this.db.findTeamsByUserAsSoleOwner(user.id); + + expect(teams.length).to.eq(1); + expect(teams[0].id).to.eq(ownTeam.id); + + } + + @test(timeout(10000)) + public async findTeamWhenUserIsSoleOwnerWithMembers() { + const user = await this.userDb.newUser(); + user.identities.push({ authProviderId: 'GitHub', authId: '2345', authName: 'Nana', primaryEmail: 'nana@example.com' }); + await this.userDb.storeUser(user); + const user2 = await this.userDb.newUser(); + user.identities.push({ authProviderId: 'GitLab', authId: '4567', authName: 'Dudu', primaryEmail: 'dudu@example.com' }); + await this.userDb.storeUser(user2); + + const ownTeam = await this.db.createTeam(user.id, 'My Own Team With Members'); + await this.db.addMemberToTeam(user2.id, ownTeam.id); + + const teams = await this.db.findTeamsByUserAsSoleOwner(user.id); + + expect(teams.length).to.eq(1); + expect(teams[0].id).to.eq(ownTeam.id); + + } + + @test(timeout(10000)) + public async findNoTeamWhenCoOwned() { + const user = await this.userDb.newUser(); + user.identities.push({ authProviderId: 'GitHub', authId: '2345', authName: 'Nana', primaryEmail: 'nana@example.com' }); + await this.userDb.storeUser(user); + const user2 = await this.userDb.newUser(); + user.identities.push({ authProviderId: 'GitLab', authId: '4567', authName: 'Dudu', primaryEmail: 'dudu@example.com' }); + await this.userDb.storeUser(user2); + + const jointTeam = await this.db.createTeam(user.id, 'Joint Team'); + await this.db.addMemberToTeam(user2.id, jointTeam.id); + await this.db.setTeamMemberRole(user2.id, jointTeam.id, 'owner'); + + const teams = await this.db.findTeamsByUserAsSoleOwner(user.id); + + expect(teams.length).to.eq(0); + } } module.exports = new TeamDBSpec() diff --git a/components/gitpod-db/src/team-db.ts b/components/gitpod-db/src/team-db.ts index 1c08e246613b69..06d658b1860672 100644 --- a/components/gitpod-db/src/team-db.ts +++ b/components/gitpod-db/src/team-db.ts @@ -11,6 +11,7 @@ export interface TeamDB { findTeamById(teamId: string): Promise; findMembersByTeam(teamId: string): Promise; findTeamsByUser(userId: string): Promise; + findTeamsByUserAsSoleOwner(userId: string): Promise; createTeam(userId: string, name: string): Promise; addMemberToTeam(userId: string, teamId: string): Promise; setTeamMemberRole(userId: string, teamId: string, role: TeamMemberRole): Promise; diff --git a/components/gitpod-db/src/typeorm/team-db-impl.ts b/components/gitpod-db/src/typeorm/team-db-impl.ts index 9e5f4d3bc36307..48012cf8677c1e 100644 --- a/components/gitpod-db/src/typeorm/team-db-impl.ts +++ b/components/gitpod-db/src/typeorm/team-db-impl.ts @@ -103,7 +103,22 @@ export class TeamDBImpl implements TeamDB { const membershipRepo = await this.getMembershipRepo(); const memberships = await membershipRepo.find({ userId, deleted: false }); const teams = await teamRepo.findByIds(memberships.map(m => m.teamId)); - return teams.filter(t => !t.deleted); + return teams.filter(t => !t.markedDeleted); + } + + public async findTeamsByUserAsSoleOwner(userId: string): Promise { + const teamRepo = await this.getTeamRepo(); + const membershipRepo = await this.getMembershipRepo(); + + // Find the memberships of this user, + // and among the memberships, get the teams where the user is the sole owner + const soleOwnedTeamIds = await membershipRepo.query(`SELECT tm_2.teamId FROM d_b_team_membership tm_1 + JOIN d_b_team_membership tm_2 ON tm_1.userId = ? AND tm_1.teamId=tm_2.teamId AND tm_1.role = 'owner' AND tm_2.role = 'owner' GROUP BY tm_2.teamId HAVING COUNT(tm_2.teamId) = 1;`, [userId]); + + // @ts-ignore + const teams = await teamRepo.findByIds(soleOwnedTeamIds.map(m => m.teamId)); + + return teams.filter(t => !t.markedDeleted); } public async createTeam(userId: string, name: string): Promise { diff --git a/components/server/src/user/user-deletion-service.ts b/components/server/src/user/user-deletion-service.ts index 0985b81a23bf2c..6ebdcfb70dfc9c 100644 --- a/components/server/src/user/user-deletion-service.ts +++ b/components/server/src/user/user-deletion-service.ts @@ -5,7 +5,7 @@ */ import { injectable, inject } from "inversify"; -import { UserDB, WorkspaceDB, UserStorageResourcesDB, TeamDB } from '@gitpod/gitpod-db/lib'; +import { UserDB, WorkspaceDB, UserStorageResourcesDB, TeamDB, ProjectDB } from '@gitpod/gitpod-db/lib'; import { User, Workspace } from "@gitpod/gitpod-protocol"; import { StorageClient } from "../storage/storage-client"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; @@ -23,6 +23,7 @@ export class UserDeletionService { @inject(WorkspaceDB) protected readonly workspaceDb: WorkspaceDB; @inject(UserStorageResourcesDB) protected readonly userStorageResourcesDb: UserStorageResourcesDB; @inject(TeamDB) protected readonly teamDb: TeamDB; + @inject(ProjectDB) protected readonly projectDb: ProjectDB; @inject(StorageClient) protected readonly storageClient: StorageClient; @inject(WorkspaceManagerClientProvider) protected readonly workspaceManagerClientProvider: WorkspaceManagerClientProvider; @inject(WorkspaceDeletionService) protected readonly workspaceDeletionService: WorkspaceDeletionService; @@ -74,8 +75,12 @@ export class UserDeletionService { this.userStorageResourcesDb.deleteAllForUser(user.id), // Bucket this.deleteUserBucket(id), + // Teams owned only by this user + this.deleteSoleOwnedTeams(id), // Team memberships this.deleteTeamMemberships(id), + // User projects + this.deleteUserProjects(id), ]); // Track the deletion Event for Analytics Purposes @@ -140,6 +145,23 @@ export class UserDeletionService { await Promise.all(teams.map(t => this.teamDb.removeMemberFromTeam(userId, t.id))); } + protected async deleteSoleOwnedTeams(userId: string) { + const ownedTeams = await this.teamDb.findTeamsByUserAsSoleOwner(userId); + + for (const team of ownedTeams) { + const teamProjects = await this.projectDb.findTeamProjects(team.id); + await Promise.all(teamProjects.map(project => this.projectDb.markDeleted(project.id))); + } + + await Promise.all(ownedTeams.map(t => this.teamDb.deleteTeam(t.id))); + } + + protected async deleteUserProjects(id: string) { + const userProjects = await this.projectDb.findUserProjects(id); + + await Promise.all(userProjects.map(project => this.projectDb.markDeleted(project.id))); + } + anonymizeWorkspace(ws: Workspace) { ws.context.title = 'deleted-title'; ws.context.normalizedContextURL = 'deleted-normalizedContextURL';