Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/gitpod-protocol/src/util/gitpod-host-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}$`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 I wonder where this regex comes from... it hopefully is identical with this one?

@filiptronicek Could you check whether we can easily unify something here? 🤔 The code I cited should be the source of truth.


// this pattern matches URL prefixes of workspaces
const workspaceUrlPrefixRegex = RegExp(`^(([0-9]{4,6}|debug)-)?${baseWorkspaceIDRegex}\\.`);
Expand Down
53 changes: 31 additions & 22 deletions components/server/src/api/workspace-service-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of method should ideally be part of parse-workspace-id.ts.

if (!workspaceId) {
return false;
}

return workspaceIDRegex.test(workspaceId);
};

@injectable()
export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceInterface> {
Expand All @@ -68,8 +77,8 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
@inject(ContextParser) private contextParser: ContextParser;

async getWorkspace(req: GetWorkspaceRequest, _: HandlerContext): Promise<GetWorkspaceResponse> {
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();
Expand Down Expand Up @@ -198,8 +207,8 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI

async startWorkspace(req: StartWorkspaceRequest): Promise<StartWorkspaceResponse> {
// 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(
Expand Down Expand Up @@ -227,8 +236,8 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
req: GetWorkspaceDefaultImageRequest,
_: HandlerContext,
): Promise<GetWorkspaceDefaultImageResponse> {
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({
Expand All @@ -246,8 +255,8 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
}

async sendHeartBeat(req: SendHeartBeatRequest, _: HandlerContext): Promise<SendHeartBeatResponse> {
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") {
Expand All @@ -265,8 +274,8 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
req: GetWorkspaceOwnerTokenRequest,
_: HandlerContext,
): Promise<GetWorkspaceOwnerTokenResponse> {
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();
Expand All @@ -278,8 +287,8 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
req: GetWorkspaceEditorCredentialsRequest,
_: HandlerContext,
): Promise<GetWorkspaceEditorCredentialsResponse> {
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();
Expand All @@ -288,8 +297,8 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
}

async updateWorkspace(req: UpdateWorkspaceRequest): Promise<UpdateWorkspaceResponse> {
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");
Expand Down Expand Up @@ -363,17 +372,17 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
}

async stopWorkspace(req: StopWorkspaceRequest): Promise<StopWorkspaceResponse> {
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();
return response;
}

async deleteWorkspace(req: DeleteWorkspaceRequest): Promise<DeleteWorkspaceResponse> {
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();
Expand All @@ -389,8 +398,8 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
}

async createWorkspaceSnapshot(req: CreateWorkspaceSnapshotRequest): Promise<CreateWorkspaceSnapshotResponse> {
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,
Expand All @@ -410,8 +419,8 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
}

async updateWorkspacePort(req: UpdateWorkspacePortRequest): Promise<UpdateWorkspacePortResponse> {
if (!req.workspaceId) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "workspaceId is required");
if (!isWorkspaceId(req.workspaceId)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧡

throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required");
}
if (!req.port) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "port is required");
Expand Down
4 changes: 4 additions & 0 deletions components/server/src/authorization/spicedb-authorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(errMessage: string, code: (attempt: number) => Promise<T>): Promise<T> {
let attempt = 0;
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment on the "why"/context 🙏

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),
Expand Down
2 changes: 1 addition & 1 deletion components/server/src/workspace/workspace-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down
Loading