mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-28 19:27:41 +00:00
fix(blueprints): fail closed on marker ownership for apply and withdraw (#1694)
* fix(blueprints): fail closed on marker ownership for apply and withdraw Require a matching .blueprint.json under the stack lock, persist required_blueprint_id on deletion intents, remove the legacy remote apply fallback, and protect the marker in the file explorer. * fix(blueprints): add CodeQL path barriers on ownership probes Use the canonical resolve-and-startsWith sanitizer inline at the marker and stack-directory fs sinks so js/path-injection clears. * fix(blueprints): block delete on failed withdraw and defer marker write Refuse Blueprint DELETE when pre-delete withdraw does not complete, and write .blueprint.json only after a successful deploy so failed applies cannot orphan stacks or claim an unapplied revision. * test(blueprints): align lock-order assert with deferred marker write Update the per-stack lock ordering expectations to compose, cleanup, deploy, then marker after the partial-apply fix. * fix(deps): bump postcss past GHSA-r28c-9q8g-f849 for npm audit Raise the Vitest/Vite transitive postcss to 8.5.23 so Backend CI audit --audit-level=high passes.
This commit is contained in:
@@ -1116,6 +1116,37 @@ describe('PUT /api/stacks/:stackName/files/content', () => {
|
||||
expect(content).toBe('community-write');
|
||||
});
|
||||
|
||||
it('blocks root trust file writes while a stack op lock is held', async () => {
|
||||
const { StackOpLockService } = await import('../services/StackOpLockService');
|
||||
StackOpLockService.getInstance().tryAcquire(1, STACK, 'deploy', 'admin');
|
||||
try {
|
||||
const composeRes = await request(app)
|
||||
.put(`/api/stacks/${STACK}/files/content`)
|
||||
.query({ path: 'compose.yaml' })
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ content: 'services:\n app:\n image: nginx\n' });
|
||||
expect(composeRes.status).toBe(409);
|
||||
expect(composeRes.body.code).toBe('stack_op_in_progress');
|
||||
|
||||
const markerRes = await request(app)
|
||||
.put(`/api/stacks/${STACK}/files/content`)
|
||||
.query({ path: '.blueprint.json' })
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ content: '{"blueprintId":1,"revision":1,"lastApplied":0}' });
|
||||
expect(markerRes.status).toBe(409);
|
||||
expect(markerRes.body.code).toBe('stack_op_in_progress');
|
||||
|
||||
const nestedRes = await request(app)
|
||||
.put(`/api/stacks/${STACK}/files/content`)
|
||||
.query({ path: 'config/app.conf' })
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ content: 'ok' });
|
||||
expect(nestedRes.status).toBe(204);
|
||||
} finally {
|
||||
StackOpLockService.getInstance().release(1, STACK);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns 400 when content is not a string', async () => {
|
||||
const res = await request(app)
|
||||
.put(`/api/stacks/${STACK}/files/content`)
|
||||
@@ -1931,6 +1962,16 @@ describe('protected stack files', () => {
|
||||
expect(res.body.code).toBe('PROTECTED_FILE');
|
||||
});
|
||||
|
||||
it('DELETE /files refuses .blueprint.json with 409 PROTECTED_FILE', async () => {
|
||||
await fs.writeFile(path.join(stacksDir, STACK, '.blueprint.json'), '{"blueprintId":1,"revision":1}\n');
|
||||
const res = await request(app)
|
||||
.delete(`/api/stacks/${STACK}/files`)
|
||||
.query({ path: '.blueprint.json' })
|
||||
.set('Cookie', adminCookie);
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.body.code).toBe('PROTECTED_FILE');
|
||||
});
|
||||
|
||||
it('PATCH /files/rename refuses compose.yaml as source with 409 PROTECTED_FILE', async () => {
|
||||
const res = await request(app)
|
||||
.patch(`/api/stacks/${STACK}/files/rename`)
|
||||
|
||||
Reference in New Issue
Block a user