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/authorization/authorizer.ts b/components/server/src/authorization/authorizer.ts index 4fad8188d744e2..667b02f29ac29d 100644 --- a/components/server/src/authorization/authorizer.ts +++ b/components/server/src/authorization/authorizer.ts @@ -25,6 +25,8 @@ 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 { tryGetSubjectId } from "../util/request-context"; export function createInitializingAuthorizer(spiceDbAuthorizer: SpiceDBAuthorizer): Authorizer { const target = new Authorizer(spiceDbAuthorizer); @@ -52,61 +54,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 +126,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 +158,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 +190,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.`); } @@ -210,8 +226,9 @@ export class Authorizer { } // write operations below - public async removeAllRelationships(userId: string, type: ResourceType, id: string) { - if (!(await isFgaWritesEnabled(userId))) { + public async removeAllRelationships(passed: Subject, type: ResourceType, id: string) { + const subjectId = await getSubjectFromCtx(passed); + if (!(await isFgaWritesEnabled(subjectId))) { return; } await this.authorizer.deleteRelationships( @@ -302,20 +319,22 @@ export class Authorizer { await this.authorizer.writeRelationships(...updates); } - async addProjectToOrg(userId: string, orgID: string, projectID: string): Promise { - if (!(await isFgaWritesEnabled(userId))) { + async addProjectToOrg(passed: Subject, orgID: string, projectID: string): Promise { + const subjectId = await getSubjectFromCtx(passed); + if (!(await isFgaWritesEnabled(subjectId))) { return; } await this.authorizer.writeRelationships(set(rel.project(projectID).org.organization(orgID))); } async setProjectVisibility( - userId: string, + passed: Subject, projectID: string, organizationId: string, visibility: Project.Visibility, ) { - if (!(await isFgaWritesEnabled(userId))) { + const subjectId = await getSubjectFromCtx(passed); + if (!(await isFgaWritesEnabled(subjectId))) { return; } const updates = []; @@ -336,8 +355,9 @@ export class Authorizer { await this.authorizer.writeRelationships(...updates); } - async removeProjectFromOrg(userId: string, orgID: string, projectID: string): Promise { - if (!(await isFgaWritesEnabled(userId))) { + async removeProjectFromOrg(passed: Subject, orgID: string, projectID: string): Promise { + const subjectId = await getSubjectFromCtx(passed); + if (!(await isFgaWritesEnabled(subjectId))) { return; } await this.authorizer.writeRelationships( @@ -348,17 +368,18 @@ export class Authorizer { } async addOrganization( - userId: string, + passed: Subject, orgId: string, members: { userId: string; role: TeamMemberRole }[], projectIds: string[], ): Promise { - if (!(await isFgaWritesEnabled(userId))) { + const subjectId = await getSubjectFromCtx(passed); + if (!(await isFgaWritesEnabled(subjectId))) { return; } await this.addOrganizationMembers(orgId, members); - await this.addOrganizationProjects(userId, orgId, projectIds); + await this.addOrganizationProjects(subjectId, orgId, projectIds); await this.authorizer.writeRelationships( set(rel.organization(orgId).installation.installation), // @@ -366,16 +387,16 @@ export class Authorizer { ); } - private async addOrganizationProjects(userId: string, orgID: string, projectIds: string[]): Promise { + private async addOrganizationProjects(subject: Subject, orgID: string, projectIds: string[]): Promise { const existing = await this.findAll(rel.project("").org.organization(orgID)); const toBeRemoved = asSet(existing.map((r) => r.resource?.objectId)); for (const projectId of projectIds) { - await this.addProjectToOrg(userId, orgID, projectId); - await this.setProjectVisibility(userId, projectId, orgID, "org-public"); + await this.addProjectToOrg(subject, orgID, projectId); + await this.setProjectVisibility(subject, projectId, orgID, "org-public"); toBeRemoved.delete(projectId); } for (const projectId of toBeRemoved) { - await this.removeProjectFromOrg(userId, orgID, projectId); + await this.removeProjectFromOrg(subject, orgID, projectId); } } @@ -503,21 +524,59 @@ export class Authorizer { } } -export async function isFgaChecksEnabled(userId: string): Promise { +async function getSubjectFromCtx(passed: Subject): Promise { + // Check feature flag, based on the passed subjectId + const passedSubjectId = Subject.toId(passed); + const passedUserId = passedSubjectId.userId(); + const authViaContext = await getExperimentsClientForBackend().getValueAsync("authViaContext", false, { + user: passedUserId + ? { + id: passedUserId, + } + : undefined, + }); + if (!authViaContext) { + return passedSubjectId; + } + + const ctxSubjectId = tryGetSubjectId(); + if (!ctxSubjectId) { + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, `No SubjectId available`); + } + 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: { - id: userId, - }, + user: userId + ? { + id: userId, + } + : undefined, }); } -export async function isFgaWritesEnabled(userId: string): 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 { @@ -541,9 +600,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 24ac3571058b94..afc3922d4fbfd5 100644 --- a/components/server/src/authorization/caching-spicedb-authorizer.spec.db.ts +++ b/components/server/src/authorization/caching-spicedb-authorizer.spec.db.ts @@ -11,7 +11,7 @@ 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 { 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"; @@ -87,7 +87,7 @@ 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")); + await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER_ID, org1.id, userA.id, "owner")); const userB = await withCtx( SYSTEM_USER, userSvc.createUser({ @@ -99,7 +99,7 @@ describe("CachingSpiceDBAuthorizer", async () => { }, }), ); - await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER, org1.id, userB.id, "member")); + await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER_ID, org1.id, userB.id, "member")); const userC = await withCtx( SYSTEM_USER, userSvc.createUser({ @@ -111,7 +111,7 @@ describe("CachingSpiceDBAuthorizer", async () => { }, }), ); - await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER, org1.id, userC.id, "member")); + await withCtx(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) @@ -131,7 +131,7 @@ describe("CachingSpiceDBAuthorizer", async () => { ).to.be.true; // userB is removed from the org - await withCtx(SYSTEM_USER, orgSvc.removeOrganizationMember(SYSTEM_USER, org1.id, userB.id)); + await withCtx(SYSTEM_USER, orgSvc.removeOrganizationMember(SYSTEM_USER_ID, org1.id, userB.id)); expect( await withCtx(userB, authorizer.hasPermissionOnWorkspace(userB.id, "read_info", ws1.id)), @@ -183,7 +183,7 @@ 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")); + await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER_ID, org1.id, userA.id, "owner")); const userC = await withCtx( SYSTEM_USER, userSvc.createUser({ @@ -195,7 +195,7 @@ describe("CachingSpiceDBAuthorizer", async () => { }, }), ); - await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER, org1.id, userC.id, "member")); + await withCtx(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)); @@ -221,7 +221,7 @@ describe("CachingSpiceDBAuthorizer", async () => { }, }), ); - await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER, org1.id, userB.id, "member")); + await withCtx(SYSTEM_USER, orgSvc.addOrUpdateMember(SYSTEM_USER_ID, org1.id, userB.id, "member")); expect( await withCtx(userB, authorizer.hasPermissionOnWorkspace(userB.id, "read_info", ws1.id)), diff --git a/components/server/src/iam/iam-session-app.ts b/components/server/src/iam/iam-session-app.ts index cd26355fa01ff2..d542230fe10015 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 { runWithChildContext, runWithRequestContext } from "../util/request-context"; @injectable() export class IamSessionApp { @@ -42,9 +43,23 @@ 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 runWithChildContext({ subjectId: 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 +189,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 d2a780aa9f2fd6..cd7d1b92718286 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 AbortSignal(), 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 72421fafb6191c..e4fee778c98acd 100644 --- a/components/server/src/prebuilds/github-app.ts +++ b/components/server/src/prebuilds/github-app.ts @@ -41,7 +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 { runWithRequestContext } from "../util/request-context"; +import { SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; +import { runWithChildContext, runWithRequestContext } from "../util/request-context"; /** * GitHub app urls: @@ -174,14 +175,16 @@ 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 this.projectService.findProjectsByCloneUrl( - SYSTEM_USER, - `https://github.com/${repository.owner.login}/${oldName}.git`, - ); - for (const project of projects) { - project.cloneUrl = repository.clone_url; - await this.projectDB.storeProject(project); - } + await runWithChildContext({ subjectId: SYSTEM_USER }, async () => { + const projects = await this.projectService.findProjectsByCloneUrl( + SYSTEM_USER_ID, + `https://github.com/${repository.owner.login}/${oldName}.git`, + ); + for (const project of projects) { + project.cloneUrl = repository.clone_url; + await this.projectDB.storeProject(project); + } + }); } } })(), @@ -298,7 +301,10 @@ 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 this.projectService.findProjectsByCloneUrl(SYSTEM_USER, context.repository.cloneUrl); + const projects = await this.projectService.findProjectsByCloneUrl( + SYSTEM_USER_ID, + context.repository.cloneUrl, + ); for (const project of projects) { try { const user = await this.findProjectOwner(project, installationOwner); diff --git a/components/server/src/prebuilds/github-enterprise-app.ts b/components/server/src/prebuilds/github-enterprise-app.ts index e34a51b2f900af..a3222726a9c021 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 { runWithChildContext } 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 runWithChildContext({ subjectId: SubjectId.fromUserId(SYSTEM_USER) }, async () => - this.projectService.findProjectsByCloneUrl(SYSTEM_USER, cloneURL), + const projects = await runWithChildContext({ subjectId: 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.ts b/components/server/src/projects/projects-service.ts index da59cd40ed3eef..53cf41494b1f5b 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -27,10 +27,11 @@ 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 { ScmService } from "./scm-service"; import { daysBefore, isDateSmaller } from "@gitpod/gitpod-protocol/lib/util/timeutil"; +import { runWithChildContext } from "../util/request-context"; const MAX_PROJECT_NAME_LENGTH = 100; @@ -452,7 +453,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 runWithChildContext({ subjectId: 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/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 785e00c820aac0..4c1fac81da7337 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -159,7 +159,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"; @@ -176,7 +176,6 @@ import { suggestionFromUserRepo, } from "./suggested-repos-sorter"; import { runWithChildContext } 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 @@ -474,8 +473,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { throw new ApplicationError(ErrorCodes.NOT_AUTHENTICATED, "User is not authenticated. Please login."); } - const user = await runWithChildContext({ subjectId: SubjectId.fromUserId(SYSTEM_USER) }, async () => - this.userService.findUserById(SYSTEM_USER, userId), + const user = await runWithChildContext({ subjectId: 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 8c1d12387e61bd..995239915e7208 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 { runWithChildContext } from "../util/request-context"; -import { SubjectId } from "../auth/subject-id"; export interface StartWorkspaceOptions extends GitpodServer.StartWorkspaceOptions { excludeFeatureFlags?: NamedWorkspaceFeatureFlag[]; @@ -489,8 +488,8 @@ export class WorkspaceStarter { if (blockedRepository.blockUser) { try { - await runWithChildContext({ subjectId: SubjectId.fromUserId(SYSTEM_USER) }, async () => - this.userService.blockUser(SYSTEM_USER, user.id, true), + await runWithChildContext({ subjectId: SYSTEM_USER }, async () => + this.userService.blockUser(SYSTEM_USER_ID, user.id, true), ); log.info({ userId: user.id }, "Blocked user.", { contextURL }); } catch (error) {