Skip to content

Commit

Permalink
feat: support reuse tenant department id (#1963)
Browse files Browse the repository at this point in the history
  • Loading branch information
narasux authored Oct 22, 2024
1 parent b1a6ddf commit 23b2e65
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 15 deletions.
21 changes: 20 additions & 1 deletion src/bk-user/bkuser/apis/web/organization/views/departments.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
from bkuser.apps.permission.constants import PermAction
from bkuser.apps.permission.permissions import perm_class
from bkuser.apps.tenant.constants import CollaborationStrategyStatus
from bkuser.apps.tenant.models import CollaborationStrategy, Tenant, TenantDepartment
from bkuser.apps.tenant.models import CollaborationStrategy, Tenant, TenantDepartment, TenantDepartmentIDRecord
from bkuser.apps.tenant.utils import TenantDeptIDGenerator
from bkuser.common.error_codes import error_codes
from bkuser.common.views import ExcludePatchAPIViewMixin
from bkuser.utils.uuid import generate_uuid
Expand Down Expand Up @@ -200,13 +201,15 @@ def post(self, request, *args, **kwargs):
)
# 不等同步,直接创建本租户的租户部门
tenant_dept = TenantDepartment.objects.create(
id=TenantDeptIDGenerator(current_tenant_id, data_source).gen(data_source_dept),
tenant_id=current_tenant_id,
data_source_department=data_source_dept,
data_source=data_source,
)
# 根据协同策略,将协同的租户部门也创建出来
collaboration_tenant_depts = [
TenantDepartment(
id=TenantDeptIDGenerator(strategy.target_tenant_id, data_source).gen(data_source_dept),
tenant_id=strategy.target_tenant_id,
data_source_department=data_source_dept,
data_source=data_source,
Expand All @@ -220,6 +223,22 @@ def post(self, request, *args, **kwargs):
if collaboration_tenant_depts:
TenantDepartment.objects.bulk_create(collaboration_tenant_depts)

# 刚创建的租户部门,需要捞出来,记录 ID 以便后续复用
records = [
TenantDepartmentIDRecord(
tenant=dept.tenant,
data_source=data_source,
code=data_source_dept.code,
tenant_department_id=dept.id,
)
for dept in TenantDepartment.objects.filter(
data_source=data_source,
data_source_department=data_source_dept,
)
]
# 由于存量历史数据(Record)也会被下发,因此需要忽略冲突保证其他数据可以正常插入
TenantDepartmentIDRecord.objects.bulk_create(records, ignore_conflicts=True)

return Response(TenantDepartmentCreateOutputSLZ(tenant_dept).data, status=status.HTTP_201_CREATED)


Expand Down
19 changes: 18 additions & 1 deletion src/bk-user/bkuser/apps/sync/syncers/tenant_department.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from bkuser.apps.data_source.models import DataSource, DataSourceDepartment
from bkuser.apps.sync.constants import SyncOperation, TenantSyncObjectType
from bkuser.apps.sync.contexts import TenantSyncTaskContext
from bkuser.apps.tenant.models import Tenant, TenantDepartment
from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantDepartmentIDRecord
from bkuser.apps.tenant.utils import TenantDeptIDGenerator


class TenantDepartmentSyncer:
Expand Down Expand Up @@ -45,6 +46,7 @@ def sync(self):
)
waiting_create_tenant_departments = [
TenantDepartment(
id=TenantDeptIDGenerator(self.tenant.id, self.data_source).gen(dept),
tenant=self.tenant,
data_source_department=dept,
data_source=self.data_source,
Expand All @@ -57,6 +59,21 @@ def sync(self):
waiting_delete_tenant_departments.delete()
TenantDepartment.objects.bulk_create(waiting_create_tenant_departments, batch_size=self.batch_size)

# 批量记录租户部门 ID(后续有复用需求)
records = [
TenantDepartmentIDRecord(
tenant=self.tenant,
data_source=self.data_source,
code=dept.data_source_department.code,
tenant_department_id=dept.id,
)
for dept in TenantDepartment.objects.filter(
tenant=self.tenant, data_source_department__in=waiting_sync_data_source_departments
).select_related("data_source_department")
]
# 由于存量历史数据(Record)也会被下发,因此需要忽略冲突保证其他数据可以正常插入
TenantDepartmentIDRecord.objects.bulk_create(records, batch_size=self.batch_size, ignore_conflicts=True)

# 记录删除日志,变更记录
self.ctx.logger.info(f"delete {len(waiting_delete_tenant_departments)} tenant departments")
self.ctx.recorder.add(SyncOperation.DELETE, TenantSyncObjectType.DEPARTMENT, waiting_delete_tenant_departments)
Expand Down
10 changes: 10 additions & 0 deletions src/bk-user/bkuser/apps/tenant/management/__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 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.
"""
10 changes: 10 additions & 0 deletions src/bk-user/bkuser/apps/tenant/management/commands/__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 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.
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017 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.core.management.base import BaseCommand

from bkuser.apps.tenant.models import TenantDepartment, TenantDepartmentIDRecord


class Command(BaseCommand):
"""
同步租户部门 ID 记录:(tenant_id, data_source_id, dept_code) -> tenant_department_id
$ python manage.py sync_tenant_dept_id_record
执行时机:首次部署 & 数据迁移完成后执行一次(可重复执行)
"""

def handle(self, *args, **options):
# 直接全部删除
TenantDepartmentIDRecord.objects.all().delete()
# 再根据租户部门数据,重新创建
records = [
TenantDepartmentIDRecord(
tenant_id=dept.tenant_id,
data_source_id=dept.data_source_id,
code=dept.data_source_department.code,
tenant_department_id=dept.id,
)
for dept in TenantDepartment.objects.select_related("data_source_department")
]
TenantDepartmentIDRecord.objects.bulk_create(records, batch_size=250)
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017 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.
"""
# Generated by Django 3.2.25 on 2024-08-28 07:55

from django.db import migrations, models
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017 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.
"""
# Generated by Django 3.2.25 on 2024-10-15 12:53

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('data_source', '0002_init_builtin_data_source_plugin'),
('tenant', '0004_tenantuseridrecord'),
]

operations = [
migrations.CreateModel(
name='TenantDepartmentIDRecord',
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)),
('code', models.CharField(max_length=128, verbose_name='部门在数据源中的唯一标识')),
('tenant_department_id', models.BigIntegerField(verbose_name='租户部门 ID')),
('data_source', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='data_source.datasource')),
('tenant', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='tenant.tenant')),
],
options={
'unique_together': {('tenant', 'data_source', 'code')},
},
),
]
12 changes: 12 additions & 0 deletions src/bk-user/bkuser/apps/tenant/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,15 @@ class TenantUserIDRecord(TimestampedModel):

class Meta:
unique_together = [("tenant", "data_source", "code")]


class TenantDepartmentIDRecord(TimestampedModel):
"""租户部门 ID 记录"""

tenant = models.ForeignKey(Tenant, on_delete=models.DO_NOTHING, db_constraint=False)
data_source = models.ForeignKey(DataSource, on_delete=models.DO_NOTHING, db_constraint=False)
code = models.CharField("部门在数据源中的唯一标识", max_length=128)
tenant_department_id = models.BigIntegerField("租户部门 ID")

class Meta:
unique_together = [("tenant", "data_source", "code")]
39 changes: 37 additions & 2 deletions src/bk-user/bkuser/apps/tenant/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
import logging
from typing import Dict, Tuple

from bkuser.apps.data_source.models import DataSource, DataSourceUser
from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser
from bkuser.apps.tenant.constants import TenantUserIdRuleEnum
from bkuser.apps.tenant.models import TenantUserIDGenerateConfig, TenantUserIDRecord
from bkuser.apps.tenant.models import TenantDepartmentIDRecord, TenantUserIDGenerateConfig, TenantUserIDRecord
from bkuser.utils.uuid import generate_uuid

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -84,3 +84,38 @@ def _reuse_or_generate_uuid(self, user: DataSourceUser) -> str:
tenant_id=self.target_tenant_id, data_source_id=self.data_source.id, code=user.code, tenant_user_id=uuid
)
return uuid


class TenantDeptIDGenerator:
"""租户部门 ID 生成器"""

def __init__(self, target_tenant_id: str, data_source: DataSource, prepare_batch: bool = False):
self.target_tenant_id = target_tenant_id
self.data_source = data_source

self.prepare_batch = prepare_batch
# 租户部门 ID 映射表:{(tenant_id, data_source_id, code): tenant_dept_id}
self.tenant_dept_id_map: Dict[Tuple[str, int, int], int] = {}
if prepare_batch:
self.tenant_dept_id_map = {
(target_tenant_id, data_source.id, record.code): record.tenant_department_id
for record in TenantDepartmentIDRecord.objects.filter(
tenant_id=target_tenant_id, data_source=data_source
)
}

def gen(self, dept: DataSourceDepartment) -> int | None:
"""生成租户部门 ID,没有历史记录,则返回 None,由 DB 生成自增 ID"""
if self.prepare_batch:
# 有准备的,直接从映射表里面查询
if dept_id := self.tenant_dept_id_map.get((self.target_tenant_id, self.data_source.id, dept.code)):
return dept_id
else:
# 没有准备的需现查 DB,没有的话就创建并生成
record = TenantDepartmentIDRecord.objects.filter(
tenant_id=self.target_tenant_id, data_source=self.data_source, code=dept.code
).first()
if record and record.tenant_department_id:
return record.tenant_department_id

return None
3 changes: 3 additions & 0 deletions src/bk-user/bkuser/biz/data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
from bkuser.apps.tenant.models import (
TenantDepartment,
TenantDepartmentIDRecord,
TenantUser,
TenantUserIDGenerateConfig,
TenantUserIDRecord,
Expand All @@ -41,6 +42,8 @@ def delete_data_source_and_related_resources(data_source: DataSource) -> None:
TenantUserIDGenerateConfig.objects.filter(data_source=data_source).delete()
# 4. 删除租户用户 ID 映射记录
TenantUserIDRecord.objects.filter(data_source=data_source).delete()
# 5. 删除租户部门 ID 映射记录
TenantDepartmentIDRecord.objects.filter(data_source=data_source).delete()

# ======== 删除数据源相关模型数据 ========
# 1. 删除部门 - 用户关系
Expand Down
18 changes: 14 additions & 4 deletions src/bk-user/tests/apis/web/organization/test_department.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
DataSourceDepartmentRelation,
DataSourceDepartmentUserRelation,
)
from bkuser.apps.tenant.models import TenantDepartment
from bkuser.apps.tenant.models import TenantDepartment, TenantDepartmentIDRecord
from django.urls import reverse
from rest_framework import status

Expand Down Expand Up @@ -119,18 +119,28 @@ def test_list_collaboration_child_depts(self, api_client, collaboration_tenant):

class TestTenantDepartmentCreateApi:
@pytest.mark.usefixtures("_init_tenant_users_depts")
def test_standard(self, api_client, random_tenant):
def test_standard(self, api_client, full_local_data_source, random_tenant):
url = reverse("organization.tenant_department.list_create", kwargs={"id": random_tenant.id})

# 创建根部门
resp = api_client.post(url, data={"parent_department_id": 0, "name": generate_random_string()})
root_dept_name = generate_random_string()
resp = api_client.post(url, data={"parent_department_id": 0, "name": root_dept_name})
assert resp.status_code == status.HTTP_201_CREATED

# 创建子部门
child_dept_name = generate_random_string()
company = TenantDepartment.objects.get(data_source_department__name="公司", tenant=random_tenant)
resp = api_client.post(url, data={"parent_department_id": company.id, "name": generate_random_string()})
resp = api_client.post(url, data={"parent_department_id": company.id, "name": child_dept_name})
assert resp.status_code == status.HTTP_201_CREATED

codes = DataSourceDepartment.objects.filter(
data_source=full_local_data_source,
name__in=[root_dept_name, child_dept_name],
).values_list("code", flat=True)

# 租户部门 ID 会被记录,以便后续复用
assert TenantDepartmentIDRecord.objects.filter(tenant=random_tenant, code__in=codes).count() == 2

def test_create_without_data_source(self, api_client, random_tenant):
url = reverse("organization.tenant_department.list_create", kwargs={"id": random_tenant.id})
resp = api_client.post(url, data={"parent_department_id": 0, "name": generate_random_string()})
Expand Down
10 changes: 8 additions & 2 deletions src/bk-user/tests/apis/web/organization/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
DataSourceUserLeaderRelation,
)
from bkuser.apps.tenant.constants import TenantUserStatus
from bkuser.apps.tenant.models import TenantDepartment, TenantUser, TenantUserCustomField
from bkuser.apps.tenant.models import TenantDepartment, TenantUser, TenantUserCustomField, TenantUserIDRecord
from django.conf import settings
from django.urls import reverse
from django.utils.http import urlencode
Expand Down Expand Up @@ -203,7 +203,7 @@ def tenant_user_data(self, random_tenant) -> Dict[str, Any]:
}

@pytest.mark.usefixtures("_init_tenant_users_depts")
def test_standard(self, api_client, random_tenant, tenant_user_data):
def test_standard(self, api_client, full_local_data_source, random_tenant, tenant_user_data):
# 在部门 B 下放一个新用户,设置其 leader 为 wangwu
dept_b = TenantDepartment.objects.get(data_source_department__name="部门B", tenant=random_tenant)
wangwu = TenantUser.objects.get(data_source_user__username="wangwu", tenant=random_tenant)
Expand All @@ -227,6 +227,12 @@ def test_standard(self, api_client, random_tenant, tenant_user_data):
assert DataSourceUserLeaderRelation.objects.filter(
user_id=tenant_user.data_source_user_id, leader_id=wangwu.data_source_user_id
).exists()
# 租户用户 ID 会被记录,以便后续复用
assert TenantUserIDRecord.objects.filter(
tenant=random_tenant,
data_source=full_local_data_source,
code=tenant_user.data_source_user.code,
).exists()

@pytest.mark.usefixtures("_init_tenant_users_depts")
def test_invalid_username(self, api_client, random_tenant, tenant_user_data):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
DataSourceDepartment,
)
from bkuser.apps.sync.syncers import TenantDepartmentSyncer
from bkuser.apps.tenant.models import Tenant, TenantDepartment
from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantDepartmentIDRecord

pytestmark = pytest.mark.django_db

Expand Down Expand Up @@ -47,6 +47,12 @@ def test_standard(self, tenant_sync_task_ctx, full_local_data_source, random_ten

assert not TenantDepartment.objects.filter(tenant=random_tenant, data_source=full_local_data_source).exists()

# 租户部门 ID 复用
assert TenantDepartmentIDRecord.objects.filter(
tenant=random_tenant,
data_source=full_local_data_source,
).exists()

@staticmethod
def _gen_ds_dept_ids_with_tenant(tenant: Tenant, data_source: DataSource) -> Set[int]:
return set(
Expand Down
Loading

0 comments on commit 23b2e65

Please sign in to comment.