Skip to content

Commit cf7fa57

Browse files
committed
fix(profiles): improve validation and address review feedback
- Improve denied_actions validation to check both tool and action parts are non-empty (fixes ':action', 'tool:', ':' edge cases) - Fix getProfileNameFromEnv JSDoc comment accuracy - Move multiple --profile warning outside loop with count - Add tests for denied_actions edge cases
1 parent aa7cbe4 commit cf7fa57

File tree

3 files changed

+80
-7
lines changed

3 files changed

+80
-7
lines changed

src/main.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ function getProfileFromArgs(): string | undefined {
2323
profileCount++;
2424
if (profileCount === 1) {
2525
profileName = value;
26-
} else if (profileCount === 2) {
27-
logger.warn("Multiple --profile flags detected, using first value");
2826
}
2927
}
3028
}
29+
30+
if (profileCount > 1) {
31+
logger.warn({ count: profileCount }, "Multiple --profile flags detected, using first value");
32+
}
33+
3134
return profileName;
3235
}
3336

src/profiles/loader.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,11 +335,18 @@ export class ProfileLoader {
335335
}
336336
}
337337

338-
// Validate denied_actions format
338+
// Validate denied_actions format (must be 'tool:action' with non-empty parts)
339339
if (profile.denied_actions) {
340340
for (const action of profile.denied_actions) {
341-
if (!action.includes(":")) {
341+
const colonIndex = action.indexOf(":");
342+
if (colonIndex === -1) {
342343
errors.push(`Invalid denied_action format '${action}', expected 'tool:action'`);
344+
} else {
345+
const tool = action.slice(0, colonIndex).trim();
346+
const act = action.slice(colonIndex + 1).trim();
347+
if (!tool || !act) {
348+
errors.push(`Invalid denied_action format '${action}', expected 'tool:action'`);
349+
}
343350
}
344351
}
345352
}
@@ -367,11 +374,18 @@ export class ProfileLoader {
367374
}
368375
}
369376

370-
// Validate denied_actions format
377+
// Validate denied_actions format (must be 'tool:action' with non-empty parts)
371378
if (preset.denied_actions) {
372379
for (const action of preset.denied_actions) {
373-
if (!action.includes(":")) {
380+
const colonIndex = action.indexOf(":");
381+
if (colonIndex === -1) {
374382
errors.push(`Invalid denied_action format '${action}', expected 'tool:action'`);
383+
} else {
384+
const tool = action.slice(0, colonIndex).trim();
385+
const act = action.slice(colonIndex + 1).trim();
386+
if (!tool || !act) {
387+
errors.push(`Invalid denied_action format '${action}', expected 'tool:action'`);
388+
}
375389
}
376390
}
377391
}
@@ -431,7 +445,7 @@ export async function loadPreset(name: string): Promise<Preset> {
431445
}
432446

433447
/**
434-
* Get profile name from CLI args or environment
448+
* Get profile name from GITLAB_PROFILE environment variable
435449
*/
436450
export function getProfileNameFromEnv(): string | undefined {
437451
return process.env.GITLAB_PROFILE;

tests/unit/profiles/loader.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,45 @@ describe("ProfileLoader", () => {
493493
expect(result.errors.some(e => e.includes("Invalid denied_action format"))).toBe(true);
494494
});
495495

496+
it("should error on denied_actions with empty tool part", async () => {
497+
const profile: Profile = {
498+
host: "gitlab.example.com",
499+
auth: { type: "pat", token_env: "TOKEN" },
500+
denied_actions: [":action"], // Empty tool
501+
};
502+
503+
const result = await loader.validateProfile(profile);
504+
505+
expect(result.valid).toBe(false);
506+
expect(result.errors.some(e => e.includes("Invalid denied_action format"))).toBe(true);
507+
});
508+
509+
it("should error on denied_actions with empty action part", async () => {
510+
const profile: Profile = {
511+
host: "gitlab.example.com",
512+
auth: { type: "pat", token_env: "TOKEN" },
513+
denied_actions: ["tool:"], // Empty action
514+
};
515+
516+
const result = await loader.validateProfile(profile);
517+
518+
expect(result.valid).toBe(false);
519+
expect(result.errors.some(e => e.includes("Invalid denied_action format"))).toBe(true);
520+
});
521+
522+
it("should error on denied_actions with only colon", async () => {
523+
const profile: Profile = {
524+
host: "gitlab.example.com",
525+
auth: { type: "pat", token_env: "TOKEN" },
526+
denied_actions: [":"], // Both empty
527+
};
528+
529+
const result = await loader.validateProfile(profile);
530+
531+
expect(result.valid).toBe(false);
532+
expect(result.errors.some(e => e.includes("Invalid denied_action format"))).toBe(true);
533+
});
534+
496535
it("should error when SSL cert path does not exist", async () => {
497536
const profile: Profile = {
498537
host: "gitlab.example.com",
@@ -659,6 +698,23 @@ describe("ProfileLoader", () => {
659698
expect(result.valid).toBe(false);
660699
expect(result.errors.some(e => e.includes("Invalid denied_action format"))).toBe(true);
661700
});
701+
702+
it("should error on preset denied_actions with empty parts", async () => {
703+
// Test all edge cases: empty tool, empty action, both empty
704+
const testCases = [":action", "tool:", ":"];
705+
706+
for (const action of testCases) {
707+
const preset: Preset = {
708+
denied_actions: [action],
709+
features: {},
710+
};
711+
712+
const result = await loader.validatePreset(preset);
713+
714+
expect(result.valid).toBe(false);
715+
expect(result.errors.some(e => e.includes("Invalid denied_action format"))).toBe(true);
716+
}
717+
});
662718
});
663719

664720
describe("clearCache", () => {

0 commit comments

Comments
 (0)