收敛用户角色并共享项目库

- 后端限制系统只保留默认 admin 管理员,新建用户固定为标注员,并拒绝观察员或额外管理员角色。

- 将项目、帧、媒体解析、AI 标注、任务、Dashboard 和导出接口改为共享项目库访问,标注员具备同等项目管理和标注能力。

- 前端用户管理移除角色选择和观察员入口,只展示唯一管理员与标注员状态。

- 更新后端/前端测试,覆盖唯一 admin、旧 viewer 归一为标注员、用户删除和共享项目库访问。

- 同步更新 AGENTS 与 doc 文档中的角色权限、共享项目库和测试计划说明。
This commit is contained in:
2026-05-04 05:20:28 +08:00
parent 02635abab1
commit 523beeb446
21 changed files with 214 additions and 172 deletions

View File

@@ -12,14 +12,14 @@ def test_admin_user_management_and_audit_logs(client, db_session):
})
assert created.status_code == 201
user_id = created.json()["id"]
assert created.json()["role"] == "annotator"
updated = client.patch(f"/api/admin/users/{user_id}", json={
"role": "viewer",
"password": "newsecret",
"is_active": False,
})
assert updated.status_code == 200
assert updated.json()["role"] == "viewer"
assert updated.json()["role"] == "annotator"
assert updated.json()["is_active"] == 0
users = client.get("/api/admin/users")
@@ -37,8 +37,41 @@ def test_admin_user_management_and_audit_logs(client, db_session):
assert "admin.user_deleted" in actions
def test_only_default_admin_role_is_supported(client, db_session):
extra_admin = client.post("/api/admin/users", json={
"username": "chief",
"password": "secret123",
"role": "admin",
"is_active": True,
})
assert extra_admin.status_code == 400
viewer = client.post("/api/admin/users", json={
"username": "observer",
"password": "secret123",
"role": "viewer",
"is_active": True,
})
assert viewer.status_code == 400
created = client.post("/api/admin/users", json={
"username": "doctor",
"password": "secret123",
"is_active": True,
})
assert created.status_code == 201
user_id = created.json()["id"]
assert created.json()["role"] == "annotator"
assert client.patch(f"/api/admin/users/{user_id}", json={"role": "admin"}).status_code == 400
assert client.patch(f"/api/admin/users/{user_id}", json={"role": "viewer"}).status_code == 400
admin_id = client.get("/api/auth/me").json()["id"]
assert client.patch(f"/api/admin/users/{admin_id}", json={"role": "annotator"}).status_code == 400
assert client.patch(f"/api/admin/users/{admin_id}", json={"username": "chief"}).status_code == 400
def test_admin_routes_require_admin_role(client, db_session):
user = User(username="viewer", password_hash=hash_password("secret123"), role="viewer", is_active=1)
user = User(username="doctor", password_hash=hash_password("secret123"), role="annotator", is_active=1)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
@@ -51,7 +84,7 @@ def test_admin_routes_require_admin_role(client, db_session):
client.headers.update({"Authorization": original_auth})
def test_viewer_role_is_read_only_for_business_mutations(client, db_session):
def test_legacy_viewer_role_is_promoted_to_annotator(client, db_session):
project = client.post("/api/projects", json={"name": "Readonly Check"}).json()
user = User(username="readonly", password_hash=hash_password("secret123"), role="viewer", is_active=1)
db_session.add(user)
@@ -61,14 +94,14 @@ def test_viewer_role_is_read_only_for_business_mutations(client, db_session):
client.headers.update({"Authorization": f"Bearer {create_access_token(user)}"})
try:
assert client.get("/api/projects").status_code == 200
assert client.post("/api/projects", json={"name": "Nope"}).status_code == 403
assert client.patch(f"/api/projects/{project['id']}", json={"name": "Nope"}).status_code == 403
assert client.post("/api/ai/annotate", json={"project_id": project["id"]}).status_code == 403
assert client.post("/api/projects", json={"name": "Annotator Project"}).status_code == 201
assert client.patch(f"/api/projects/{project['id']}", json={"name": "Shared Edit"}).status_code == 200
assert client.get("/api/auth/me").json()["role"] == "annotator"
finally:
client.headers.update({"Authorization": original_auth})
def test_admin_cannot_delete_self_or_user_with_projects(client, db_session):
def test_admin_cannot_delete_self_but_can_delete_project_author(client, db_session):
me = client.get("/api/auth/me").json()
assert client.delete(f"/api/admin/users/{me['id']}").status_code == 400
@@ -80,7 +113,8 @@ def test_admin_cannot_delete_self_or_user_with_projects(client, db_session):
db_session.commit()
response = client.delete(f"/api/admin/users/{user.id}")
assert response.status_code == 409
assert response.status_code == 204
assert db_session.query(Project).filter(Project.name == "Owned").count() == 1
def test_demo_factory_reset_leaves_admin_and_parsed_demo_dicom(client, db_session, monkeypatch, tmp_path):

View File

@@ -186,7 +186,7 @@ def test_project_and_frame_404s(client):
assert client.get("/api/projects/999/frames/1").status_code == 404
def test_projects_are_scoped_to_authenticated_owner(client, db_session):
def test_projects_are_shared_between_authenticated_users(client, db_session):
owner_project = client.post("/api/projects", json={"name": "Owner Project"}).json()
other_user = User(
username="other",
@@ -203,14 +203,19 @@ def test_projects_are_scoped_to_authenticated_owner(client, db_session):
db_session.refresh(other_project)
listing = client.get("/api/projects")
assert [project["id"] for project in listing.json()] == [owner_project["id"]]
assert client.get(f"/api/projects/{other_project.id}").status_code == 404
assert {project["id"] for project in listing.json()} == {owner_project["id"], other_project.id}
assert client.get(f"/api/projects/{other_project.id}").status_code == 200
original_auth = client.headers["Authorization"]
client.headers.update({"Authorization": f"Bearer {create_access_token(other_user)}"})
try:
other_listing = client.get("/api/projects")
assert [project["id"] for project in other_listing.json()] == [other_project.id]
assert client.get(f"/api/projects/{owner_project['id']}").status_code == 404
assert {project["id"] for project in other_listing.json()} == {owner_project["id"], other_project.id}
assert client.get(f"/api/projects/{owner_project['id']}").status_code == 200
renamed = client.patch(f"/api/projects/{owner_project['id']}", json={"name": "Edited By Other"})
assert renamed.status_code == 200
assert renamed.json()["name"] == "Edited By Other"
finally:
client.headers.update({"Authorization": original_auth})
assert client.get(f"/api/projects/{owner_project['id']}").json()["name"] == "Edited By Other"