diff --git a/components/gitpod-protocol/src/util/gitpod-host-url.ts b/components/gitpod-protocol/src/util/gitpod-host-url.ts index 39988cf2c13c0d..12c7208b4fa050 100644 --- a/components/gitpod-protocol/src/util/gitpod-host-url.ts +++ b/components/gitpod-protocol/src/util/gitpod-host-url.ts @@ -16,7 +16,7 @@ const baseWorkspaceIDRegex = "(([a-f][0-9a-f]{7}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|([0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8,11}))"; // this pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21) -const workspaceIDRegex = RegExp(`^(?:debug-)?${baseWorkspaceIDRegex}$`); +export const workspaceIDRegex = RegExp(`^(?:debug-)?${baseWorkspaceIDRegex}$`); // this pattern matches URL prefixes of workspaces const workspaceUrlPrefixRegex = RegExp(`^(([0-9]{4,6}|debug)-)?${baseWorkspaceIDRegex}\\.`); diff --git a/components/server/src/api/workspace-service-api.ts b/components/server/src/api/workspace-service-api.ts index cb1c1f58035eef..b8d64ba8363c78 100644 --- a/components/server/src/api/workspace-service-api.ts +++ b/components/server/src/api/workspace-service-api.ts @@ -58,6 +58,15 @@ import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messag import { ContextService } from "../workspace/context-service"; import { UserService } from "../user/user-service"; import { ContextParser } from "../workspace/context-parser-service"; +import { workspaceIDRegex } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url"; + +const isWorkspaceId = (workspaceId?: string) => { + if (!workspaceId) { + return false; + } + + return workspaceIDRegex.test(workspaceId); +}; @injectable() export class WorkspaceServiceAPI implements ServiceImpl { @@ -68,8 +77,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } const info = await this.workspaceService.getWorkspace(ctxUserId(), req.workspaceId); const response = new GetWorkspaceResponse(); @@ -198,8 +207,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { // We rely on FGA to do the permission checking - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } const user = await this.userService.findUserById(ctxUserId(), ctxUserId()); const { workspace, latestInstance: instance } = await this.workspaceService.getWorkspace( @@ -227,8 +236,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } const result = await this.workspaceService.getWorkspaceDefaultImage(ctxUserId(), req.workspaceId); const response = new GetWorkspaceDefaultImageResponse({ @@ -246,8 +255,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } const info = await this.workspaceService.getWorkspace(ctxUserId(), req.workspaceId); if (!info.latestInstance?.id || info.latestInstance.status.phase !== "running") { @@ -265,8 +274,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } const ownerToken = await this.workspaceService.getOwnerToken(ctxUserId(), req.workspaceId); const response = new GetWorkspaceOwnerTokenResponse(); @@ -278,8 +287,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } const credentials = await this.workspaceService.getIDECredentials(ctxUserId(), req.workspaceId); const response = new GetWorkspaceEditorCredentialsResponse(); @@ -288,8 +297,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } if (req.spec?.timeout?.inactivity?.seconds || (req.spec?.sshPublicKeys && req.spec?.sshPublicKeys.length > 0)) { throw new ApplicationError(ErrorCodes.UNIMPLEMENTED, "not implemented"); @@ -363,8 +372,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } await this.workspaceService.stopWorkspace(ctxUserId(), req.workspaceId, "stopped via API"); const response = new StopWorkspaceResponse(); @@ -372,8 +381,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } await this.workspaceService.deleteWorkspace(ctxUserId(), req.workspaceId, "user"); const response = new DeleteWorkspaceResponse(); @@ -389,8 +398,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } const snapshot = await this.workspaceService.takeSnapshot(ctxUserId(), { workspaceId: req.workspaceId, @@ -410,8 +419,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (!req.workspaceId) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required"); + if (!isWorkspaceId(req.workspaceId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); } if (!req.port) { throw new ApplicationError(ErrorCodes.BAD_REQUEST, "port is required"); diff --git a/components/server/src/authorization/spicedb-authorizer.ts b/components/server/src/authorization/spicedb-authorizer.ts index 4eb8202727a8b2..a35a74e6697ffb 100644 --- a/components/server/src/authorization/spicedb-authorizer.ts +++ b/components/server/src/authorization/spicedb-authorizer.ts @@ -15,6 +15,7 @@ import { base64decode } from "@jmondi/oauth2-server"; import { DecodedZedToken } from "@gitpod/spicedb-impl/lib/impl/v1/impl.pb"; import { ctxTryGetCache, ctxTrySetCache } from "../util/request-context"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { isGrpcError } from "@gitpod/gitpod-protocol/lib/util/grpc"; async function tryThree(errMessage: string, code: (attempt: number) => Promise): Promise { let attempt = 0; @@ -104,6 +105,9 @@ export class SpiceDBAuthorizer { const permitted = response.permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION; return { permitted, checkedAt: response.checkedAt?.token }; } catch (err) { + if (isGrpcError(err) && err.code === grpc.status.INVALID_ARGUMENT) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Invalid request for permission check: ${err}`); + } error = err; log.error("[spicedb] Failed to perform authorization check.", err, { request: new TrustedValue(req), diff --git a/components/server/src/workspace/workspace-service.ts b/components/server/src/workspace/workspace-service.ts index f437d31bf8aaf7..78f3e539d58db6 100644 --- a/components/server/src/workspace/workspace-service.ts +++ b/components/server/src/workspace/workspace-service.ts @@ -248,7 +248,7 @@ export class WorkspaceService { const res = await this.db.find({ limit: 20, ...options, - userId, // gpl: We probably want to removed this limitation in the future, butkeeping the old behavior for now due to focus on FGA + userId, // gpl: We probably want to removed this limitation in the future, but keeping the old behavior for now due to focus on FGA includeHeadless: false, });