mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 15:22:59 +00:00
feat: add confirmed Take down stack action with optional volume removal (#1599)
* feat: add confirmed Take down stack action with optional volume removal Expose Take down in the stack header and sidebar with a confirmation dialog that runs compose down while keeping the stack definition on disk. Optional volume removal is gated by node capability and stack:deploy permission, with remote gateway preflight before proxying removeVolumes requests. Closes #1582 * fix: reset take-down volume checkbox when dialog closes * test: align getStackMenuVisibility assertions with showTakeDown key getStackMenuVisibility now returns a fifth lifecycle flag, showTakeDown, but three exhaustive toEqual assertions still listed only the prior four keys and failed. Add the expected showTakeDown value to each: true for the partial and exited running-stack cases, false for the self stack. * test: cover Take down visibility for running non-self stacks The getStackMenuVisibility assertions exercised the partial and exited branches and the self-stack guard, but not the raw === 'running' literal that drives showTakeDown for a normal running stack. Add a case so a regression dropping 'running' from that check is caught. * fix: drop Take down from header overflow and wire activity shortcut Remove duplicate Take down from More actions. Keep inline button when running, sidebar menu, and Cmd+ArrowDown. Record stack_taken_down in activity on successful POST /down.
This commit is contained in:
@@ -4,7 +4,8 @@ import { NodeRegistry } from '../services/NodeRegistry';
|
||||
import { PROXY_TIER_HEADER, PROXY_ROLE_HEADER } from '../services/license-headers';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import { isProxyExemptPath } from '../helpers/proxyExemptPaths';
|
||||
import { remoteSupportsCrossNodeRbac } from '../helpers/remoteCapabilities';
|
||||
import { remoteSupportsCrossNodeRbac, remoteAdvertisesCapability } from '../helpers/remoteCapabilities';
|
||||
import { STACK_DOWN_REMOVE_VOLUMES_CAPABILITY } from '../services/CapabilityRegistry';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { DatabaseService } from '../services/DatabaseService';
|
||||
import { redactSensitiveText } from '../utils/safeLog';
|
||||
@@ -146,31 +147,37 @@ export function createRemoteProxyMiddleware(): RequestHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mixed-version RBAC gate. The forwarded actor role is enforced only by a
|
||||
// remote that advertises cross-node-rbac; an older remote ignores the
|
||||
// header and runs the proxied request as admin. So a non-admin must not be
|
||||
// forwarded to a remote that does not advertise the capability. Admins are
|
||||
// unaffected (they are admin on the remote regardless), and the check is
|
||||
// skipped for them so it never adds latency to the admin path. Fails closed
|
||||
// when the capability cannot be determined. Using `?.` so an unresolved user
|
||||
// (not reachable past authGate, but defensive) is gated, never waved through.
|
||||
if (req.user?.role !== 'admin') {
|
||||
remoteSupportsCrossNodeRbac(req.nodeId)
|
||||
.then((supported) => {
|
||||
if (!supported) {
|
||||
res.status(403).json({
|
||||
error: `Remote node "${node.name}" is running a version that does not enforce per-user permissions. Upgrade it before non-admin users can act on it.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
req.proxyTarget = target;
|
||||
proxy(req, res, next);
|
||||
})
|
||||
.catch(next);
|
||||
return;
|
||||
}
|
||||
const runGatedProxy = async (): Promise<void> => {
|
||||
if (isStackDownWithRemoveVolumes(req)) {
|
||||
const supported = await remoteAdvertisesCapability(req.nodeId, STACK_DOWN_REMOVE_VOLUMES_CAPABILITY);
|
||||
if (!supported) {
|
||||
res.status(400).json({ error: 'Volume removal is not supported on this node' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
req.proxyTarget = target;
|
||||
proxy(req, res, next);
|
||||
// Mixed-version RBAC gate (non-admin only).
|
||||
if (req.user?.role !== 'admin') {
|
||||
const rbacSupported = await remoteSupportsCrossNodeRbac(req.nodeId);
|
||||
if (!rbacSupported) {
|
||||
res.status(403).json({
|
||||
error: `Remote node "${node.name}" is running a version that does not enforce per-user permissions. Upgrade it before non-admin users can act on it.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
req.proxyTarget = target;
|
||||
proxy(req, res, next);
|
||||
};
|
||||
|
||||
runGatedProxy().catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
/** POST /stacks/:stackName/down with ?removeVolumes=true (path is post-/api strip). */
|
||||
function isStackDownWithRemoveVolumes(req: Request): boolean {
|
||||
if (req.method !== 'POST') return false;
|
||||
if (!/^\/stacks\/[^/]+\/down$/.test(req.path)) return false;
|
||||
return req.query.removeVolumes === 'true';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user