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
}