Skip to content

Commit

Permalink
feat: create data_source user #1154
Browse files Browse the repository at this point in the history
  • Loading branch information
Canway-shiisa committed Aug 16, 2023
1 parent b39162d commit c91d46f
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 0 deletions.
10 changes: 10 additions & 0 deletions src/bk-user/bkuser/apis/web/data_source/__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.
"""
35 changes: 35 additions & 0 deletions src/bk-user/bkuser/apis/web/data_source/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- 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 rest_framework import serializers

from bkuser.apps.data_source.models import DataSourceUser


class UserCreateInputSLZ(serializers.ModelSerializer):
department_ids = serializers.ListField(help_text="部门", child=serializers.IntegerField(), default=[])
leader_ids = serializers.ListField(help_text="上级", child=serializers.IntegerField(), default=[])

class Meta:
model = DataSourceUser
fields = [
"username",
"full_name",
"email",
"phone_country_code",
"phone",
"logo",
"department_ids",
"leader_ids",
]


class UserCreateOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="数据源用户ID")
17 changes: 17 additions & 0 deletions src/bk-user/bkuser/apis/web/data_source/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -*- 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 bkuser.apis.web.data_source import views

urlpatterns = [
path("<int:id>/users/", views.DataSourceUserListCreateApi.as_view(), name="data_source.list_create"),
]
80 changes: 80 additions & 0 deletions src/bk-user/bkuser/apis/web/data_source/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# -*- 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 drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, status
from rest_framework.response import Response

from bkuser.apis.web.data_source.serializers import UserCreateInputSLZ, UserCreateOutputSLZ
from bkuser.apps.data_source.models import DataSource, DataSourceUser
from bkuser.biz.data_source_organization import (
DataSourceOrganizationHandler,
DataSourceUserBaseInfo,
DataSourceUserRelationInfo,
)
from bkuser.common.error_codes import error_codes


class DataSourceUserListCreateApi(generics.ListCreateAPIView):
pagination_class = None

@swagger_auto_schema(
operation_description="新建数据源用户",
query_serializer=UserCreateInputSLZ(),
responses={status.HTTP_201_CREATED: UserCreateOutputSLZ(many=True)},
tags=["data_source"],
)
def post(self, request, *args, **kwargs):
slz = UserCreateInputSLZ(data=request.data)
slz.is_valid(raise_exception=True)
data = slz.validated_data
data_source_id = kwargs["id"]

# 校验数据源是否存在
try:
data_source = DataSource.objects.get(id=data_source_id)
except Exception:
raise error_codes.DATA_SOURCE_NOT_EXIST

# 不允许对非本地数据源进行用户新增操作
else:
if data_source.plugin.id != "local":
raise error_codes.CANNOT_CREATE_USER

# 校验是否已存在该用户
try:
DataSourceUser.objects.get(
username=data["username"],
data_source=data_source,
)
except Exception:
pass

else:
raise error_codes.DATA_SOURCE_USER_ALREADY_EXISTED

# 用户数据整合
base_user_info = DataSourceUserBaseInfo(
data_source=data_source,
username=data["username"],
full_name=data["full_name"],
email=data["email"],
phone=data["phone"],
phone_country_code=data["phone_country_code"],
)

relation_info = DataSourceUserRelationInfo(
department_ids=data["department_ids"], leader_ids=data["leader_ids"]
)

user_id = DataSourceOrganizationHandler.create_user(
data_source=data_source, base_user_info=base_user_info, relation_info=relation_info
)
return Response({"id": user_id})
2 changes: 2 additions & 0 deletions src/bk-user/bkuser/apis/web/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@

urlpatterns = [
path("tenants/", include("bkuser.apis.web.tenant.urls")),
path("data_source/", include("bkuser.apis.web.data_source.urls")),

]
9 changes: 9 additions & 0 deletions src/bk-user/bkuser/apps/data_source/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from mptt.models import MPTTModel, TreeForeignKey

from bkuser.common.models import TimestampedModel
from bkuser.apps.data_source.validators import validate_username, validate_phone


class DataSourcePlugin(models.Model):
Expand Down Expand Up @@ -68,6 +69,14 @@ class Meta:
("full_name", "data_source"),
]

def custom_validate(self):
validate_username(self.username)
validate_phone(self.phone_country_code, self.phone)

def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.custom_validate()
super().save(force_insert, force_update, using, update_fields)


class LocalDataSourceIdentityInfo(TimestampedModel):
"""
Expand Down
47 changes: 47 additions & 0 deletions src/bk-user/bkuser/apps/data_source/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- 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.
"""
import logging
import re

import phonenumbers
from django.utils.translation import gettext_lazy as _
from phonenumbers import region_code_for_country_code
from rest_framework.exceptions import ValidationError

USERNAME_REGEX = r"^(\d|[a-zA-Z])([a-zA-Z0-9._-]){0,31}"
logger = logging.getLogger(__name__)


def validate_username(value):
if not re.fullmatch(re.compile(USERNAME_REGEX), value):
raise ValidationError(_("{} 不符合 username 命名规范: 由1-32位字母、数字、下划线(_)、点(.)、减号(-)字符组成,以字母或数字开头").format(value))


def validate_phone(phone_country_code: str, phone: str):
try:
# 根据国家码获取对应地区码
region = region_code_for_country_code(int(phone_country_code))

except phonenumbers.NumberParseException:
logger.debug("failed to parse phone_country_code: %s, ", phone_country_code)

else:
# phonenumbers库在验证号码的时:过短会解析为有效号码,超过250的字节才算超长=》所以这里需要显式做中国号码的长度校验
if region == "CN" and len(phone) != 11:
raise ValidationError(_("{} 不符合 长度要求").format(phone))

try:
# 按照指定地区码解析手机号
phonenumbers.parse(phone, region)

except Exception: # pylint: disable=broad-except
logger.debug("failed to parse phone number: %s", phone)
raise ValidationError
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Generated by Django 3.2.20 on 2023-08-10 12:17

from django.db import migrations, models
import django.db.models.deletion
import django.db.models.manager
import mptt.fields


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='DataSourceUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('data_source_id', models.IntegerField(verbose_name='数据源 ID')),
('username', models.CharField(max_length=128, verbose_name='用户名')),
('full_name', models.CharField(max_length=128, verbose_name='姓名')),
('email', models.EmailField(blank=True, default='', max_length=254, null=True, verbose_name='邮箱')),
('phone', models.CharField(max_length=32, verbose_name='手机号')),
('phone_country_code', models.CharField(blank=True, default='86', max_length=16, null=True, verbose_name='手机国际区号')),
('logo', models.TextField(blank=True, default='', max_length=256, null=True, verbose_name='Logo')),
('extras', models.JSONField(default=dict, verbose_name='自定义字段')),
('leader', models.ManyToManyField(blank=True, related_name='subordinate_staff', to='data_source_organization.DataSourceUser')),
],
options={
'ordering': ['id'],
'unique_together': {('username', 'data_source_id')},
},
),
migrations.CreateModel(
name='LocalDataSourceIdentityInfo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('password', models.CharField(blank=True, default='', max_length=255, null=True, verbose_name='用户密码')),
('password_updated_at', models.DateTimeField(blank=True, null=True, verbose_name='密码最后更新时间')),
('password_expired_at', models.DateTimeField(blank=True, null=True, verbose_name='密码过期时间')),
('data_source_id', models.IntegerField(verbose_name='数据源 ID')),
('username', models.CharField(max_length=128, verbose_name='用户名')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='data_source_organization.datasourceuser')),
],
options={
'unique_together': {('username', 'data_source_id')},
},
),
migrations.CreateModel(
name='DataSourceDepartment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('data_source_id', models.IntegerField(verbose_name='数据源 ID')),
('code', models.CharField(blank=True, max_length=128, null=True, verbose_name='部门标识')),
('name', models.CharField(max_length=255, verbose_name='部门名称')),
('extras', models.JSONField(default=dict, verbose_name='自定义字段')),
('order', models.IntegerField(default=1, verbose_name='顺序')),
('lft', models.PositiveIntegerField(editable=False)),
('rght', models.PositiveIntegerField(editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(editable=False)),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='data_source_organization.datasourcedepartment')),
('users', models.ManyToManyField(blank=True, related_name='departments', to='data_source_organization.DataSourceUser', verbose_name='成员')),
],
options={
'verbose_name': '部门表',
'verbose_name_plural': '部门表',
'ordering': ['id'],
'index_together': {('tree_id', 'lft', 'rght'), ('parent_id', 'tree_id', 'lft')},
},
managers=[
('tree_objects', django.db.models.manager.Manager()),
],
),
]
89 changes: 89 additions & 0 deletions src/bk-user/bkuser/biz/data_source_organization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# -*- 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 List

from django.db import transaction
from pydantic import BaseModel

from bkuser.apps.data_source.models import (
DataSource,
DataSourceDepartmentUserRelation,
DataSourceUser,
DataSourceUserLeaderRelation,
)
from bkuser.apps.tenant.models import Tenant, TenantUser
from bkuser.utils.uuid import generate_uuid


class DataSourceUserBaseInfo(
BaseModel,
):
"""数据源用户信息"""

username: str
full_name: str
email: str
phone: str
phone_country_code: str


class DataSourceUserRelationInfo(
BaseModel,
):
"""数据源用户关系信息"""

department_ids: List
leader_ids: List


class DataSourceOrganizationHandler:
@staticmethod
def create_user(
data_source: DataSource, base_user_info: DataSourceUserBaseInfo, relation_info: DataSourceUserRelationInfo
) -> str:
"""
创建数据源用户
"""
# TODO:补充日志
with transaction.atomic():
# 创建数据源用户
create_user_info_map = {"data_source": data_source, **base_user_info.model_dump()}
user = DataSourceUser.objects.create(**create_user_info_map)

# 批量创建数据源用户-部门关系
department_user_relation_objs = [
DataSourceDepartmentUserRelation(department_id=department_id, user_id=user.id)
for department_id in relation_info.model_dump()["department_ids"]
]

if department_user_relation_objs:
DataSourceDepartmentUserRelation.objects.bulk_create(department_user_relation_objs, batch_size=100)

# 批量创建数据源用户-上级关系
user_leader_relation_objs = [
DataSourceUserLeaderRelation(leader_id=leader_id, user_id=user.id)
for leader_id in relation_info.model_dump()["leader_ids"]
]

if user_leader_relation_objs:
DataSourceUserLeaderRelation.objects.bulk_create(user_leader_relation_objs)

# 查询关联的租户
tenant = Tenant.objects.get(id=data_source.owner_tenant_id)
# 创建租户用户
TenantUser.objects.create(
data_source_user=user,
tenant=tenant,
data_source=data_source,
id=generate_uuid(),
)

return user.id
Loading

0 comments on commit c91d46f

Please sign in to comment.