From b6fa451a947ee219ad90702594f166c9cf36b6e5 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann <32448529+geropl@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:00:02 +0100 Subject: [PATCH] [server] Use RequestContext.subjectId for authorization (#19091) --- components/server/src/api/server.ts | 2 +- components/server/src/api/user.ts | 50 ++++-- components/server/src/auth/subject-id.ts | 13 ++ .../server/src/authorization/authorizer.ts | 164 +++++++++++++----- .../caching-spicedb-authorizer.spec.db.ts | 92 ++++------ components/server/src/iam/iam-session-app.ts | 19 +- components/server/src/jobs/runner.ts | 3 +- components/server/src/jobs/workspace-gc.ts | 6 +- .../server/src/messaging/redis-subscriber.ts | 3 +- components/server/src/prebuilds/github-app.ts | 11 +- .../src/prebuilds/github-enterprise-app.ts | 7 +- .../src/projects/projects-service.spec.db.ts | 8 +- .../server/src/projects/projects-service.ts | 7 +- components/server/src/prometheus-metrics.ts | 12 ++ .../test/service-testing-container-module.ts | 15 ++ .../src/workspace/gitpod-server-impl.ts | 44 +++-- .../server/src/workspace/workspace-starter.ts | 7 +- 17 files changed, 296 insertions(+), 167 deletions(-) diff --git a/components/server/src/api/server.ts b/components/server/src/api/server.ts index db2405a7fabc0c..54645b216c3a8e 100644 --- a/components/server/src/api/server.ts +++ b/components/server/src/api/server.ts @@ -332,7 +332,7 @@ export class API { } private async ensureFgaMigration(subjectId: SubjectId): Promise { - const fgaChecksEnabled = await isFgaChecksEnabled(subjectId.userId()); + const fgaChecksEnabled = await isFgaChecksEnabled(subjectId); if (!fgaChecksEnabled) { throw new ConnectError("unauthorized", Code.PermissionDenied); } diff --git a/components/server/src/api/user.ts b/components/server/src/api/user.ts index b1f701b83b866e..410e803d4a4247 100644 --- a/components/server/src/api/user.ts +++ b/components/server/src/api/user.ts @@ -25,10 +25,11 @@ import { } from "@gitpod/public-api/lib/gitpod/experimental/v1/user_pb"; import { UserAuthentication } from "../user/user-authentication"; import { WorkspaceService } from "../workspace/workspace-service"; -import { SYSTEM_USER } from "../authorization/authorizer"; import { validate } from "uuid"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib"; +import { SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; +import { runWithRequestContext } from "../util/request-context"; @injectable() export class APIUserService implements ServiceImpl { @@ -59,6 +60,7 @@ export class APIUserService implements ServiceImpl throw new ConnectError("unimplemented", Code.Unimplemented); } + // INTERNAL ONLY public async blockUser(req: BlockUserRequest): Promise { const { userId, reason } = req; @@ -74,27 +76,37 @@ export class APIUserService implements ServiceImpl // TODO: Once connect-node supports middlewares, lift the tracing into the middleware. const trace = {}; - // TODO for now we use SYSTEM_USER, since it is only called by internal componenets like usage + // TODO(gpl) for now we use SYSTEM_USER, since it is only called by internal componenets like usage // and not exposed publically, but there should be better way to get an authenticated user - await this.userService.blockUser(SYSTEM_USER, userId, true); - log.info(`Blocked user ${userId}.`, { - userId, - reason, - }); + await runWithRequestContext( + { + requestKind: "user-service", + requestMethod: "blockUser", + signal: new AbortController().signal, + subjectId: SYSTEM_USER, + }, + async () => { + await this.userService.blockUser(SYSTEM_USER_ID, userId, true); + log.info(`Blocked user ${userId}.`, { + userId, + reason, + }); - const stoppedWorkspaces = await this.workspaceService.stopRunningWorkspacesForUser( - trace, - SYSTEM_USER, - userId, - reason, - StopWorkspacePolicy.IMMEDIATELY, - ); + const stoppedWorkspaces = await this.workspaceService.stopRunningWorkspacesForUser( + trace, + SYSTEM_USER_ID, + userId, + reason, + StopWorkspacePolicy.IMMEDIATELY, + ); - log.info(`Stopped ${stoppedWorkspaces.length} workspaces in response to BlockUser.`, { - userId, - reason, - workspaceIds: stoppedWorkspaces.map((w) => w.id), - }); + log.info(`Stopped ${stoppedWorkspaces.length} workspaces in response to BlockUser.`, { + userId, + reason, + workspaceIds: stoppedWorkspaces.map((w) => w.id), + }); + }, + ); return new BlockUserResponse(); } diff --git a/components/server/src/auth/subject-id.ts b/components/server/src/auth/subject-id.ts index 53cc91d00ece95..2ed6e367984f80 100644 --- a/components/server/src/auth/subject-id.ts +++ b/components/server/src/auth/subject-id.ts @@ -74,3 +74,16 @@ export class SubjectId { * Interface type meant for backwards compatibility */ export type Subject = string | SubjectId; +export namespace Subject { + export function toId(subject: Subject): SubjectId { + if (SubjectId.is(subject)) { + return subject; + } + if (typeof subject === "string") { + // either a subjectId string or a userId string + const parsed = SubjectId.parse(subject); + return parsed || SubjectId.fromUserId(subject); + } + throw new Error("Invalid Subject"); + } +} diff --git a/components/server/src/authorization/authorizer.ts b/components/server/src/authorization/authorizer.ts index af0bb7180e9114..671ff376dac0a9 100644 --- a/components/server/src/authorization/authorizer.ts +++ b/components/server/src/authorization/authorizer.ts @@ -25,6 +25,9 @@ import { import { SpiceDBAuthorizer } from "./spicedb-authorizer"; import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { Subject, SubjectId } from "../auth/subject-id"; +import { ctxTrySubjectId } from "../util/request-context"; +import { reportAuthorizerSubjectId } from "../prometheus-metrics"; export function createInitializingAuthorizer(spiceDbAuthorizer: SpiceDBAuthorizer): Authorizer { const target = new Authorizer(spiceDbAuthorizer); @@ -52,61 +55,69 @@ export function createInitializingAuthorizer(spiceDbAuthorizer: SpiceDBAuthorize * We need to call our internal API with system permissions in some cases. * As we don't have other ways to represent that (e.g. ServiceAccounts), we use this magic constant to designated it. */ -export const SYSTEM_USER = "SYSTEM_USER"; +export const SYSTEM_USER_ID = "SYSTEM_USER"; +export const SYSTEM_USER = SubjectId.fromUserId(SYSTEM_USER_ID); +export function isSystemUser(subjectId: SubjectId): boolean { + return subjectId.equals(SYSTEM_USER); +} export class Authorizer { constructor(private authorizer: SpiceDBAuthorizer) {} - async hasPermissionOnInstallation(userId: string, permission: InstallationPermission): Promise { - if (userId === SYSTEM_USER) { + async hasPermissionOnInstallation(passed: Subject, permission: InstallationPermission): Promise { + const subjectId = await getSubjectFromCtx(passed); + if (isSystemUser(subjectId)) { return true; } const req = v1.CheckPermissionRequest.create({ - subject: subject("user", userId), + subject: sub(subjectId), permission, resource: object("installation", InstallationID), consistency, }); - return await this.authorizer.check(req, { userId }); + return await this.authorizer.check(req, { userId: getUserId(subjectId) }); } - async checkPermissionOnInstallation(userId: string, permission: InstallationPermission): Promise { - if (await this.hasPermissionOnInstallation(userId, permission)) { + async checkPermissionOnInstallation(passed: Subject, permission: InstallationPermission): Promise { + const subjectId = await getSubjectFromCtx(passed); + if (await this.hasPermissionOnInstallation(subjectId, permission)) { return; } throw new ApplicationError( ErrorCodes.PERMISSION_DENIED, - `User ${userId} does not have permission '${permission}' on the installation.`, + `Subject ${subjectId.toString()} does not have permission '${permission}' on the installation.`, ); } async hasPermissionOnOrganization( - userId: string, + passed: Subject, permission: OrganizationPermission, orgId: string, ): Promise { - if (userId === SYSTEM_USER) { + const subjectId = await getSubjectFromCtx(passed); + if (isSystemUser(subjectId)) { return true; } const req = v1.CheckPermissionRequest.create({ - subject: subject("user", userId), + subject: sub(subjectId), permission, resource: object("organization", orgId), consistency, }); - return await this.authorizer.check(req, { userId }); + return await this.authorizer.check(req, { userId: getUserId(subjectId) }); } - async checkPermissionOnOrganization(userId: string, permission: OrganizationPermission, orgId: string) { - if (await this.hasPermissionOnOrganization(userId, permission, orgId)) { + async checkPermissionOnOrganization(passed: Subject, permission: OrganizationPermission, orgId: string) { + const subjectId = await getSubjectFromCtx(passed); + if (await this.hasPermissionOnOrganization(subjectId, permission, orgId)) { return; } // check if the user has read permission - if ("read_info" === permission || !(await this.hasPermissionOnOrganization(userId, "read_info", orgId))) { + if ("read_info" === permission || !(await this.hasPermissionOnOrganization(subjectId, "read_info", orgId))) { throw new ApplicationError(ErrorCodes.NOT_FOUND, `Organization ${orgId} not found.`); } @@ -116,27 +127,29 @@ export class Authorizer { ); } - async hasPermissionOnProject(userId: string, permission: ProjectPermission, projectId: string): Promise { - if (userId === SYSTEM_USER) { + async hasPermissionOnProject(passed: Subject, permission: ProjectPermission, projectId: string): Promise { + const subjectId = await getSubjectFromCtx(passed); + if (isSystemUser(subjectId)) { return true; } const req = v1.CheckPermissionRequest.create({ - subject: subject("user", userId), + subject: sub(subjectId), permission, resource: object("project", projectId), consistency, }); - return await this.authorizer.check(req, { userId }); + return await this.authorizer.check(req, { userId: getUserId(subjectId) }); } - async checkPermissionOnProject(userId: string, permission: ProjectPermission, projectId: string) { - if (await this.hasPermissionOnProject(userId, permission, projectId)) { + async checkPermissionOnProject(passed: Subject, permission: ProjectPermission, projectId: string) { + const subjectId = await getSubjectFromCtx(passed); + if (await this.hasPermissionOnProject(subjectId, permission, projectId)) { return; } // check if the user has read permission - if ("read_info" === permission || !(await this.hasPermissionOnProject(userId, "read_info", projectId))) { + if ("read_info" === permission || !(await this.hasPermissionOnProject(subjectId, "read_info", projectId))) { throw new ApplicationError(ErrorCodes.NOT_FOUND, `Project ${projectId} not found.`); } @@ -146,26 +159,28 @@ export class Authorizer { ); } - async hasPermissionOnUser(userId: string, permission: UserPermission, resourceUserId: string): Promise { - if (userId === SYSTEM_USER) { + async hasPermissionOnUser(passed: Subject, permission: UserPermission, resourceUserId: string): Promise { + const subjectId = await getSubjectFromCtx(passed); + if (isSystemUser(subjectId)) { return true; } const req = v1.CheckPermissionRequest.create({ - subject: subject("user", userId), + subject: sub(subjectId), permission, resource: object("user", resourceUserId), consistency, }); - return await this.authorizer.check(req, { userId }); + return await this.authorizer.check(req, { userId: getUserId(subjectId) }); } - async checkPermissionOnUser(userId: string, permission: UserPermission, resourceUserId: string) { - if (await this.hasPermissionOnUser(userId, permission, resourceUserId)) { + async checkPermissionOnUser(passed: Subject, permission: UserPermission, resourceUserId: string) { + const subjectId = await getSubjectFromCtx(passed); + if (await this.hasPermissionOnUser(subjectId, permission, resourceUserId)) { return; } - if ("read_info" === permission || !(await this.hasPermissionOnUser(userId, "read_info", resourceUserId))) { + if ("read_info" === permission || !(await this.hasPermissionOnUser(subjectId, "read_info", resourceUserId))) { throw new ApplicationError(ErrorCodes.NOT_FOUND, `User ${resourceUserId} not found.`); } @@ -176,30 +191,32 @@ export class Authorizer { } async hasPermissionOnWorkspace( - userId: string, + passed: Subject, permission: WorkspacePermission, workspaceId: string, forceEnablement?: boolean, // temporary to find an issue with workspace sharing ): Promise { - if (userId === SYSTEM_USER) { + const subjectId = await getSubjectFromCtx(passed); + if (isSystemUser(subjectId)) { return true; } const req = v1.CheckPermissionRequest.create({ - subject: subject("user", userId), + subject: sub(subjectId), permission, resource: object("workspace", workspaceId), consistency, }); - return await this.authorizer.check(req, { userId }, forceEnablement); + return await this.authorizer.check(req, { userId: getUserId(subjectId) }, forceEnablement); } - async checkPermissionOnWorkspace(userId: string, permission: WorkspacePermission, workspaceId: string) { - if (await this.hasPermissionOnWorkspace(userId, permission, workspaceId)) { + async checkPermissionOnWorkspace(passed: Subject, permission: WorkspacePermission, workspaceId: string) { + const subjectId = await getSubjectFromCtx(passed); + if (await this.hasPermissionOnWorkspace(subjectId, permission, workspaceId)) { return; } - if ("read_info" === permission || !(await this.hasPermissionOnWorkspace(userId, "read_info", workspaceId))) { + if ("read_info" === permission || !(await this.hasPermissionOnWorkspace(subjectId, "read_info", workspaceId))) { throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} not found.`); } @@ -503,7 +520,61 @@ export class Authorizer { } } -export async function isFgaChecksEnabled(userId: string | undefined): Promise { +async function getSubjectFromCtx(passed: Subject): Promise { + const ctxSubjectId = ctxTrySubjectId(); + const ctxUserId = ctxSubjectId?.userId(); + + const passedSubjectId = Subject.toId(passed); + const passedUserId = passedSubjectId.userId(); + + // Check: Do the subjectIds match? + const matchingSubjectId = ctxUserId === passedUserId; + const match = !ctxUserId ? "ctx-user-id-missing" : matchingSubjectId ? "match" : "mismatch"; + reportAuthorizerSubjectId(match); + if (match !== "match") { + try { + // Get hold of the stack trace + throw new Error("Authorizer: SubjectId mismatch"); + } catch (err) { + log.error("Authorizer: SubjectId mismatch", err, { + match, + ctxUserId, + passedUserId, + }); + } + } + + // Check feature flag, based on the passed subjectId + const authViaContext = await getExperimentsClientForBackend().getValueAsync("authWithRequestContext", false, { + user: ctxUserId + ? { + id: ctxUserId, + } + : undefined, + }); + if (!authViaContext) { + return passedSubjectId; + } + + if (!ctxSubjectId || match === "mismatch") { + const err = new ApplicationError(ErrorCodes.PERMISSION_DENIED, `Cannot authorize request`); + log.error("Authorizer: Cannot authorize request", err, { match, ctxSubjectId, ctxUserId, passedUserId }); + throw err; + } + return ctxSubjectId; +} + +function getUserId(subjectId: SubjectId): string { + const userId = subjectId.userId(); + if (!userId) { + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, `No userId available`); + } + return userId; +} + +export async function isFgaChecksEnabled(subject: Subject): Promise { + const subjectId = Subject.toId(subject); + const userId = subjectId.userId(); return getExperimentsClientForBackend().getValueAsync("centralizedPermissions", false, { user: userId ? { @@ -513,13 +584,17 @@ export async function isFgaChecksEnabled(userId: string | undefined): Promise { +export async function isFgaWritesEnabled(subject: Subject): Promise { + const subjectId = Subject.toId(subject); + const userId = subjectId.userId(); const result = await getExperimentsClientForBackend().getValueAsync("spicedb_relationship_updates", false, { - user: { - id: userId, - }, + user: userId + ? { + id: userId, + } + : undefined, }); - return result || (await isFgaChecksEnabled(userId)); + return result || (await isFgaChecksEnabled(subjectId)); } function set(rs: v1.Relationship): v1.RelationshipUpdate { @@ -543,9 +618,10 @@ function object(type: ResourceType, id?: string): v1.ObjectReference { }); } -function subject(type: ResourceType, id?: string, relation?: Relation | Permission): v1.SubjectReference { +function sub(subject: Subject, relation?: Relation | Permission): v1.SubjectReference { + const subjectId = Subject.toId(subject); return v1.SubjectReference.create({ - object: object(type, id), + object: object(subjectId.kind, subjectId.value), optionalRelation: relation, }); } diff --git a/components/server/src/authorization/caching-spicedb-authorizer.spec.db.ts b/components/server/src/authorization/caching-spicedb-authorizer.spec.db.ts index 16e562ad68c36c..16dfd7ff92627f 100644 --- a/components/server/src/authorization/caching-spicedb-authorizer.spec.db.ts +++ b/components/server/src/authorization/caching-spicedb-authorizer.spec.db.ts @@ -10,35 +10,17 @@ import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-s import * as chai from "chai"; import { Container } from "inversify"; import "mocha"; -import { createTestContainer } from "../test/service-testing-container-module"; -import { Authorizer, SYSTEM_USER } from "./authorizer"; +import { createTestContainer, withTestCtx } from "../test/service-testing-container-module"; +import { Authorizer, SYSTEM_USER, SYSTEM_USER_ID } from "./authorizer"; import { OrganizationService } from "../orgs/organization-service"; import { WorkspaceService } from "../workspace/workspace-service"; import { UserService } from "../user/user-service"; import { ConfigProvider } from "../workspace/config-provider"; import { v1 } from "@authzed/authzed-node"; -import { runWithRequestContext } from "../util/request-context"; import { RequestLocalZedTokenCache } from "./spicedb-authorizer"; -import { Subject, SubjectId } from "../auth/subject-id"; const expect = chai.expect; -const withCtx = (subject: Subject | User, p: Promise | (() => Promise)) => - runWithRequestContext( - { - requestKind: "testContext", - requestMethod: "testMethod", - signal: new AbortController().signal, - subjectId: SubjectId.is(subject) ? subject : SubjectId.fromUserId(User.is(subject) ? subject.id : subject), - }, - () => { - if (typeof p === "function") { - return p(); - } - return p; - }, - ); - describe("CachingSpiceDBAuthorizer", async () => { let container: Container; let userSvc: UserService; @@ -75,8 +57,7 @@ describe("CachingSpiceDBAuthorizer", async () => { it("should avoid new-enemy after removal", async () => { // userB and userC are members of org1, userA is owner. // All users are installation owned. - const userA = await withCtx( - SYSTEM_USER, + const userA = await withTestCtx(SYSTEM_USER, () => userSvc.createUser({ organizationId: undefined, identity: { @@ -86,10 +67,9 @@ describe("CachingSpiceDBAuthorizer", async () => { }, }), ); - const org1 = await withCtx(userA, orgSvc.createOrganization(userA.id, "org1")); - await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER, org1.id, userA.id, "owner")); - const userB = await withCtx( - SYSTEM_USER, + const org1 = await withTestCtx(userA, () => orgSvc.createOrganization(userA.id, "org1")); + await withTestCtx(SYSTEM_USER, () => orgSvc.addOrUpdateMember(SYSTEM_USER_ID, org1.id, userA.id, "owner")); + const userB = await withTestCtx(SYSTEM_USER, () => userSvc.createUser({ organizationId: undefined, identity: { @@ -99,9 +79,8 @@ describe("CachingSpiceDBAuthorizer", async () => { }, }), ); - await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER, org1.id, userB.id, "member")); - const userC = await withCtx( - SYSTEM_USER, + await withTestCtx(SYSTEM_USER, () => orgSvc.addOrUpdateMember(SYSTEM_USER_ID, org1.id, userB.id, "member")); + const userC = await withTestCtx(SYSTEM_USER, () => userSvc.createUser({ organizationId: undefined, identity: { @@ -111,38 +90,38 @@ describe("CachingSpiceDBAuthorizer", async () => { }, }), ); - await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER, org1.id, userC.id, "member")); + await withTestCtx(SYSTEM_USER, () => orgSvc.addOrUpdateMember(SYSTEM_USER_ID, org1.id, userC.id, "member")); // userA creates a workspace when userB is still member of the org // All members have "read_info" (derived from membership) - const ws1 = await withCtx(userA, createTestWorkspace(org1, userA)); + const ws1 = await withTestCtx(userA, () => createTestWorkspace(org1, userA)); expect( - await withCtx(userB, authorizer.hasPermissionOnWorkspace(userB.id, "read_info", ws1.id)), + await withTestCtx(userB, () => authorizer.hasPermissionOnWorkspace(userB.id, "read_info", ws1.id)), "userB should have read_info after removal", ).to.be.true; expect( - await withCtx(userA, authorizer.hasPermissionOnWorkspace(userA.id, "read_info", ws1.id)), + await withTestCtx(userA, () => authorizer.hasPermissionOnWorkspace(userA.id, "read_info", ws1.id)), "userA should have read_info after removal of userB", ).to.be.true; expect( - await withCtx(userC, authorizer.hasPermissionOnWorkspace(userC.id, "read_info", ws1.id)), + await withTestCtx(userC, () => authorizer.hasPermissionOnWorkspace(userC.id, "read_info", ws1.id)), "userC should have read_info after removal of userB", ).to.be.true; // userB is removed from the org - await withCtx(SYSTEM_USER, orgSvc.removeOrganizationMember(SYSTEM_USER, org1.id, userB.id)); + await withTestCtx(SYSTEM_USER, () => orgSvc.removeOrganizationMember(SYSTEM_USER_ID, org1.id, userB.id)); expect( - await withCtx(userB, authorizer.hasPermissionOnWorkspace(userB.id, "read_info", ws1.id)), + await withTestCtx(userB, () => authorizer.hasPermissionOnWorkspace(userB.id, "read_info", ws1.id)), "userB should have read_info after removal", ).to.be.false; expect( - await withCtx(userA, authorizer.hasPermissionOnWorkspace(userA.id, "read_info", ws1.id)), + await withTestCtx(userA, () => authorizer.hasPermissionOnWorkspace(userA.id, "read_info", ws1.id)), "userA should have read_info after removal of userB", ).to.be.true; expect( - await withCtx(userC, authorizer.hasPermissionOnWorkspace(userC.id, "read_info", ws1.id)), + await withTestCtx(userC, () => authorizer.hasPermissionOnWorkspace(userC.id, "read_info", ws1.id)), "userC should have read_info after removal of userB", ).to.be.true; }); @@ -171,8 +150,7 @@ describe("CachingSpiceDBAuthorizer", async () => { it("should avoid read-your-writes problem when adding a new user", async () => { // userB and userC are members of org1, userA is owner. // All users are installation owned. - const userA = await withCtx( - SYSTEM_USER, + const userA = await withTestCtx(SYSTEM_USER, () => userSvc.createUser({ organizationId: undefined, identity: { @@ -182,10 +160,9 @@ describe("CachingSpiceDBAuthorizer", async () => { }, }), ); - const org1 = await withCtx(userA, orgSvc.createOrganization(userA.id, "org1")); - await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER, org1.id, userA.id, "owner")); - const userC = await withCtx( - SYSTEM_USER, + const org1 = await withTestCtx(userA, () => orgSvc.createOrganization(userA.id, "org1")); + await withTestCtx(SYSTEM_USER, () => orgSvc.addOrUpdateMember(SYSTEM_USER_ID, org1.id, userA.id, "owner")); + const userC = await withTestCtx(SYSTEM_USER, () => userSvc.createUser({ organizationId: undefined, identity: { @@ -195,23 +172,22 @@ describe("CachingSpiceDBAuthorizer", async () => { }, }), ); - await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER, org1.id, userC.id, "member")); + await withTestCtx(SYSTEM_USER, () => orgSvc.addOrUpdateMember(SYSTEM_USER_ID, org1.id, userC.id, "member")); // userA creates a workspace before userB is member of the org - const ws1 = await withCtx(userA, createTestWorkspace(org1, userA)); + const ws1 = await withTestCtx(userA, () => createTestWorkspace(org1, userA)); expect( - await withCtx(SYSTEM_USER, authorizer.hasPermissionOnWorkspace(userA.id, "read_info", ws1.id)), + await withTestCtx(SYSTEM_USER, () => authorizer.hasPermissionOnWorkspace(userA.id, "read_info", ws1.id)), "userA should have read_info after removal of userB", ).to.be.true; expect( - await withCtx(userC, authorizer.hasPermissionOnWorkspace(userC.id, "read_info", ws1.id)), + await withTestCtx(userC, () => authorizer.hasPermissionOnWorkspace(userC.id, "read_info", ws1.id)), "userC should have read_info after removal of userB", ).to.be.true; // userB is added to the org - const userB = await withCtx( - SYSTEM_USER, + const userB = await withTestCtx(SYSTEM_USER, () => userSvc.createUser({ organizationId: undefined, identity: { @@ -221,18 +197,18 @@ describe("CachingSpiceDBAuthorizer", async () => { }, }), ); - await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER, org1.id, userB.id, "member")); + await withTestCtx(SYSTEM_USER, () => orgSvc.addOrUpdateMember(SYSTEM_USER_ID, org1.id, userB.id, "member")); expect( - await withCtx(userB, authorizer.hasPermissionOnWorkspace(userB.id, "read_info", ws1.id)), + await withTestCtx(userB, () => authorizer.hasPermissionOnWorkspace(userB.id, "read_info", ws1.id)), "userB should have read_info after removal", ).to.be.true; expect( - await withCtx(userA, authorizer.hasPermissionOnWorkspace(userA.id, "read_info", ws1.id)), + await withTestCtx(userA, () => authorizer.hasPermissionOnWorkspace(userA.id, "read_info", ws1.id)), "userA should have read_info after removal of userB", ).to.be.true; expect( - await withCtx(userC, authorizer.hasPermissionOnWorkspace(userC.id, "read_info", ws1.id)), + await withTestCtx(userC, () => authorizer.hasPermissionOnWorkspace(userC.id, "read_info", ws1.id)), "userC should have read_info after removal of userB", ).to.be.true; }); @@ -274,7 +250,7 @@ describe("RequestLocalZedTokenCache", async () => { }); it("should store token", async () => { - await withCtx(SYSTEM_USER, async () => { + await withTestCtx(SYSTEM_USER, async () => { expect(await cache.get(ws1)).to.be.undefined; await cache.set([ws1, rawToken1]); expect(await cache.get(ws1)).to.equal(rawToken1); @@ -282,7 +258,7 @@ describe("RequestLocalZedTokenCache", async () => { }); it("should return newest token", async () => { - await withCtx(SYSTEM_USER, async () => { + await withTestCtx(SYSTEM_USER, async () => { await cache.set([ws1, rawToken1]); await cache.set([ws1, rawToken2]); expect(await cache.get(ws1)).to.equal(rawToken2); @@ -292,7 +268,7 @@ describe("RequestLocalZedTokenCache", async () => { }); it("should return proper consistency", async () => { - await withCtx(SYSTEM_USER, async () => { + await withTestCtx(SYSTEM_USER, async () => { expect(await cache.consistency(ws1)).to.deep.equal(fullyConsistent()); await cache.set([ws1, rawToken1]); expect(await cache.consistency(ws1)).to.deep.equal(atLeastAsFreshAs(rawToken1)); @@ -300,7 +276,7 @@ describe("RequestLocalZedTokenCache", async () => { }); it("should clear cache", async () => { - await withCtx(SYSTEM_USER, async () => { + await withTestCtx(SYSTEM_USER, async () => { await cache.set([ws1, rawToken1]); expect(await cache.get(ws1)).to.equal(rawToken1); await cache.set([ws1, undefined]); // this should trigger a clear diff --git a/components/server/src/iam/iam-session-app.ts b/components/server/src/iam/iam-session-app.ts index cd26355fa01ff2..8bf4ee856914fd 100644 --- a/components/server/src/iam/iam-session-app.ts +++ b/components/server/src/iam/iam-session-app.ts @@ -17,7 +17,8 @@ import { ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { OrganizationService } from "../orgs/organization-service"; import { UserService } from "../user/user-service"; import { BUILTIN_INSTLLATION_ADMIN_USER_ID, TeamDB, UserDB } from "@gitpod/gitpod-db/lib"; -import { SYSTEM_USER } from "../authorization/authorizer"; +import { SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; +import { runWithSubjectId, runWithRequestContext } from "../util/request-context"; @injectable() export class IamSessionApp { @@ -42,9 +43,21 @@ export class IamSessionApp { app.use(middleware); }); + // Use RequestContext + app.use((req, res, next) => { + runWithRequestContext( + { + requestKind: "iam-session-app", + requestMethod: req.path, + signal: new AbortController().signal, + }, + () => next(), + ); + }); + app.post("/session", async (req: express.Request, res: express.Response) => { try { - const result = await this.doCreateSession(req, res); + const result = await runWithSubjectId(SYSTEM_USER, async () => this.doCreateSession(req, res)); res.status(200).json(result); } catch (error) { log.error("Error creating session on behalf of IAM", error); @@ -174,7 +187,7 @@ export class IamSessionApp { ctx, ); - await this.orgService.addOrUpdateMember(SYSTEM_USER, organizationId, user.id, "member", ctx); + await this.orgService.addOrUpdateMember(SYSTEM_USER_ID, organizationId, user.id, "member", ctx); return user; }); } diff --git a/components/server/src/jobs/runner.ts b/components/server/src/jobs/runner.ts index cc6c80577f6c57..820cafae2ae25b 100644 --- a/components/server/src/jobs/runner.ts +++ b/components/server/src/jobs/runner.ts @@ -20,7 +20,6 @@ import { RelationshipUpdateJob } from "../authorization/relationship-updater-job import { WorkspaceStartController } from "../workspace/workspace-start-controller"; import { runWithRequestContext } from "../util/request-context"; import { SYSTEM_USER } from "../authorization/authorizer"; -import { SubjectId } from "../auth/subject-id"; export const Job = Symbol("Job"); @@ -87,7 +86,7 @@ export class JobRunner { signal, requestKind: "job", requestMethod: job.name, - subjectId: SubjectId.fromUserId(SYSTEM_USER), + subjectId: SYSTEM_USER, }; await runWithRequestContext(ctx, async () => { log.info(`Acquired lock for job ${job.name}.`, logCtx); diff --git a/components/server/src/jobs/workspace-gc.ts b/components/server/src/jobs/workspace-gc.ts index 5679e706d12492..608eae3d600316 100644 --- a/components/server/src/jobs/workspace-gc.ts +++ b/components/server/src/jobs/workspace-gc.ts @@ -18,7 +18,7 @@ import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { Config } from "../config"; import { Job } from "./runner"; import { WorkspaceService } from "../workspace/workspace-service"; -import { SYSTEM_USER } from "../authorization/authorizer"; +import { SYSTEM_USER_ID } from "../authorization/authorizer"; import { StorageClient } from "../storage/storage-client"; /** @@ -93,7 +93,7 @@ export class WorkspaceGarbageCollector implements Job { log.info(`workspace-gc: about to soft-delete ${workspaces.length} workspaces`); for (const ws of workspaces) { try { - await this.workspaceService.deleteWorkspace(SYSTEM_USER, ws.id, "gc"); + await this.workspaceService.deleteWorkspace(SYSTEM_USER_ID, ws.id, "gc"); } catch (err) { log.error({ workspaceId: ws.id }, "workspace-gc: error during workspace soft-deletion", err); } @@ -165,7 +165,7 @@ export class WorkspaceGarbageCollector implements Job { log.info(`workspace-gc: about to purge ${workspaces.length} workspaces`); for (const ws of workspaces) { try { - await this.workspaceService.hardDeleteWorkspace(SYSTEM_USER, ws.id); + await this.workspaceService.hardDeleteWorkspace(SYSTEM_USER_ID, ws.id); } catch (err) { log.error({ workspaceId: ws.id }, "workspace-gc: failed to purge workspace", err); } diff --git a/components/server/src/messaging/redis-subscriber.ts b/components/server/src/messaging/redis-subscriber.ts index cf36c18446ae20..a482307d4f29c4 100644 --- a/components/server/src/messaging/redis-subscriber.ts +++ b/components/server/src/messaging/redis-subscriber.ts @@ -31,7 +31,6 @@ import { Redis } from "ioredis"; import { WorkspaceDB } from "@gitpod/gitpod-db/lib"; import { runWithRequestContext } from "../util/request-context"; import { SYSTEM_USER } from "../authorization/authorizer"; -import { SubjectId } from "../auth/subject-id"; const UNDEFINED_KEY = "undefined"; @@ -61,7 +60,7 @@ export class RedisSubscriber { signal: new AbortController().signal, requestKind: "redis-subscriber", requestMethod: channel, - subjectId: SubjectId.fromUserId(SYSTEM_USER), + subjectId: SYSTEM_USER, }; await runWithRequestContext(ctx, async () => { reportRedisUpdateReceived(channel); diff --git a/components/server/src/prebuilds/github-app.ts b/components/server/src/prebuilds/github-app.ts index 3f99559c7870d1..6591a10a58cc75 100644 --- a/components/server/src/prebuilds/github-app.ts +++ b/components/server/src/prebuilds/github-app.ts @@ -41,9 +41,8 @@ import { RepoURL } from "../repohost"; import { ApplicationError, ErrorCode } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { UserService } from "../user/user-service"; import { ProjectsService } from "../projects/projects-service"; +import { SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; import { runWithSubjectId, runWithRequestContext } from "../util/request-context"; -import { SYSTEM_USER } from "../authorization/authorizer"; -import { SubjectId } from "../auth/subject-id"; /** * GitHub app urls: @@ -175,9 +174,9 @@ export class GithubApp { // To implement this in a more robust way, we'd need to store `repository.id` with the project, next to the cloneUrl. const oldName = (ctx.payload as any)?.changes?.repository?.name?.from; if (oldName) { - const projects = await runWithSubjectId(SubjectId.fromUserId(SYSTEM_USER), async () => + const projects = await runWithSubjectId(SYSTEM_USER, async () => this.projectService.findProjectsByCloneUrl( - SYSTEM_USER, + SYSTEM_USER_ID, `https://github.com/${repository.owner.login}/${oldName}.git`, ), ); @@ -301,8 +300,8 @@ export class GithubApp { const contextURL = `${repo.html_url}/tree/${branch}`; span.setTag("contextURL", contextURL); const context = (await this.contextParser.handle({ span }, installationOwner, contextURL)) as CommitContext; - const projects = await runWithSubjectId(SubjectId.fromUserId(SYSTEM_USER), async () => - this.projectService.findProjectsByCloneUrl(SYSTEM_USER, context.repository.cloneUrl), + const projects = await runWithSubjectId(SYSTEM_USER, async () => + this.projectService.findProjectsByCloneUrl(SYSTEM_USER_ID, context.repository.cloneUrl), ); for (const project of projects) { try { diff --git a/components/server/src/prebuilds/github-enterprise-app.ts b/components/server/src/prebuilds/github-enterprise-app.ts index 5ad2ee77907cca..8cefa7bb1f450b 100644 --- a/components/server/src/prebuilds/github-enterprise-app.ts +++ b/components/server/src/prebuilds/github-enterprise-app.ts @@ -22,9 +22,8 @@ import { RepoURL } from "../repohost"; import { UserService } from "../user/user-service"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { ProjectsService } from "../projects/projects-service"; -import { SYSTEM_USER } from "../authorization/authorizer"; +import { SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; import { runWithSubjectId } from "../util/request-context"; -import { SubjectId } from "../auth/subject-id"; @injectable() export class GitHubEnterpriseApp { @@ -260,8 +259,8 @@ export class GitHubEnterpriseApp { private async findProjectOwners(cloneURL: string): Promise<{ users: User[]; project: Project } | undefined> { try { - const projects = await runWithSubjectId(SubjectId.fromUserId(SYSTEM_USER), async () => - this.projectService.findProjectsByCloneUrl(SYSTEM_USER, cloneURL), + const projects = await runWithSubjectId(SYSTEM_USER, async () => + this.projectService.findProjectsByCloneUrl(SYSTEM_USER_ID, cloneURL), ); const project = projects[0]; if (project) { diff --git a/components/server/src/projects/projects-service.spec.db.ts b/components/server/src/projects/projects-service.spec.db.ts index 072cb22f3089ff..bc26f7cf5a1661 100644 --- a/components/server/src/projects/projects-service.spec.db.ts +++ b/components/server/src/projects/projects-service.spec.db.ts @@ -14,7 +14,7 @@ import { Container } from "inversify"; import "mocha"; import { OrganizationService } from "../orgs/organization-service"; import { expectError } from "../test/expect-utils"; -import { createTestContainer } from "../test/service-testing-container-module"; +import { createTestContainer, withTestCtx } from "../test/service-testing-container-module"; import { OldProjectSettings, ProjectsService } from "./projects-service"; import { daysBefore } from "@gitpod/gitpod-protocol/lib/util/timeutil"; @@ -232,7 +232,7 @@ describe("ProjectsService", async () => { prebuildBranchPattern: "feature-*", }, }); - const project = await ps.getProject(owner.id, oldProject.id); + const project = await withTestCtx(owner, () => ps.getProject(owner.id, oldProject.id)); expect(project.settings).to.deep.equal({ prebuilds: { ...Project.PREBUILD_SETTINGS_DEFAULTS, @@ -257,7 +257,7 @@ describe("ProjectsService", async () => { prebuildBranchPattern: "feature-*", }, }); - const project = await ps.getProject(owner.id, oldProject.id); + const project = await withTestCtx(owner, () => ps.getProject(owner.id, oldProject.id)); expect(project.settings).to.deep.equal({ prebuilds: { ...Project.PREBUILD_SETTINGS_DEFAULTS, @@ -283,7 +283,7 @@ describe("ProjectsService", async () => { }, }); await createWorkspaceForProject(oldProject.id, 1); - const project = await ps.getProject(owner.id, oldProject.id); + const project = await withTestCtx(owner, () => ps.getProject(owner.id, oldProject.id)); expect(project.settings).to.deep.equal({ prebuilds: { enable: true, diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index a8925288895432..c5add4edab9110 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -26,11 +26,12 @@ import { import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { URL } from "url"; -import { Authorizer, SYSTEM_USER } from "../authorization/authorizer"; +import { Authorizer, SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; import { TransactionalContext } from "@gitpod/gitpod-db/lib/typeorm/transactional-db-impl"; import { daysBefore, isDateSmaller } from "@gitpod/gitpod-protocol/lib/util/timeutil"; import deepmerge from "deepmerge"; import { ScmService } from "../scm/scm-service"; +import { runWithSubjectId } from "../util/request-context"; const MAX_PROJECT_NAME_LENGTH = 100; @@ -450,7 +451,9 @@ export class ProjectsService { const newPrebuildSettings: PrebuildSettings = { enable: false, ...Project.PREBUILD_SETTINGS_DEFAULTS }; // if workspaces were running in the past week - const isInactive = await this.isProjectConsideredInactive(SYSTEM_USER, project.id); + const isInactive = await runWithSubjectId(SYSTEM_USER, async () => + this.isProjectConsideredInactive(SYSTEM_USER_ID, project.id), + ); logCtx.isInactive = isInactive; if (!isInactive) { const sevenDaysAgo = new Date(daysBefore(new Date().toISOString(), 7)); diff --git a/components/server/src/prometheus-metrics.ts b/components/server/src/prometheus-metrics.ts index 0d356dc0ae37fb..8db1ef40cd54cb 100644 --- a/components/server/src/prometheus-metrics.ts +++ b/components/server/src/prometheus-metrics.ts @@ -35,6 +35,8 @@ export function registerServerMetrics(registry: prometheusClient.Registry) { registry.registerMetric(dbConnectionsFree); registry.registerMetric(grpcServerStarted); registry.registerMetric(grpcServerHandling); + registry.registerMetric(spicedbCheckRequestsTotal); + registry.registerMetric(authorizerSubjectId); } export const grpcServerStarted = new prometheusClient.Counter({ @@ -368,3 +370,13 @@ export type SpiceDBCheckConsistency = export function incSpiceDBRequestsCheckTotal(consistency: SpiceDBCheckConsistency) { spicedbCheckRequestsTotal.labels(consistency).inc(); } + +export const authorizerSubjectId = new prometheusClient.Counter({ + name: "gitpod_authorizer_subject_id_total", + help: "Counter for the number of authorizer permission checks", + labelNames: ["match"], +}); +type AuthorizerSubjectIdMatch = "ctx-user-id-missing" | "match" | "mismatch"; +export function reportAuthorizerSubjectId(match: AuthorizerSubjectIdMatch) { + authorizerSubjectId.labels(match).inc(); +} diff --git a/components/server/src/test/service-testing-container-module.ts b/components/server/src/test/service-testing-container-module.ts index 104eede97e118a..67edb870d7e742 100644 --- a/components/server/src/test/service-testing-container-module.ts +++ b/components/server/src/test/service-testing-container-module.ts @@ -38,6 +38,9 @@ import { GitHubScope } from "../github/scopes"; import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url"; import * as crypto from "crypto"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { Subject, SubjectId } from "../auth/subject-id"; +import { User } from "@gitpod/gitpod-protocol"; +import { runWithRequestContext } from "../util/request-context"; const signingKeyPair = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 }); const validatingKeyPair1 = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 }); @@ -329,3 +332,15 @@ export function createTestContainer() { container.load(mockApplyingContainerModule); return container; } + +export function withTestCtx(subject: Subject | User, p: () => Promise): Promise { + return runWithRequestContext( + { + requestKind: "testContext", + requestMethod: "testMethod", + signal: new AbortController().signal, + subjectId: SubjectId.is(subject) ? subject : SubjectId.fromUserId(User.is(subject) ? subject.id : subject), + }, + p, + ); +} diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index beb0b103309b28..4b947b237bc6fc 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -139,7 +139,7 @@ import { } from "@gitpod/usage-api/lib/usage/v1/billing.pb"; import { ClientError } from "nice-grpc-common"; import { BillingModes } from "../billing/billing-mode"; -import { Authorizer, SYSTEM_USER, isFgaChecksEnabled } from "../authorization/authorizer"; +import { Authorizer, SYSTEM_USER, SYSTEM_USER_ID, isFgaChecksEnabled } from "../authorization/authorizer"; import { OrganizationService } from "../orgs/organization-service"; import { RedisSubscriber } from "../messaging/redis-subscriber"; import { UsageService } from "../orgs/usage-service"; @@ -148,10 +148,10 @@ import { SSHKeyService } from "../user/sshkey-service"; import { StartWorkspaceOptions, WorkspaceService } from "./workspace-service"; import { GitpodTokenService } from "../user/gitpod-token-service"; import { EnvVarService } from "../user/env-var-service"; -import { SubjectId } from "../auth/subject-id"; -import { runWithSubjectId } from "../util/request-context"; import { ScmService } from "../scm/scm-service"; import { ContextService } from "./context-service"; +import { runWithRequestContext, runWithSubjectId } from "../util/request-context"; +import { SubjectId } from "../auth/subject-id"; // shortcut export const traceWI = (ctx: TraceContext, wi: Omit) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager @@ -284,17 +284,29 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } private async getAccessibleProjects() { - if (!this.userID) { + const userId = this.userID; + if (!userId) { return []; } // update all project this user has access to - const allProjects: Project[] = []; - const teams = await this.organizationService.listOrganizationsByMember(this.userID, this.userID); - for (const team of teams) { - allProjects.push(...(await this.projectsService.getProjects(this.userID, team.id))); - } - return allProjects; + // gpl: This call to runWithRequestContext is not nice, but it's only there to please the old impl for a limited time, so it's fine. + return runWithRequestContext( + { + requestKind: "gitpod-server-impl-listener", + requestMethod: "getAccessibleProjects", + signal: new AbortController().signal, + subjectId: SubjectId.fromUserId(userId), + }, + async () => { + const allProjects: Project[] = []; + const teams = await this.organizationService.listOrganizationsByMember(userId, userId); + for (const team of teams) { + allProjects.push(...(await this.projectsService.getProjects(userId, team.id))); + } + return allProjects; + }, + ); } private listenForWorkspaceInstanceUpdates(): void { @@ -313,9 +325,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { private forwardInstanceUpdateToClient(ctx: TraceContext, instance: WorkspaceInstance) { // gpl: We decided against tracing updates here, because it create far too much noise (cmp. history) - if (this.userID) { - this.workspaceService - .getWorkspace(this.userID, instance.workspaceId) + const userId = this.userID; + if (userId) { + runWithSubjectId(SubjectId.fromUserId(userId), () => + this.workspaceService.getWorkspace(userId, instance.workspaceId), + ) .then((ws) => { this.client?.onInstanceUpdate(this.censorInstance(instance)); }) @@ -376,8 +390,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "User is not authenticated. Please login."); } - const user = await runWithSubjectId(SubjectId.fromUserId(SYSTEM_USER), async () => - this.userService.findUserById(SYSTEM_USER, userId), + 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."); diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index ba71abb9094751..b421bb6f024d8c 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -126,13 +126,12 @@ import { TokenProvider } from "../user/token-provider"; import { UserAuthentication } from "../user/user-authentication"; import { ImageSourceProvider } from "./image-source-provider"; import { WorkspaceClassesConfig } from "./workspace-classes"; -import { SYSTEM_USER } from "../authorization/authorizer"; +import { SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; import { EnvVarService, ResolvedEnvVars } from "../user/env-var-service"; import { RedlockAbortSignal } from "redlock"; import { ConfigProvider } from "./config-provider"; import { isGrpcError } from "@gitpod/gitpod-protocol/lib/util/grpc"; import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; -import { SubjectId } from "../auth/subject-id"; import { runWithSubjectId } from "../util/request-context"; export interface StartWorkspaceOptions extends GitpodServer.StartWorkspaceOptions { @@ -490,8 +489,8 @@ export class WorkspaceStarter { if (blockedRepository.blockUser) { try { - await runWithSubjectId(SubjectId.fromUserId(SYSTEM_USER), async () => - this.userService.blockUser(SYSTEM_USER, user.id, true), + await runWithSubjectId(SYSTEM_USER, async () => + this.userService.blockUser(SYSTEM_USER_ID, user.id, true), ); log.info({ userId: user.id }, "Blocked user.", { contextURL }); } catch (error) {