fix(cron): expose callerSessionKey in AJV schemas so session isolation reaches handlers
The per-caller ownership enforcement introduced for issue #35447 was silently bypassed: all four mutation/list schemas used additionalProperties:false but did not declare callerSessionKey, causing AJV to strip the field before the handler could read it. As a result resolveCronCallerOptions always received an empty caller and fell back to allow-all behaviour. Fix: - Add optional callerSessionKey (NonEmptyString) to CronListParamsSchema, CronUpdateParamsSchema, CronRemoveParamsSchema and CronRunParamsSchema. - Update the four handlers in server-methods/cron.ts to read p.callerSessionKey instead of the previous p.sessionKey (which was never populated through these schemas). - Add validator tests covering acceptance of the new field and rejection of empty strings across all four operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
467c2078ea
commit
7f2778b2bc
@ -79,6 +79,59 @@ describe("cron protocol validators", () => {
|
||||
expect(validateCronListParams({ offset: -1 })).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts callerSessionKey on list params", () => {
|
||||
expect(validateCronListParams({ callerSessionKey: "telegram:direct:111" })).toBe(true);
|
||||
expect(validateCronListParams({ callerSessionKey: "" })).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts callerSessionKey on update params", () => {
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: { enabled: false },
|
||||
callerSessionKey: "telegram:direct:111",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
jobId: "job-2",
|
||||
patch: { enabled: true },
|
||||
callerSessionKey: "telegram:direct:222",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateCronUpdateParams({ id: "job-1", patch: { enabled: false }, callerSessionKey: "" }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts callerSessionKey on remove params", () => {
|
||||
expect(validateCronRemoveParams({ id: "job-1", callerSessionKey: "telegram:direct:111" })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
validateCronRemoveParams({ jobId: "job-2", callerSessionKey: "telegram:direct:222" }),
|
||||
).toBe(true);
|
||||
expect(validateCronRemoveParams({ id: "job-1", callerSessionKey: "" })).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts callerSessionKey on run params", () => {
|
||||
expect(
|
||||
validateCronRunParams({
|
||||
id: "job-1",
|
||||
mode: "force",
|
||||
callerSessionKey: "telegram:direct:111",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateCronRunParams({
|
||||
jobId: "job-2",
|
||||
mode: "due",
|
||||
callerSessionKey: "telegram:direct:222",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(validateCronRunParams({ id: "job-1", callerSessionKey: "" })).toBe(false);
|
||||
});
|
||||
|
||||
it("enforces runs limit minimum for id and jobId selectors", () => {
|
||||
expect(validateCronRunsParams({ id: "job-1", limit: 1 })).toBe(true);
|
||||
expect(validateCronRunsParams({ jobId: "job-2", limit: 1 })).toBe(true);
|
||||
|
||||
@ -275,6 +275,7 @@ export const CronListParamsSchema = Type.Object(
|
||||
enabled: Type.Optional(CronJobsEnabledFilterSchema),
|
||||
sortBy: Type.Optional(CronJobsSortBySchema),
|
||||
sortDir: Type.Optional(CronSortDirSchema),
|
||||
callerSessionKey: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
@ -312,12 +313,16 @@ export const CronJobPatchSchema = Type.Object(
|
||||
|
||||
export const CronUpdateParamsSchema = cronIdOrJobIdParams({
|
||||
patch: CronJobPatchSchema,
|
||||
callerSessionKey: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
export const CronRemoveParamsSchema = cronIdOrJobIdParams({});
|
||||
export const CronRemoveParamsSchema = cronIdOrJobIdParams({
|
||||
callerSessionKey: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
export const CronRunParamsSchema = cronIdOrJobIdParams({
|
||||
mode: Type.Optional(Type.Union([Type.Literal("due"), Type.Literal("force")])),
|
||||
callerSessionKey: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
export const CronRunsParamsSchema = Type.Object(
|
||||
|
||||
@ -83,9 +83,9 @@ export const cronHandlers: GatewayRequestHandlers = {
|
||||
enabled?: "all" | "enabled" | "disabled";
|
||||
sortBy?: "nextRunAtMs" | "updatedAtMs" | "name";
|
||||
sortDir?: "asc" | "desc";
|
||||
sessionKey?: string;
|
||||
callerSessionKey?: string;
|
||||
};
|
||||
const callerOpts = resolveCronCallerOptions(client, p.sessionKey);
|
||||
const callerOpts = resolveCronCallerOptions(client, p.callerSessionKey);
|
||||
const page = await context.cron.listPage({
|
||||
includeDisabled: p.includeDisabled,
|
||||
limit: p.limit,
|
||||
@ -169,7 +169,7 @@ export const cronHandlers: GatewayRequestHandlers = {
|
||||
id?: string;
|
||||
jobId?: string;
|
||||
patch: Record<string, unknown>;
|
||||
sessionKey?: string;
|
||||
callerSessionKey?: string;
|
||||
};
|
||||
const jobId = p.id ?? p.jobId;
|
||||
if (!jobId) {
|
||||
@ -192,7 +192,7 @@ export const cronHandlers: GatewayRequestHandlers = {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const callerOpts = resolveCronCallerOptions(client, p.sessionKey);
|
||||
const callerOpts = resolveCronCallerOptions(client, p.callerSessionKey);
|
||||
try {
|
||||
const job = await context.cron.update(jobId, patch, callerOpts);
|
||||
context.logGateway.info("cron: job updated", { jobId });
|
||||
@ -217,7 +217,7 @@ export const cronHandlers: GatewayRequestHandlers = {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const p = params as { id?: string; jobId?: string; sessionKey?: string };
|
||||
const p = params as { id?: string; jobId?: string; callerSessionKey?: string };
|
||||
const jobId = p.id ?? p.jobId;
|
||||
if (!jobId) {
|
||||
respond(
|
||||
@ -227,7 +227,7 @@ export const cronHandlers: GatewayRequestHandlers = {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const callerOpts = resolveCronCallerOptions(client, p.sessionKey);
|
||||
const callerOpts = resolveCronCallerOptions(client, p.callerSessionKey);
|
||||
try {
|
||||
const result = await context.cron.remove(jobId, callerOpts);
|
||||
if (result.removed) {
|
||||
@ -258,7 +258,7 @@ export const cronHandlers: GatewayRequestHandlers = {
|
||||
id?: string;
|
||||
jobId?: string;
|
||||
mode?: "due" | "force";
|
||||
sessionKey?: string;
|
||||
callerSessionKey?: string;
|
||||
};
|
||||
const jobId = p.id ?? p.jobId;
|
||||
if (!jobId) {
|
||||
@ -269,7 +269,7 @@ export const cronHandlers: GatewayRequestHandlers = {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const callerOpts = resolveCronCallerOptions(client, p.sessionKey);
|
||||
const callerOpts = resolveCronCallerOptions(client, p.callerSessionKey);
|
||||
try {
|
||||
const result = await context.cron.enqueueRun(jobId, p.mode ?? "force", callerOpts);
|
||||
respond(true, result, undefined);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user