refactor: dedupe discord native command auth

This commit is contained in:
Peter Steinberger
2026-04-06 00:07:22 +01:00
parent a35ac86c84
commit 510344a687
4 changed files with 177 additions and 89 deletions

View File

@@ -532,6 +532,26 @@ export function isDiscordGroupAllowedByPolicy(params: {
}).allowed;
}
export function resolveDiscordChannelPolicyCommandAuthorizer(params: {
groupPolicy: "open" | "disabled" | "allowlist";
guildInfo?: DiscordGuildEntryResolved | null;
channelConfig?: DiscordChannelConfigResolved | null;
}) {
const channelAllowlistConfigured =
Boolean(params.guildInfo?.channels) && Object.keys(params.guildInfo?.channels ?? {}).length > 0;
return {
configured:
params.groupPolicy === "allowlist" &&
(Boolean(params.guildInfo) || channelAllowlistConfigured),
allowed: isDiscordGroupAllowedByPolicy({
groupPolicy: params.groupPolicy,
guildAllowlisted: Boolean(params.guildInfo),
channelAllowlistConfigured,
channelAllowed: params.channelConfig?.allowed !== false,
}),
} as const;
}
export function resolveGroupDmAllow(params: {
channels?: string[];
channelId: string;

View File

@@ -166,6 +166,36 @@ describe("Discord native slash commands with commands.allowFrom", () => {
expectNotUnauthorizedReply(interaction);
});
it("rejects guild slash commands outside the Discord allowlist when commands.useAccessGroups is false and commands.allowFrom is not configured", async () => {
const { dispatchSpy, interaction } = await runGuildSlashCommand({
mutateConfig: (cfg) => {
cfg.commands = {
...cfg.commands,
useAccessGroups: false,
allowFrom: undefined,
};
cfg.channels = {
...cfg.channels,
discord: {
...cfg.channels?.discord,
guilds: {
"000000000000000000": {
channels: {
"111111111111111111": {
enabled: true,
requireMention: false,
},
},
},
},
},
};
},
});
expect(dispatchSpy).not.toHaveBeenCalled();
expectUnauthorizedReply(interaction);
});
it("rejects guild slash commands when commands.allowFrom.discord does not match the sender", async () => {
const { dispatchSpy, interaction } = await runGuildSlashCommand({
userId: "999999999999999999",

View File

@@ -266,6 +266,63 @@ describe("createDiscordNativeCommand option wiring", () => {
expect(respond).not.toHaveBeenCalledWith([]);
});
it("returns no autocomplete choices outside the Discord allowlist when commands.useAccessGroups is false and commands.allowFrom is not configured", async () => {
const command = createNativeCommand("think", {
cfg: {
commands: {
useAccessGroups: false,
},
channels: {
discord: {
groupPolicy: "allowlist",
guilds: {
"other-guild": {
channels: {
"other-channel": {
enabled: true,
requireMention: false,
},
},
},
},
},
},
} as ReturnType<typeof loadConfig>,
});
const level = requireOption(command, "level");
const autocomplete = readAutocomplete(level);
if (typeof autocomplete !== "function") {
throw new Error("think level option did not wire autocomplete");
}
const respond = vi.fn(async (_choices: unknown[]) => undefined);
await autocomplete({
user: {
id: "allowed-user",
username: "allowed",
globalName: "Allowed",
},
channel: {
type: ChannelType.GuildText,
id: "channel-1",
name: "general",
},
guild: {
id: "guild-1",
},
rawData: {
member: { roles: [] },
},
options: {
getFocused: () => ({ value: "xh" }),
},
respond,
client: {},
} as never);
expect(respond).toHaveBeenCalledWith([]);
});
it("returns no autocomplete choices for group DMs outside dm.groupChannels", async () => {
const discordConfig = {
dm: {

View File

@@ -55,6 +55,7 @@ import {
isDiscordGroupAllowedByPolicy,
normalizeDiscordAllowList,
normalizeDiscordSlug,
resolveDiscordChannelPolicyCommandAuthorizer,
resolveGroupDmAllow,
resolveDiscordChannelConfigWithFallback,
resolveDiscordAllowListMatch,
@@ -185,24 +186,62 @@ function resolveDiscordNativeCommandAllowlistAccess(params: {
return { configured: true, allowed: match.allowed } as const;
}
function resolveDiscordChannelPolicyCommandAuthorizer(params: {
groupPolicy: "open" | "disabled" | "allowlist";
function resolveDiscordGuildNativeCommandAuthorized(params: {
cfg: ReturnType<typeof loadConfig>;
discordConfig: DiscordConfig;
useAccessGroups: boolean;
commandsAllowFromAccess: ReturnType<typeof resolveDiscordNativeCommandAllowlistAccess>;
guildInfo?: ReturnType<typeof resolveDiscordGuildEntry> | null;
channelConfig?: ReturnType<typeof resolveDiscordChannelConfigWithFallback> | null;
memberRoleIds: string[];
sender: { id: string; name?: string; tag?: string };
allowNameMatching: boolean;
ownerAllowListConfigured: boolean;
ownerAllowed: boolean;
}) {
const channelAllowlistConfigured =
Boolean(params.guildInfo?.channels) && Object.keys(params.guildInfo?.channels ?? {}).length > 0;
return {
configured:
params.groupPolicy === "allowlist" &&
(Boolean(params.guildInfo) || channelAllowlistConfigured),
allowed: isDiscordGroupAllowedByPolicy({
groupPolicy: params.groupPolicy,
guildAllowlisted: Boolean(params.guildInfo),
channelAllowlistConfigured,
channelAllowed: params.channelConfig?.allowed !== false,
}),
} as const;
const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: params.cfg.channels?.discord !== undefined,
groupPolicy: params.discordConfig?.groupPolicy,
defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy,
});
const policyAuthorizer = resolveDiscordChannelPolicyCommandAuthorizer({
groupPolicy,
guildInfo: params.guildInfo,
channelConfig: params.channelConfig,
});
if (!policyAuthorizer.allowed) {
return false;
}
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
channelConfig: params.channelConfig,
guildInfo: params.guildInfo,
memberRoleIds: params.memberRoleIds,
sender: params.sender,
allowNameMatching: params.allowNameMatching,
});
const commandAllowlistAuthorizer = {
configured: params.commandsAllowFromAccess.configured,
allowed: params.commandsAllowFromAccess.allowed,
};
const ownerAuthorizer = {
configured: params.ownerAllowListConfigured,
allowed: params.ownerAllowed,
};
const memberAuthorizer = {
configured: hasAccessRestrictions,
allowed: memberAllowed,
};
return resolveCommandAuthorizedFromAuthorizers({
useAccessGroups: params.useAccessGroups,
authorizers: params.useAccessGroups
? params.commandsAllowFromAccess.configured
? [commandAllowlistAuthorizer]
: [policyAuthorizer, ownerAuthorizer, memberAuthorizer]
: params.commandsAllowFromAccess.configured
? [commandAllowlistAuthorizer]
: [memberAuthorizer],
modeWhenAccessGroupsOff: "configured",
});
}
function buildDiscordCommandOptions(params: {
@@ -479,47 +518,18 @@ async function resolveDiscordNativeAutocompleteAuthorized(params: {
return false;
}
if (!isDirectMessage) {
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
channelConfig,
return resolveDiscordGuildNativeCommandAuthorized({
cfg,
discordConfig,
useAccessGroups,
commandsAllowFromAccess,
guildInfo,
channelConfig,
memberRoleIds,
sender,
allowNameMatching,
});
const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.discord !== undefined,
groupPolicy: discordConfig?.groupPolicy,
defaultGroupPolicy: cfg.channels?.defaults?.groupPolicy,
});
const policyAuthorizer = resolveDiscordChannelPolicyCommandAuthorizer({
groupPolicy,
guildInfo,
channelConfig,
});
const authorizers = useAccessGroups
? commandsAllowFromAccess.configured
? [
{
configured: commandsAllowFromAccess.configured,
allowed: commandsAllowFromAccess.allowed,
},
]
: [
policyAuthorizer,
{ configured: ownerAllowList != null, allowed: ownerOk },
{ configured: hasAccessRestrictions, allowed: memberAllowed },
]
: [
{
configured: commandsAllowFromAccess.configured,
allowed: commandsAllowFromAccess.allowed,
},
{ configured: hasAccessRestrictions, allowed: memberAllowed },
];
return resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
authorizers,
modeWhenAccessGroupsOff: "configured",
ownerAllowListConfigured: ownerAllowList != null,
ownerAllowed: ownerOk,
});
}
return true;
@@ -919,47 +929,18 @@ async function dispatchDiscordCommandInteraction(params: {
return;
}
if (!isDirectMessage) {
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
channelConfig,
commandAuthorized = resolveDiscordGuildNativeCommandAuthorized({
cfg,
discordConfig,
useAccessGroups,
commandsAllowFromAccess,
guildInfo,
channelConfig,
memberRoleIds,
sender,
allowNameMatching,
});
const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.discord !== undefined,
groupPolicy: discordConfig?.groupPolicy,
defaultGroupPolicy: cfg.channels?.defaults?.groupPolicy,
});
const policyAuthorizer = resolveDiscordChannelPolicyCommandAuthorizer({
groupPolicy,
guildInfo,
channelConfig,
});
const authorizers = useAccessGroups
? commandsAllowFromAccess.configured
? [
{
configured: commandsAllowFromAccess.configured,
allowed: commandsAllowFromAccess.allowed,
},
]
: [
policyAuthorizer,
{ configured: ownerAllowList != null, allowed: ownerOk },
{ configured: hasAccessRestrictions, allowed: memberAllowed },
]
: [
{
configured: commandsAllowFromAccess.configured,
allowed: commandsAllowFromAccess.allowed,
},
{ configured: hasAccessRestrictions, allowed: memberAllowed },
];
commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
authorizers,
modeWhenAccessGroupsOff: "configured",
ownerAllowListConfigured: ownerAllowList != null,
ownerAllowed: ownerOk,
});
if (!commandAuthorized) {
await respond("You are not authorized to use this command.", { ephemeral: true });