diff --git a/components/server/src/iam/iam-session-app.spec.ts b/components/server/src/iam/iam-session-app.spec.ts index 32c6d7f8ea169e..14e4b55ba36a6b 100644 --- a/components/server/src/iam/iam-session-app.spec.ts +++ b/components/server/src/iam/iam-session-app.spec.ts @@ -19,7 +19,7 @@ import request from "supertest"; import * as chai from "chai"; import { OIDCCreateSessionPayload } from "./iam-oidc-create-session-payload"; -import { TeamMemberInfo, TeamMemberRole, User } from "@gitpod/gitpod-protocol"; +import { TeamMemberInfo, User } from "@gitpod/gitpod-protocol"; import { OrganizationService } from "../orgs/organization-service"; import { UserService } from "../user/user-service"; import { TeamDB, UserDB } from "@gitpod/gitpod-db/lib"; @@ -40,9 +40,6 @@ class TestIamSessionApp { }; protected userServiceMock: Partial = { - createUser: (params) => { - return { id: "id-new-user" } as any; - }, updateUser: (userId, update) => { return {} as any; }, @@ -67,8 +64,10 @@ class TestIamSessionApp { listMembers: async (teamId: string): Promise => { return []; }, - async addOrUpdateMember(userId: string, teamId: string, memberId: string, role: TeamMemberRole): Promise { - this.memberships.add(memberId); + async createOrgOwnedUser(params): Promise { + const user = { id: "id-new-user" } as any as User; + this.memberships.add(user.id); + return user; }, }; diff --git a/components/server/src/iam/iam-session-app.ts b/components/server/src/iam/iam-session-app.ts index c4908ce7778fb8..27f7ef3444edb7 100644 --- a/components/server/src/iam/iam-session-app.ts +++ b/components/server/src/iam/iam-session-app.ts @@ -16,7 +16,6 @@ import { reportJWTCookieIssued } from "../prometheus-metrics"; import { ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { OrganizationService } from "../orgs/organization-service"; import { UserService } from "../user/user-service"; -import { UserDB } from "@gitpod/gitpod-db/lib"; import { SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; import { runWithSubjectId, runWithRequestContext } from "../util/request-context"; @@ -29,7 +28,6 @@ export class IamSessionApp { @inject(UserService) private readonly userService: UserService, @inject(OrganizationService) private readonly orgService: OrganizationService, @inject(SessionHandler) private readonly session: SessionHandler, - @inject(UserDB) private readonly userDb: UserDB, ) {} public getMiddlewares() { @@ -167,30 +165,15 @@ export class IamSessionApp { private async createNewOIDCUser(payload: OIDCCreateSessionPayload): Promise { const { claims, organizationId } = payload; - return this.userDb.transaction(async (_, ctx) => { - // Until we support SKIM (or any other means to sync accounts) we create new users here as a side-effect of the login - const user = await this.userService.createUser( - { - organizationId, - identity: { ...this.mapOIDCProfileToIdentity(payload), lastSigninTime: new Date().toISOString() }, - userUpdate: (user) => { - user.fullName = claims.name; - user.name = claims.name; - user.avatarUrl = claims.picture; - }, - }, - ctx, - ); - - await this.orgService.addOrUpdateMember( - SYSTEM_USER_ID, - organizationId, - user.id, - "member", - { flexibleRole: true }, - ctx, - ); - return user; + // Until we support SKIM (or any other means to sync accounts) we create new users here as a side-effect of the login + return this.orgService.createOrgOwnedUser({ + organizationId, + identity: { ...this.mapOIDCProfileToIdentity(payload), lastSigninTime: new Date().toISOString() }, + userUpdate: (user) => { + user.fullName = claims.name; + user.name = claims.name; + user.avatarUrl = claims.picture; + }, }); } } diff --git a/components/server/src/orgs/organization-service.spec.db.ts b/components/server/src/orgs/organization-service.spec.db.ts index bf225fc04f3084..73f6b8509472ca 100644 --- a/components/server/src/orgs/organization-service.spec.db.ts +++ b/components/server/src/orgs/organization-service.spec.db.ts @@ -4,7 +4,7 @@ * See License.AGPL.txt in the project root for license information. */ -import { BUILTIN_INSTLLATION_ADMIN_USER_ID, TypeORM } from "@gitpod/gitpod-db/lib"; +import { BUILTIN_INSTLLATION_ADMIN_USER_ID, TypeORM, UserDB } from "@gitpod/gitpod-db/lib"; import { Organization, OrganizationSettings, TeamMemberRole, User } from "@gitpod/gitpod-protocol"; import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; @@ -473,4 +473,54 @@ describe("OrganizationService", async () => { ); await assertUpdateSettings("should enable workspace sharing", { workspaceSharingDisabled: false }, {}); }); + + it("org-owned users can't create new organizations", async () => { + const userDB = container.get(UserDB); + const os = container.get(OrganizationService); + + // create the owner (installation-level) + const owner = await userDB.newUser(); + + // create an org + const orgService = container.get(OrganizationService); + const myOrg = await 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")); + }); + + it("org-owned users can't join another org", async () => { + const userDB = container.get(UserDB); + const os = container.get(OrganizationService); + + // create the owner (installation-level) + const owner = await userDB.newUser(); + + // 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"); + + // 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)); + }); }); + +async function createOrgOwnedUser(os: OrganizationService, organizationId: string) { + // create org-owned member + return os.createOrgOwnedUser({ + organizationId, + identity: { + authId: "123", + authProviderId: "https://accounts.google.com", + authName: "member", + lastSigninTime: new Date().toISOString(), + }, + userUpdate: (user) => {}, + }); +} diff --git a/components/server/src/orgs/organization-service.spec.ts b/components/server/src/orgs/organization-service.spec.ts index 29f7ba3fd9e906..cec702b6a28413 100644 --- a/components/server/src/orgs/organization-service.spec.ts +++ b/components/server/src/orgs/organization-service.spec.ts @@ -78,7 +78,7 @@ describe("OrganizationService", async () => { } as any as InstallationService); container.bind(StripeService).toConstantValue({} as any as StripeService); container.bind(UsageService).toConstantValue({} as any as UsageService); - container.bind(UserAuthentication).toSelf().inSingletonScope(); + container.bind(UserAuthentication).toConstantValue({} as any as UserAuthentication); os = container.get(OrganizationService); }); diff --git a/components/server/src/orgs/organization-service.ts b/components/server/src/orgs/organization-service.ts index 6a83a83dbaca4c..c4f8a9b15caeaf 100644 --- a/components/server/src/orgs/organization-service.ts +++ b/components/server/src/orgs/organization-service.ts @@ -13,6 +13,7 @@ import { TeamMembershipInvite, WorkspaceTimeoutDuration, OrgMemberRole, + User, } from "@gitpod/gitpod-protocol"; import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; @@ -33,7 +34,7 @@ import { StripeService } from "../billing/stripe-service"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; import { UsageService } from "./usage-service"; import { CostCenter_BillingStrategy } from "@gitpod/gitpod-protocol/lib/usage"; -import { UserAuthentication } from "../user/user-authentication"; +import { CreateUserParams, UserAuthentication } from "../user/user-authentication"; @injectable() export class OrganizationService { @@ -324,6 +325,26 @@ export class OrganizationService { return invite.teamId; } + /** + * Convenience method, analogue to UserService.createUser() +`` + */ + public async createOrgOwnedUser(params: CreateUserParams & { organizationId: string }): Promise { + return this.userDB.transaction(async (_, ctx) => { + const user = await this.userService.createUser(params, ctx); + + await this.addOrUpdateMember( + SYSTEM_USER_ID, + params.organizationId, + user.id, + "member", + { flexibleRole: true }, + ctx, + ); + return user; + }); + } + /** * Add or update member to an organization, if there's no `owner` in the organization, target role will be owner * diff --git a/components/server/src/user/env-var-service.spec.db.ts b/components/server/src/user/env-var-service.spec.db.ts index 8c43dbc763bc87..01ca2aa69843be 100644 --- a/components/server/src/user/env-var-service.spec.db.ts +++ b/components/server/src/user/env-var-service.spec.db.ts @@ -19,7 +19,7 @@ import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-s import * as chai from "chai"; import { Container } from "inversify"; import "mocha"; -import { createTestContainer, withTestCtx } from "../test/service-testing-container-module"; +import { createTestContainer } from "../test/service-testing-container-module"; import { resetDB } from "@gitpod/gitpod-db/lib/test/reset-db"; import { OrganizationService } from "../orgs/organization-service"; import { UserService } from "./user-service"; @@ -27,7 +27,6 @@ import { expectError } from "../test/expect-utils"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { EnvVarService } from "./env-var-service"; import { ProjectsService } from "../projects/projects-service"; -import { SYSTEM_USER } from "../authorization/authorizer"; const expect = chai.expect; @@ -98,9 +97,8 @@ describe("EnvVarService", async () => { const orgService = container.get(OrganizationService); org = await orgService.createOrganization(BUILTIN_INSTLLATION_ADMIN_USER_ID, "myOrg"); - const invite = await orgService.getOrCreateInvite(BUILTIN_INSTLLATION_ADMIN_USER_ID, org.id); - member = await userService.createUser({ + member = await orgService.createOrgOwnedUser({ organizationId: org.id, identity: { authId: "foo", @@ -109,7 +107,6 @@ describe("EnvVarService", async () => { primaryEmail: "yolo@yolo.com", }, }); - await withTestCtx(SYSTEM_USER, () => orgService.joinOrganization(member.id, invite.id)); stranger = await userService.createUser({ identity: { authId: "foo2", diff --git a/components/server/src/user/gitpod-token-service.spec.db.ts b/components/server/src/user/gitpod-token-service.spec.db.ts index bf942095b0e274..171140bd619cee 100644 --- a/components/server/src/user/gitpod-token-service.spec.db.ts +++ b/components/server/src/user/gitpod-token-service.spec.db.ts @@ -10,14 +10,13 @@ import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-s import * as chai from "chai"; import { Container } from "inversify"; import "mocha"; -import { createTestContainer, withTestCtx } from "../test/service-testing-container-module"; +import { createTestContainer } from "../test/service-testing-container-module"; import { resetDB } from "@gitpod/gitpod-db/lib/test/reset-db"; import { OrganizationService } from "../orgs/organization-service"; import { UserService } from "./user-service"; import { expectError } from "../test/expect-utils"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { GitpodTokenService } from "./gitpod-token-service"; -import { SYSTEM_USER } from "../authorization/authorizer"; const expect = chai.expect; @@ -35,10 +34,9 @@ describe("GitpodTokenService", async () => { const orgService = container.get(OrganizationService); org = await orgService.createOrganization(BUILTIN_INSTLLATION_ADMIN_USER_ID, "myOrg"); - const invite = await orgService.getOrCreateInvite(BUILTIN_INSTLLATION_ADMIN_USER_ID, org.id); const userService = container.get(UserService); - member = await userService.createUser({ + member = await orgService.createOrgOwnedUser({ organizationId: org.id, identity: { authId: "foo", @@ -47,7 +45,6 @@ describe("GitpodTokenService", async () => { primaryEmail: "yolo@yolo.com", }, }); - await withTestCtx(SYSTEM_USER, () => orgService.joinOrganization(member.id, invite.id)); stranger = await userService.createUser({ identity: { authId: "foo2", diff --git a/components/server/src/user/sshkey-service.spec.db.ts b/components/server/src/user/sshkey-service.spec.db.ts index babd8427ffe7c1..3adf16644d893a 100644 --- a/components/server/src/user/sshkey-service.spec.db.ts +++ b/components/server/src/user/sshkey-service.spec.db.ts @@ -10,14 +10,13 @@ import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-s import * as chai from "chai"; import { Container } from "inversify"; import "mocha"; -import { createTestContainer, withTestCtx } from "../test/service-testing-container-module"; +import { createTestContainer } from "../test/service-testing-container-module"; import { resetDB } from "@gitpod/gitpod-db/lib/test/reset-db"; import { SSHKeyService } from "./sshkey-service"; import { OrganizationService } from "../orgs/organization-service"; import { UserService } from "./user-service"; import { expectError } from "../test/expect-utils"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; -import { SYSTEM_USER } from "../authorization/authorizer"; const expect = chai.expect; @@ -46,10 +45,9 @@ describe("SSHKeyService", async () => { const orgService = container.get(OrganizationService); org = await orgService.createOrganization(BUILTIN_INSTLLATION_ADMIN_USER_ID, "myOrg"); - const invite = await orgService.getOrCreateInvite(BUILTIN_INSTLLATION_ADMIN_USER_ID, org.id); const userService = container.get(UserService); - member = await userService.createUser({ + member = await orgService.createOrgOwnedUser({ organizationId: org.id, identity: { authId: "foo", @@ -58,7 +56,6 @@ describe("SSHKeyService", async () => { primaryEmail: "yolo@yolo.com", }, }); - await withTestCtx(SYSTEM_USER, () => orgService.joinOrganization(member.id, invite.id)); stranger = await userService.createUser({ identity: { authId: "foo2", diff --git a/components/server/src/user/token-service.spec.db.ts b/components/server/src/user/token-service.spec.db.ts index 8b0f1bdb403c5c..50d1af5382dabc 100644 --- a/components/server/src/user/token-service.spec.db.ts +++ b/components/server/src/user/token-service.spec.db.ts @@ -8,12 +8,10 @@ import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-s import * as chai from "chai"; import "mocha"; import { Container } from "inversify"; -import { createTestContainer, withTestCtx } from "../test/service-testing-container-module"; +import { createTestContainer } from "../test/service-testing-container-module"; import { BUILTIN_INSTLLATION_ADMIN_USER_ID, TypeORM, UserDB } from "@gitpod/gitpod-db/lib"; import { resetDB } from "@gitpod/gitpod-db/lib/test/reset-db"; import { OrganizationService } from "../orgs/organization-service"; -import { SYSTEM_USER } from "../authorization/authorizer"; -import { UserService } from "./user-service"; import { Organization, Token, User } from "@gitpod/gitpod-protocol"; import { TokenService } from "./token-service"; import { TokenProvider } from "./token-provider"; @@ -33,11 +31,9 @@ describe("TokenService", async () => { let container: Container; let tokenService: TokenService; - let userService: UserService; let userDB: UserDB; let orgService: OrganizationService; let org: Organization; - let owner: User; let user: User; let token: Token; @@ -127,23 +123,10 @@ describe("TokenService", async () => { tokenService = container.get(TokenService); userDB = container.get(UserDB); - userService = container.get(UserService); orgService = container.get(OrganizationService); org = await orgService.createOrganization(BUILTIN_INSTLLATION_ADMIN_USER_ID, "myOrg"); - const invite = await orgService.getOrCreateInvite(BUILTIN_INSTLLATION_ADMIN_USER_ID, org.id); - // first not builtin user join an org will be an owner - owner = await userService.createUser({ - organizationId: org.id, - identity: { - authId: "foo", - authName: "bar", - authProviderId: "github", - primaryEmail: "yolo@yolo.com", - }, - }); - await withTestCtx(SYSTEM_USER, () => orgService.joinOrganization(owner.id, invite.id)); - user = await userService.createUser({ + user = await orgService.createOrgOwnedUser({ organizationId: org.id, identity: { authId: githubUserAuthId, @@ -159,7 +142,6 @@ describe("TokenService", async () => { primaryEmail: "yolo@yolo.com", }); await userDB.storeUser(user); - await withTestCtx(SYSTEM_USER, () => orgService.joinOrganization(user.id, invite.id)); // test data token = { diff --git a/components/server/src/user/user-service.spec.db.ts b/components/server/src/user/user-service.spec.db.ts index 5a0a06adba5927..9e1ee7e3b5ffc8 100644 --- a/components/server/src/user/user-service.spec.db.ts +++ b/components/server/src/user/user-service.spec.db.ts @@ -8,11 +8,11 @@ import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-s import * as chai from "chai"; import "mocha"; import { Container } from "inversify"; -import { createTestContainer, withTestCtx } from "../test/service-testing-container-module"; +import { createTestContainer } from "../test/service-testing-container-module"; import { BUILTIN_INSTLLATION_ADMIN_USER_ID, TypeORM } from "@gitpod/gitpod-db/lib"; import { resetDB } from "@gitpod/gitpod-db/lib/test/reset-db"; import { OrganizationService } from "../orgs/organization-service"; -import { Authorizer, SYSTEM_USER } from "../authorization/authorizer"; +import { Authorizer } from "../authorization/authorizer"; import { UserService } from "./user-service"; import { Organization, User } from "@gitpod/gitpod-protocol"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; @@ -38,9 +38,8 @@ describe("UserService", async () => { auth = container.get(Authorizer); orgService = container.get(OrganizationService); org = await orgService.createOrganization(BUILTIN_INSTLLATION_ADMIN_USER_ID, "myOrg"); - const invite = await orgService.getOrCreateInvite(BUILTIN_INSTLLATION_ADMIN_USER_ID, org.id); // first not builtin user join an org will be an owner - owner = await userService.createUser({ + owner = await orgService.createOrgOwnedUser({ organizationId: org.id, identity: { authId: "foo", @@ -49,9 +48,8 @@ describe("UserService", async () => { primaryEmail: "yolo@yolo.com", }, }); - await withTestCtx(SYSTEM_USER, () => orgService.joinOrganization(owner.id, invite.id)); - user = await userService.createUser({ + user = await orgService.createOrgOwnedUser({ organizationId: org.id, identity: { authId: "foo", @@ -60,9 +58,8 @@ describe("UserService", async () => { primaryEmail: "yolo@yolo.com", }, }); - await withTestCtx(SYSTEM_USER, () => orgService.joinOrganization(user.id, invite.id)); - user2 = await userService.createUser({ + user2 = await orgService.createOrgOwnedUser({ organizationId: org.id, identity: { authId: "foo", @@ -71,7 +68,6 @@ describe("UserService", async () => { primaryEmail: "yolo@yolo.com", }, }); - await withTestCtx(SYSTEM_USER, () => orgService.joinOrganization(user2.id, invite.id)); nonOrgUser = await userService.createUser({ identity: {