feat(auth): 登录白名单细化为黑名单模式(approved 默认放行)+ 同步构建产物

- approved 放行标记默认 True:新账号首次登录即放行,登录白名单开关打开时只拦截
  被管理员显式「取消放行」(approved=False) 的用户;enforce_user_approved 放在
  各登录端点拿到 user 后、签发 JWT 前调用,admin 永远放行。
- 同步调整 LoginWhitelistPage / local_provider / models / system_settings /
  迁移 20260625_02 / user model / column_additions_mysql.sql 与 CLAUDE.md。
- 更新 backend/backend.zip 构建产物。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-25 21:41:15 +08:00
1 parent f3f11461de
commit a417124c21
9 files changed
+47 -39

No files matched your search

@@ -79,7 +79,7 @@ class LocalAuthProvider(AuthProvider):
"""
return await self._repo.find_users_by_email_local_part_like(substring, limit=limit)
async def create_user(self, email: str, password: str | None = None, system_role: str = "user", needs_setup: bool = False, approved: bool = False) -> User:
async def create_user(self, email: str, password: str | None = None, system_role: str = "user", needs_setup: bool = False, approved: bool = True) -> User:
"""Create a new local user.
Args:
@@ -87,8 +87,10 @@ class LocalAuthProvider(AuthProvider):
password: Plain text password (will be hashed)
system_role: Role to assign ("admin" or "user")
needs_setup: If True, user must complete setup on first login
approved: Login-whitelist 放行标记. Regular self-registered users
default to ``False`` (待审批); admins are created ``True``.
approved: Login-whitelist 放行标记. **黑名单模式**:新账号默认 ``True``
(从未登录过的用户首次登录即放行),管理员可在白名单管理页对个别
用户「取消放行」(置 ``False``) 来封禁。仅当 ``login_whitelist.enabled``
打开时 ``approved=False`` 才会被拦。
Returns:
Created User instance
@@ -30,7 +30,7 @@ class User(BaseModel):
# Auth lifecycle
needs_setup: bool = Field(default=False, description="True for auto-created admin until setup completes")
token_version: int = Field(default=0, description="Incremented on password change to invalidate old JWTs")
approved: bool = Field(default=False, description="登录白名单放行标记;仅当系统设置 login_whitelist.enabled 打开时拦截未放行用户。新普通用户默认待审批(False)。")
approved: bool = Field(default=True, description="登录白名单放行标记(黑名单模式);仅当系统设置 login_whitelist.enabled 打开时拦截被取消放行的用户。新账号默认放行(True),管理员可「取消放行」(False)封禁个别用户。")
class UserResponse(BaseModel):
@@ -256,9 +256,9 @@ async def _get_or_create_user_by_username(username: str):
if user is None:
try:
role = "admin" if username == "admin" else "user"
# admin 创建即放行;普通用户自助注册后默认「待审批」(approved=False),
# 需管理员在白名单管理页放行——但仅当登录白名单开关打开时才会被拦。
user = await provider.create_user(email=email, password=None, system_role=role, needs_setup=False, approved=(role == "admin"))
# 黑名单模式:从未登录过的新用户(此处即首次自动注册)默认放行(approved=True)。
# 管理员可在白名单管理页对个别用户「取消放行」来封禁——仅当登录白名单开关打开时生效。
user = await provider.create_user(email=email, password=None, system_role=role, needs_setup=False, approved=True)
created = True
logger.info("Auto-registered passwordless user: %s", email)
except ValueError:
@@ -280,16 +280,19 @@ async def _get_or_create_user_by_username(username: str):
def enforce_user_approved(user) -> None:
"""登录白名单拦截:开关打开且用户未放行时拒绝登录(注册照常,只是不发 token)。
"""登录白名单拦截(**黑名单模式**):开关打开且用户被「取消放行」时拒绝登录。
新账号首次登录即默认放行(``approved=True``,见 ``_get_or_create_user_by_username``
与 ``create_user``),所以从未在本系统登录过的用户可以直接使用;白名单开关只用来
封禁管理员显式「取消放行」(``approved=False``) 的个别用户。
规则(三条硬约束防锁死):
- 开关关(``login_whitelist.enabled`` 默认 False)→ 直接放行,人人可登录。
- admin → 永远放行(避免管理员把自己关在门外)。
- 其余:``approved`` 为 False(注册后待审批)→ 403 ``PENDING_APPROVAL``;
- 其余:``approved`` 为 False(被管理员取消放行)→ 403 ``PENDING_APPROVAL``;
前端据此提示「等待管理员开通」,且不写任何登录态。
放在各登录端点拿到 user 之后、签发 JWT 之前调用——账号此时已自助注册完成,
被拦只是拿不到 token,管理员可在白名单管理页看到该待审批账号并放行。
放在各登录端点拿到 user 之后、签发 JWT 之前调用。
"""
try:
enabled = load_system_settings().login_whitelist.enabled
@@ -304,7 +307,7 @@ def enforce_user_approved(user) -> None:
if not getattr(user, "approved", False):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "PENDING_APPROVAL", "message": "您的账号正在等待管理员开通,请联系管理员"},
detail={"code": "PENDING_APPROVAL", "message": "您的账号已被限制登录,请联系管理员开通"},
)
@@ -224,9 +224,10 @@ def set_module_tag_ids(
class LoginWhitelistSettings(BaseModel):
"""登录白名单总开关(管理员可在「白名单管理」页切换,即时生效、免重启)。
``enabled`` 关(默认) → 忽略每个用户的 ``approved`` 放行标记,人人可登录;
开 → 仅 ``approved=True`` 或 admin 可登录,其余用户注册后处于「待审批」,
被管理员放行前一律 403。空/默认即关,故部署后行为零变化。
黑名单模式:``enabled`` 关(默认) → 忽略每个用户的 ``approved`` 放行标记,
人人可登录;开 → 仅被管理员显式「取消放行」(``approved=False``) 的用户被 403,
新账号默认 ``approved=True`` 故从未登录过的用户照样放行,admin 永远放行。
空/默认即关,故部署后行为零变化。
"""
enabled: bool = Field(default=False, description="是否启用登录白名单;关=所有人可登录,开=仅已放行用户(或 admin)可登录")
@@ -1,8 +1,10 @@
"""Add users.approved (登录白名单放行标记).
Backs the login-whitelist feature: when system setting ``login_whitelist.enabled``
is on, only ``approved=True`` users (or admins) may log in; self-registered
regular users default to ``approved=False`` (待审批) until an admin approves them.
Backs the login-whitelist feature (黑名单模式): when system setting
``login_whitelist.enabled`` is on, only users an admin has explicitly
「取消放行」 (``approved=False``) are blocked; everyone else — including brand-new
accounts that have never logged in (which default to ``approved=True``) — may log
in. Admins always pass.
Existing rows are backfilled to TRUE (``server_default="1"``) so flipping the
switch on for the first time never locks out users who already had accounts.
@@ -49,12 +49,12 @@ class UserRow(Base):
needs_setup: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
token_version: Mapped[int] = mapped_column(nullable=False, default=0)
# 登录白名单(放行)标记。仅当系统设置 ``login_whitelist.enabled`` 打开时生效:
# 关 → 忽略此列,人人可登录;开 → 仅 approved=True(或 admin)可登录,其余
# 注册后处于「待审批」直到管理员放行。新注册的普通用户默认 False(待审批),
# admin 创建即 True;存量用户由迁移回填 True(避免首次打开开关时把老用户锁死)。
# server_default=1 让迁移 ADD COLUMN 时把已存在行置为已放行。
approved: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=text("1"))
# 登录白名单(放行)标记 —— **黑名单模式**。仅当系统设置 ``login_whitelist.enabled``
# 打开时生效:关 → 忽略此列,人人可登录;开 → 仅 approved=False(被管理员显式
# 「取消放行」)的用户被拦,其余(含从未登录过的新账号)放行。新账号默认 True
# (首次登录即放行);存量用户由迁移回填 True。server_default=1 让迁移 ADD COLUMN
# 时把已存在行置为已放行。
approved: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=text("1"))
# 岗位 (position) the user is assigned to (1:1). NULL = no explicit
# position → the user inherits the ``is_default`` position's grants. Drives
@@ -117,11 +117,11 @@ CALL _add_column_if_missing('knowledge_notes', 'folder', 'VARCHAR(512) NULL DEFA
-- ORM: RoundtableJobRow.task_id = String(128), nullable, indexed。
CALL _add_column_if_missing('roundtable_jobs', 'task_id', 'VARCHAR(128) NULL DEFAULT NULL');
-- ---- users.approved: 登录白名单放行标记,对应 migration 20260625_02 -----------
-- 系统设置 login_whitelist.enabled 打开后,仅 approved=1(或 admin)可登录;普通用户
-- 自助注册后默认 0(待审批),管理员在白名单管理页放行。DEFAULT 1 让存量用户(执行本
-- 脚本补列时)一并置为已放行,避免首次打开开关把老用户锁死。
-- ORM: UserRow.approved = Boolean, NOT NULL, server_default '1'。
-- ---- users.approved: 登录白名单放行标记(黑名单模式),对应 migration 20260625_02 ----
-- 系统设置 login_whitelist.enabled 打开后,仅被管理员显式「取消放行」(approved=0) 的
-- 用户被拦;新账号(含从未登录过的用户)默认 1(已放行),admin 永远放行。DEFAULT 1 让
-- 存量用户(执行本脚本补列时)一并置为已放行,避免首次打开开关把老用户锁死。
-- ORM: UserRow.approved = Boolean, NOT NULL, default True, server_default '1'。
CALL _add_column_if_missing('users', 'approved', 'TINYINT(1) NOT NULL DEFAULT 1');
-- ---- 清理临时存储过程 --------------------------------------------------------