Skip to content

Commit

Permalink
feat(bkuser): virtual user web api (#1632)
Browse files Browse the repository at this point in the history
  • Loading branch information
nannan00 authored Apr 9, 2024
1 parent 943ed4f commit c9703e9
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 12 deletions.
3 changes: 3 additions & 0 deletions src/bk-user/bkuser/apis/web/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""

from django.urls import include, path

urlpatterns = [
Expand All @@ -19,6 +20,8 @@
# ------------------------------------------ 面向租户管理员 --------------------------------------------
# 租户本身 & 租户管理员
path("tenant-info/", include("bkuser.apis.web.tenant_info.urls")),
# 虚拟账号管理
path("virtual-users/", include("bkuser.apis.web.virtual_user.urls")),
# 租户组织架构
path("tenant-organization/", include("bkuser.apis.web.tenant_organization.urls")),
# 数据源 & 数据源用户/部门
Expand Down
10 changes: 10 additions & 0 deletions src/bk-user/bkuser/apis/web/virtual_user/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
31 changes: 31 additions & 0 deletions src/bk-user/bkuser/apis/web/virtual_user/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
from bkuser.apis.web.mixins import CurrentUserTenantMixin
from bkuser.apps.data_source.constants import DataSourceTypeEnum
from bkuser.apps.data_source.models import DataSource, DataSourcePlugin
from bkuser.plugins.constants import DataSourcePluginEnum
from bkuser.plugins.local.models import LocalDataSourcePluginConfig


class CurrentTenantVirtualDataSource(CurrentUserTenantMixin):
"""当前租户虚拟数据源"""

def get_current_virtual_data_source(self):
data_source, _ = DataSource.objects.get_or_create(
owner_tenant_id=self.get_current_tenant_id(),
type=DataSourceTypeEnum.VIRTUAL,
defaults={
"plugin": DataSourcePlugin.objects.get(id=DataSourcePluginEnum.LOCAL),
"plugin_config": LocalDataSourcePluginConfig(enable_account_password_login=False),
},
)

return data_source
97 changes: 97 additions & 0 deletions src/bk-user/bkuser/apis/web/virtual_user/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
from typing import Any, Dict

from django.conf import settings
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from bkuser.apps.data_source.models import DataSourceUser
from bkuser.biz.validators import validate_data_source_user_username
from bkuser.common.validators import validate_phone_with_country_code


class VirtualUserListInputSLZ(serializers.Serializer):
keyword = serializers.CharField(help_text="搜索关键字", required=False, allow_blank=True, default="")


class VirtualUserListOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="用户 ID")
username = serializers.CharField(help_text="用户名", source="data_source_user.username")
full_name = serializers.CharField(help_text="姓名", source="data_source_user.full_name")
# Note: 这里并不获取租户用户的联系方式,因为虚拟账号并不是同步而来,也无法通过登录后修改
email = serializers.CharField(help_text="邮箱", source="data_source_user.email")
phone = serializers.CharField(help_text="手机号", source="data_source_user.phone")
phone_country_code = serializers.CharField(help_text="手机国际区号", source="data_source_user.phone_country_code")


def _validate_duplicate_data_source_username(data_source_id: str, username: str, data_source_user_id: int = 0) -> str:
"""校验数据源用户名是否重复"""
queryset = DataSourceUser.objects.filter(data_source_id=data_source_id, username=username)
# 过滤掉自身
if data_source_user_id:
queryset = queryset.exclude(id=data_source_user_id)

if queryset.exists():
raise ValidationError(_("用户名 {} 已存在").format(username))

return username


class VirtualUserCreateInputSLZ(serializers.Serializer):
username = serializers.CharField(help_text="用户名", validators=[validate_data_source_user_username])
full_name = serializers.CharField(help_text="姓名")
email = serializers.EmailField(help_text="邮箱", required=False, default="", allow_blank=True)
phone = serializers.CharField(help_text="手机号", required=False, default="", allow_blank=True)
phone_country_code = serializers.CharField(
help_text="手机国际区号", required=False, default=settings.DEFAULT_PHONE_COUNTRY_CODE, allow_blank=True
)

def validate_username(self, username: str) -> str:
return _validate_duplicate_data_source_username(self.context["data_source_id"], username)

def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]:
# 如果提供了手机号,则校验手机号是否合法
if attrs["phone"]:
try:
validate_phone_with_country_code(phone=attrs["phone"], country_code=attrs["phone_country_code"])
except ValueError as e:
raise ValidationError(str(e))

return attrs


class VirtualUserCreateOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="用户 ID")


class VirtualUserRetrieveOutputSLZ(VirtualUserListOutputSLZ):
pass


class VirtualUserUpdateInputSLZ(serializers.Serializer):
full_name = serializers.CharField(help_text="姓名")
email = serializers.EmailField(help_text="邮箱", required=False, default="", allow_blank=True)
phone = serializers.CharField(help_text="手机号", required=False, default="", allow_blank=True)
phone_country_code = serializers.CharField(
help_text="手机国际区号", required=False, default=settings.DEFAULT_PHONE_COUNTRY_CODE, allow_blank=True
)

def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]:
# 如果提供了手机号,则校验手机号是否合法
if attrs["phone"]:
try:
validate_phone_with_country_code(phone=attrs["phone"], country_code=attrs["phone_country_code"])
except ValueError as e:
raise ValidationError(str(e))

return attrs
20 changes: 20 additions & 0 deletions src/bk-user/bkuser/apis/web/virtual_user/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
from django.urls import path

from . import views

urlpatterns = [
path("", views.VirtualUserListCreateApi.as_view(), name="virtual_user.list_create"),
path(
"<str:id>/", views.VirtualUserRetrieveUpdateDestroyApi.as_view(), name="virtual_user.retrieve_update_destroy"
),
]
162 changes: 162 additions & 0 deletions src/bk-user/bkuser/apis/web/virtual_user/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
from django.db import transaction
from django.db.models import Q, QuerySet
from drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from bkuser.apps.data_source.constants import DataSourceTypeEnum
from bkuser.apps.data_source.models import DataSourceUser
from bkuser.apps.data_source.utils import gen_tenant_user_id
from bkuser.apps.permission.constants import PermAction
from bkuser.apps.permission.permissions import perm_class
from bkuser.apps.tenant.models import TenantUser
from bkuser.common.views import ExcludePatchAPIViewMixin

from .mixins import CurrentTenantVirtualDataSource
from .serializers import (
VirtualUserCreateInputSLZ,
VirtualUserCreateOutputSLZ,
VirtualUserListInputSLZ,
VirtualUserListOutputSLZ,
VirtualUserRetrieveOutputSLZ,
VirtualUserUpdateInputSLZ,
)


class VirtualUserListCreateApi(CurrentTenantVirtualDataSource, generics.ListCreateAPIView):
permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]

serializer_class = VirtualUserListOutputSLZ

def get_queryset(self) -> QuerySet[TenantUser]:
slz = VirtualUserListInputSLZ(data=self.request.query_params)
slz.is_valid(raise_exception=True)
data = slz.validated_data

# 过滤当前租户的虚拟用户
queryset = TenantUser.objects.filter(
tenant_id=self.get_current_tenant_id(), data_source__type=DataSourceTypeEnum.VIRTUAL
).select_related("data_source_user")

# 关键字过滤
if keyword := data.get("keyword"):
queryset = queryset.filter(
Q(data_source_user__username__icontains=keyword) | Q(data_source_user__full_name__icontains=keyword)
)
return queryset

@swagger_auto_schema(
tags=["virtual_user"],
operation_description="虚拟用户列表",
query_serializer=VirtualUserListInputSLZ(),
responses={status.HTTP_200_OK: VirtualUserListOutputSLZ(many=True)},
)
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

@swagger_auto_schema(
tags=["virtual_user"],
operation_description="新建虚拟用户",
request_body=VirtualUserCreateInputSLZ(),
responses={status.HTTP_201_CREATED: VirtualUserCreateOutputSLZ()},
)
def post(self, request, *args, **kwargs):
data_source = self.get_current_virtual_data_source()
slz = VirtualUserCreateInputSLZ(data=request.data, context={"data_source_id": data_source.id})
slz.is_valid(raise_exception=True)
data = slz.validated_data

with transaction.atomic():
# 创建数据源用户
user = DataSourceUser.objects.create(
data_source=data_source,
code=data["username"],
username=data["username"],
full_name=data["full_name"],
email=data["email"],
phone=data["phone"],
phone_country_code=data["phone_country_code"],
)
# 虚拟用户只会同步到数据源所属租户下
tenant_id = data_source.owner_tenant_id
tenant_user = TenantUser.objects.create(
id=gen_tenant_user_id(tenant_id, data_source, user),
data_source_user=user,
tenant_id=tenant_id,
data_source=data_source,
)

return Response(status=status.HTTP_201_CREATED, data=VirtualUserCreateOutputSLZ(tenant_user).data)


class VirtualUserRetrieveUpdateDestroyApi(
CurrentTenantVirtualDataSource, ExcludePatchAPIViewMixin, generics.RetrieveUpdateDestroyAPIView
):
permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]

lookup_url_kwarg = "id"
serializer_class = VirtualUserRetrieveOutputSLZ

def get_queryset(self) -> QuerySet[TenantUser]:
# 过滤当前租户的虚拟用户
return TenantUser.objects.filter(
tenant_id=self.get_current_tenant_id(), data_source__type=DataSourceTypeEnum.VIRTUAL
)

@swagger_auto_schema(
tags=["virtual_user"],
operation_description="虚拟用户详情",
responses={status.HTTP_200_OK: VirtualUserRetrieveOutputSLZ()},
)
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)

@swagger_auto_schema(
tags=["virtual_user"],
operation_description="更新虚拟用户",
request_body=VirtualUserUpdateInputSLZ(),
responses={status.HTTP_204_NO_CONTENT: ""},
)
def put(self, request, *args, **kwargs):
tenant_user = self.get_object()
slz = VirtualUserUpdateInputSLZ(data=request.data)
slz.is_valid(raise_exception=True)
data = slz.validated_data

# 实际修改的字段属性都在关联的数据源用户上
data_source_user = tenant_user.data_source_user

# 覆盖更新
data_source_user.full_name = data["full_name"]
data_source_user.email = data["email"]
data_source_user.phone = data["phone"]
data_source_user.full_name = data["phone_country_code"]
data_source_user.save(update_fields=["full_name", "email", "phone", "phone_country_code", "updated_at"])

return Response(status=status.HTTP_204_NO_CONTENT)

@swagger_auto_schema(
tags=["virtual_user"],
operation_description="删除虚拟用户",
responses={status.HTTP_204_NO_CONTENT: ""},
)
def delete(self, request, *args, **kwargs):
tenant_user = self.get_object()
data_source_user = tenant_user.data_source_user

with transaction.atomic():
tenant_user.delete()
data_source_user.delete()

return Response(status=status.HTTP_204_NO_CONTENT)
14 changes: 9 additions & 5 deletions src/bk-user/bkuser/apps/data_source/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,26 @@ class DataSourcePlugin(models.Model):
logo = models.TextField("Logo", null=True, blank=True, default="")


class DataSourceManager(models.Manager):
"""数据源管理器类"""
class DataSourceQuerySet(models.QuerySet):
"""数据源 QuerySet 类"""

@transaction.atomic()
def create(self, *args, **kwargs):
def create(self, **kwargs):
if "plugin_config" not in kwargs:
return super().create(*args, **kwargs)
return super().create(**kwargs)

plugin_cfg = kwargs.pop("plugin_config")
assert isinstance(plugin_cfg, BasePluginConfig)

data_source: DataSource = super().create(*args, **kwargs)
data_source: DataSource = super().create(**kwargs)
data_source.set_plugin_cfg(plugin_cfg)
return data_source


# 数据源管理器类
DataSourceManager = models.Manager.from_queryset(DataSourceQuerySet)


class DataSource(AuditedModel):
owner_tenant_id = models.CharField("归属租户", max_length=64, db_index=True)
type = models.CharField(
Expand Down
Loading

0 comments on commit c9703e9

Please sign in to comment.