Permissions
Permissions
Section titled “Permissions”Lingchu Bot uses a layered permission system that combines a Lingchu-wide UID identity, platform account bindings, identity-group memberships, and per-command allow-list grants. This page describes how the runtime resolves permissions, how superusers are configured, and how blocklist and protected-subject policies gate destructive commands.
Architecture layers
Section titled “Architecture layers”| Layer | File | Responsibility |
|---|---|---|
| Bootstrap | permissions/bootstrap.py |
Validates LINGCHU_SUPERUSERS, seeds default identity groups, syncs superuser memberships and command grants at startup |
| Service | permissions/service.py |
Resolves PermissionContext, runs check_permission, expands runtime + membership groups, queries grants |
| Admin API | permissions/admin.py |
SUPERUSERS-only mutations: create / update / delete identity groups, add / remove members |
| Subject policy | permissions/subject_policy.py |
Blocked and protected subject entries, active-policy lookup with expiry |
| Platform integration | permissions/platforms.py |
Discovers platform permission modules via PlatformProfile.permission_module, resolves runtime identity groups |
| Repository | repositories/permissions.py |
ORM CRUD on IdentityUser, PlatformAccount, PlatformIdentityGroup, IdentityMembership, PermissionGrant |
UID superusers and platform account mapping
Section titled “UID superusers and platform account mapping”Superusers are declared through the LINGCHU_SUPERUSERS environment key (NoneBot config alias lingchu_superusers). The value is a JSON mapping from a Lingchu UID to one or more platform account bindings:
{ "alice": { "qq": 10001 }, "bob": { "qq": 10002 }}At startup, validate_and_seed_permission_system() in permissions/bootstrap.py:
- Reads the configured mapping from
runtime_config.lingchu_superusers. - Normalizes each UID and platform account ID to strings (QQ accounts are validated as positive integers).
- Validates that every
platform_idis a known platform profile and that no(platform_id, account_id)pair is bound twice. - Upserts each UID into
IdentityUser, adds a membership in thesystem.superusersgroup (sourcesuperusers_config), and binds each platform account viabind_platform_account(). - Grants every
MENU_FEATURES.command_keyto thesystem.superusersgroup so superusers can run all commands.
If LINGCHU_SUPERUSERS is missing or empty, startup raises PermissionConfigError and aborts.
Command authorization and MENU_FEATURES.command_key
Section titled “Command authorization and MENU_FEATURES.command_key”Each command is identified by a stable command_key declared on a MenuFeature in handle/menu.py. The same command_key is the shared identifier across:
- Permission checks (
check_permission(command_key, bot, event)) - Menu filtering (
allowed_command_keys()hides commands the current identity cannot execute) - Handler decorators and trigger registration
- Permission grants in the
lingchu_permission_grantstable
check_permission_for_context() in permissions/service.py resolves a PermissionDecision with the following short-circuit order:
- If
context.uidisNone→allowed=False, reason="anonymous". - If
repo.is_superuser(uid)→allowed=True, reason="superuser". - Otherwise, collect effective identity groups from runtime passthrough (when enabled) plus matching
IdentityMembershiprows, expand to ancestor groups viaparent_group_id, and look upPermissionGrantrows witheffect == "allow". - If any grant matches →
allowed=True, reason="granted"; otherwise →allowed=False, reason="missing_grant".
Platform runtime role passthrough
Section titled “Platform runtime role passthrough”permission_platform_runtime_passthrough controls whether platform-resolved runtime roles (for example QQ group owner / admin / member) participate in permission checks. It accepts either a global boolean or a per-platform mapping:
permission_platform_runtime_passthrough = true# or per-platform:# permission_platform_runtime_passthrough = { qq = true }When passthrough is enabled for the current platform_id, the runtime identity groups returned by the platform module (see permissions/platforms.py::resolve_runtime_identity_groups) are added to the direct group set before grant lookup. When disabled, only explicit IdentityMembership rows count. The default is True; if a per-platform entry is missing it also falls back to True.
platform_runtime_passthrough_enabled() in permissions/service.py is the single resolver used by both check_permission_for_context() and the menu visibility path.
Blocklist and protected-subject interception
Section titled “Blocklist and protected-subject interception”protected_subject_feature_keys is the allow-list of command_key values that must be gated by subject policy before execution. The default covers destructive operations:
protected_subject_feature_keys = [ "kick_member", "block_member", "global_block_member", "member_mute", "recall_message", "set_member_card", "set_member_title", "set_member_admin", "unset_member_admin", "remote_kick", "remote_block", "remote_mute",]Subject policies are stored in the lingchu_subject_policy table and exposed through permissions/subject_policy.py:
| Policy type | Effect |
|---|---|
blocked |
Target user is on the blocklist; protected commands are denied |
protected |
Target user is on the protection list; destructive commands against them are denied |
find_active_subject_policy() checks the global scope first, then the group scope, and lazily deletes expired entries. active_subject_policy_condition() returns a SQLAlchemy filter that excludes rows whose expires_at is in the past, used by repository queries that need to join against the policy table.
Protected-user auto-restore (OneBot V11)
Section titled “Protected-user auto-restore (OneBot V11)”On the OneBot V11 adapter, a protected user who is muted is automatically unmuted again. handle/qq/adapters/onebot11/default/protect_notice.py listens to GroupBanNoticeEvent (sub_type == "ban" with a positive duration) and, when the muted user matches a group or global protected policy, calls set_group_ban(group_id, user_id, duration=0). This covers mutes issued by human admins in the QQ client, not just the bot’s own commands. The listener always runs, regardless of the boot/shutdown gate.
Exceptions:
- Whole-group mute: during a group-wide mute the bot cannot unmute an individual, so restoration is skipped when the bot itself is muted in that group.
- Superuser override: a superuser muting a non-superuser protected member takes effect and is not reverted. Muting another superuser is still restored.
Permission seeds and platform role tree
Section titled “Permission seeds and platform role tree”Default identity groups are seeded from each enabled platform’s permission module. permissions/platforms.py::iter_default_identity_groups() walks only the PlatformProfile.permission_module entries whose adapter is selected by LINGCHUAdapter (via iter_enabled_profiles()) and calls get_default_identity_groups() on them. Platforms whose adapter is not configured are never imported or seeded, so you do not need to install an adapter for a platform you do not use. If a platform is enabled later, its identity groups are seeded on the next startup.
For QQ, platforms/qq/permissions.py declares PLATFORM_ID = "qq" and seeds the following tree:
| Group ID | Parent | Display name |
|---|---|---|
qq.group |
— | QQ群聊 |
qq.group.owner |
qq.group |
QQ群主 |
qq.group.admin |
qq.group |
QQ群管理员 |
qq.group.member |
qq.group |
QQ群成员 |
qq.friend |
— | QQ好友 |
qq.channel |
— | QQ频道 |
qq.bot |
— | QQ机器人 |
qq.device |
— | QQ设备 |
At runtime, resolve_runtime_identity_groups() maps the OneBot V11 sender role (owner / admin / member) to the corresponding qq.group.* group IDs. If the role is missing from the event, it falls back to bot.call_api("get_group_member_info", ...) and ultimately to the member role on API failure.
Platform permission modules are discovered dynamically through the adapter registry’s PlatformProfile.permission_module field — there are no hard-coded module paths in the core permission layer. resolve_runtime_identity_groups() only imports the permission module of the platform the event originates from, and only when that platform’s adapter is enabled.
Permission data model
Section titled “Permission data model”The ORM models live in database/models/identity.py and use the cross-dialect compatibility types from database/_dialect_compat.py:
| Table | Model | Purpose |
|---|---|---|
lingchu_identity_users |
IdentityUser |
Lingchu-wide UID with optional nickname |
lingchu_platform_accounts |
PlatformAccount |
Binding between a UID and a (platform_id, account_id) pair |
lingchu_platform_identity_groups |
PlatformIdentityGroup |
Platform-scoped group with optional parent_group_id and builtin flag |
lingchu_identity_memberships |
IdentityMembership |
UID → group membership, optionally scoped by scope_type / scope_id |
lingchu_permission_grants |
PermissionGrant |
Allow-list grant from a group to a command_key |
SUPERUSERS_GROUP_ID = "system.superusers" is the only built-in group that is not platform-scoped; it is seeded by bootstrap.py rather than by a platform module.
Administration APIs
Section titled “Administration APIs”permissions/admin.py exposes SUPERUSERS-only mutations. Every function calls assert_superuser() first and raises PermissionDeniedError if the actor is not a superuser:
create_platform_identity_group()/update_platform_identity_group()/delete_platform_identity_group()add_identity_group_member()/remove_identity_group_member()/list_identity_group_members()
Builtin groups (builtin=True) cannot be updated or deleted, and groups that still have memberships or grants cannot be deleted.