Skip to content

Commit 325292c

Browse files
committed
enforce command safety rules generically in BaseCommand and fix compound operations
- Evaluate command rules in BaseCommand.init() so every command is checked against safety rules automatically (e.g., { command: "code:deploy", action: "block" }) - Add temporary DELETE allow rules for code:deploy and code:watch to prevent internal WebDAV cleanup from being blocked by safety middleware - Simplify job commands to only evaluate job-specific rules (command rules now handled generically) - Update safety guide to document automatic command rule enforcement
1 parent 2c443de commit 325292c

7 files changed

Lines changed: 66 additions & 23 deletions

File tree

docs/guide/safety.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,12 @@ Matches OCAPI job execution by job ID. This catches both direct job commands and
112112

113113
#### CLI Command ID
114114

115-
Matches CLI commands by their oclif command ID:
115+
Matches CLI commands by their oclif command ID. Command rules are enforced automatically for **every** command before `run()` executes -- no per-command opt-in is needed:
116116

117117
```json
118118
{ "command": "sandbox:delete", "action": "confirm" }
119119
{ "command": "sandbox:*", "action": "block" }
120+
{ "command": "code:deploy", "action": "block" }
120121
```
121122

122123
### Evaluation Order

packages/b2c-cli/src/commands/code/deploy.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ export default class CodeDeploy extends CartridgeCommand<typeof CodeDeploy> {
161161
this.log(` ${c.name} (${c.src})`);
162162
}
163163

164+
// After safety evaluation passes, temporarily allow WebDAV DELETE operations
165+
// that are part of the deploy flow (cleanup of temp zip, --delete cartridge removal).
166+
const cleanupSafetyRule = this.safetyGuard.temporarilyAddRule({
167+
method: 'DELETE',
168+
path: '**/Cartridges/**',
169+
action: 'allow',
170+
});
171+
164172
try {
165173
// Optionally delete existing cartridges first
166174
if (this.flags.delete) {
@@ -240,6 +248,8 @@ export default class CodeDeploy extends CartridgeCommand<typeof CodeDeploy> {
240248
this.error(t('commands.code.deploy.failed', 'Deployment failed: {{message}}', {message: error.message}));
241249
}
242250
throw error;
251+
} finally {
252+
cleanupSafetyRule();
243253
}
244254
}
245255
}

packages/b2c-cli/src/commands/code/watch.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ export default class CodeWatch extends CartridgeCommand<typeof CodeWatch> {
5656
this.log(t('commands.code.watch.codeVersion', 'Code Version: {{version}}', {version}));
5757
}
5858

59+
// Temporarily allow WebDAV DELETE on Cartridges paths for the watch lifecycle.
60+
// The watcher DELETEs temp zip files after upload and syncs local file deletions.
61+
const cleanupSafetyRule = this.safetyGuard.temporarilyAddRule({
62+
method: 'DELETE',
63+
path: '**/Cartridges/**',
64+
action: 'allow',
65+
});
66+
5967
try {
6068
const result = await this.operations.watchCartridges(this.instance, this.cartridgePath, {
6169
...this.cartridgeOptions,
@@ -92,6 +100,8 @@ export default class CodeWatch extends CartridgeCommand<typeof CodeWatch> {
92100
this.error(t('commands.code.watch.failed', 'Watch failed: {{message}}', {message: error.message}));
93101
}
94102
throw error;
103+
} finally {
104+
cleanupSafetyRule();
95105
}
96106
}
97107
}

packages/b2c-cli/src/commands/job/export.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,14 @@ export default class JobExport extends JobCommand<typeof JobExport> {
129129

130130
const hostname = this.resolvedConfig.values.hostname!;
131131

132-
// Safety evaluation — check rules for export job before executing
132+
// Safety evaluation — check rules for export job before executing.
133+
// Command-level rules are already evaluated generically in BaseCommand.init().
133134
const jobEvaluation = this.safetyGuard.evaluate({type: 'job', jobId: 'sfcc-site-archive-export'});
134-
const cmdEvaluation = this.safetyGuard.evaluate({type: 'command', commandId: this.id});
135-
const evaluation = jobEvaluation.action === 'allow' ? cmdEvaluation : jobEvaluation;
136-
if (evaluation.action === 'block') {
137-
this.error(evaluation.reason, {exit: 1});
135+
if (jobEvaluation.action === 'block') {
136+
this.error(jobEvaluation.reason, {exit: 1});
138137
}
139-
if (evaluation.action === 'confirm') {
140-
await this.confirmOrBlock(evaluation);
138+
if (jobEvaluation.action === 'confirm') {
139+
await this.confirmOrBlock(jobEvaluation);
141140
}
142141

143142
// Build data units configuration

packages/b2c-cli/src/commands/job/import.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,14 @@ export default class JobImport extends JobCommand<typeof JobImport> {
7272

7373
const hostname = this.resolvedConfig.values.hostname!;
7474

75-
// Safety evaluation — check rules for import job before executing
75+
// Safety evaluation — check rules for import job before executing.
76+
// Command-level rules are already evaluated generically in BaseCommand.init().
7677
const jobEvaluation = this.safetyGuard.evaluate({type: 'job', jobId: 'sfcc-site-archive-import'});
77-
const cmdEvaluation = this.safetyGuard.evaluate({type: 'command', commandId: this.id});
78-
const evaluation = jobEvaluation.action === 'allow' ? cmdEvaluation : jobEvaluation;
79-
if (evaluation.action === 'block') {
80-
this.error(evaluation.reason, {exit: 1});
78+
if (jobEvaluation.action === 'block') {
79+
this.error(jobEvaluation.reason, {exit: 1});
8180
}
82-
if (evaluation.action === 'confirm') {
83-
await this.confirmOrBlock(evaluation);
81+
if (jobEvaluation.action === 'confirm') {
82+
await this.confirmOrBlock(jobEvaluation);
8483
}
8584

8685
// Create lifecycle context

packages/b2c-cli/src/commands/job/run.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,14 @@ export default class JobRun extends JobCommand<typeof JobRun> {
9595
'show-log': showLog,
9696
} = this.flags;
9797

98-
// Safety evaluation — check rules for this job before executing
98+
// Safety evaluation — check rules for this job before executing.
99+
// Command-level rules are already evaluated generically in BaseCommand.init().
99100
const jobEvaluation = this.safetyGuard.evaluate({type: 'job', jobId});
100-
const cmdEvaluation = this.safetyGuard.evaluate({type: 'command', commandId: this.id});
101-
// Use the more restrictive evaluation
102-
const evaluation = jobEvaluation.action === 'allow' ? cmdEvaluation : jobEvaluation;
103-
if (evaluation.action === 'block') {
104-
this.error(evaluation.reason, {exit: 1});
101+
if (jobEvaluation.action === 'block') {
102+
this.error(jobEvaluation.reason, {exit: 1});
105103
}
106-
if (evaluation.action === 'confirm') {
107-
await this.confirmOrBlock(evaluation);
104+
if (jobEvaluation.action === 'confirm') {
105+
await this.confirmOrBlock(jobEvaluation);
108106
}
109107

110108
// Parse parameters or body

packages/b2c-tooling-sdk/src/cli/base-command.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
203203
// Update safety guard with config-provided safety settings (merges env + config)
204204
this.initializeSafetyGuard();
205205

206+
// Evaluate command-level safety rules for every command.
207+
// This enforces rules like { command: "code:deploy", action: "block" } generically.
208+
await this.evaluateCommandSafety();
209+
206210
this.addTelemetryContext();
207211
}
208212

@@ -753,6 +757,28 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
753757
}
754758
}
755759

760+
/**
761+
* Evaluate command-level safety rules for the current command.
762+
*
763+
* This runs at the end of init() so every command is evaluated against
764+
* command rules (e.g., `{ command: "code:deploy", action: "block" }`).
765+
* If no command rule matches, this is a no-op — level-based blocking
766+
* is handled by the HTTP middleware and assertDestructiveOperationAllowed().
767+
*/
768+
private async evaluateCommandSafety(): Promise<void> {
769+
const evaluation = this.safetyGuard.evaluate({
770+
type: 'command',
771+
commandId: this.id,
772+
});
773+
774+
if (evaluation.action === 'block' && evaluation.rule) {
775+
this.error(evaluation.reason, {exit: 1});
776+
}
777+
if (evaluation.action === 'confirm' && evaluation.rule) {
778+
await this.confirmOrBlock(evaluation);
779+
}
780+
}
781+
756782
/**
757783
* Require interactive confirmation for a safety-guarded operation.
758784
*

0 commit comments

Comments
 (0)