fix(server): harden environment read and probe paths

This commit is contained in:
Devin Foley committed 2026-06-20 01:46:55 -07:00
1 parent 2fd3fa44de
commit ce2fd09269
3 files changed
+137 -5

No files matched your search

@@ -247,6 +247,51 @@ describe("environment routes", () => {
});
});
it("redacts environment config for non-admin board readers", async () => {
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
const app = createApp({
type: "board",
userId: "user-2",
source: "session",
companyIds: ["company-1"],
memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }],
isInstanceAdmin: false,
});
const res = await request(app).get("/api/companies/company-1/environments");
expect(res.status).toBe(200);
expect(res.body[0]).toMatchObject({
id: "env-1",
name: "Local",
config: {},
envVars: {},
metadata: null,
});
});
it("redacts environment detail config for non-admin board readers", async () => {
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
const app = createApp({
type: "board",
userId: "user-2",
source: "session",
companyIds: ["company-1"],
memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }],
isInstanceAdmin: false,
});
const res = await request(app).get("/api/environments/env-1");
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
id: "env-1",
config: {},
envVars: {},
metadata: null,
});
});
it("returns provider capabilities for the company", async () => {
const app = createApp({
type: "board",
@@ -1262,6 +1307,49 @@ describe("environment routes", () => {
);
});
it("requires explicit companyId when probing a secret-backed environment without inferable secret context", async () => {
const environment = {
...createEnvironment(),
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: {
type: "secret_ref",
secretId: "11111111-1111-4111-8111-111111111111",
version: "latest",
},
knownHosts: null,
strictHostKeyChecking: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]);
const app = createApp({
type: "board",
userId: "user-1",
source: "session",
companyIds: ["company-1", "company-2"],
memberships: [
{ companyId: "company-1", status: "active", membershipRole: "member" },
{ companyId: "company-2", status: "active", membershipRole: "member" },
],
isInstanceAdmin: true,
runId: "run-1",
});
const res = await request(app)
.post(`/api/environments/${environment.id}/probe`)
.send({});
expect(res.status).toBe(422);
expect(res.body.error).toContain("explicit companyId");
expect(mockProbeEnvironment).not.toHaveBeenCalled();
});
it("probes a sandbox environment and logs the result", async () => {
const environment = {
...createEnvironment(),
@@ -114,9 +114,17 @@ describeEmbeddedPostgres("accepted plan workspace refresh", () => {
await db.delete(documents);
await db.delete(agentTaskSessions);
await db.delete(executionWorkspaces);
await db.delete(activityLog);
await db.delete(heartbeatRunEvents);
await db.delete(heartbeatRuns);
for (let attempt = 0; attempt < 5; attempt += 1) {
await db.delete(activityLog);
await db.delete(heartbeatRunEvents);
try {
await db.delete(heartbeatRuns);
break;
} catch (error) {
if (attempt === 4) throw error;
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
await db.delete(issueComments);
await db.delete(issues);
await db.delete(projectWorkspaces);
+38 -2
View File
@@ -62,6 +62,34 @@ export function environmentRoutes(
assertBoardOrgAccess(req);
}
function canReadFullInstanceEnvironment(req: Request) {
return req.actor.type === "board"
&& (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin);
}
function redactEnvironmentForRestrictedView<T extends {
config: Record<string, unknown> | null;
envVars?: Record<string, unknown> | null;
metadata: Record<string, unknown> | null;
}>(environment: T): T {
return {
...environment,
config: {},
...(Object.prototype.hasOwnProperty.call(environment, "envVars") ? { envVars: {} } : {}),
metadata: null,
};
}
function presentEnvironmentForRead<T extends {
config: Record<string, unknown> | null;
envVars?: Record<string, unknown> | null;
metadata: Record<string, unknown> | null;
}>(req: Request, environment: T): T {
return canReadFullInstanceEnvironment(req)
? environment
: redactEnvironmentForRestrictedView(environment);
}
async function assertCanReadSecretsForDraftProbe(req: Request, companyId: string) {
assertCanAccessInstanceEnvironments(req);
return companyId;
@@ -164,7 +192,7 @@ export function environmentRoutes(
status: req.query.status as string | undefined,
driver: req.query.driver as string | undefined,
});
res.json(rows);
res.json(rows.map((row) => presentEnvironmentForRead(req, row)));
});
router.get("/companies/:companyId/environments/capabilities", async (req, res) => {
@@ -258,7 +286,7 @@ export function environmentRoutes(
return;
}
assertCanReadInstanceEnvironments(req);
res.json(environment);
res.json(presentEnvironmentForRead(req, environment));
});
router.get("/environments/:id/leases", async (req, res) => {
@@ -413,6 +441,14 @@ export function environmentRoutes(
assertCanAccessInstanceEnvironments(req);
const actor = getActorInfo(req);
const companyIdForSecrets = await resolveEnvironmentSecretContextCompanyId(req, environment.id, { required: false });
if (!companyIdForSecrets) {
const secretRefs = await collectEnvironmentSecretRefs({ db, environment });
if (secretRefs.length > 0) {
throw unprocessable(
"Environment probe requires an explicit companyId to resolve secret-backed config for this environment.",
);
}
}
const probe = await probeEnvironment(db, environment, {
companyId: companyIdForSecrets,
pluginWorkerManager: options.pluginWorkerManager,