From d93c009b4f35d152a65ffdda4c41591d92fb7b1e Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Tue, 6 May 2025 07:15:42 +0000 Subject: [PATCH 1/3] [server] Fix OrganizationService.addOrUpdateMember --- .../server/src/orgs/organization-service.spec.db.ts | 7 +++++-- components/server/src/orgs/organization-service.ts | 11 +++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/components/server/src/orgs/organization-service.spec.db.ts b/components/server/src/orgs/organization-service.spec.db.ts index 8999d39ca58753..c7fd2d321c29ce 100644 --- a/components/server/src/orgs/organization-service.spec.db.ts +++ b/components/server/src/orgs/organization-service.spec.db.ts @@ -342,8 +342,11 @@ describe("OrganizationService", async () => { const myOrg = await os.createOrganization(adminId, "My Org"); expect((await os.listMembers(adminId, myOrg.id)).length).to.eq(1); - // add a another member which should become owner - await os.addOrUpdateMember(adminId, myOrg.id, owner.id, "member"); + await withTestCtx(adminId, async () => { + // add a another member which should become owner + await os.addOrUpdateMember(adminId, myOrg.id, owner.id, "member"); + }); + // admin should have been removed const members = await os.listMembers(owner.id, myOrg.id); expect(members.length).to.eq(1); diff --git a/components/server/src/orgs/organization-service.ts b/components/server/src/orgs/organization-service.ts index eccff66aeff447..c99004418263ab 100644 --- a/components/server/src/orgs/organization-service.ts +++ b/components/server/src/orgs/organization-service.ts @@ -403,7 +403,14 @@ export class OrganizationService { // we can remove the built-in installation admin if we have added an owner if (!hasOtherRegularOwners && members.some((m) => m.userId === BUILTIN_INSTLLATION_ADMIN_USER_ID)) { try { - await this.removeOrganizationMember(memberId, orgId, BUILTIN_INSTLLATION_ADMIN_USER_ID, txCtx); + await runWithSubjectId(SYSTEM_USER, async () => { + return this.removeOrganizationMember( + SYSTEM_USER_ID, + orgId, + BUILTIN_INSTLLATION_ADMIN_USER_ID, + txCtx, + ); + }); } catch (error) { log.warn("Failed to remove built-in installation admin from organization.", error); } @@ -476,7 +483,7 @@ export class OrganizationService { event: "team_user_removed", properties: { team_id: orgId, - removed_user_id: userId, + removed_user_id: memberId, }, }); } From d656bc60c7fe0b88ead8e74d31155330bce3b0a6 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Tue, 6 May 2025 07:21:38 +0000 Subject: [PATCH 2/3] [server] OrganizationService test: apply withTestCtx --- .../src/orgs/organization-service.spec.db.ts | 296 +++++++++++------- 1 file changed, 185 insertions(+), 111 deletions(-) diff --git a/components/server/src/orgs/organization-service.spec.db.ts b/components/server/src/orgs/organization-service.spec.db.ts index c7fd2d321c29ce..1d12a0c9c68bc7 100644 --- a/components/server/src/orgs/organization-service.spec.db.ts +++ b/components/server/src/orgs/organization-service.spec.db.ts @@ -88,8 +88,10 @@ describe("OrganizationService", async () => { authProviderId: "github", }, }); - org = await os.createOrganization(owner.id, "myorg"); - const invite = await os.getOrCreateInvite(owner.id, org.id); + const invite = await withTestCtx(owner.id, async () => { + org = await os.createOrganization(owner.id, "myorg"); + return await os.getOrCreateInvite(owner.id, org.id); + }); member = await userService.createUser({ identity: { @@ -98,7 +100,9 @@ describe("OrganizationService", async () => { authProviderId: "github", }, }); - await os.addOrUpdateMember(owner.id, org.id, member.id, "member", { flexibleRole: false }); + await withTestCtx(owner.id, () => + os.addOrUpdateMember(owner.id, org.id, member.id, "member", { flexibleRole: false }), + ); collaborator = await userService.createUser({ identity: { @@ -110,7 +114,7 @@ describe("OrganizationService", async () => { await withTestCtx(SYSTEM_USER, () => os.joinOrganization(collaborator.id, invite.id)); - org2 = await os.createOrganization(owner.id, "org2"); + org2 = await withTestCtx(owner.id, () => os.createOrganization(owner.id, "org2")); stranger = await userService.createUser({ identity: { @@ -129,37 +133,51 @@ describe("OrganizationService", async () => { }); it("should deleteOrganization", async () => { - await expectError(ErrorCodes.PERMISSION_DENIED, os.deleteOrganization(member.id, org.id)); - await expectError(ErrorCodes.PERMISSION_DENIED, os.deleteOrganization(collaborator.id, org.id)); - await expectError(ErrorCodes.NOT_FOUND, os.deleteOrganization(stranger.id, org.id)); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(member.id, () => os.deleteOrganization(member.id, org.id)), + ); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(collaborator.id, () => os.deleteOrganization(collaborator.id, org.id)), + ); + await expectError(ErrorCodes.NOT_FOUND, () => + withTestCtx(stranger.id, () => os.deleteOrganization(stranger.id, org.id)), + ); - await os.deleteOrganization(owner.id, org.id); + await withTestCtx(owner.id, () => os.deleteOrganization(owner.id, org.id)); }); it("should getOrCreateInvite and resetInvite", async () => { expect(org.name).to.equal("myorg"); - const invite = await os.getOrCreateInvite(owner.id, org.id); + const invite = await withTestCtx(owner.id, () => os.getOrCreateInvite(owner.id, org.id)); expect(invite).to.not.be.undefined; - const invite2 = await os.getOrCreateInvite(member.id, org.id); + const invite2 = await withTestCtx(member.id, () => os.getOrCreateInvite(member.id, org.id)); expect(invite2.id).to.equal(invite.id); - const invite3 = await os.resetInvite(owner.id, org.id); + const invite3 = await withTestCtx(owner.id, () => os.resetInvite(owner.id, org.id)); expect(invite3.id).to.not.equal(invite.id); - const invite4 = await os.resetInvite(member.id, org.id); + const invite4 = await withTestCtx(member.id, () => os.resetInvite(member.id, org.id)); expect(invite4.id).to.not.equal(invite3.id); - await expectError(ErrorCodes.PERMISSION_DENIED, os.getOrCreateInvite(collaborator.id, org.id)); - await expectError(ErrorCodes.PERMISSION_DENIED, os.resetInvite(collaborator.id, org.id)); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(collaborator.id, () => os.getOrCreateInvite(collaborator.id, org.id)), + ); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(collaborator.id, () => os.resetInvite(collaborator.id, org.id)), + ); - await expectError(ErrorCodes.NOT_FOUND, os.getOrCreateInvite(stranger.id, org.id)); - await expectError(ErrorCodes.NOT_FOUND, os.resetInvite(stranger.id, org.id)); + await expectError(ErrorCodes.NOT_FOUND, () => + withTestCtx(stranger.id, () => os.getOrCreateInvite(stranger.id, org.id)), + ); + await expectError(ErrorCodes.NOT_FOUND, () => + withTestCtx(stranger.id, () => os.resetInvite(stranger.id, org.id)), + ); }); it("re-join org should not change role", async () => { - const invite = await os.getOrCreateInvite(owner.id, org.id); + const invite = await withTestCtx(owner.id, () => os.getOrCreateInvite(owner.id, org.id)); expect(invite).to.not.be.undefined; await assertUserRole(owner.id, "owner"); @@ -177,55 +195,67 @@ describe("OrganizationService", async () => { }); it("should listMembers", async () => { - let members = await os.listMembers(owner.id, org.id); + let members = await withTestCtx(owner.id, () => os.listMembers(owner.id, org.id)); expect(members.length).to.eq(3); expect(members.some((m) => m.userId === owner.id)).to.be.true; expect(members.some((m) => m.userId === member.id)).to.be.true; - members = await os.listMembers(member.id, org.id); + members = await withTestCtx(member.id, () => os.listMembers(member.id, org.id)); expect(members.length).to.eq(3); expect(members.some((m) => m.userId === owner.id)).to.be.true; expect(members.some((m) => m.userId === member.id)).to.be.true; - await expectError(ErrorCodes.PERMISSION_DENIED, os.listMembers(collaborator.id, org.id)); - await expectError(ErrorCodes.NOT_FOUND, () => os.listMembers(stranger.id, org.id)); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(collaborator.id, () => os.listMembers(collaborator.id, org.id)), + ); + await expectError(ErrorCodes.NOT_FOUND, () => + withTestCtx(stranger.id, () => os.listMembers(stranger.id, org.id)), + ); }); const assertUserRole = async (userId: string, role: TeamMemberRole) => { - const list = await os.listMembers(owner.id, org.id); + const list = await withTestCtx(owner.id, () => os.listMembers(owner.id, org.id)); expect(list.find((m) => m.userId === userId)?.role).to.be.equal(role); }; it("should setOrganizationMemberRole and removeOrganizationMember", async () => { - await expectError(ErrorCodes.PERMISSION_DENIED, os.addOrUpdateMember(member.id, org.id, owner.id, "member")); - await expectError( - ErrorCodes.PERMISSION_DENIED, - os.addOrUpdateMember(collaborator.id, org.id, owner.id, "member"), + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(member.id, () => os.addOrUpdateMember(member.id, org.id, owner.id, "member")), + ); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(collaborator.id, () => os.addOrUpdateMember(collaborator.id, org.id, owner.id, "member")), ); // try upgrade the member to owner - await expectError(ErrorCodes.PERMISSION_DENIED, os.addOrUpdateMember(member.id, org.id, member.id, "owner")); - await expectError( - ErrorCodes.PERMISSION_DENIED, - os.addOrUpdateMember(collaborator.id, org.id, member.id, "owner"), + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(member.id, () => os.addOrUpdateMember(member.id, org.id, member.id, "owner")), + ); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(collaborator.id, () => os.addOrUpdateMember(collaborator.id, org.id, member.id, "owner")), ); // try removing the owner - await expectError(ErrorCodes.PERMISSION_DENIED, os.removeOrganizationMember(member.id, org.id, owner.id)); - await expectError(ErrorCodes.PERMISSION_DENIED, os.removeOrganizationMember(collaborator.id, org.id, owner.id)); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(member.id, () => os.removeOrganizationMember(member.id, org.id, owner.id)), + ); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(collaborator.id, () => os.removeOrganizationMember(collaborator.id, org.id, owner.id)), + ); // owner can't downgrade if org only have on owner - await os.addOrUpdateMember(owner.id, org.id, owner.id, "member"); + await withTestCtx(owner.id, () => os.addOrUpdateMember(owner.id, org.id, owner.id, "member")); await assertUserRole(owner.id, "owner"); // owners can upgrade members - await os.addOrUpdateMember(owner.id, org.id, member.id, "owner"); + await withTestCtx(owner.id, () => os.addOrUpdateMember(owner.id, org.id, member.id, "owner")); // owner can downgrade themselves - await os.addOrUpdateMember(owner.id, org.id, owner.id, "member"); + await withTestCtx(owner.id, () => os.addOrUpdateMember(owner.id, org.id, owner.id, "member")); // assert that the member no longer has owner permissions - await expectError(ErrorCodes.PERMISSION_DENIED, os.deleteOrganization(owner.id, org.id)); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(owner.id, () => os.deleteOrganization(owner.id, org.id)), + ); // owner and member have switched roles now const previouslyMember = member; @@ -233,21 +263,27 @@ describe("OrganizationService", async () => { owner = previouslyMember; // owner can downgrade themselves only if they are not the last owner - await os.addOrUpdateMember(owner.id, org.id, owner.id, "member"); + await withTestCtx(owner.id, () => os.addOrUpdateMember(owner.id, org.id, owner.id, "member")); // verify they are still an owner await assertUserRole(owner.id, "owner"); // owner can delete themselves only if they are not the last owner - await expectError(ErrorCodes.CONFLICT, os.removeOrganizationMember(owner.id, org.id, owner.id)); + await expectError(ErrorCodes.CONFLICT, () => + withTestCtx(owner.id, () => os.removeOrganizationMember(owner.id, org.id, owner.id)), + ); // members can remove themselves - await os.removeOrganizationMember(member.id, org.id, member.id); + await withTestCtx(member.id, () => os.removeOrganizationMember(member.id, org.id, member.id)); // collaborators can remove themselves - await os.removeOrganizationMember(collaborator.id, org.id, collaborator.id); + await withTestCtx(collaborator.id, () => os.removeOrganizationMember(collaborator.id, org.id, collaborator.id)); // try remove the member again - await expectError(ErrorCodes.NOT_FOUND, os.removeOrganizationMember(member.id, org.id, member.id)); - await expectError(ErrorCodes.NOT_FOUND, os.removeOrganizationMember(collaborator.id, org.id, collaborator.id)); + await expectError(ErrorCodes.NOT_FOUND, () => + withTestCtx(member.id, () => os.removeOrganizationMember(member.id, org.id, member.id)), + ); + await expectError(ErrorCodes.NOT_FOUND, () => + withTestCtx(collaborator.id, () => os.removeOrganizationMember(collaborator.id, org.id, collaborator.id)), + ); }); it("should delete owned user when removing it", async () => { @@ -259,88 +295,112 @@ describe("OrganizationService", async () => { authProviderId: "github", }, }); - await os.addOrUpdateMember(owner.id, org.id, ownedMember.id, "member"); + await withTestCtx(owner.id, () => os.addOrUpdateMember(owner.id, org.id, ownedMember.id, "member")); - const members = await os.listMembers(owner.id, org.id); + const members = await withTestCtx(owner.id, () => os.listMembers(owner.id, org.id)); expect(members.some((m) => m.userId === ownedMember.id)).to.be.true; // remove it and assert it's gone - await os.removeOrganizationMember(owner.id, org.id, ownedMember.id); - const members2 = await os.listMembers(owner.id, org.id); + await withTestCtx(owner.id, () => os.removeOrganizationMember(owner.id, org.id, ownedMember.id)); + const members2 = await withTestCtx(owner.id, () => os.listMembers(owner.id, org.id)); expect(members2.some((m) => m.userId === ownedMember.id)).to.be.false; // also assert that the user is gone - const deleted = await userService.findUserById(ownedMember.id, ownedMember.id); + const deleted = await withTestCtx(ownedMember.id, () => + userService.findUserById(ownedMember.id, ownedMember.id), + ); // await expectError(ErrorCodes.NOT_FOUND, () => deleted); expect(deleted.markedDeleted).to.be.true; }); it("should listOrganizationsByMember", async () => { - await os.createOrganization(owner.id, "org1"); - await os.createOrganization(owner.id, "org2"); - let orgs = await os.listOrganizationsByMember(owner.id, owner.id); + await withTestCtx(owner.id, async () => { + await os.createOrganization(owner.id, "org1"); + await os.createOrganization(owner.id, "org2"); + }); + let orgs = await withTestCtx(owner.id, () => os.listOrganizationsByMember(owner.id, owner.id)); expect(orgs.length).to.eq(4); - orgs = await os.listOrganizationsByMember(member.id, member.id); + orgs = await withTestCtx(member.id, () => os.listOrganizationsByMember(member.id, member.id)); expect(orgs.length).to.eq(1); - orgs = await os.listOrganizationsByMember(collaborator.id, collaborator.id); + orgs = await withTestCtx(collaborator.id, () => os.listOrganizationsByMember(collaborator.id, collaborator.id)); expect(orgs.length).to.eq(1); - orgs = await os.listOrganizationsByMember(stranger.id, stranger.id); + orgs = await withTestCtx(stranger.id, () => os.listOrganizationsByMember(stranger.id, stranger.id)); expect(orgs.length).to.eq(0); - await expectError(ErrorCodes.NOT_FOUND, os.listOrganizationsByMember(stranger.id, owner.id)); + await expectError(ErrorCodes.NOT_FOUND, () => + withTestCtx(stranger.id, () => os.listOrganizationsByMember(stranger.id, owner.id)), + ); }); it("should getOrganization", async () => { - const foundOrg = await os.getOrganization(owner.id, org.id); + const foundOrg = await withTestCtx(owner.id, () => os.getOrganization(owner.id, org.id)); expect(foundOrg.name).to.equal(org.name); - const foundByMember = await os.getOrganization(member.id, org.id); + const foundByMember = await withTestCtx(member.id, () => os.getOrganization(member.id, org.id)); expect(foundByMember.name).to.equal(org.name); - const foundByCollaborator = await os.getOrganization(collaborator.id, org.id); + const foundByCollaborator = await withTestCtx(collaborator.id, () => + os.getOrganization(collaborator.id, org.id), + ); expect(foundByCollaborator.name).to.equal(org.name); - await expectError(ErrorCodes.NOT_FOUND, os.getOrganization(stranger.id, org.id)); + await expectError(ErrorCodes.NOT_FOUND, () => + withTestCtx(stranger.id, () => os.getOrganization(stranger.id, org.id)), + ); }); it("should updateOrganization", async () => { org.name = "newName"; - await os.updateOrganization(owner.id, org.id, org); - const updated = await os.getOrganization(owner.id, org.id); + await withTestCtx(owner.id, () => os.updateOrganization(owner.id, org.id, org)); + const updated = await withTestCtx(owner.id, () => os.getOrganization(owner.id, org.id)); expect(updated.name).to.equal(org.name); - await expectError(ErrorCodes.PERMISSION_DENIED, os.updateOrganization(member.id, org.id, org)); - await expectError(ErrorCodes.PERMISSION_DENIED, os.updateOrganization(collaborator.id, org.id, org)); - await expectError(ErrorCodes.NOT_FOUND, os.updateOrganization(stranger.id, org.id, org)); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(member.id, () => os.updateOrganization(member.id, org.id, org)), + ); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(collaborator.id, () => os.updateOrganization(collaborator.id, org.id, org)), + ); + await expectError(ErrorCodes.NOT_FOUND, () => + withTestCtx(stranger.id, () => os.updateOrganization(stranger.id, org.id, org)), + ); }); it("should getSettings and updateSettings", async () => { - const settings = await os.getSettings(owner.id, org.id); + const settings = await withTestCtx(owner.id, () => os.getSettings(owner.id, org.id)); expect(settings).to.not.be.undefined; expect(settings).to.not.be.null; settings.workspaceSharingDisabled = true; - await os.updateSettings(owner.id, org.id, settings); - const updated = await os.getSettings(owner.id, org.id); + await withTestCtx(owner.id, () => os.updateSettings(owner.id, org.id, settings)); + const updated = await withTestCtx(owner.id, () => os.getSettings(owner.id, org.id)); expect(updated.workspaceSharingDisabled).to.be.true; - await expectError(ErrorCodes.PERMISSION_DENIED, os.updateSettings(member.id, org.id, settings)); - await expectError(ErrorCodes.PERMISSION_DENIED, os.updateSettings(collaborator.id, org.id, settings)); - await expectError(ErrorCodes.NOT_FOUND, os.updateSettings(stranger.id, org.id, settings)); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(member.id, () => os.updateSettings(member.id, org.id, settings)), + ); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(collaborator.id, () => os.updateSettings(collaborator.id, org.id, settings)), + ); + await expectError(ErrorCodes.NOT_FOUND, () => + withTestCtx(stranger.id, () => os.updateSettings(stranger.id, org.id, settings)), + ); }); it("should allow admins to do its thing", async () => { - await os.updateOrganization(adminId, org.id, { name: "Name Changed" }); - const updated = await os.getOrganization(adminId, org.id); - expect(updated.name).to.equal("Name Changed"); + await withTestCtx(adminId, async () => { + await os.updateOrganization(adminId, org.id, { name: "Name Changed" }); + const updated = await os.getOrganization(adminId, org.id); + expect(updated.name).to.equal("Name Changed"); - await os.updateSettings(adminId, org.id, { workspaceSharingDisabled: true }); - const settings = await os.getSettings(adminId, org.id); - expect(settings.workspaceSharingDisabled).to.be.true; + await os.updateSettings(adminId, org.id, { workspaceSharingDisabled: true }); + const settings = await os.getSettings(adminId, org.id); + expect(settings.workspaceSharingDisabled).to.be.true; + }); }); it("should remove the admin on first join", async () => { - const myOrg = await os.createOrganization(adminId, "My Org"); - expect((await os.listMembers(adminId, myOrg.id)).length).to.eq(1); + const myOrg = await withTestCtx(adminId, () => os.createOrganization(adminId, "My Org")); + expect((await withTestCtx(adminId, () => os.listMembers(adminId, myOrg.id))).length).to.eq(1); await withTestCtx(adminId, async () => { // add a another member which should become owner @@ -348,23 +408,23 @@ describe("OrganizationService", async () => { }); // admin should have been removed - const members = await os.listMembers(owner.id, myOrg.id); + const members = await withTestCtx(owner.id, () => os.listMembers(owner.id, myOrg.id)); expect(members.length).to.eq(1); expect(members.some((m) => m.userId === owner.id && m.role === "owner")).to.be.true; }); it("should listOrganizations (for installation)", async () => { - const strangerOrg = await os.createOrganization(stranger.id, "stranger-org"); - let orgs = await os.listOrganizations(owner.id, {}, "installation"); + const strangerOrg = await withTestCtx(stranger.id, () => os.createOrganization(stranger.id, "stranger-org")); + let orgs = await withTestCtx(owner.id, () => os.listOrganizations(owner.id, {}, "installation")); expect(orgs.rows.map((o) => o.id)).to.contain(org.id); expect(orgs.rows.map((o) => o.id)).to.contain(org2.id); expect(orgs.total).to.eq(2); - orgs = await os.listOrganizations(stranger.id, {}, "installation"); + orgs = await withTestCtx(stranger.id, () => os.listOrganizations(stranger.id, {}, "installation")); expect(orgs.rows[0].id).to.eq(strangerOrg.id); expect(orgs.total).to.eq(1); - orgs = await os.listOrganizations(adminId, {}, "installation"); + orgs = await withTestCtx(adminId, () => os.listOrganizations(adminId, {}, "installation")); expect(orgs.rows.some((org) => org.id === org.id)).to.be.true; expect(orgs.rows.some((org) => org.id === strangerOrg.id)).to.be.true; expect(orgs.total).to.eq(3); @@ -372,22 +432,24 @@ describe("OrganizationService", async () => { it("should listOrganizations (for member)", async () => { // Owner is member of both orgs - const ownerResult = await os.listOrganizations(owner.id, {}, "member"); + const ownerResult = await withTestCtx(owner.id, () => os.listOrganizations(owner.id, {}, "member")); expect(ownerResult.rows.map((o) => o.id)).to.include(org.id); expect(ownerResult.rows.map((o) => o.id)).to.include(org2.id); // Member is only in org1 - const memberResult = await os.listOrganizations(member.id, {}, "member"); + const memberResult = await withTestCtx(member.id, () => os.listOrganizations(member.id, {}, "member")); expect(memberResult.rows.map((o) => o.id)).to.include(org.id); expect(memberResult.rows.map((o) => o.id)).to.not.include(org2.id); // Collaborator is only in org1 - const collaboratorResults = await os.listOrganizations(collaborator.id, {}, "member"); + const collaboratorResults = await withTestCtx(collaborator.id, () => + os.listOrganizations(collaborator.id, {}, "member"), + ); expect(collaboratorResults.rows.map((o) => o.id)).to.include(org.id); expect(collaboratorResults.rows.map((o) => o.id)).to.not.include(org2.id); // Stranger is in no orgs - const strangerResult = await os.listOrganizations(stranger.id, {}, "member"); + const strangerResult = await withTestCtx(stranger.id, () => os.listOrganizations(stranger.id, {}, "member")); expect(strangerResult.total).to.equal(0); }); @@ -400,11 +462,13 @@ describe("OrganizationService", async () => { authProviderId: "github", }, }); - await os.addOrUpdateMember(owner.id, org.id, u2.id, "member"); - await assertUserRole(u2.id, "member"); - // flexibleRole: true + dataops should be collaborator - await os.addOrUpdateMember(owner.id, org.id, u2.id, "member", { flexibleRole: true }); - await assertUserRole(u2.id, "collaborator"); + await withTestCtx(owner.id, async () => { + await os.addOrUpdateMember(owner.id, org.id, u2.id, "member"); + await assertUserRole(u2.id, "member"); + // flexibleRole: true + dataops should be collaborator + await os.addOrUpdateMember(owner.id, org.id, u2.id, "member", { flexibleRole: true }); + await assertUserRole(u2.id, "collaborator"); + }); }); it("should add as set defaultRole with flexibleRole", async () => { @@ -412,7 +476,7 @@ describe("OrganizationService", async () => { dataops: false, }); await assertUserRole(collaborator.id, "collaborator"); - await os.updateSettings(adminId, org.id, { defaultRole: "owner" }); + await withTestCtx(adminId, () => os.updateSettings(adminId, org.id, { defaultRole: "owner" })); const u2 = await userService.createUser({ identity: { authId: "github|1234", @@ -420,7 +484,9 @@ describe("OrganizationService", async () => { authProviderId: "github", }, }); - await os.addOrUpdateMember(owner.id, org.id, u2.id, "member", { flexibleRole: true }); + await withTestCtx(owner.id, () => + os.addOrUpdateMember(owner.id, org.id, u2.id, "member", { flexibleRole: true }), + ); await assertUserRole(u2.id, "owner"); }); @@ -435,7 +501,7 @@ describe("OrganizationService", async () => { authProviderId: "github", }, }); - const invite = await os.getOrCreateInvite(owner.id, org.id); + const invite = await withTestCtx(owner.id, () => os.getOrCreateInvite(owner.id, org.id)); await withTestCtx(SYSTEM_USER, () => os.joinOrganization(u1.id, invite.id)); await assertUserRole(u1.id, "member"); @@ -455,8 +521,8 @@ describe("OrganizationService", async () => { }); it("should manage settings", async () => { - const myOrg = await os.createOrganization(adminId, "My Org"); - const settings = await os.getSettings(adminId, myOrg.id); + const myOrg = await withTestCtx(adminId, () => os.createOrganization(adminId, "My Org")); + const settings = await withTestCtx(adminId, () => os.getSettings(adminId, myOrg.id)); expect(settings).to.deep.eq({}, "initial setttings"); const assertUpdateSettings = async ( @@ -464,9 +530,9 @@ describe("OrganizationService", async () => { update: Partial, expected: OrganizationSettings, ) => { - const updated = await os.updateSettings(adminId, myOrg.id, update); + const updated = await withTestCtx(adminId, () => os.updateSettings(adminId, myOrg.id, update)); expect(updated).to.deep.eq(expected, message + " (update)"); - const verified = await os.getSettings(adminId, myOrg.id); + const verified = await withTestCtx(adminId, () => os.getSettings(adminId, myOrg.id)); expect(verified).to.deep.eq(expected, message + " (get)"); }; @@ -497,7 +563,9 @@ describe("OrganizationService", async () => { ); try { - await os.updateSettings(adminId, myOrg.id, { allowedWorkspaceClasses: ["foo"] }); + await withTestCtx(adminId, () => + os.updateSettings(adminId, myOrg.id, { allowedWorkspaceClasses: ["foo"] }), + ); expect.fail("should have failed"); } catch (err) { expect(err.message).to.equal("items in allowedWorkspaceClasses are not all allowed", "invalid classes"); @@ -517,7 +585,7 @@ describe("OrganizationService", async () => { throw new Error("invalid image"); }; try { - await os.updateSettings(adminId, myOrg.id, { defaultWorkspaceImage: "lalala" }); + await withTestCtx(adminId, () => os.updateSettings(adminId, myOrg.id, { defaultWorkspaceImage: "lalala" })); expect.fail("should have failed"); } catch (err) { expect(err.message).to.equal("invalid image", "should validate default workspace image"); @@ -552,12 +620,14 @@ describe("OrganizationService", async () => { // create an org const orgService = container.get(OrganizationService); - const myOrg = await orgService.createOrganization(owner.id, "my-org"); + const myOrg = await withTestCtx(owner.id, () => orgService.createOrganization(owner.id, "my-org")); // create org-owned user const member = await createOrgOwnedUser(os, myOrg.id); - await expectError(ErrorCodes.PERMISSION_DENIED, () => os.createOrganization(member.id, "member's crew")); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(member.id, () => os.createOrganization(member.id, "member's crew")), + ); }); it("org-owned users can't join another org", async () => { @@ -569,33 +639,37 @@ describe("OrganizationService", async () => { // create the orgs const orgService = container.get(OrganizationService); - const myOrg = await orgService.createOrganization(owner.id, "my-org"); - const anotherOrg = await orgService.createOrganization(owner.id, "another-org"); + const myOrg = await withTestCtx(owner.id, () => orgService.createOrganization(owner.id, "my-org")); + const anotherOrg = await withTestCtx(owner.id, () => orgService.createOrganization(owner.id, "another-org")); // create org-owned user const member = await createOrgOwnedUser(os, myOrg.id); - const failingInvite = await orgService.getOrCreateInvite(owner.id, anotherOrg.id); - await expectError(ErrorCodes.PERMISSION_DENIED, () => os.joinOrganization(member.id, failingInvite.id)); + const failingInvite = await withTestCtx(owner.id, () => orgService.getOrCreateInvite(owner.id, anotherOrg.id)); + await expectError(ErrorCodes.PERMISSION_DENIED, () => + withTestCtx(member.id, () => os.joinOrganization(member.id, failingInvite.id)), + ); }); it("should update pinnedEditorVersions", async () => { // Create a test organization - const myOrg = await os.createOrganization(adminId, "My Org"); + const myOrg = await withTestCtx(adminId, () => os.createOrganization(adminId, "My Org")); // Test 1: Set specific pinned editor versions const pinnedVersions = { code: "1.2.3", intellij: "4.5.6" }; - await os.updateSettings(adminId, myOrg.id, { pinnedEditorVersions: pinnedVersions }); + await withTestCtx(adminId, () => + os.updateSettings(adminId, myOrg.id, { pinnedEditorVersions: pinnedVersions }), + ); // Verify the settings were updated correctly - let settings = await os.getSettings(adminId, myOrg.id); + let settings = await withTestCtx(adminId, () => os.getSettings(adminId, myOrg.id)); expect(settings.pinnedEditorVersions).to.deep.equal(pinnedVersions); // Test 2: Unset all pinned versions by setting an empty object - await os.updateSettings(adminId, myOrg.id, { pinnedEditorVersions: {} }); + await withTestCtx(adminId, () => os.updateSettings(adminId, myOrg.id, { pinnedEditorVersions: {} })); // Verify all pinned versions were removed - settings = await os.getSettings(adminId, myOrg.id); + settings = await withTestCtx(adminId, () => os.getSettings(adminId, myOrg.id)); expect(settings.pinnedEditorVersions).to.deep.equal({}); }); }); From 7265d2d84d97b74b2d1791182d64f3517a25dd75 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Tue, 6 May 2025 12:14:02 +0000 Subject: [PATCH 3/3] [server, dashboard] Replace USER_DELETED with NOT_FOUND + error details, and ensure it's properly mapped across the API --- .../error-boundaries/QueryErrorBoundary.tsx | 4 +- .../ReloadPageErrorBoundary.tsx | 2 +- .../dashboard/src/hooks/use-user-loader.ts | 7 +- components/gitpod-protocol/go/error.go | 3 - .../src/messaging/error.spec.ts | 22 + .../gitpod-protocol/src/messaging/error.ts | 7 +- components/public-api/gitpod/v1/error.proto | 8 + components/public-api/go/v1/error.pb.go | 169 ++- .../java/io/gitpod/publicapi/v1/Error.java | 1048 ++++++++++++++++- .../src/public-api-converter.spec.ts | 18 + .../src/public-api-converter.ts | 17 + .../typescript/src/gitpod/v1/error_pb.ts | 74 ++ components/server/src/api/server.ts | 9 +- .../src/orgs/organization-service.spec.db.ts | 7 +- components/server/src/user/user-controller.ts | 12 +- .../server/src/user/user-service.spec.db.ts | 12 +- components/server/src/user/user-service.ts | 3 + .../src/workspace/gitpod-server-impl.ts | 3 - 18 files changed, 1376 insertions(+), 49 deletions(-) create mode 100644 components/gitpod-protocol/src/messaging/error.spec.ts diff --git a/components/dashboard/src/components/error-boundaries/QueryErrorBoundary.tsx b/components/dashboard/src/components/error-boundaries/QueryErrorBoundary.tsx index adf8e36d5ee7c9..34802f02d11600 100644 --- a/components/dashboard/src/components/error-boundaries/QueryErrorBoundary.tsx +++ b/components/dashboard/src/components/error-boundaries/QueryErrorBoundary.tsx @@ -4,7 +4,7 @@ * See License.AGPL.txt in the project root for license information. */ -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { QueryErrorResetBoundary, useQueryClient } from "@tanstack/react-query"; import { FC } from "react"; import { ErrorBoundary, FallbackProps } from "react-error-boundary"; @@ -36,7 +36,7 @@ const ExpectedQueryErrorsFallback: FC = ({ error, resetErrorBound const caughtError = error as CaughtError; // user deleted needs a n explicit logout to destroy the session - if (caughtError.code === ErrorCodes.USER_DELETED) { + if (ApplicationError.isUserDeletedError(caughtError)) { console.log("clearing query cache for deleted user"); client.clear(); diff --git a/components/dashboard/src/components/error-boundaries/ReloadPageErrorBoundary.tsx b/components/dashboard/src/components/error-boundaries/ReloadPageErrorBoundary.tsx index 846bec11aa953a..e52aa2ec2d7db9 100644 --- a/components/dashboard/src/components/error-boundaries/ReloadPageErrorBoundary.tsx +++ b/components/dashboard/src/components/error-boundaries/ReloadPageErrorBoundary.tsx @@ -11,7 +11,7 @@ import { Heading1, Subheading } from "../typography/headings"; import { reportError } from "../../service/metrics"; import { Button } from "@podkit/buttons/Button"; -export type CaughtError = Error & { code?: number }; +export type CaughtError = Error & { code?: number; data?: any }; // Catches any unexpected errors w/ a UI to reload the page. Also reports errors to api export const ReloadPageErrorBoundary: FC = ({ children }) => { diff --git a/components/dashboard/src/hooks/use-user-loader.ts b/components/dashboard/src/hooks/use-user-loader.ts index 1abf1bc3ac86cb..503dda77df69e9 100644 --- a/components/dashboard/src/hooks/use-user-loader.ts +++ b/components/dashboard/src/hooks/use-user-loader.ts @@ -9,7 +9,7 @@ import { UserContext } from "../user-context"; import { trackLocation } from "../Analytics"; import { useQuery } from "@tanstack/react-query"; import { noPersistence } from "../data/setup"; -import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { userClient } from "../service/public-api"; export const useUserLoader = () => { @@ -28,10 +28,11 @@ export const useUserLoader = () => { useErrorBoundary: true, // It's important we don't retry as we want to show the login screen as quickly as possible if a 401 retry: (_failureCount: number, error: Error & { code?: number }) => { + const isUserDeletedError = ApplicationError.isUserDeletedError(error); return ( error.code !== ErrorCodes.NOT_AUTHENTICATED && - error.code !== ErrorCodes.USER_DELETED && - error.code !== ErrorCodes.CELL_EXPIRED + error.code !== ErrorCodes.CELL_EXPIRED && + !isUserDeletedError ); }, // docs: https://tanstack.com/query/v4/docs/react/guides/query-retries diff --git a/components/gitpod-protocol/go/error.go b/components/gitpod-protocol/go/error.go index 295c93dc4dbfe4..84f399c4b72672 100644 --- a/components/gitpod-protocol/go/error.go +++ b/components/gitpod-protocol/go/error.go @@ -48,9 +48,6 @@ const ( // 470 User Blocked (custom status code) USER_BLOCKED = 470 - // 471 User Deleted (custom status code) - USER_DELETED = 471 - // 472 Terms Acceptance Required (custom status code) USER_TERMS_ACCEPTANCE_REQUIRED = 472 diff --git a/components/gitpod-protocol/src/messaging/error.spec.ts b/components/gitpod-protocol/src/messaging/error.spec.ts new file mode 100644 index 00000000000000..daed25694718ed --- /dev/null +++ b/components/gitpod-protocol/src/messaging/error.spec.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2021 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { suite, test } from "@testdeck/mocha"; +import { ApplicationError, ErrorCodes } from "./error"; + +import { expect } from "chai"; + +@suite +class TestApplicationError { + @test public async ApplicationError_isUserDeletedError() { + expect( + ApplicationError.isUserDeletedError( + new ApplicationError(ErrorCodes.NOT_FOUND, "not found", { userDeleted: true }), + ), + ).to.be.true; + } +} +module.exports = new TestApplicationError(); diff --git a/components/gitpod-protocol/src/messaging/error.ts b/components/gitpod-protocol/src/messaging/error.ts index 59696b5b59715a..83947cd35e4848 100644 --- a/components/gitpod-protocol/src/messaging/error.ts +++ b/components/gitpod-protocol/src/messaging/error.ts @@ -36,6 +36,10 @@ export namespace ApplicationError { throw e; } } + + export function isUserDeletedError(e: any): boolean { + return hasErrorCode(e) && e.code === ErrorCodes.NOT_FOUND && e.data?.userDeleted === true; + } } export namespace ErrorCode { @@ -98,9 +102,6 @@ export const ErrorCodes = { // 470 User Blocked (custom status code) USER_BLOCKED: 470 as const, - // 471 User Deleted (custom status code) - USER_DELETED: 471 as const, - // 472 Terms Acceptance Required (custom status code) USER_TERMS_ACCEPTANCE_REQUIRED: 472 as const, diff --git a/components/public-api/gitpod/v1/error.proto b/components/public-api/gitpod/v1/error.proto index 25018da4c11b16..aa9b2662a1496f 100644 --- a/components/public-api/gitpod/v1/error.proto +++ b/components/public-api/gitpod/v1/error.proto @@ -67,6 +67,14 @@ message ImageBuildLogsNotYetAvailableError {} message CellDisabledError {} +message NotFoundDetails { + oneof reason { + UserDeletedError user_deleted = 1; + } +} + +message UserDeletedError {} + /* // details for INVALID_ARGUMENT status code // TODO: this is not yet implemented in the backend diff --git a/components/public-api/go/v1/error.pb.go b/components/public-api/go/v1/error.pb.go index 447e5aedaf9e14..ebaacf7f60a533 100644 --- a/components/public-api/go/v1/error.pb.go +++ b/components/public-api/go/v1/error.pb.go @@ -778,6 +778,111 @@ func (*CellDisabledError) Descriptor() ([]byte, []int) { return file_gitpod_v1_error_proto_rawDescGZIP(), []int{11} } +type NotFoundDetails struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Reason: + // + // *NotFoundDetails_UserDeleted + Reason isNotFoundDetails_Reason `protobuf_oneof:"reason"` +} + +func (x *NotFoundDetails) Reset() { + *x = NotFoundDetails{} + if protoimpl.UnsafeEnabled { + mi := &file_gitpod_v1_error_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NotFoundDetails) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NotFoundDetails) ProtoMessage() {} + +func (x *NotFoundDetails) ProtoReflect() protoreflect.Message { + mi := &file_gitpod_v1_error_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NotFoundDetails.ProtoReflect.Descriptor instead. +func (*NotFoundDetails) Descriptor() ([]byte, []int) { + return file_gitpod_v1_error_proto_rawDescGZIP(), []int{12} +} + +func (m *NotFoundDetails) GetReason() isNotFoundDetails_Reason { + if m != nil { + return m.Reason + } + return nil +} + +func (x *NotFoundDetails) GetUserDeleted() *UserDeletedError { + if x, ok := x.GetReason().(*NotFoundDetails_UserDeleted); ok { + return x.UserDeleted + } + return nil +} + +type isNotFoundDetails_Reason interface { + isNotFoundDetails_Reason() +} + +type NotFoundDetails_UserDeleted struct { + UserDeleted *UserDeletedError `protobuf:"bytes,1,opt,name=user_deleted,json=userDeleted,proto3,oneof"` +} + +func (*NotFoundDetails_UserDeleted) isNotFoundDetails_Reason() {} + +type UserDeletedError struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *UserDeletedError) Reset() { + *x = UserDeletedError{} + if protoimpl.UnsafeEnabled { + mi := &file_gitpod_v1_error_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UserDeletedError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserDeletedError) ProtoMessage() {} + +func (x *UserDeletedError) ProtoReflect() protoreflect.Message { + mi := &file_gitpod_v1_error_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserDeletedError.ProtoReflect.Descriptor instead. +func (*UserDeletedError) Descriptor() ([]byte, []int) { + return file_gitpod_v1_error_proto_rawDescGZIP(), []int{13} +} + var File_gitpod_v1_error_proto protoreflect.FileDescriptor var file_gitpod_v1_error_proto_rawDesc = []byte{ @@ -897,12 +1002,20 @@ var file_gitpod_v1_error_proto_rawDesc = []byte{ 0x67, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4c, 0x6f, 0x67, 0x73, 0x4e, 0x6f, 0x74, 0x59, 0x65, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x13, 0x0a, 0x11, 0x43, 0x65, 0x6c, 0x6c, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x42, 0x51, 0x0a, 0x16, 0x69, 0x6f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, - 0x64, 0x2e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x5a, 0x37, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, - 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x70, - 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x61, 0x70, - 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x72, 0x6f, 0x72, 0x22, 0x5d, 0x0a, 0x0f, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, + 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x40, 0x0a, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, + 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x0b, 0x75, 0x73, + 0x65, 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x55, 0x73, 0x65, 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x42, 0x51, 0x0a, 0x16, 0x69, 0x6f, 0x2e, 0x67, 0x69, + 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x31, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, + 0x74, 0x70, 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x63, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -917,7 +1030,7 @@ func file_gitpod_v1_error_proto_rawDescGZIP() []byte { return file_gitpod_v1_error_proto_rawDescData } -var file_gitpod_v1_error_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_gitpod_v1_error_proto_msgTypes = make([]protoimpl.MessageInfo, 14) var file_gitpod_v1_error_proto_goTypes = []interface{}{ (*PermissionDeniedDetails)(nil), // 0: gitpod.v1.PermissionDeniedDetails (*UserBlockedError)(nil), // 1: gitpod.v1.UserBlockedError @@ -931,6 +1044,8 @@ var file_gitpod_v1_error_proto_goTypes = []interface{}{ (*RepositoryUnauthorizedError)(nil), // 9: gitpod.v1.RepositoryUnauthorizedError (*ImageBuildLogsNotYetAvailableError)(nil), // 10: gitpod.v1.ImageBuildLogsNotYetAvailableError (*CellDisabledError)(nil), // 11: gitpod.v1.CellDisabledError + (*NotFoundDetails)(nil), // 12: gitpod.v1.NotFoundDetails + (*UserDeletedError)(nil), // 13: gitpod.v1.UserDeletedError } var file_gitpod_v1_error_proto_depIdxs = []int32{ 1, // 0: gitpod.v1.PermissionDeniedDetails.user_blocked:type_name -> gitpod.v1.UserBlockedError @@ -943,11 +1058,12 @@ var file_gitpod_v1_error_proto_depIdxs = []int32{ 9, // 7: gitpod.v1.FailedPreconditionDetails.repository_unauthorized:type_name -> gitpod.v1.RepositoryUnauthorizedError 10, // 8: gitpod.v1.FailedPreconditionDetails.image_build_logs_not_yet_available:type_name -> gitpod.v1.ImageBuildLogsNotYetAvailableError 11, // 9: gitpod.v1.FailedPreconditionDetails.cell_is_disabled:type_name -> gitpod.v1.CellDisabledError - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 13, // 10: gitpod.v1.NotFoundDetails.user_deleted:type_name -> gitpod.v1.UserDeletedError + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_gitpod_v1_error_proto_init() } @@ -1100,6 +1216,30 @@ func file_gitpod_v1_error_proto_init() { return nil } } + file_gitpod_v1_error_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NotFoundDetails); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gitpod_v1_error_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UserDeletedError); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_gitpod_v1_error_proto_msgTypes[0].OneofWrappers = []interface{}{ (*PermissionDeniedDetails_UserBlocked)(nil), @@ -1115,13 +1255,16 @@ func file_gitpod_v1_error_proto_init() { (*FailedPreconditionDetails_ImageBuildLogsNotYetAvailable)(nil), (*FailedPreconditionDetails_CellIsDisabled)(nil), } + file_gitpod_v1_error_proto_msgTypes[12].OneofWrappers = []interface{}{ + (*NotFoundDetails_UserDeleted)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_gitpod_v1_error_proto_rawDesc, NumEnums: 0, - NumMessages: 12, + NumMessages: 14, NumExtensions: 0, NumServices: 0, }, diff --git a/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Error.java b/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Error.java index ef784ef977be43..340247447f550e 100644 --- a/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Error.java +++ b/components/public-api/java/src/main/java/io/gitpod/publicapi/v1/Error.java @@ -8924,6 +8924,1023 @@ public io.gitpod.publicapi.v1.Error.CellDisabledError getDefaultInstanceForType( } + public interface NotFoundDetailsOrBuilder extends + // @@protoc_insertion_point(interface_extends:gitpod.v1.NotFoundDetails) + com.google.protobuf.MessageOrBuilder { + + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + * @return Whether the userDeleted field is set. + */ + boolean hasUserDeleted(); + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + * @return The userDeleted. + */ + io.gitpod.publicapi.v1.Error.UserDeletedError getUserDeleted(); + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + */ + io.gitpod.publicapi.v1.Error.UserDeletedErrorOrBuilder getUserDeletedOrBuilder(); + + io.gitpod.publicapi.v1.Error.NotFoundDetails.ReasonCase getReasonCase(); + } + /** + * Protobuf type {@code gitpod.v1.NotFoundDetails} + */ + public static final class NotFoundDetails extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:gitpod.v1.NotFoundDetails) + NotFoundDetailsOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 27, + /* patch= */ 2, + /* suffix= */ "", + NotFoundDetails.class.getName()); + } + // Use NotFoundDetails.newBuilder() to construct. + private NotFoundDetails(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private NotFoundDetails() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return io.gitpod.publicapi.v1.Error.internal_static_gitpod_v1_NotFoundDetails_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return io.gitpod.publicapi.v1.Error.internal_static_gitpod_v1_NotFoundDetails_fieldAccessorTable + .ensureFieldAccessorsInitialized( + io.gitpod.publicapi.v1.Error.NotFoundDetails.class, io.gitpod.publicapi.v1.Error.NotFoundDetails.Builder.class); + } + + private int reasonCase_ = 0; + @SuppressWarnings("serial") + private java.lang.Object reason_; + public enum ReasonCase + implements com.google.protobuf.Internal.EnumLite, + com.google.protobuf.AbstractMessage.InternalOneOfEnum { + USER_DELETED(1), + REASON_NOT_SET(0); + private final int value; + private ReasonCase(int value) { + this.value = value; + } + /** + * @param value The number of the enum to look for. + * @return The enum associated with the given number. + * @deprecated Use {@link #forNumber(int)} instead. + */ + @java.lang.Deprecated + public static ReasonCase valueOf(int value) { + return forNumber(value); + } + + public static ReasonCase forNumber(int value) { + switch (value) { + case 1: return USER_DELETED; + case 0: return REASON_NOT_SET; + default: return null; + } + } + public int getNumber() { + return this.value; + } + }; + + public ReasonCase + getReasonCase() { + return ReasonCase.forNumber( + reasonCase_); + } + + public static final int USER_DELETED_FIELD_NUMBER = 1; + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + * @return Whether the userDeleted field is set. + */ + @java.lang.Override + public boolean hasUserDeleted() { + return reasonCase_ == 1; + } + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + * @return The userDeleted. + */ + @java.lang.Override + public io.gitpod.publicapi.v1.Error.UserDeletedError getUserDeleted() { + if (reasonCase_ == 1) { + return (io.gitpod.publicapi.v1.Error.UserDeletedError) reason_; + } + return io.gitpod.publicapi.v1.Error.UserDeletedError.getDefaultInstance(); + } + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + */ + @java.lang.Override + public io.gitpod.publicapi.v1.Error.UserDeletedErrorOrBuilder getUserDeletedOrBuilder() { + if (reasonCase_ == 1) { + return (io.gitpod.publicapi.v1.Error.UserDeletedError) reason_; + } + return io.gitpod.publicapi.v1.Error.UserDeletedError.getDefaultInstance(); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (reasonCase_ == 1) { + output.writeMessage(1, (io.gitpod.publicapi.v1.Error.UserDeletedError) reason_); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (reasonCase_ == 1) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(1, (io.gitpod.publicapi.v1.Error.UserDeletedError) reason_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof io.gitpod.publicapi.v1.Error.NotFoundDetails)) { + return super.equals(obj); + } + io.gitpod.publicapi.v1.Error.NotFoundDetails other = (io.gitpod.publicapi.v1.Error.NotFoundDetails) obj; + + if (!getReasonCase().equals(other.getReasonCase())) return false; + switch (reasonCase_) { + case 1: + if (!getUserDeleted() + .equals(other.getUserDeleted())) return false; + break; + case 0: + default: + } + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + switch (reasonCase_) { + case 1: + hash = (37 * hash) + USER_DELETED_FIELD_NUMBER; + hash = (53 * hash) + getUserDeleted().hashCode(); + break; + case 0: + default: + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static io.gitpod.publicapi.v1.Error.NotFoundDetails parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(io.gitpod.publicapi.v1.Error.NotFoundDetails prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code gitpod.v1.NotFoundDetails} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:gitpod.v1.NotFoundDetails) + io.gitpod.publicapi.v1.Error.NotFoundDetailsOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return io.gitpod.publicapi.v1.Error.internal_static_gitpod_v1_NotFoundDetails_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return io.gitpod.publicapi.v1.Error.internal_static_gitpod_v1_NotFoundDetails_fieldAccessorTable + .ensureFieldAccessorsInitialized( + io.gitpod.publicapi.v1.Error.NotFoundDetails.class, io.gitpod.publicapi.v1.Error.NotFoundDetails.Builder.class); + } + + // Construct using io.gitpod.publicapi.v1.Error.NotFoundDetails.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + if (userDeletedBuilder_ != null) { + userDeletedBuilder_.clear(); + } + reasonCase_ = 0; + reason_ = null; + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return io.gitpod.publicapi.v1.Error.internal_static_gitpod_v1_NotFoundDetails_descriptor; + } + + @java.lang.Override + public io.gitpod.publicapi.v1.Error.NotFoundDetails getDefaultInstanceForType() { + return io.gitpod.publicapi.v1.Error.NotFoundDetails.getDefaultInstance(); + } + + @java.lang.Override + public io.gitpod.publicapi.v1.Error.NotFoundDetails build() { + io.gitpod.publicapi.v1.Error.NotFoundDetails result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public io.gitpod.publicapi.v1.Error.NotFoundDetails buildPartial() { + io.gitpod.publicapi.v1.Error.NotFoundDetails result = new io.gitpod.publicapi.v1.Error.NotFoundDetails(this); + if (bitField0_ != 0) { buildPartial0(result); } + buildPartialOneofs(result); + onBuilt(); + return result; + } + + private void buildPartial0(io.gitpod.publicapi.v1.Error.NotFoundDetails result) { + int from_bitField0_ = bitField0_; + } + + private void buildPartialOneofs(io.gitpod.publicapi.v1.Error.NotFoundDetails result) { + result.reasonCase_ = reasonCase_; + result.reason_ = this.reason_; + if (reasonCase_ == 1 && + userDeletedBuilder_ != null) { + result.reason_ = userDeletedBuilder_.build(); + } + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof io.gitpod.publicapi.v1.Error.NotFoundDetails) { + return mergeFrom((io.gitpod.publicapi.v1.Error.NotFoundDetails)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(io.gitpod.publicapi.v1.Error.NotFoundDetails other) { + if (other == io.gitpod.publicapi.v1.Error.NotFoundDetails.getDefaultInstance()) return this; + switch (other.getReasonCase()) { + case USER_DELETED: { + mergeUserDeleted(other.getUserDeleted()); + break; + } + case REASON_NOT_SET: { + break; + } + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + input.readMessage( + getUserDeletedFieldBuilder().getBuilder(), + extensionRegistry); + reasonCase_ = 1; + break; + } // case 10 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int reasonCase_ = 0; + private java.lang.Object reason_; + public ReasonCase + getReasonCase() { + return ReasonCase.forNumber( + reasonCase_); + } + + public Builder clearReason() { + reasonCase_ = 0; + reason_ = null; + onChanged(); + return this; + } + + private int bitField0_; + + private com.google.protobuf.SingleFieldBuilder< + io.gitpod.publicapi.v1.Error.UserDeletedError, io.gitpod.publicapi.v1.Error.UserDeletedError.Builder, io.gitpod.publicapi.v1.Error.UserDeletedErrorOrBuilder> userDeletedBuilder_; + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + * @return Whether the userDeleted field is set. + */ + @java.lang.Override + public boolean hasUserDeleted() { + return reasonCase_ == 1; + } + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + * @return The userDeleted. + */ + @java.lang.Override + public io.gitpod.publicapi.v1.Error.UserDeletedError getUserDeleted() { + if (userDeletedBuilder_ == null) { + if (reasonCase_ == 1) { + return (io.gitpod.publicapi.v1.Error.UserDeletedError) reason_; + } + return io.gitpod.publicapi.v1.Error.UserDeletedError.getDefaultInstance(); + } else { + if (reasonCase_ == 1) { + return userDeletedBuilder_.getMessage(); + } + return io.gitpod.publicapi.v1.Error.UserDeletedError.getDefaultInstance(); + } + } + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + */ + public Builder setUserDeleted(io.gitpod.publicapi.v1.Error.UserDeletedError value) { + if (userDeletedBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + reason_ = value; + onChanged(); + } else { + userDeletedBuilder_.setMessage(value); + } + reasonCase_ = 1; + return this; + } + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + */ + public Builder setUserDeleted( + io.gitpod.publicapi.v1.Error.UserDeletedError.Builder builderForValue) { + if (userDeletedBuilder_ == null) { + reason_ = builderForValue.build(); + onChanged(); + } else { + userDeletedBuilder_.setMessage(builderForValue.build()); + } + reasonCase_ = 1; + return this; + } + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + */ + public Builder mergeUserDeleted(io.gitpod.publicapi.v1.Error.UserDeletedError value) { + if (userDeletedBuilder_ == null) { + if (reasonCase_ == 1 && + reason_ != io.gitpod.publicapi.v1.Error.UserDeletedError.getDefaultInstance()) { + reason_ = io.gitpod.publicapi.v1.Error.UserDeletedError.newBuilder((io.gitpod.publicapi.v1.Error.UserDeletedError) reason_) + .mergeFrom(value).buildPartial(); + } else { + reason_ = value; + } + onChanged(); + } else { + if (reasonCase_ == 1) { + userDeletedBuilder_.mergeFrom(value); + } else { + userDeletedBuilder_.setMessage(value); + } + } + reasonCase_ = 1; + return this; + } + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + */ + public Builder clearUserDeleted() { + if (userDeletedBuilder_ == null) { + if (reasonCase_ == 1) { + reasonCase_ = 0; + reason_ = null; + onChanged(); + } + } else { + if (reasonCase_ == 1) { + reasonCase_ = 0; + reason_ = null; + } + userDeletedBuilder_.clear(); + } + return this; + } + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + */ + public io.gitpod.publicapi.v1.Error.UserDeletedError.Builder getUserDeletedBuilder() { + return getUserDeletedFieldBuilder().getBuilder(); + } + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + */ + @java.lang.Override + public io.gitpod.publicapi.v1.Error.UserDeletedErrorOrBuilder getUserDeletedOrBuilder() { + if ((reasonCase_ == 1) && (userDeletedBuilder_ != null)) { + return userDeletedBuilder_.getMessageOrBuilder(); + } else { + if (reasonCase_ == 1) { + return (io.gitpod.publicapi.v1.Error.UserDeletedError) reason_; + } + return io.gitpod.publicapi.v1.Error.UserDeletedError.getDefaultInstance(); + } + } + /** + * .gitpod.v1.UserDeletedError user_deleted = 1 [json_name = "userDeleted"]; + */ + private com.google.protobuf.SingleFieldBuilder< + io.gitpod.publicapi.v1.Error.UserDeletedError, io.gitpod.publicapi.v1.Error.UserDeletedError.Builder, io.gitpod.publicapi.v1.Error.UserDeletedErrorOrBuilder> + getUserDeletedFieldBuilder() { + if (userDeletedBuilder_ == null) { + if (!(reasonCase_ == 1)) { + reason_ = io.gitpod.publicapi.v1.Error.UserDeletedError.getDefaultInstance(); + } + userDeletedBuilder_ = new com.google.protobuf.SingleFieldBuilder< + io.gitpod.publicapi.v1.Error.UserDeletedError, io.gitpod.publicapi.v1.Error.UserDeletedError.Builder, io.gitpod.publicapi.v1.Error.UserDeletedErrorOrBuilder>( + (io.gitpod.publicapi.v1.Error.UserDeletedError) reason_, + getParentForChildren(), + isClean()); + reason_ = null; + } + reasonCase_ = 1; + onChanged(); + return userDeletedBuilder_; + } + + // @@protoc_insertion_point(builder_scope:gitpod.v1.NotFoundDetails) + } + + // @@protoc_insertion_point(class_scope:gitpod.v1.NotFoundDetails) + private static final io.gitpod.publicapi.v1.Error.NotFoundDetails DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new io.gitpod.publicapi.v1.Error.NotFoundDetails(); + } + + public static io.gitpod.publicapi.v1.Error.NotFoundDetails getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public NotFoundDetails parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public io.gitpod.publicapi.v1.Error.NotFoundDetails getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface UserDeletedErrorOrBuilder extends + // @@protoc_insertion_point(interface_extends:gitpod.v1.UserDeletedError) + com.google.protobuf.MessageOrBuilder { + } + /** + * Protobuf type {@code gitpod.v1.UserDeletedError} + */ + public static final class UserDeletedError extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:gitpod.v1.UserDeletedError) + UserDeletedErrorOrBuilder { + private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 27, + /* patch= */ 2, + /* suffix= */ "", + UserDeletedError.class.getName()); + } + // Use UserDeletedError.newBuilder() to construct. + private UserDeletedError(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private UserDeletedError() { + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return io.gitpod.publicapi.v1.Error.internal_static_gitpod_v1_UserDeletedError_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return io.gitpod.publicapi.v1.Error.internal_static_gitpod_v1_UserDeletedError_fieldAccessorTable + .ensureFieldAccessorsInitialized( + io.gitpod.publicapi.v1.Error.UserDeletedError.class, io.gitpod.publicapi.v1.Error.UserDeletedError.Builder.class); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof io.gitpod.publicapi.v1.Error.UserDeletedError)) { + return super.equals(obj); + } + io.gitpod.publicapi.v1.Error.UserDeletedError other = (io.gitpod.publicapi.v1.Error.UserDeletedError) obj; + + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static io.gitpod.publicapi.v1.Error.UserDeletedError parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(io.gitpod.publicapi.v1.Error.UserDeletedError prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code gitpod.v1.UserDeletedError} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:gitpod.v1.UserDeletedError) + io.gitpod.publicapi.v1.Error.UserDeletedErrorOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return io.gitpod.publicapi.v1.Error.internal_static_gitpod_v1_UserDeletedError_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return io.gitpod.publicapi.v1.Error.internal_static_gitpod_v1_UserDeletedError_fieldAccessorTable + .ensureFieldAccessorsInitialized( + io.gitpod.publicapi.v1.Error.UserDeletedError.class, io.gitpod.publicapi.v1.Error.UserDeletedError.Builder.class); + } + + // Construct using io.gitpod.publicapi.v1.Error.UserDeletedError.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return io.gitpod.publicapi.v1.Error.internal_static_gitpod_v1_UserDeletedError_descriptor; + } + + @java.lang.Override + public io.gitpod.publicapi.v1.Error.UserDeletedError getDefaultInstanceForType() { + return io.gitpod.publicapi.v1.Error.UserDeletedError.getDefaultInstance(); + } + + @java.lang.Override + public io.gitpod.publicapi.v1.Error.UserDeletedError build() { + io.gitpod.publicapi.v1.Error.UserDeletedError result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public io.gitpod.publicapi.v1.Error.UserDeletedError buildPartial() { + io.gitpod.publicapi.v1.Error.UserDeletedError result = new io.gitpod.publicapi.v1.Error.UserDeletedError(this); + onBuilt(); + return result; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof io.gitpod.publicapi.v1.Error.UserDeletedError) { + return mergeFrom((io.gitpod.publicapi.v1.Error.UserDeletedError)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(io.gitpod.publicapi.v1.Error.UserDeletedError other) { + if (other == io.gitpod.publicapi.v1.Error.UserDeletedError.getDefaultInstance()) return this; + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + + // @@protoc_insertion_point(builder_scope:gitpod.v1.UserDeletedError) + } + + // @@protoc_insertion_point(class_scope:gitpod.v1.UserDeletedError) + private static final io.gitpod.publicapi.v1.Error.UserDeletedError DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new io.gitpod.publicapi.v1.Error.UserDeletedError(); + } + + public static io.gitpod.publicapi.v1.Error.UserDeletedError getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public UserDeletedError parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public io.gitpod.publicapi.v1.Error.UserDeletedError getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + private static final com.google.protobuf.Descriptors.Descriptor internal_static_gitpod_v1_PermissionDeniedDetails_descriptor; private static final @@ -8984,6 +10001,16 @@ public io.gitpod.publicapi.v1.Error.CellDisabledError getDefaultInstanceForType( private static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_gitpod_v1_CellDisabledError_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_gitpod_v1_NotFoundDetails_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_gitpod_v1_NotFoundDetails_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_gitpod_v1_UserDeletedError_descriptor; + private static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_gitpod_v1_UserDeletedError_fieldAccessorTable; public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { @@ -9039,9 +10066,12 @@ public io.gitpod.publicapi.v1.Error.CellDisabledError getDefaultInstanceForType( "_connected\030\005 \001(\010R\023providerIsConnected\022*\n" + "\021is_missing_scopes\030\006 \001(\010R\017isMissingScope" + "s\"$\n\"ImageBuildLogsNotYetAvailableError\"" + - "\023\n\021CellDisabledErrorBQ\n\026io.gitpod.public" + - "api.v1Z7github.com/gitpod-io/gitpod/comp" + - "onents/public-api/go/v1b\006proto3" + "\023\n\021CellDisabledError\"]\n\017NotFoundDetails\022" + + "@\n\014user_deleted\030\001 \001(\0132\033.gitpod.v1.UserDe" + + "letedErrorH\000R\013userDeletedB\010\n\006reason\"\022\n\020U" + + "serDeletedErrorBQ\n\026io.gitpod.publicapi.v" + + "1Z7github.com/gitpod-io/gitpod/component" + + "s/public-api/go/v1b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, @@ -9120,6 +10150,18 @@ public io.gitpod.publicapi.v1.Error.CellDisabledError getDefaultInstanceForType( com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_gitpod_v1_CellDisabledError_descriptor, new java.lang.String[] { }); + internal_static_gitpod_v1_NotFoundDetails_descriptor = + getDescriptor().getMessageTypes().get(12); + internal_static_gitpod_v1_NotFoundDetails_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_gitpod_v1_NotFoundDetails_descriptor, + new java.lang.String[] { "UserDeleted", "Reason", }); + internal_static_gitpod_v1_UserDeletedError_descriptor = + getDescriptor().getMessageTypes().get(13); + internal_static_gitpod_v1_UserDeletedError_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_gitpod_v1_UserDeletedError_descriptor, + new java.lang.String[] { }); descriptor.resolveAllFeaturesImmutable(); com.google.protobuf.DescriptorProtos.getDescriptor(); } diff --git a/components/public-api/typescript-common/src/public-api-converter.spec.ts b/components/public-api/typescript-common/src/public-api-converter.spec.ts index 0dae99513a44ae..b6c68ead9e7f10 100644 --- a/components/public-api/typescript-common/src/public-api-converter.spec.ts +++ b/components/public-api/typescript-common/src/public-api-converter.spec.ts @@ -23,6 +23,8 @@ import { InvalidCostCenterError, ImageBuildLogsNotYetAvailableError, TooManyRunningWorkspacesError, + UserDeletedError, + NotFoundDetails, } from "@gitpod/public-api/lib/gitpod/v1/error_pb"; import { startFixtureTest } from "./fixtures.spec"; import { OrganizationRole } from "@gitpod/public-api/lib/gitpod/v1/organization_pb"; @@ -377,6 +379,22 @@ describe("PublicAPIConverter", () => { expect(appError.message).to.equal("user blocked"); }); + it("USER_DELETED", () => { + const connectError = converter.toError(new ApplicationError(ErrorCodes.NOT_FOUND, "not found: user deleted", { userDeleted: true})); + expect(connectError.code).to.equal(Code.NotFound); + expect(connectError.rawMessage).to.equal("not found: user deleted"); + + const details = connectError.findDetails(NotFoundDetails)[0]; + expect(details).to.not.be.undefined; + expect(details?.reason?.case).to.equal("userDeleted"); + expect(details?.reason?.value).to.be.instanceOf(UserDeletedError); + + const appError = converter.fromError(connectError); + expect(appError.code).to.equal(ErrorCodes.NOT_FOUND); + expect(appError.message).to.equal("not found: user deleted"); + expect(appError.data.userDeleted).to.equal(true); + }); + it("NEEDS_VERIFICATION", () => { const connectError = converter.toError( new ApplicationError(ErrorCodes.NEEDS_VERIFICATION, "needs verification"), diff --git a/components/public-api/typescript-common/src/public-api-converter.ts b/components/public-api/typescript-common/src/public-api-converter.ts index 21a054a50af630..451844a48f4a30 100644 --- a/components/public-api/typescript-common/src/public-api-converter.ts +++ b/components/public-api/typescript-common/src/public-api-converter.ts @@ -113,7 +113,9 @@ import { RepositoryNotFoundError as RepositoryNotFoundErrorData, RepositoryUnauthorizedError as RepositoryUnauthorizedErrorData, TooManyRunningWorkspacesError, + NotFoundDetails, UserBlockedError, + UserDeletedError, } from "@gitpod/public-api/lib/gitpod/v1/error_pb"; import { BlockedEmailDomain, @@ -742,6 +744,17 @@ export class PublicAPIConverter { return new ConnectError(reason.message, Code.InvalidArgument, undefined, undefined, reason); } if (reason.code === ErrorCodes.NOT_FOUND) { + if (reason.data?.userDeleted) { + return new ConnectError(reason.message, Code.NotFound, undefined, + [ + new NotFoundDetails({ + reason: { + case: "userDeleted", + value: new UserDeletedError(), + }, + }), + ], reason); + } return new ConnectError(reason.message, Code.NotFound, undefined, undefined, reason); } if (reason.code === ErrorCodes.NOT_AUTHENTICATED) { @@ -797,6 +810,10 @@ export class PublicAPIConverter { return new ApplicationError(ErrorCodes.BAD_REQUEST, reason.rawMessage); } if (reason.code === Code.NotFound) { + const details = reason.findDetails(NotFoundDetails)[0]; + if (details?.reason?.case === "userDeleted") { + return new ApplicationError(ErrorCodes.NOT_FOUND, reason.rawMessage, { userDeleted: true }); + } return new ApplicationError(ErrorCodes.NOT_FOUND, reason.rawMessage); } if (reason.code === Code.Unauthenticated) { diff --git a/components/public-api/typescript/src/gitpod/v1/error_pb.ts b/components/public-api/typescript/src/gitpod/v1/error_pb.ts index 3716a851b2c902..e08d67013f7dfc 100644 --- a/components/public-api/typescript/src/gitpod/v1/error_pb.ts +++ b/components/public-api/typescript/src/gitpod/v1/error_pb.ts @@ -557,3 +557,77 @@ export class CellDisabledError extends Message { return proto3.util.equals(CellDisabledError, a, b); } } + +/** + * @generated from message gitpod.v1.NotFoundDetails + */ +export class NotFoundDetails extends Message { + /** + * @generated from oneof gitpod.v1.NotFoundDetails.reason + */ + reason: { + /** + * @generated from field: gitpod.v1.UserDeletedError user_deleted = 1; + */ + value: UserDeletedError; + case: "userDeleted"; + } | { case: undefined; value?: undefined } = { case: undefined }; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "gitpod.v1.NotFoundDetails"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "user_deleted", kind: "message", T: UserDeletedError, oneof: "reason" }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): NotFoundDetails { + return new NotFoundDetails().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): NotFoundDetails { + return new NotFoundDetails().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): NotFoundDetails { + return new NotFoundDetails().fromJsonString(jsonString, options); + } + + static equals(a: NotFoundDetails | PlainMessage | undefined, b: NotFoundDetails | PlainMessage | undefined): boolean { + return proto3.util.equals(NotFoundDetails, a, b); + } +} + +/** + * @generated from message gitpod.v1.UserDeletedError + */ +export class UserDeletedError extends Message { + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "gitpod.v1.UserDeletedError"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): UserDeletedError { + return new UserDeletedError().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): UserDeletedError { + return new UserDeletedError().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): UserDeletedError { + return new UserDeletedError().fromJsonString(jsonString, options); + } + + static equals(a: UserDeletedError | PlainMessage | undefined, b: UserDeletedError | PlainMessage | undefined): boolean { + return proto3.util.equals(UserDeletedError, a, b); + } +} diff --git a/components/server/src/api/server.ts b/components/server/src/api/server.ts index e01e80799dfda4..4a8adff082e379 100644 --- a/components/server/src/api/server.ts +++ b/components/server/src/api/server.ts @@ -386,7 +386,14 @@ export class API { try { const claims = await this.sessionHandler.verifyJWTCookie(cookieHeader); const userId = claims?.sub; - return !!userId ? SubjectId.fromUserId(userId) : undefined; + if (!userId) { + return undefined; + } + + // 3. Verify user + await this.userServiceInternal.findUserById(userId, userId); + + return SubjectId.fromUserId(userId); } catch (err) { log.warn("Failed to authenticate user with JWT Session", err); return undefined; diff --git a/components/server/src/orgs/organization-service.spec.db.ts b/components/server/src/orgs/organization-service.spec.db.ts index 1d12a0c9c68bc7..b170dfd63539a3 100644 --- a/components/server/src/orgs/organization-service.spec.db.ts +++ b/components/server/src/orgs/organization-service.spec.db.ts @@ -305,11 +305,8 @@ describe("OrganizationService", async () => { const members2 = await withTestCtx(owner.id, () => os.listMembers(owner.id, org.id)); expect(members2.some((m) => m.userId === ownedMember.id)).to.be.false; // also assert that the user is gone - const deleted = await withTestCtx(ownedMember.id, () => - userService.findUserById(ownedMember.id, ownedMember.id), - ); - // await expectError(ErrorCodes.NOT_FOUND, () => deleted); - expect(deleted.markedDeleted).to.be.true; + const deleted = withTestCtx(ownedMember.id, () => userService.findUserById(ownedMember.id, ownedMember.id)); + await expectError(ErrorCodes.NOT_FOUND, () => deleted); }); it("should listOrganizationsByMember", async () => { diff --git a/components/server/src/user/user-controller.ts b/components/server/src/user/user-controller.ts index 5fa91dd28d116a..558c9c86c87365 100644 --- a/components/server/src/user/user-controller.ts +++ b/components/server/src/user/user-controller.ts @@ -258,18 +258,18 @@ export class UserController { // stop all running workspaces const user = req.user as User; - await runWithSubjectId(SubjectId.fromUserId(user.id), async () => { - if (user) { + if (user) { + await runWithSubjectId(SubjectId.fromUserId(user.id), async () => { this.workspaceService .stopRunningWorkspacesForUser({}, user.id, user.id, "logout", StopWorkspacePolicy.NORMALLY) .catch((error) => log.error(logContext, "cannot stop workspaces on logout", { error, ...logPayload }), ); - } - // reset the FGA state - await this.userService.resetFgaVersion(user.id, user.id); - }); + // reset the FGA state + await this.userService.resetFgaVersion(user.id, user.id); + }); + } const redirectToUrl = this.getSafeReturnToParam(req) || this.config.hostUrl.toString(); diff --git a/components/server/src/user/user-service.spec.db.ts b/components/server/src/user/user-service.spec.db.ts index 9e1ee7e3b5ffc8..6ecb56711e496d 100644 --- a/components/server/src/user/user-service.spec.db.ts +++ b/components/server/src/user/user-service.spec.db.ts @@ -220,8 +220,8 @@ describe("UserService", async () => { await expectError(ErrorCodes.PERMISSION_DENIED, userService.deleteUser(user.id, user2.id)); // user can delete themselves await userService.deleteUser(user.id, user.id); - user = await userService.findUserById(user.id, user.id); - expect(user.markedDeleted).to.be.true; + const deleted = userService.findUserById(user.id, user.id); + expectError(ErrorCodes.NOT_FOUND, deleted); // org owners can delete users owned by org const orgOwner = await userService.createUser({ @@ -237,12 +237,12 @@ describe("UserService", async () => { await expectError(ErrorCodes.NOT_FOUND, userService.deleteUser(orgOwner.id, nonOrgUser.id)); await userService.deleteUser(orgOwner.id, user2.id); - user2 = await userService.findUserById(orgOwner.id, user2.id); - expect(user2.markedDeleted).to.be.true; + const deleted2 = userService.findUserById(orgOwner.id, user2.id); + expectError(ErrorCodes.NOT_FOUND, deleted2); // admins can delete any user await userService.deleteUser(BUILTIN_INSTLLATION_ADMIN_USER_ID, nonOrgUser.id); - nonOrgUser = await userService.findUserById(BUILTIN_INSTLLATION_ADMIN_USER_ID, nonOrgUser.id); - expect(nonOrgUser.markedDeleted).to.be.true; + const deletedNonOrg = userService.findUserById(BUILTIN_INSTLLATION_ADMIN_USER_ID, nonOrgUser.id); + expectError(ErrorCodes.NOT_FOUND, deletedNonOrg); }); }); diff --git a/components/server/src/user/user-service.ts b/components/server/src/user/user-service.ts index 359956cf3b2b6d..2f9a673a060ed4 100644 --- a/components/server/src/user/user-service.ts +++ b/components/server/src/user/user-service.ts @@ -88,6 +88,9 @@ export class UserService { if (!result) { throw new ApplicationError(ErrorCodes.NOT_FOUND, "not found"); } + if (result.markedDeleted) { + throw new ApplicationError(ErrorCodes.NOT_FOUND, "not found: user deleted", { userDeleted: true }); + } try { return await this.relationshipUpdater.migrate(result); } catch (error) { diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index d30751cfd7d9bf..f8dbafef14d95d 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -379,9 +379,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await runWithSubjectId(SYSTEM_USER, async () => this.userService.findUserById(SYSTEM_USER_ID, userId), ); - if (user.markedDeleted === true) { - throw new ApplicationError(ErrorCodes.USER_DELETED, "User has been deleted."); - } const userContext: LogContext = { ...ctx, userId: user.id,