- 可观测性
- 后端
- 前端
- 运维
【免费下载链接】kener
Stunning status pages, batteries included!
本篇基于 Kener 官方文档 user-management.md/docs/content/v4/user-management.md) 展开,系统讲解 Kener 后台(Manage → Users / Manage → Roles)的用户管理全貌:基于 RBAC 的角色与权限模型、内置与自定义角色的差异、domain.action格式的权限域划分、两层权限执行机制(路由级 + 动作级),以及完整的邀请、激活与验证邮件流程。读完后你将能够:为团队成员正确分配admin/editor/member或自定义角色、创建符合命名规范的自定义角色并精细授权、理解邀请 token 的生命周期与激活链路,并结合源码定位权限被拒(403)的具体执行点。
一、RBAC 数据模型:四张表构成权限骨架
Kener 采用基于角色的访问控制(RBAC)系统:每个用户可被分配一个或多个角色,每个角色拥有一组权限,权限决定用户能执行哪些操作。从迁移文件 20260330120000_add_rbac_tables.ts 可以看到,整个体系由四张表构成:
| 表名 | 作用 | 关键字段 |
|---|---|---|
roles | 角色定义 | id(角色 ID)、role_name、readonly(内置角色标记)、status(默认ACTIVE) |
permissions | 权限定义 | id(即domain.action字符串)、permission_name(展示名) |
roles_permissions | 角色 ↔ 权限多对多 | roles_id、permissions_id、status,复合主键 |
users_roles | 用户 ↔ 角色多对多 | roles_id、users_id,复合主键,并对users_id建了idx_users_roles_users_id索引 |
两个多对多连接表都使用onDelete("CASCADE"),这意味着删除角色时其权限映射与用户分配会被级联清理——后文删除自定义角色的“迁移/移除”逻辑正建立在这一级联行为之上。
权限与角色的初始数据由种子脚本写入:
- seeds/permissions.ts:从 src/lib/allPerms.ts 的
permissions数组同步权限表,采用“插入缺失项 + 删除不在清单中的项”的双向对齐策略,保证数据库权限集合与代码定义始终一致; - seeds/roles.ts:注入三个只读角色(
admin、editor、member),按角色分配权限,并把旧版users.role单值字段迁移到users_roles多对多关系。
readonly字段是“内置角色不可编辑/删除”这一规则的底层来源,后文的控制器代码会反复校验该标志。
二、内置角色(Built-in roles)
三个只读角色由种子自动创建(见 seeds/roles.ts 中的readonlyRoles与rolePermissions映射):
| Role | Permissions | Notes |
|---|---|---|
admin | 全部权限 | 完全访问,包含api_keys.delete |
editor | 除api_keys.delete外的全部权限 | 日常运营角色 |
member | 仅全部.read权限 | 只读访问 |
从源码结构看,editor的计算方式是allPermissionIds.filter((id) => id !== "api_keys.delete"),member是id.endsWith(".read")过滤——即“API key 的删除权”被刻意收敛为仅admin可用,源码注释也标注了 “AdminCan-only”。内置角色readonly = 1,不能编辑、停用或删除:UpdateRole/DeleteRole/UpdateRolePermissions在 userController.ts 中都会先检查existing.readonly === 1并抛出对应错误(如 "Readonly roles cannot be deleted")。
三、自定义角色(Custom roles)
拥有roles.write权限的用户可以从Manage → Roles创建自定义角色,操作步骤:
- 点击Create Role;
- 输入角色 ID(仅允许小写字母、数字、下划线、连字符)和显示名;
- 可选:从已有角色克隆权限;
- 创建后在Permissions面板中逐项分配权限。
角色 ID 的合法性在CreateRole(userController.ts)中做了三层校验:
- 归一化:
id.trim().toLowerCase().replace(/\s+/g, "_"),空格会被替换为下划线; - 正则约束:
ROLE_ID_REGEX = /^[a-z0-9_-]+$/,不满足则报 "Role ID can only contain lowercase letters, numbers, underscores, and hyphens"; - 保留字保护:
RESTRICTED_ROLE_IDS = ["admin", "editor", "member"],自定义角色禁止使用这三个 ID,防止与内置角色冲突。
此外还会做重名检查("Role with ID ... already exists")。
自定义角色支持编辑、停用(status只能取ACTIVE/INACTIVE)和删除。删除时(DeleteRole,userController.ts)二选一:
- migrate:把该角色下的用户迁移到另一个目标角色;目标角色必须存在且为
ACTIVE("Cannot migrate users to an inactive role"),且不能与被删角色相同; - remove:直接解除用户分配。
删除完成后,users_roles与roles_permissions由外键 CASCADE 自动清理。
四、权限域(Permission domains)
所有权限遵循domain.action格式,完整清单定义在 src/lib/allPerms.ts 的permissions数组中,共 13 个域、29 项权限:
| Domain | Actions |
|---|---|
monitors | read,write |
incidents | read,write |
maintenances | read,write |
pages | read,write |
triggers | read,write |
alerts | read,write |
api_keys | read,write,delete |
users | read,write |
settings | read,write |
subscribers | read,write |
email_templates | read,write |
images | write |
roles | read,write,assign_permissions,assign_users |
几个值得注意的细节:
api_keys域比其他域多出delete动作,形成三级粒度(读 / 写 / 删),这是整个权限体系中最细的一处拆分;images域只有write(上传与删除图片),没有单独的read;roles域额外拆出assign_permissions与assign_users,使“管权限”与“管人的角色分配”可以分离授权。
allPerms.ts文件头的注释还给出了每个权限对应的具体 API action 映射(ACTION_PERMISSION_MAP),例如monitors.write覆盖storeMonitorData、updateMonitoringData、deleteMonitor、cloneMonitor、testMonitor、listDockerContainers;users.write覆盖manualUpdate、createNewUser、resendInvitation、sendVerificationEmail。而updateUser、updatePassword映射为null,表示这些“自操作”只需登录即可,无需额外权限。
五、双层权限执行:路由级 + 动作级
文档明确写道:权限在路由级(页面访问)和动作级(API 操作)两处强制执行。源码中这两处执行点非常清晰:
5.1 路由级(页面访问)
管理区布局加载函数 +layout.server.ts/+layout.server.ts) 在每次进入 manage 路由时:
- 校验 setup 是否完成、登录会话是否有效(未登录则重定向到
/account/signin); - 通过
GetUserPermissions(loggedInUser.id)取出用户全部权限; - 查
ROUTE_PERMISSION_MAP[route.id]:若路由不在映射表中直接抛 403;若要求某权限而用户没有,同样抛error(403, "Forbidden")。
ROUTE_PERMISSION_MAP(allPerms.ts)覆盖了 monitors、incidents、maintenances、pages、triggers、alerts、api-keys、users、settings 各配置页、subscriptions、email templates、roles、oidc 等全部后台页面,统一映射到对应的*.read权限——即“进得了页面”的前提是拥有该域的读权限。布局函数同时把userPermissions数组下发给前端组件,供按钮/菜单按权限显隐。
5.2 动作级(API 操作)
后台所有变更操作都汇聚到单一入口 manage/api/+server.ts/manage/api/+server.ts) 的POSThandler,其权限检查流程(第 144–169 行附近):
let userDB = await GetLoggedInSession(cookies); if (!userDB) { return json({ error: "User not logged in" }, { status: 401 }); } // Fetch user permissions once for the entire request const userPermissions = await GetUserPermissions(userDB.id); // Check permission for the action const requiredPermission = ACTION_PERMISSION_MAP[action]; if (requiredPermission === undefined) { return json({ error: "Unknown action" }, { status: 400 }); } if (requiredPermission !== null) { try { RequirePermission(userPermissions, requiredPermission); } catch { return json({ error: "You do not have permission to perform this action" }, { status: 403 }); } }要点:每个请求只查一次用户权限(GetUserPermissions返回Set<string>);ACTION_PERMISSION_MAP查不到的 action 直接 400(防止未知 action 被误放行);null表示免权限的自操作;校验失败统一 403。RequirePermission本身(userController.ts)只是一次Set.has判断,抛错即被上层捕获转换为 403 响应。
会话层面还有一道前置闸:GetLoggedInSession会校验 cookie token、并检查userDB.is_active——被停用的用户即便 token 未过期也无法通过会话校验,这与文档中“停用用户会使既有会话失效”的描述一致。
六、管理角色权限与角色用户
- Permissions 面板:在角色表中点击任意角色的Permissions即可查看或编辑其权限;权限按域分组、可逐项开关。操作走
updateRolePermissionsaction,由UpdateRolePermissions实现(userController.ts):先做只读角色检查,再对“期望集合”与“当前集合”做差集,分别调用addRolePermission/removeRolePermission增量落库。只读(内置)角色在此面板中呈只读模式。 - Users 面板:点击任意角色的Users查看已分配用户;拥有
roles.assign_users权限的用户可增删成员。AddUserToRole会拒绝向INACTIVE角色添加用户,并防止重复分配;RemoveUserFromRole有一条例外保护:owner 不能被移出admin角色("The owner cannot be removed from the admin role")。
七、用户管理(User management)
拥有users.write权限的用户可以:
- 邀请新用户(invite new users)
- 重发邀请邮件(resend invitation emails)
- 更新用户角色(update user roles)
- 激活/停用用户(activate/deactivate users)
- 发送验证邮件(send verification emails)
Owner 专属限制(由 userController.ts 的ManualUpdateUserData与首个用户创建逻辑保证):
- owner 必须始终保留
admin角色:updateType == "role"时若目标用户is_owner === "YES"且role_ids不含admin,直接抛错 "Owner must retain the admin role"; - owner 账号不能被停用:
updateType == "is_active"且is_active === 0时,对 owner 抛错 "Owner account cannot be deactivated"。
owner 标志在首个用户创建时写入:CreateFirstUser固定赋予role_ids: ["admin"]与is_owner: "YES"(userController.ts)。另外,修改角色时对每个role_id都会做存在性与ACTIVE状态双重校验——这是“所有被选角色必须处于激活状态”这一约束的代码来源。
八、邀请流程(Invite flow)
重要前提:必须先在后台配置好 Email,否则邀请流程不可用(见文末依赖章节)。
操作路径为Manage → Users:
- 点击Add User;
- 输入姓名、邮箱,勾选一个或多个角色;
- 系统创建用户并发送携带安全 token 链接的邀请邮件。
SendInvitationEmail(userController.ts)的实际行为与文档描述逐条对应:
- 被邀请者以未激活账号 + 空密码创建:
db.insertUser({ ..., password_hash: "", is_active: 0 }); - 邀请 token 7 天过期:
GenerateToken({ email, validTill: Date.now() + 7 * 24 * 60 * 60 * 1000 }),链接格式为{site_url}account/invitation?view=confirm_token&token={token}; - 所有选定角色必须存在且 ACTIVE,否则报错(如
Role "xxx" is not active); - 邮箱重复会抛 "A user with email ... already exists",数据库层的 UNIQUE 约束冲突也被捕获并转换为同一友好错误。
邮件正文使用invite_user通用模板(可在后台Email Templates中自定义),模板缺失时不发邮件(静默跳过)。
九、用户如何接受邀请(Accept invitation)
被邀请者打开邀请链接后,前端页面提交到 accept-invitation API/account/invitation/api/accept-invitation/+server.ts),服务端按顺序执行四道检查:
- Token 校验:
VerifyToken(receivedToken)失败即返回 "Invalid or expired invitation link"; - 过期检查:
Date.now() > tokenData.validTill返回 "This invitation link has expired"; - 防重复激活:查询用户密码哈希,若非空说明密码已设置,返回 "This invitation has already been accepted"(这正是“链接已使用则无法再从该链接激活”的实现);
- 密码强度校验:至少 8 位,且包含大写、小写字母和数字。
全部通过后:写入密码哈希 →updateUserIsActive(user.id, 1)激活账号 →updateIsVerified(user.id, 1)标记邮箱已验证。被邀请者随后即可用正常方式登录。
十、验证邮件(Verification emails)
- 拥有
users.write权限的用户可以给其他用户发送验证邮件; - 任何用户都可以为自己的未验证账号触发验证(无需额外权限)。
这一“自己 vs 他人”的差异化逻辑在 manage/api/+server.ts/manage/api/+server.ts) 的sendVerificationEmail分支中实现:当toId !== userDB.id时才额外检查users.write。SendVerificationEmail(userController.ts)会先确认目标用户未验证("User email is already verified"),生成的验证 token 有效期为24 小时(区别于邀请的 7 天),链接指向{site_url}account/verify?view=confirm_token&token={token},使用verify_email模板——该模板缺失时会直接抛错。
十一、常见任务与 UI 行为
常见任务
- 修改用户角色:打开用户设置 sheet,勾选/取消角色,点击Update Roles;用户可以同时拥有多个角色。
- 停用用户:在用户设置 sheet 中把账号切换为 inactive。由于会话校验依赖
is_active(见GetLoggedInSession),停用后该用户的既有登录会话立即失效。 - 重新邀请用户:对尚未设置密码的用户重发邀请。
ResendInvitationEmail会先检查用户是否存在、密码哈希是否为空(已设密码则报 "User has already set their password"),然后生成新的 7 天 token 并走invite_user模板发信。
UI 行为细节(源自 users/+page.svelte/manage/app/users/+page.svelte))
- 当前登录用户在用户表格中会被高亮显示(组件以
currentUser与行数据比对); - 用户表格支持按 active/inactive 状态过滤,请求参数
is_active会传给分页查询getUsers; - 角色徽章(Badge)展示用户已分配的角色 ID 列表;
- 页面级权限按钮(邀请、重发邀请、发验证邮件等)依据
userPermissions数组做前端显隐,后端动作级检查仍是最终防线。
十二、前提与依赖
以下功能依赖 Email 配置,未配置前邀请与验证流程不可用:
- 邀请用户(inviting users)
- 重发邀请邮件(resending invitation emails)
- 验证邮件(verification emails)
Email 相关配置参见 email-setup.md/docs/content/v4/setup/email-setup.md)。
相关页面
- 站点配置(Site Configuration)/docs/content/v4/setup/site-configuration.md)
- 用户订阅(User Subscriptions)/docs/content/v4/subscriptions.md)
关键源码索引
| 文件 | 说明 |
|---|---|
| migrations/20260330120000_add_rbac_tables.ts | RBAC 四表建表迁移 |
| src/lib/allPerms.ts | 权限清单、action→权限映射、路由→权限映射 |
| seeds/roles.ts / seeds/permissions.ts | 内置角色与权限种子 |
| src/lib/server/controllers/userController.ts | 角色 CRUD、邀请/验证邮件、owner 保护逻辑 |
| src/routes/(manage)/manage/api/+server.ts/manage/api/+server.ts) | 动作级权限执行点 |
| src/routes/(manage)/+layout.server.ts/+layout.server.ts) | 路由级权限执行点 |
| src/routes/(account)/account/invitation/api/accept-invitation/+server.ts/account/invitation/api/accept-invitation/+server.ts) | 邀请接受与激活链路 |
- 可观测性
- 后端
- 前端
- 运维
【免费下载链接】kener
Stunning status pages, batteries included!
相关推荐
Kener v4.0.23 引入完整 RBAC 权限体系:角色、权限与多角色用户管理实战指南
Kener v4.0.23 引入完整 RBAC 权限体系:角色、权限与多角色用户管理实战指南 Kener v4.0.23 将原有的"单用户单角色"模型升级为完整
可观测性后端前端运维Flagsmith 角色权限管理(RBAC)完全指南:角色、用户组与四级权限体系
Flagsmith 角色权限管理(RBAC)完全指南:角色、用户组与四级权限体系 本指南基于 Flagsmith 开源仓库中的官方 RBAC 文档,系统讲解 F
后端前端ToolJet 用户角色与 RBAC 权限体系全解析:Admin、Builder 与 End-user 的角色管理实战
ToolJet 用户角色与 RBAC 权限体系全解析:Admin、Builder 与 End user 的角色管理实战 导读 本文基于 ToolJet 官方用户
低代码后端前端AI 应用MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考