fix(rbac): cover stack assignment cleanup on blueprint withdraw (#1664)

Main already clears stack-scoped role_assignments through
DeployedStackDeletionService (used by local blueprint withdraw and
stack DELETE). Add call-path regressions for successful cleanup,
ENOENT-as-absent, non-ENOENT filesystem failure, and db_failed when
RBAC cleanup throws, plus remote hub isolation.
This commit is contained in:
Anso
2026-07-22 20:29:08 -04:00
committed by GitHub
parent 698b7d0713
commit 6484c79015
4 changed files with 194 additions and 0 deletions
@@ -214,4 +214,42 @@ describe('BlueprintService remote deploy', () => {
expect(delSpy.mock.calls[0][0]).toMatch(/\/api\/stacks\//);
expect(DatabaseService.getInstance().getDeployment(bp.id, node.id)).toBeUndefined();
});
it('does not clear hub role assignments when withdrawing a remote deployment', async () => {
const bcrypt = await import('bcrypt');
const db = DatabaseService.getInstance();
const node = seedRemoteNode();
const bp = seedBlueprint([node.id]);
const nodeObj = db.getNode(node.id)!;
const bpObj = db.getBlueprint(bp.id)!;
db.upsertDeployment({
blueprint_id: bp.id,
node_id: node.id,
status: 'active',
applied_revision: bpObj.revision,
});
const hash = await bcrypt.hash('password123', 1);
const userId = db.addUser({
username: `remote-wd-rbac-${counter}`, password_hash: hash, role: 'viewer',
});
db.addRoleAssignment({
user_id: userId, role: 'deployer', resource_type: 'stack', resource_id: bpObj.name,
});
vi.spyOn(axios, 'get').mockResolvedValue({ status: 404, data: {} });
vi.spyOn(axios, 'post').mockResolvedValue({ status: 200, data: {} });
const delSpy = vi.spyOn(axios, 'delete').mockResolvedValue({ status: 200, data: {} });
const rbacSpy = vi.spyOn(db, 'deleteRoleAssignmentsByResource');
const result = await BlueprintService.getInstance().withdrawFromNode(bpObj, nodeObj);
expect(result.status).toBe('withdrawn');
expect(delSpy.mock.calls[0][0]).toMatch(/\/api\/stacks\//);
expect(rbacSpy).not.toHaveBeenCalled();
expect(db.getAllRoleAssignments(userId)
.some((a) => a.resource_type === 'stack' && a.resource_id === bpObj.name)).toBe(true);
db.deleteUser(userId);
});
});