mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-02 05:38:00 +00:00
fix: gate cross-node HTTP and stop-by-label on remote RBAC capability (#1509)
An older remote node ignores the forwarded actor-role header (running proxied requests as admin) and ignores the stop-by-label stack allowlist (stopping every label-matched stack). The control could neither detect nor prevent this on a mixed-version fleet. Instances now advertise a cross-node-rbac capability, and the control refuses to act when a remote lacks it: - HTTP proxy: a non-admin user's request is not forwarded to a remote that does not advertise the capability (fails closed when it cannot be determined). Admins are unaffected. - Stop-by-label: a real stop bound to a confirmed stack set is not sent to a remote lacking the capability; the node is reported as needing an upgrade. As defense in depth, a node whose results name stacks outside the confirmed set is failed rather than rendered as a clean stop. Separately, the stop's lock-contention path now reports every confirmed stack as a contention failure (including one that lost its label), so a confirmed stack is never silently dropped and the result is never empty.
This commit is contained in:
@@ -321,6 +321,30 @@ describe('local-stop behavior', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('reports every confirmed stack under lock contention, including one that lost its label', async () => {
|
||||
const label = db.createLabel(nodeId, 'contend-label', 'slate');
|
||||
db.setStackLabels('contend-kept', nodeId, [label.id]);
|
||||
// contend-lost was confirmed in the preview but no longer carries the label.
|
||||
const { activeBulkActions } = await import('../routes/labels');
|
||||
activeBulkActions.add(`bulk:${nodeId}`);
|
||||
try {
|
||||
const res = await request(app)
|
||||
.post('/api/fleet-actions/labels/local-stop')
|
||||
.set('Authorization', authHeader)
|
||||
.send({ labelName: 'contend-label', stackNames: ['contend-kept', 'contend-lost'] });
|
||||
expect(res.status).toBe(200);
|
||||
// Both confirmed stacks surface as contention failures; the lost one is
|
||||
// not dropped (which would read as "no stacks assigned"), and the result
|
||||
// is never empty when stacks were confirmed.
|
||||
expect(res.body.results).toHaveLength(2);
|
||||
const byName = Object.fromEntries(res.body.results.map((r: { stackName: string }) => [r.stackName, r]));
|
||||
expect(byName['contend-kept'].error).toMatch(/already running/);
|
||||
expect(byName['contend-lost'].error).toMatch(/already running/);
|
||||
} finally {
|
||||
activeBulkActions.delete(`bulk:${nodeId}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('dry run returns dryRun:true per on-disk stack without touching Docker', async () => {
|
||||
makeStack('dry-stack');
|
||||
const label = db.createLabel(nodeId, 'dry-label', 'slate');
|
||||
|
||||
Reference in New Issue
Block a user