mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat(admin): scope groups to specific GitHub orgs/users
Add an optional owners[] field to groups (super-admin only). When set, only webhook events whose repository owner or organization login matches enter that group's routes; empty owners means no restriction, keeping existing routes backward compatible. - types: Group.owners?, groups.ts groupAcceptsOwners() - webhook.ts eventOwners() extracts repo owner + org login - discord.ts dispatch skips routes whose group rejects the event owner - admin-routes.ts validateGroups() validates owners list
This commit is contained in:
parent
667b8038af
commit
537f4cbb84
5 changed files with 46 additions and 1 deletions
|
|
@ -106,6 +106,13 @@ function validateGroups(
|
|||
) {
|
||||
return { ok: false, error: `group "${g.id}".adminIds must be a list of strings` };
|
||||
}
|
||||
if (
|
||||
g.owners !== undefined &&
|
||||
(!Array.isArray(g.owners) ||
|
||||
!g.owners.every((o) => typeof o === "string" && o.trim().length > 0))
|
||||
) {
|
||||
return { ok: false, error: `group "${g.id}".owners must be a list of strings` };
|
||||
}
|
||||
}
|
||||
return { ok: true, groups: groups as Group[] };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import type { Config, FormattedMessage, WebhookEvent, Env } from "./types";
|
||||
import { formatEvent } from "./formatter";
|
||||
import { matchRoute } from "./webhook";
|
||||
import { matchRoute, eventOwners } from "./webhook";
|
||||
import { log } from "./log";
|
||||
import { loadTranslations, type Translations } from "./i18n";
|
||||
import { sendMessage } from "./discord-rest";
|
||||
import { recordSend } from "./send-log";
|
||||
import { loadGroups, groupAcceptsOwners } from "./groups";
|
||||
|
||||
export function isGatewayEnabled(env: Env): boolean {
|
||||
return env.DISCORD_GATEWAY_ENABLED === "true";
|
||||
|
|
@ -37,9 +38,18 @@ export async function dispatchEvent(config: Config, event: WebhookEvent, env: En
|
|||
}),
|
||||
);
|
||||
|
||||
const groups = await loadGroups(env.KV);
|
||||
const groupById = new Map(groups.map((g) => [g.id, g]));
|
||||
const owners = eventOwners(event);
|
||||
|
||||
for (const route of config.routes) {
|
||||
if (!matchRoute(route, event)) continue;
|
||||
|
||||
if (route.groupId) {
|
||||
const group = groupById.get(route.groupId);
|
||||
if (group && !groupAcceptsOwners(group, owners)) continue;
|
||||
}
|
||||
|
||||
const target = route.target.threadId
|
||||
? `${route.target.channelId}/${route.target.threadId}`
|
||||
: route.target.channelId;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,17 @@ export function isGroupAdmin(group: Group, userId: string, login: string): boole
|
|||
return identityMatches(group.adminIds ?? [], userId, login);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an event originating from `owners` (org/user logins) is allowed into
|
||||
* this group. A group with no owner restriction accepts everything.
|
||||
*/
|
||||
export function groupAcceptsOwners(group: Group, owners: string[]): boolean {
|
||||
const restrict = (group.owners ?? []).map((s) => s.trim().toLowerCase()).filter(Boolean);
|
||||
if (restrict.length === 0) return true;
|
||||
const seen = owners.map((s) => s.trim().toLowerCase()).filter(Boolean);
|
||||
return seen.some((o) => restrict.includes(o));
|
||||
}
|
||||
|
||||
export interface AccessScope {
|
||||
isSuper: boolean;
|
||||
/** Groups the user may view/edit. When isSuper, this is every group. */
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ export interface Group {
|
|||
* Super admins (ADMIN_USER_IDS) always have access regardless of this list.
|
||||
*/
|
||||
adminIds: string[];
|
||||
/**
|
||||
* GitHub organization/user logins (case-insensitive) whose webhook events are
|
||||
* allowed into this group's routes. Empty/omitted = no owner restriction.
|
||||
* Only super admins may edit this field.
|
||||
*/
|
||||
owners?: string[];
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
|
|
|
|||
|
|
@ -131,6 +131,17 @@ function matchFilter(filter: Filter, event: WebhookEvent, keywordBody?: string):
|
|||
return filter.exclude ? !matches : matches;
|
||||
}
|
||||
|
||||
/** Org/user logins that own the event (repository owner + organization). */
|
||||
export function eventOwners(event: WebhookEvent): string[] {
|
||||
const owners = new Set<string>();
|
||||
const repoOwner = (event.payload.repository as { owner?: { login?: string } } | undefined)?.owner
|
||||
?.login;
|
||||
if (repoOwner) owners.add(repoOwner);
|
||||
const org = (event.payload.organization as { login?: string } | undefined)?.login;
|
||||
if (org) owners.add(org);
|
||||
return [...owners];
|
||||
}
|
||||
|
||||
export function matchRoute(route: Route, event: WebhookEvent): boolean {
|
||||
if (!route.enabled) return false;
|
||||
const hasKeyword = route.filters.some((f) => f.type === "keyword");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue