feat(sync): 支持升级实例创建临时管理员
This commit is contained in:
@@ -31,6 +31,12 @@ uv run pytest
|
|||||||
`0.0.0.0` 并直接开放测试端口;该模式不作为生产发布配置。
|
`0.0.0.0` 并直接开放测试端口;该模式不作为生产发布配置。
|
||||||
6. 检查 `/health`、`/ready` 及经过授权的上传/读取;`/ready` 探测数据库 schema、staging 读写和对象存储测试前缀。
|
6. 检查 `/health`、`/ready` 及经过授权的上传/读取;`/ready` 探测数据库 schema、staging 读写和对象存储测试前缀。
|
||||||
|
|
||||||
|
如需让保留旧数据的升级实例执行一次首次设置,可运行下列命令。它只新增临时管理员,不删除旧账户、Vault 或对象;命令输出的随机密码在固定前也会随服务重启而失效。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose run --rm sync /service/.venv/bin/python -m sync_server bootstrap-user
|
||||||
|
```
|
||||||
|
|
||||||
`initialize` 命令已通过真实 PostgreSQL/MinIO 的空实例与重复运行验证,并由 Compose 的一次性服务调用。MinIO 同步账号仍须由管理员创建并限制到 `opennexus` Bucket,`.env` 中的 root 与同步凭据必须不同。
|
`initialize` 命令已通过真实 PostgreSQL/MinIO 的空实例与重复运行验证,并由 Compose 的一次性服务调用。MinIO 同步账号仍须由管理员创建并限制到 `opennexus` Bucket,`.env` 中的 root 与同步凭据必须不同。
|
||||||
|
|
||||||
2026-09-08 已在独立 Docker 项目完成真实 PostgreSQL/MinIO 双 worker 测试部署,修正基础镜像中的 `sync` 系统用户名冲突。测试专用 HTTP 地址、故障检查、完整验收缺口与运维入口见[验收报告](../docs/development/OpenNexus验收报告-2026-09-08.md)。S-07 已在原生 PostgreSQL 17.11/MinIO 实例完成 1 GiB/10,000 文件的删除源实例与空实例恢复;当前机器没有 Docker CLI,因此修改后的 Compose 编排仍需在发布环境复演,生产 TLS 也仍是独立发布门。
|
2026-09-08 已在独立 Docker 项目完成真实 PostgreSQL/MinIO 双 worker 测试部署,修正基础镜像中的 `sync` 系统用户名冲突。测试专用 HTTP 地址、故障检查、完整验收缺口与运维入口见[验收报告](../docs/development/OpenNexus验收报告-2026-09-08.md)。S-07 已在原生 PostgreSQL 17.11/MinIO 实例完成 1 GiB/10,000 文件的删除源实例与空实例恢复;当前机器没有 Docker CLI,因此修改后的 Compose 编排仍需在发布环境复演,生产 TLS 也仍是独立发布门。
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"command",
|
"command",
|
||||||
choices=["serve", "initialize", "migrate", "create-user", "cleanup-uploads", "backup", "restore"],
|
choices=["serve", "initialize", "migrate", "create-user", "bootstrap-user", "cleanup-uploads", "backup", "restore"],
|
||||||
)
|
)
|
||||||
parser.add_argument("--workers", type=int, choices=[1, 2], default=2)
|
parser.add_argument("--workers", type=int, choices=[1, 2], default=2)
|
||||||
parser.add_argument("--username")
|
parser.add_argument("--username")
|
||||||
@@ -80,6 +80,15 @@ def main():
|
|||||||
elif args.command == "create-user":
|
elif args.command == "create-user":
|
||||||
db.migrate()
|
db.migrate()
|
||||||
db.add_user(args.username or input("用户名: "), getpass.getpass("密码(至少12字符): "))
|
db.add_user(args.username or input("用户名: "), getpass.getpass("密码(至少12字符): "))
|
||||||
|
elif args.command == "bootstrap-user":
|
||||||
|
db.migrate()
|
||||||
|
bootstrap = db.prepare_bootstrap_user(force=True)
|
||||||
|
print(json.dumps({
|
||||||
|
"event": "SYNC_BOOTSTRAP_CREDENTIALS",
|
||||||
|
"username": bootstrap["username"],
|
||||||
|
"password": bootstrap["password"],
|
||||||
|
"must_change_credentials": True,
|
||||||
|
}), flush=True)
|
||||||
elif args.command == "serve":
|
elif args.command == "serve":
|
||||||
db.migrate()
|
db.migrate()
|
||||||
bootstrap = db.prepare_bootstrap_user()
|
bootstrap = db.prepare_bootstrap_user()
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class Database:
|
|||||||
conn.execute(text("INSERT INTO users VALUES (:id,:name,:password)"),
|
conn.execute(text("INSERT INTO users VALUES (:id,:name,:password)"),
|
||||||
{"id": secrets.token_hex(16), "name": username, "password": password_hash(password)})
|
{"id": secrets.token_hex(16), "name": username, "password": password_hash(password)})
|
||||||
|
|
||||||
def prepare_bootstrap_user(self):
|
def prepare_bootstrap_user(self, *, force=False):
|
||||||
"""在账户尚未固定时生成本次服务启动专用的临时密码。"""
|
"""在账户尚未固定时生成本次服务启动专用的临时密码。"""
|
||||||
password = secrets.token_urlsafe(24)
|
password = secrets.token_urlsafe(24)
|
||||||
with self.transaction() as conn:
|
with self.transaction() as conn:
|
||||||
@@ -78,11 +78,14 @@ class Database:
|
|||||||
run(conn, "DELETE FROM bootstrap_state")
|
run(conn, "DELETE FROM bootstrap_state")
|
||||||
state = None
|
state = None
|
||||||
if not state:
|
if not state:
|
||||||
if row(conn, "SELECT id FROM users LIMIT 1"):
|
if row(conn, "SELECT id FROM users LIMIT 1") and not force:
|
||||||
return None
|
return None
|
||||||
user_id = secrets.token_hex(16)
|
user_id = secrets.token_hex(16)
|
||||||
|
username = "admin"
|
||||||
|
if row(conn, "SELECT id FROM users WHERE username=:name", name=username):
|
||||||
|
username = "bootstrap-admin-" + secrets.token_hex(3)
|
||||||
run(conn, "INSERT INTO users VALUES (:id,:name,:password)",
|
run(conn, "INSERT INTO users VALUES (:id,:name,:password)",
|
||||||
id=user_id, name="admin", password=password_hash(password))
|
id=user_id, name=username, password=password_hash(password))
|
||||||
run(conn, "INSERT INTO bootstrap_state VALUES (:user,:created)",
|
run(conn, "INSERT INTO bootstrap_state VALUES (:user,:created)",
|
||||||
user=user_id, created=int(time.time()))
|
user=user_id, created=int(time.time()))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ def env(tmp_path):
|
|||||||
db.engine.dispose()
|
db.engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
def test_upgrade_can_add_bootstrap_without_removing_existing_accounts(tmp_path):
|
||||||
|
db = Database("sqlite:///" + str(tmp_path / "upgrade.db"))
|
||||||
|
db.migrate()
|
||||||
|
db.add_user("existing", "existing-account-password")
|
||||||
|
assert db.prepare_bootstrap_user() is None
|
||||||
|
bootstrap = db.prepare_bootstrap_user(force=True)
|
||||||
|
assert bootstrap["username"] == "admin"
|
||||||
|
with db.transaction() as conn:
|
||||||
|
assert conn.exec_driver_sql("SELECT COUNT(*) FROM users").scalar() == 2
|
||||||
|
db.engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
def session(client, user="alice"):
|
def session(client, user="alice"):
|
||||||
response = client.post("/sync/v1/auth/sessions", json={"username": user, "password": "controlled-fixture-password", "device_name": "测试设备"})
|
response = client.post("/sync/v1/auth/sessions", json={"username": user, "password": "controlled-fixture-password", "device_name": "测试设备"})
|
||||||
assert response.status_code == 200, response.text
|
assert response.status_code == 200, response.text
|
||||||
|
|||||||
Reference in New Issue
Block a user