docs: document roles, invites, audit log and self-signup

- configuration.md (en/zh): ALLOW_SELF_SIGNUP, AUDIT_RETENTION_DAYS,
  member schema, role table, invite/self-signup sections, invite KV key,
  audit_logs D1 table, new admin endpoints
- deployment.md (en/zh): migration list through 0005
- README (en/zh) and AGENTS.md: access model, invites, audit log
- config.example.yaml: members example; .env.example: new variables
- .gitignore: ignore local saas-roadmap.md
This commit is contained in:
RhenCloud 2026-08-11 23:38:50 +08:00
parent 11b74aa82a
commit ce9f6147f1
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
10 changed files with 184 additions and 76 deletions

View file

@ -30,6 +30,8 @@ WebHooker requires several secrets to function. For local development, store the
| `TELEGRAM_RICH_HEADER_HOST` | Base URL of an external rich-header service; when unset, the built-in `GET /api/richheader` serves the Telegram avatar card | Built-in `/api/richheader` |
| `BASE_URL` | Public URL for OAuth callbacks | `http://localhost:8787` |
| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access the Web UI | Disabled |
| `ALLOW_SELF_SIGNUP` | When enabled (`1`/`true`), GitHub users without any group access get a personal group on first login instead of `403` | Disabled |
| `AUDIT_RETENTION_DAYS` | Audit-log retention in days for the scheduled cleanup | `90` |
## Webhook Providers
@ -48,26 +50,31 @@ WebHooker ships with a built-in config console at `/admin` for managing routes i
### Setup
1. Configure `ADMIN_USER_IDS` with the GitHub user IDs allowed to manage routes. Logins are also accepted, e.g. `ADMIN_USER_IDS=12345,RhenCloud`. If unset, the console is disabled.
1. Configure `ADMIN_USER_IDS` with the GitHub user IDs allowed to manage everything. Logins are also accepted, e.g. `ADMIN_USER_IDS=12345,RhenCloud`. If unset, the console is disabled (unless `ALLOW_SELF_SIGNUP` is enabled).
2. Open `/admin` and sign in with GitHub.
3. Only users in the whitelist receive a session cookie; everyone else gets `403`.
3. Users without any access get `403`, except when `ALLOW_SELF_SIGNUP=1` (they receive a personal group) or when they follow a group [invite link](#invites).
### Endpoints
| Endpoint | Description |
| ---------------------------------- | --------------------------------------- |
| `GET /admin` | Config console UI |
| `GET /admin/login` | Start GitHub OAuth sign-in |
| `GET /admin/logout` | Destroy session |
| `GET /admin/api/me` | Current session, scope, and groups |
| `GET /admin/api/routes` | List routes (admin only) |
| `PUT /admin/api/routes` | Replace routes (admin only) |
| `GET /admin/api/groups` | List groups (scoped to access) |
| `PUT /admin/api/groups` | Replace groups (super admin only) |
| `GET /admin/api/groups/:id/routes` | List a group's routes |
| `PUT /admin/api/groups/:id/routes` | Replace a group's routes |
| `GET /admin/api/logs` | Send logs (scoped to accessible routes) |
| `GET /admin/api/logs/:id` | Single send-log entry (scoped) |
| Endpoint | Description |
| ----------------------------------------- | -------------------------------------------- |
| `GET /admin` | Config console UI |
| `GET /admin/login` | Start GitHub OAuth sign-in |
| `GET /admin/logout` | Destroy session |
| `GET /admin/invite?token=…` | Accept a group invite (browser page) |
| `GET /admin/api/me` | Current session, scope, groups, and roles |
| `GET /admin/api/routes` | List routes (scoped to access) |
| `PUT /admin/api/routes` | Replace routes (owner/admin per group) |
| `GET /admin/api/groups` | List groups + the signed-in user's role each |
| `PUT /admin/api/groups` | Replace groups (super: all; owner: own only) |
| `GET /admin/api/groups/:id/routes` | List a group's routes |
| `PUT /admin/api/groups/:id/routes` | Replace a group's routes (owner/admin) |
| `GET /admin/api/logs` | Send logs (scoped to accessible routes) |
| `GET /admin/api/logs/:id` | Single send-log entry (scoped) |
| `POST /admin/api/groups/:id/invites` | Create an invite link (owner) |
| `GET /admin/api/groups/:id/invites` | List pending invites (owner) |
| `DELETE /admin/api/invites/:token` | Revoke an invite (owner) |
| `GET /admin/api/audit` | Audit log (scoped to accessible groups) |
The console lets you add, edit, delete, and toggle routes. Saved routes are written to KV `config:routes` immediately and the config cache is invalidated so the webhook pipeline picks them up on the next run.
@ -167,29 +174,53 @@ Routes belong to groups. Groups scope admin access and can restrict which events
{
"id": "backend-team",
"name": "Backend Team",
"adminIds": ["rhencloud"],
"members": [
{ "login": "rhencloud", "role": "owner" },
{ "login": "octobot", "role": "admin" },
{ "login": "reader", "role": "viewer" }
],
"owners": ["myorg"],
"providers": ["github", "gitea"]
}
```
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ------------------------------------------------------------------------- |
| `id` | string | Yes | Lowercase id (`a-z0-9`, `-`); referenced by each route's `groupId` |
| `name` | string | Yes | Human-readable group name |
| `adminIds` | string[] | Yes | GitHub user IDs or logins who may manage this group's routes |
| `owners` | string[] | No | Org/user logins whose events are accepted into this group; empty = all |
| `providers` | string[] | No | Source platforms allowed into this group (`github`, `gitea`); empty = all |
| `emoji` | boolean | No | Whether to include emoji in this group's messages (default `true`) |
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ---------------------------------------------------------------------------- |
| `id` | string | Yes | Lowercase id (`a-z0-9`, `-`); referenced by each route's `groupId` |
| `name` | string | Yes | Human-readable group name |
| `members` | object[] | No | `{ login, role }` entries; role is `owner`, `admin`, or `viewer` |
| `adminIds` | string[] | No | Deprecated legacy field; treated as `members` with role `owner` when present |
| `owners` | string[] | No | Org/user logins whose events are accepted into this group; empty = all |
| `providers` | string[] | No | Source platforms allowed into this group (`github`, `gitea`); empty = all |
| `emoji` | boolean | No | Whether to include emoji in this group's messages (default `true`) |
### Roles
Every group member has one of three roles. Super admins (`ADMIN_USER_IDS`) always bypass them.
| Role | View routes/logs | Edit routes | Manage members & invites | Edit group settings |
| -------- | ---------------- | ----------- | ------------------------ | ------------------- |
| `owner` | ✓ | ✓ | ✓ | ✓ (except `owners`) |
| `admin` | ✓ | ✓ | ✗ | ✗ |
| `viewer` | ✓ (read-only) | ✗ | ✗ | ✗ |
### Access Model
- **Super admins** (`ADMIN_USER_IDS`) see and edit every group and all routes.
- **Group admins** (`adminIds`) only see and edit the groups they manage; submitting a route outside their groups returns `403`.
- **Super admins** (`ADMIN_USER_IDS`) see and edit every group and all routes; only they can edit a group's `owners` list.
- **Owners** manage their group's routes, members, invites, name, `emoji`, and `providers`. They cannot remove the last owner or demote themselves when no other owner remains.
- **Admins** edit routes inside their groups and view logs; **viewers** get a read-only console.
- Group admin endpoints operate on a single group at a time via `/admin/api/groups/:id/routes`; `groupId` is forced from the path parameter.
- The `owners` list restricts which event actors (sender logins) the group's routes will dispatch at all.
- The `providers` list restricts which forge's events (`github`, `gitea`) the group's routes will dispatch. This lets you keep GitHub and Gitea groups separate even when org/user names collide.
### Invites
Owners (and super admins) can create single-use invite links valid for 7 days from the group's _Members_ panel. Accepting an invite adds the user with the invited role (`admin` or `viewer` — never `owner`); an existing `viewer` is upgraded to `admin`. Invites are stored in KV as `invite:{token}`.
### Self Sign-up
With `ALLOW_SELF_SIGNUP=1`, a GitHub user who has no group access gets a personal group (`u-{userId}`, owned by them) on first login instead of a `403`. This is the entry point for a fully self-service SaaS install; disable it to keep the console invite-only.
## Filter Types
See the [Filter Tutorial](./filters) for a hands-on guide with worked examples.
@ -230,6 +261,7 @@ Filters accept either a single string or an array of strings:
| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × token expiry |
| `token-reverse:{sha256}` | User id for reverse lookup by token | 0.9 × token expiry |
| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 seconds |
| `invite:{token}` | `{ groupId, role, expiresAt, createdBy, note? }` | 7 days |
| `delivery:{id}` | Webhook delivery id (dedup marker) | 300 seconds |
| `msg:{routeId}:{key}:{target}` | Message id tracking for in-place updates (e.g. `workflow_run`) | 7 days |
| `cmd:guild:{id}` | Guild id whose commands were registered (dedup) | Permanent |
@ -239,10 +271,13 @@ Filters accept either a single string or an array of strings:
## D1 Storage Layout
The D1 database (`DB` binding, database `webhooker`) holds three tables:
The D1 database (`DB` binding, database `webhooker`) holds four tables:
| Table | Purpose |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| `send_logs` | One row per dispatch attempt (route id, event, target, ok/error, duration, error code, detail) |
| `audit_logs` | One row per admin operation (login/logout, group/route/member/invite changes) |
| `discord_links` | Maps `discord_user_id``github_user_id` for `/gh` Discord commands |
| `telegram_links` | Maps `telegram_user_id``github_user_id` for `/gh` Telegram commands |
`audit_logs` is pruned automatically by the scheduled trigger after `AUDIT_RETENTION_DAYS` (default 90).

View file

@ -81,6 +81,8 @@ If the database already has these tables/columns (e.g. previously migrated with
npx wrangler d1 execute webhooker --remote --file ./migrations/0001_init.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0002_log_detail.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0003_telegram_links.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0004_add_group_id.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0005_audit_logs.sql
```
:::

View file

@ -26,6 +26,8 @@ WebHooker 需要多个密钥才能运行。本地开发时存储在 `.dev.vars`
| --------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------- |
| `BASE_URL` | OAuth 回调的公开 URL | `http://localhost:8787` |
| `ADMIN_USER_IDS` | 允许访问 WebUI 的 GitHub 用户 ID或登录名逗号分隔 | 未设置时 WebUI 关闭 |
| `ALLOW_SELF_SIGNUP` | 开启(`1`/`true`)后,没有任何分组权限的 GitHub 用户首次登录会自动获得个人分组而非 403 | 关闭 |
| `AUDIT_RETENTION_DAYS` | 定时清理时审计日志的保留天数 | `90` |
| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取),交互功能必需 | 未设置时交互返回 401 |
| `DISCORD_APPLICATION_ID` | Discord 应用 ID省略时自动获取 | 自动获取 |
| `TELEGRAM_WEBHOOK_SECRET` | `POST /telegram/webhook` 验签密钥X-Telegram-Bot-Api-Secret-Token | 未设置时不校验 |
@ -48,26 +50,31 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
### 设置
1. 配置 `ADMIN_USER_IDS`,填写允许管理路由的 GitHub 用户 ID也支持登录名例如 `ADMIN_USER_IDS=12345,RhenCloud`。未设置时控制台禁用。
1. 配置 `ADMIN_USER_IDS`,填写允许管理路由的 GitHub 用户 ID也支持登录名例如 `ADMIN_USER_IDS=12345,RhenCloud`。未设置时控制台禁用(除非开启 `ALLOW_SELF_SIGNUP`
2. 打开 `/admin` 并使用 GitHub 登录。
3. 只有白名单中的用户会获得会话 Cookie其他人收到 `403`
3. 没有任何权限的用户收到 `403`,除非开启 `ALLOW_SELF_SIGNUP=1`(自动获得个人分组)或通过分组[邀请链接](#邀请)加入
### 端点
| 端点 | 说明 |
| ---------------------------------- | ---------------------------- |
| `GET /admin` | 配置控制台页面 |
| `GET /admin/login` | 开始 GitHub OAuth 登录 |
| `GET /admin/logout` | 销毁会话 |
| `GET /admin/api/me` | 当前会话、权限范围和分组 |
| `GET /admin/api/routes` | 列出路由(仅管理员) |
| `PUT /admin/api/routes` | 替换路由(仅管理员) |
| `GET /admin/api/groups` | 列出分组(按权限过滤) |
| `PUT /admin/api/groups` | 替换分组(仅超级管理员) |
| `GET /admin/api/groups/:id/routes` | 列出某分组的路由 |
| `PUT /admin/api/groups/:id/routes` | 替换某分组的路由 |
| `GET /admin/api/logs` | 发送日志(按可访问路由过滤) |
| `GET /admin/api/logs/:id` | 单条发送日志(按权限过滤) |
| 端点 | 说明 |
| ------------------------------------- | ------------------------------------ |
| `GET /admin` | 配置控制台页面 |
| `GET /admin/login` | 开始 GitHub OAuth 登录 |
| `GET /admin/logout` | 销毁会话 |
| `GET /admin/invite?token=…` | 接受分组邀请(浏览器页面) |
| `GET /admin/api/me` | 当前会话、权限范围、分组和角色 |
| `GET /admin/api/routes` | 列出路由(按权限过滤) |
| `PUT /admin/api/routes` | 替换路由(按分组 owner/admin 权限) |
| `GET /admin/api/groups` | 列出分组 + 当前用户在各组的角色 |
| `PUT /admin/api/groups` | 替换分组超管全量owner 仅自己的组) |
| `GET /admin/api/groups/:id/routes` | 列出某分组的路由 |
| `PUT /admin/api/groups/:id/routes` | 替换某分组的路由owner/admin |
| `GET /admin/api/logs` | 发送日志(按可访问路由过滤) |
| `GET /admin/api/logs/:id` | 单条发送日志(按权限过滤) |
| `POST /admin/api/groups/:id/invites` | 创建邀请链接owner |
| `GET /admin/api/groups/:id/invites` | 列出待接受邀请owner |
| `DELETE /admin/api/invites/:token` | 撤销邀请owner |
| `GET /admin/api/audit` | 审计日志(按可访问分组过滤) |
控制台支持新增、编辑、删除和开关路由。保存后立即写入 KV `config:routes` 并使配置缓存失效,下一次 webhook 处理即会生效。
@ -167,29 +174,53 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
{
"id": "backend-team",
"name": "后端团队",
"adminIds": ["rhencloud"],
"members": [
{ "login": "rhencloud", "role": "owner" },
{ "login": "octobot", "role": "admin" },
{ "login": "reader", "role": "viewer" }
],
"owners": ["myorg"],
"providers": ["github", "gitea"]
}
```
| 字段 | 类型 | 必需 | 说明 |
| ----------- | -------- | ---- | ----------------------------------------------------------- |
| `id` | string | 是 | 小写 id`a-z0-9``-`),由每条路由的 `groupId` 引用 |
| `name` | string | 是 | 可读的分组名称 |
| `adminIds` | string[] | 是 | 可管理该分组路由的 GitHub 用户 ID 或登录名 |
| `owners` | string[] | 否 | 允许事件进入该分组的组织/用户登录名;为空表示不限制 |
| `providers` | string[] | 否 | 允许进入该分组的来源平台(`github``gitea`);为空表示全部 |
| `emoji` | boolean | 否 | 是否在该分组消息中显示 emoji默认 `true` |
| 字段 | 类型 | 必需 | 说明 |
| ----------- | -------- | ---- | ------------------------------------------------------------------ |
| `id` | string | 是 | 小写 id`a-z0-9``-`),由每条路由的 `groupId` 引用 |
| `name` | string | 是 | 可读的分组名称 |
| `members` | object[] | 否 | `{ login, role }` 列表;角色为 `owner``admin``viewer` |
| `adminIds` | string[] | 否 | 已废弃的旧字段;存在时按 role 为 `owner` 的成员处理 |
| `owners` | string[] | 否 | 允许事件进入该分组的组织/用户登录名;为空表示不限制 |
| `providers` | string[] | 否 | 允许进入该分组的来源平台(`github``gitea`);为空表示全部 |
| `emoji` | boolean | 否 | 是否在该分组消息中显示 emoji默认 `true` |
### 角色
每个分组成员拥有三种角色之一。超级管理员(`ADMIN_USER_IDS`)始终绕过角色限制。
| 角色 | 查看路由/日志 | 编辑路由 | 管理成员与邀请 | 编辑分组设置 |
| -------- | ------------- | -------- | -------------- | ------------ |
| `owner` | ✓ | ✓ | ✓ | ✓(`owners` 除外) |
| `admin` | ✓ | ✓ | ✗ | ✗ |
| `viewer` | ✓(只读) | ✗ | ✗ | ✗ |
### 权限模型
- **超级管理员**`ADMIN_USER_IDS`)可查看和编辑所有分组及全部路由。
- **分组管理员**`adminIds`)只能查看和编辑其管理的分组;提交其分组之外的路由返回 `403`
- **超级管理员**`ADMIN_USER_IDS`)可查看和编辑所有分组及全部路由;只有他们能修改分组的 `owners` 列表。
- **owner** 管理本组的路由、成员、邀请、名称、`emoji``providers`;不能移除最后一位 owner也没有其他 owner 时不能把自己降级。
- **admin** 可编辑本组路由并查看日志;**viewer** 只读控制台。
- 分组管理端点通过 `/admin/api/groups/:id/routes` 一次只操作一个分组;`groupId` 由路径参数强制指定。
- `owners` 列表限定哪些事件参与者(发送者登录名)的事件会被该分组的路由投递。
- `providers` 列表限定哪个 forge`github``gitea`)的事件会被该分组的路由投递。即使组织/用户同名,也可以借此将 GitHub 与 Gitea 分组区分开。
### 邀请
owner及超级管理员可在分组的「成员」面板创建一次性邀请链接7 天内有效。接受邀请后用户以邀请角色(`admin``viewer`,绝不授予 `owner`)加入;已有的 `viewer` 会被升级为 `admin`。邀请存储在 KV `invite:{token}`
### 自助注册
开启 `ALLOW_SELF_SIGNUP=1` 后,没有分组权限的 GitHub 用户首次登录会获得一个由自己担任 owner 的个人分组(`u-{userId}`),而不是 `403`。这是全自助 SaaS 部署的入口;关闭它则控制台保持仅邀请制。
## 过滤器类型
实操指南见[过滤器教程](./filters),包含完整示例。
@ -230,6 +261,7 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × Token 有效期 |
| `token-reverse:{sha256}` | 用于按 Token 反查的用户 id | 0.9 × Token 有效期 |
| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 秒 |
| `invite:{token}` | `{ groupId, role, expiresAt, createdBy, note? }` | 7 天 |
| `delivery:{id}` | Webhook 投递 id去重标记 | 300 秒 |
| `msg:{routeId}:{key}:{target}` | 原地更新用消息 id 追踪(如 `workflow_run` | 7 天 |
| `cmd:guild:{id}` | 已注册命令的服务器 id去重标记 | 永久 |
@ -239,10 +271,13 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
## D1 存储布局
D1 数据库(`DB` 绑定,数据库 `webhooker`)包含张表:
D1 数据库(`DB` 绑定,数据库 `webhooker`)包含张表:
| 表 | 用途 |
| ---------------- | ---------------------------------------------------------------------- |
| `send_logs` | 每次分发尝试一行(路由 id、事件、目标、成功/失败、耗时、错误码、详情) |
| `audit_logs` | 每次管理操作一行(登录/登出、分组/路由/成员/邀请变更) |
| `discord_links` | 映射 `discord_user_id``github_user_id`,用于 Discord `/gh` 命令 |
| `telegram_links` | 映射 `telegram_user_id``github_user_id`,用于 Telegram `/gh` 命令 |
`audit_logs` 由定时触发器按 `AUDIT_RETENTION_DAYS`(默认 90自动清理。

View file

@ -80,6 +80,8 @@ npm run db:migrate # 将迁移应用到本地Miniflare数据库
npx wrangler d1 execute webhooker --remote --file ./migrations/0001_init.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0002_log_detail.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0003_telegram_links.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0004_add_group_id.sql
npx wrangler d1 execute webhooker --remote --file ./migrations/0005_audit_logs.sql
```
:::