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` };
|
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[] };
|
return { ok: true, groups: groups as Group[] };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import type { Config, FormattedMessage, WebhookEvent, Env } from "./types";
|
import type { Config, FormattedMessage, WebhookEvent, Env } from "./types";
|
||||||
import { formatEvent } from "./formatter";
|
import { formatEvent } from "./formatter";
|
||||||
import { matchRoute } from "./webhook";
|
import { matchRoute, eventOwners } from "./webhook";
|
||||||
import { log } from "./log";
|
import { log } from "./log";
|
||||||
import { loadTranslations, type Translations } from "./i18n";
|
import { loadTranslations, type Translations } from "./i18n";
|
||||||
import { sendMessage } from "./discord-rest";
|
import { sendMessage } from "./discord-rest";
|
||||||
import { recordSend } from "./send-log";
|
import { recordSend } from "./send-log";
|
||||||
|
import { loadGroups, groupAcceptsOwners } from "./groups";
|
||||||
|
|
||||||
export function isGatewayEnabled(env: Env): boolean {
|
export function isGatewayEnabled(env: Env): boolean {
|
||||||
return env.DISCORD_GATEWAY_ENABLED === "true";
|
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) {
|
for (const route of config.routes) {
|
||||||
if (!matchRoute(route, event)) continue;
|
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
|
const target = route.target.threadId
|
||||||
? `${route.target.channelId}/${route.target.threadId}`
|
? `${route.target.channelId}/${route.target.threadId}`
|
||||||
: route.target.channelId;
|
: route.target.channelId;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,17 @@ export function isGroupAdmin(group: Group, userId: string, login: string): boole
|
||||||
return identityMatches(group.adminIds ?? [], userId, login);
|
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 {
|
export interface AccessScope {
|
||||||
isSuper: boolean;
|
isSuper: boolean;
|
||||||
/** Groups the user may view/edit. When isSuper, this is every group. */
|
/** 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.
|
* Super admins (ADMIN_USER_IDS) always have access regardless of this list.
|
||||||
*/
|
*/
|
||||||
adminIds: string[];
|
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 {
|
export interface Filter {
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,17 @@ function matchFilter(filter: Filter, event: WebhookEvent, keywordBody?: string):
|
||||||
return filter.exclude ? !matches : matches;
|
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 {
|
export function matchRoute(route: Route, event: WebhookEvent): boolean {
|
||||||
if (!route.enabled) return false;
|
if (!route.enabled) return false;
|
||||||
const hasKeyword = route.filters.some((f) => f.type === "keyword");
|
const hasKeyword = route.filters.some((f) => f.type === "keyword");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue