feat: add governance management api
This commit is contained in:
@@ -1,11 +1,31 @@
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth import authenticate, get_user_model, login, logout
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.middleware.csrf import get_token
|
||||
from ninja import Router
|
||||
from ninja.errors import HttpError
|
||||
|
||||
from apps.accounts.models import MenuItem
|
||||
from apps.accounts.models import MenuItem, UserGroup
|
||||
from apps.accounts.permissions import PERMISSIONS
|
||||
from apps.accounts.schemas import CurrentUserOut, LoginIn
|
||||
from apps.accounts.schemas import (
|
||||
BasicUserOut,
|
||||
CurrentUserOut,
|
||||
IdsIn,
|
||||
LoginIn,
|
||||
MenuOut,
|
||||
MenuUpdateIn,
|
||||
PasswordResetIn,
|
||||
PermissionCodesIn,
|
||||
PermissionOut,
|
||||
RoleCreateIn,
|
||||
RoleDetailOut,
|
||||
RoleUpdateIn,
|
||||
UserCreateIn,
|
||||
UserGroupCreateIn,
|
||||
UserGroupOut,
|
||||
UserGroupUpdateIn,
|
||||
UserUpdateIn,
|
||||
)
|
||||
|
||||
router = Router(tags=["accounts"])
|
||||
|
||||
@@ -20,6 +40,11 @@ def permission_codes_for_user(user) -> list[str]:
|
||||
return sorted(codes)
|
||||
|
||||
|
||||
def permission_objects_for_codes(codes: list[str]):
|
||||
content_type = ContentType.objects.get(app_label="system", model="platformpermission")
|
||||
return list(Permission.objects.filter(content_type=content_type, codename__in=codes))
|
||||
|
||||
|
||||
def serialize_menu(menu: MenuItem, children: list[dict]) -> dict:
|
||||
return {
|
||||
"id": menu.id,
|
||||
@@ -82,6 +107,52 @@ def serialize_current_user(user) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def serialize_basic_user(user) -> dict:
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"display_name": user.get_full_name() or user.username,
|
||||
"email": user.email,
|
||||
"is_active": user.is_active,
|
||||
"is_staff": user.is_staff,
|
||||
"is_superuser": user.is_superuser,
|
||||
"roles": [{"id": group.id, "name": group.name} for group in user.groups.all()],
|
||||
"user_groups": [
|
||||
{
|
||||
"id": group.id,
|
||||
"name": group.name,
|
||||
"code": group.code,
|
||||
"description": group.description,
|
||||
"is_active": group.is_active,
|
||||
"created_at": group.created_at,
|
||||
"updated_at": group.updated_at,
|
||||
}
|
||||
for group in user.business_groups.all()
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def serialize_role(group: Group) -> dict:
|
||||
codes = sorted(group.permissions.values_list("codename", flat=True))
|
||||
return {"id": group.id, "name": group.name, "permission_codes": codes}
|
||||
|
||||
|
||||
def serialize_group(group) -> dict:
|
||||
return {
|
||||
"id": group.id,
|
||||
"name": group.name,
|
||||
"code": group.code,
|
||||
"description": group.description,
|
||||
"is_active": group.is_active,
|
||||
"created_at": group.created_at,
|
||||
"updated_at": group.updated_at,
|
||||
}
|
||||
|
||||
|
||||
def serialize_menu_flat(menu: MenuItem) -> dict:
|
||||
return serialize_menu(menu, [])
|
||||
|
||||
|
||||
def require_login(request):
|
||||
if not request.user.is_authenticated:
|
||||
raise HttpError(401, "Authentication required")
|
||||
@@ -119,3 +190,162 @@ def logout_user(request):
|
||||
def current_user(request):
|
||||
user = require_login(request)
|
||||
return serialize_current_user(user)
|
||||
|
||||
|
||||
@router.get("/users", response=list[BasicUserOut])
|
||||
def list_users(request):
|
||||
require_permission(request, "system.user.view")
|
||||
return [serialize_basic_user(user) for user in get_user_model().objects.all().order_by("id")]
|
||||
|
||||
|
||||
@router.post("/users", response=BasicUserOut)
|
||||
def create_user(request, payload: UserCreateIn):
|
||||
require_permission(request, "system.user.manage")
|
||||
user = get_user_model().objects.create_user(
|
||||
username=payload.username,
|
||||
email=payload.email,
|
||||
password=payload.password,
|
||||
is_active=payload.is_active,
|
||||
)
|
||||
if payload.display_name:
|
||||
user.first_name = payload.display_name
|
||||
user.save(update_fields=["first_name"])
|
||||
return serialize_basic_user(user)
|
||||
|
||||
|
||||
@router.get("/users/{user_id}", response=BasicUserOut)
|
||||
def get_user(request, user_id: int):
|
||||
require_permission(request, "system.user.view")
|
||||
return serialize_basic_user(get_user_model().objects.get(id=user_id))
|
||||
|
||||
|
||||
@router.patch("/users/{user_id}", response=BasicUserOut)
|
||||
def update_user(request, user_id: int, payload: UserUpdateIn):
|
||||
require_permission(request, "system.user.manage")
|
||||
user = get_user_model().objects.get(id=user_id)
|
||||
update_fields = []
|
||||
if payload.display_name is not None:
|
||||
user.first_name = payload.display_name
|
||||
update_fields.append("first_name")
|
||||
if payload.email is not None:
|
||||
user.email = payload.email
|
||||
update_fields.append("email")
|
||||
if payload.is_active is not None:
|
||||
user.is_active = payload.is_active
|
||||
update_fields.append("is_active")
|
||||
if update_fields:
|
||||
user.save(update_fields=update_fields)
|
||||
return serialize_basic_user(user)
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/reset-password", response=BasicUserOut)
|
||||
def reset_user_password(request, user_id: int, payload: PasswordResetIn):
|
||||
require_permission(request, "system.user.manage")
|
||||
user = get_user_model().objects.get(id=user_id)
|
||||
user.set_password(payload.password)
|
||||
user.save(update_fields=["password"])
|
||||
return serialize_basic_user(user)
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/roles", response=BasicUserOut)
|
||||
def set_user_roles(request, user_id: int, payload: IdsIn):
|
||||
require_permission(request, "system.user.manage")
|
||||
user = get_user_model().objects.get(id=user_id)
|
||||
user.groups.set(Group.objects.filter(id__in=payload.role_ids))
|
||||
return serialize_basic_user(user)
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/user-groups", response=BasicUserOut)
|
||||
def set_user_groups(request, user_id: int, payload: IdsIn):
|
||||
require_permission(request, "system.user.manage")
|
||||
user = get_user_model().objects.get(id=user_id)
|
||||
user.business_groups.set(UserGroup.objects.filter(id__in=payload.user_ids))
|
||||
return serialize_basic_user(user)
|
||||
|
||||
|
||||
@router.get("/user-groups", response=list[UserGroupOut])
|
||||
def list_user_groups(request):
|
||||
require_permission(request, "system.user_group.view")
|
||||
return [serialize_group(group) for group in UserGroup.objects.all()]
|
||||
|
||||
|
||||
@router.post("/user-groups", response=UserGroupOut)
|
||||
def create_user_group(request, payload: UserGroupCreateIn):
|
||||
require_permission(request, "system.user_group.manage")
|
||||
group = UserGroup.objects.create(**payload.dict())
|
||||
return serialize_group(group)
|
||||
|
||||
|
||||
@router.patch("/user-groups/{group_id}", response=UserGroupOut)
|
||||
def update_user_group(request, group_id: int, payload: UserGroupUpdateIn):
|
||||
require_permission(request, "system.user_group.manage")
|
||||
group = UserGroup.objects.get(id=group_id)
|
||||
for field, value in payload.dict(exclude_unset=True).items():
|
||||
setattr(group, field, value)
|
||||
group.save()
|
||||
return serialize_group(group)
|
||||
|
||||
|
||||
@router.post("/user-groups/{group_id}/members", response=UserGroupOut)
|
||||
def set_user_group_members(request, group_id: int, payload: IdsIn):
|
||||
require_permission(request, "system.user_group.manage")
|
||||
group = UserGroup.objects.get(id=group_id)
|
||||
group.users.set(get_user_model().objects.filter(id__in=payload.user_ids))
|
||||
return serialize_group(group)
|
||||
|
||||
|
||||
@router.get("/roles", response=list[RoleDetailOut])
|
||||
def list_roles(request):
|
||||
require_permission(request, "system.role.view")
|
||||
return [serialize_role(group) for group in Group.objects.all().order_by("id")]
|
||||
|
||||
|
||||
@router.post("/roles", response=RoleDetailOut)
|
||||
def create_role(request, payload: RoleCreateIn):
|
||||
require_permission(request, "system.role.manage")
|
||||
group = Group.objects.create(name=payload.name)
|
||||
group.permissions.set(permission_objects_for_codes(payload.permission_codes))
|
||||
return serialize_role(group)
|
||||
|
||||
|
||||
@router.patch("/roles/{role_id}", response=RoleDetailOut)
|
||||
def update_role(request, role_id: int, payload: RoleUpdateIn):
|
||||
require_permission(request, "system.role.manage")
|
||||
group = Group.objects.get(id=role_id)
|
||||
if payload.name is not None:
|
||||
group.name = payload.name
|
||||
group.save(update_fields=["name"])
|
||||
return serialize_role(group)
|
||||
|
||||
|
||||
@router.post("/roles/{role_id}/permissions", response=RoleDetailOut)
|
||||
def set_role_permissions(request, role_id: int, payload: PermissionCodesIn):
|
||||
require_permission(request, "system.role.manage")
|
||||
group = Group.objects.get(id=role_id)
|
||||
group.permissions.set(permission_objects_for_codes(payload.permission_codes))
|
||||
return serialize_role(group)
|
||||
|
||||
|
||||
@router.get("/permissions", response=list[PermissionOut])
|
||||
def list_permissions(request):
|
||||
require_permission(request, "system.role.view")
|
||||
return [
|
||||
{"codename": permission.codename, "name": permission.name, "module": permission.module}
|
||||
for permission in PERMISSIONS
|
||||
]
|
||||
|
||||
|
||||
@router.get("/menus", response=list[MenuOut])
|
||||
def list_menus(request):
|
||||
require_permission(request, "system.menu.view")
|
||||
return [serialize_menu_flat(menu) for menu in MenuItem.objects.all()]
|
||||
|
||||
|
||||
@router.patch("/menus/{menu_id}", response=MenuOut)
|
||||
def update_menu(request, menu_id: int, payload: MenuUpdateIn):
|
||||
require_permission(request, "system.menu.manage")
|
||||
menu = MenuItem.objects.get(id=menu_id)
|
||||
for field, value in payload.dict(exclude_unset=True).items():
|
||||
setattr(menu, field, value)
|
||||
menu.save()
|
||||
return serialize_menu_flat(menu)
|
||||
|
||||
@@ -57,3 +57,74 @@ class BasicUserOut(Schema):
|
||||
is_superuser: bool
|
||||
roles: list[RoleOut] = []
|
||||
user_groups: list[UserGroupOut] = []
|
||||
|
||||
|
||||
class UserCreateIn(Schema):
|
||||
username: str
|
||||
display_name: str = ""
|
||||
email: str = ""
|
||||
password: str
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class UserUpdateIn(Schema):
|
||||
display_name: str | None = None
|
||||
email: str | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class PasswordResetIn(Schema):
|
||||
password: str
|
||||
|
||||
|
||||
class IdsIn(Schema):
|
||||
user_ids: list[int] = []
|
||||
role_ids: list[int] = []
|
||||
|
||||
|
||||
class UserGroupCreateIn(Schema):
|
||||
name: str
|
||||
code: str
|
||||
description: str = ""
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class UserGroupUpdateIn(Schema):
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class PermissionOut(Schema):
|
||||
codename: str
|
||||
name: str
|
||||
module: str
|
||||
|
||||
|
||||
class RoleDetailOut(Schema):
|
||||
id: int
|
||||
name: str
|
||||
permission_codes: list[str]
|
||||
|
||||
|
||||
class RoleCreateIn(Schema):
|
||||
name: str
|
||||
permission_codes: list[str] = []
|
||||
|
||||
|
||||
class RoleUpdateIn(Schema):
|
||||
name: str | None = None
|
||||
|
||||
|
||||
class PermissionCodesIn(Schema):
|
||||
permission_codes: list[str]
|
||||
|
||||
|
||||
class MenuUpdateIn(Schema):
|
||||
title: str | None = None
|
||||
path: str | None = None
|
||||
icon: str | None = None
|
||||
permission_code: str | None = None
|
||||
is_visible: bool | None = None
|
||||
is_enabled: bool | None = None
|
||||
sort_order: int | None = None
|
||||
|
||||
87
backend/apps/accounts/tests/test_governance_api.py
Normal file
87
backend/apps/accounts/tests/test_governance_api.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.management import call_command
|
||||
|
||||
from apps.accounts.models import MenuItem, UserGroup
|
||||
|
||||
|
||||
def login_admin(client):
|
||||
call_command("bootstrap_governance")
|
||||
user = get_user_model().objects.get(username="admin")
|
||||
client.force_login(user)
|
||||
return user
|
||||
|
||||
|
||||
def test_admin_manages_users_groups_roles_permissions_and_menus(client, db):
|
||||
login_admin(client)
|
||||
|
||||
created_user = client.post(
|
||||
"/api/accounts/users",
|
||||
data={
|
||||
"username": "support",
|
||||
"display_name": "技术支持一号",
|
||||
"email": "support@example.com",
|
||||
"password": "support12345",
|
||||
"is_active": True,
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert created_user.status_code == 200
|
||||
user_id = created_user.json()["id"]
|
||||
|
||||
user_group = client.post(
|
||||
"/api/accounts/user-groups",
|
||||
data={"name": "一线支持组", "code": "support_l1", "description": "处理一线工单"},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert user_group.status_code == 200
|
||||
user_group_id = user_group.json()["id"]
|
||||
|
||||
members = client.post(
|
||||
f"/api/accounts/user-groups/{user_group_id}/members",
|
||||
data={"user_ids": [user_id]},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert members.status_code == 200
|
||||
assert UserGroup.objects.get(id=user_group_id).users.filter(id=user_id).exists()
|
||||
|
||||
role = client.post(
|
||||
"/api/accounts/roles",
|
||||
data={"name": "工单观察员", "permission_codes": ["ticket.view"]},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert role.status_code == 200
|
||||
role_id = role.json()["id"]
|
||||
|
||||
role_permissions = client.post(
|
||||
f"/api/accounts/roles/{role_id}/permissions",
|
||||
data={"permission_codes": ["ticket.view", "solution.view"]},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert role_permissions.status_code == 200
|
||||
assert set(role_permissions.json()["permission_codes"]) == {"ticket.view", "solution.view"}
|
||||
|
||||
user_roles = client.post(
|
||||
f"/api/accounts/users/{user_id}/roles",
|
||||
data={"role_ids": [role_id]},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert user_roles.status_code == 200
|
||||
assert Group.objects.get(id=role_id).user_set.filter(id=user_id).exists()
|
||||
|
||||
menus = client.get("/api/accounts/menus")
|
||||
assert menus.status_code == 200
|
||||
menu_id = MenuItem.objects.get(code="system.users").id
|
||||
updated_menu = client.patch(
|
||||
f"/api/accounts/menus/{menu_id}",
|
||||
data={"title": "账号管理", "permission_code": "system.user.view"},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert updated_menu.status_code == 200
|
||||
assert updated_menu.json()["title"] == "账号管理"
|
||||
|
||||
|
||||
def test_anonymous_governance_access_is_denied(client, db):
|
||||
response = client.get("/api/accounts/users")
|
||||
|
||||
assert response.status_code == 401
|
||||
Reference in New Issue
Block a user