diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts index 1b8da312660c5a..4c9d250c4c35d1 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts @@ -128,15 +128,12 @@ export function deduplicateAndFilterRepositories( if (results.length === 0) { // If the searchString is a URL, and it's not present in the proposed results, "artificially" add it here. if (isValidGitUrl(searchString)) { - console.log("It's valid man"); results.push( new SuggestedRepository({ url: searchString, }), ); } - - console.log("Valid after man"); } // Limit what we show to 200 results @@ -145,7 +142,7 @@ export function deduplicateAndFilterRepositories( const ALLOWED_GIT_PROTOCOLS = ["ssh:", "git:", "http:", "https:"]; /** - * An opionated git URL validator + * An opinionated git URL validator * * Assumptions: * - Git hosts are not themselves TLDs (like .com) or reserved names like `localhost` diff --git a/components/dashboard/src/prebuilds/detail/PrebuildDetailPage.tsx b/components/dashboard/src/prebuilds/detail/PrebuildDetailPage.tsx index cbc4349abaebac..0cebed55be2c5a 100644 --- a/components/dashboard/src/prebuilds/detail/PrebuildDetailPage.tsx +++ b/components/dashboard/src/prebuilds/detail/PrebuildDetailPage.tsx @@ -361,15 +361,13 @@ export const PrebuildDetailPage: FC = () => { > View Prebuild Settings - + diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index 9a5bb727965b4a..19e820cfc4b085 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -62,8 +62,14 @@ export class ProjectsService { @inject(InstallationService) private readonly installationService: InstallationService, ) {} - async getProject(userId: string, projectId: string): Promise { - await this.auth.checkPermissionOnProject(userId, "read_info", projectId); + /** + * Returns a project by its ID. + * @param skipPermissionCheck useful either when the caller already checked permissions or when we need to do something purely server-side (e.g. looking up a project when starting a workspace by a collaborator) + */ + async getProject(userId: string, projectId: string, skipPermissionCheck?: boolean): Promise { + if (!skipPermissionCheck) { + await this.auth.checkPermissionOnProject(userId, "read_info", projectId); + } const project = await this.projectDB.findProjectById(projectId); if (!project) { throw new ApplicationError(ErrorCodes.NOT_FOUND, `Project ${projectId} not found.`); @@ -132,11 +138,18 @@ export class ProjectsService { return filteredProjects; } - async findProjectsByCloneUrl(userId: string, cloneUrl: string, organizationId?: string): Promise { + async findProjectsByCloneUrl( + userId: string, + cloneUrl: string, + organizationId?: string, + skipPermissionCheck?: boolean, + ): Promise { const projects = await this.projectDB.findProjectsByCloneUrl(cloneUrl, organizationId); const result: Project[] = []; for (const project of projects) { - if (await this.auth.hasPermissionOnProject(userId, "read_info", project.id)) { + const hasPermission = + skipPermissionCheck || (await this.auth.hasPermissionOnProject(userId, "read_info", project.id)); + if (hasPermission) { result.push(project); } } diff --git a/components/server/src/workspace/context-service.ts b/components/server/src/workspace/context-service.ts index 1f09b817d8f7d7..3ce0f82011d33c 100644 --- a/components/server/src/workspace/context-service.ts +++ b/components/server/src/workspace/context-service.ts @@ -158,7 +158,9 @@ export class ContextService { user.id, context.repository.cloneUrl, options?.organizationId, + true, ); + // todo(ft): solve for this case with collaborators who can't select projects directly if (projects.length > 1) { throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Multiple projects found for clone URL."); } diff --git a/components/server/src/workspace/suggested-repos-sorter.ts b/components/server/src/workspace/suggested-repos-sorter.ts index 04ddf73bbda2c8..37713abf349908 100644 --- a/components/server/src/workspace/suggested-repos-sorter.ts +++ b/components/server/src/workspace/suggested-repos-sorter.ts @@ -20,7 +20,7 @@ export const sortSuggestedRepositories = (repos: SuggestedRepositoryWithSorting[ // This allows us to consider the lastUse of a recently used project when sorting // as it will may have an entry for the project (no lastUse), and another for recent workspaces (w/ lastUse) - const projectURLs: string[] = []; + let projectURLs: string[] = []; let uniqueRepositories: SuggestedRepositoryWithSorting[] = []; for (const repo of repos) { @@ -88,7 +88,7 @@ export const sortSuggestedRepositories = (repos: SuggestedRepositoryWithSorting[ uniqueRepositories = uniqueRepositories.map((repo) => { if (repo.projectId && !repo.projectName) { delete repo.projectId; - delete projectURLs[projectURLs.indexOf(repo.url)]; + projectURLs = projectURLs.filter((url) => url !== repo.url); } return repo; diff --git a/components/server/src/workspace/workspace-service.ts b/components/server/src/workspace/workspace-service.ts index defd384e1f3c33..b406c0ca5f0fe6 100644 --- a/components/server/src/workspace/workspace-service.ts +++ b/components/server/src/workspace/workspace-service.ts @@ -819,7 +819,7 @@ export class WorkspaceService { } const projectPromise = workspace.projectId - ? ApplicationError.notFoundToUndefined(this.projectsService.getProject(user.id, workspace.projectId)) + ? ApplicationError.notFoundToUndefined(this.projectsService.getProject(user.id, workspace.projectId, true)) : Promise.resolve(undefined); await mayStartPromise; @@ -866,7 +866,7 @@ export class WorkspaceService { result = await this.entitlementService.mayStartWorkspace(user, organizationId, runningInstances); TraceContext.addNestedTags(ctx, { mayStartWorkspace: { result } }); } catch (err) { - log.error({ userId: user.id }, "EntitlementSerivce.mayStartWorkspace error", err); + log.error({ userId: user.id }, "EntitlementService.mayStartWorkspace error", err); TraceContext.setError(ctx, err); return; // we don't want to block workspace starts because of internal errors } diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index 605d5e358c1750..faff4301894f49 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -566,8 +566,8 @@ export class WorkspaceStarter { return; } - // implicit project (existing on the same clone URL) - const projects = await this.projectService.findProjectsByCloneUrl(user.id, contextURL, organizationId); + // implicit project (existing on the same clone URL). We skip the permission check so that collaborators are not stuck + const projects = await this.projectService.findProjectsByCloneUrl(user.id, contextURL, organizationId, true); if (projects.length === 0) { throw new ApplicationError( ErrorCodes.PRECONDITION_FAILED, @@ -1951,10 +1951,12 @@ export class WorkspaceStarter { {}, ); if (isEnabledPrebuildFullClone) { - const project = await this.projectService.getProject(user.id, workspace.projectId).catch((err) => { - log.error("failed to get project", err); - return undefined; - }); + const project = await this.projectService + .getProject(user.id, workspace.projectId, true) + .catch((err) => { + log.error("failed to get project", err); + return undefined; + }); if (project && project.settings?.prebuilds?.cloneSettings?.fullClone) { result.setFullClone(true); } diff --git a/components/spicedb/schema/schema.yaml b/components/spicedb/schema/schema.yaml index c021f451add539..01b579d068f890 100644 --- a/components/spicedb/schema/schema.yaml +++ b/components/spicedb/schema/schema.yaml @@ -92,7 +92,7 @@ schema: |- permission read_billing = member + owner + installation->admin permission write_billing = owner + installation->admin - permission read_prebuild = member + owner + installation->admin + permission read_prebuild = collaborator + member + owner + installation->admin permission create_workspace = member + collaborator @@ -118,10 +118,10 @@ schema: |- permission write_info = editor + org->owner + org->installation_admin permission delete = editor + org->owner + org->installation_admin - permission read_env_var = viewer + editor + org->owner + org->installation_admin + permission read_env_var = viewer + editor + org->collaborator + org->owner + org->installation_admin permission write_env_var = editor + org->owner + org->installation_admin - permission read_prebuild = viewer + editor + org->owner + org->installation_admin + permission read_prebuild = viewer + editor + org->collaborator + org->owner + org->installation_admin permission write_prebuild = editor + org->owner }