From a852eb7e4738b215e8e36684a502d5dab3237d85 Mon Sep 17 00:00:00 2001
From: neronkl <49228807+neronkl@users.noreply.github.com>
Date: Mon, 13 Nov 2023 20:25:15 +0800
Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE-=20=E8=B4=A6=E6=88=B7=E6=9C=89=E6=95=88=E6=9C=9F=20(#?=
=?UTF-8?q?1361)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/bk-user/bkuser/apis/web/tenant/views.py | 2 +
.../apis/web/tenant_setting/serializers.py | 32 +++-
.../bkuser/apis/web/tenant_setting/urls.py | 5 +
.../bkuser/apis/web/tenant_setting/views.py | 67 ++++++-
src/bk-user/bkuser/apps/tenant/constants.py | 92 ++++++++++
.../migrations/0003_auto_20231113_2017.py | 38 ++++
src/bk-user/bkuser/apps/tenant/models.py | 41 ++---
src/bk-user/bkuser/apps/tenant/notifier.py | 164 ++++++++++++++++++
src/bk-user/bkuser/apps/tenant/tasks.py | 101 +++++++++++
.../bkuser/biz/data_source_organization.py | 13 +-
src/bk-user/bkuser/biz/tenant.py | 12 +-
src/bk-user/bkuser/settings.py | 12 +-
12 files changed, 550 insertions(+), 29 deletions(-)
create mode 100644 src/bk-user/bkuser/apps/tenant/migrations/0003_auto_20231113_2017.py
create mode 100644 src/bk-user/bkuser/apps/tenant/notifier.py
create mode 100644 src/bk-user/bkuser/apps/tenant/tasks.py
diff --git a/src/bk-user/bkuser/apis/web/tenant/views.py b/src/bk-user/bkuser/apis/web/tenant/views.py
index 5d3635f65..c11c5335d 100644
--- a/src/bk-user/bkuser/apis/web/tenant/views.py
+++ b/src/bk-user/bkuser/apis/web/tenant/views.py
@@ -99,6 +99,8 @@ def post(self, request, *args, **kwargs):
]
# 本地数据源密码初始化配置
config = PasswordInitialConfig(**data["password_initial_config"])
+
+ # 创建租户和租户管理员
tenant_id = TenantHandler.create_with_managers(tenant_info, managers, config)
return Response(TenantCreateOutputSLZ(instance={"id": tenant_id}).data)
diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py
index 59e381d3a..cbeed75c0 100644
--- a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py
+++ b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py
@@ -16,7 +16,7 @@
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
-from bkuser.apps.tenant.constants import UserFieldDataType
+from bkuser.apps.tenant.constants import NotificationMethod, NotificationScene, UserFieldDataType
from bkuser.apps.tenant.data_models import TenantUserCustomFieldOptions
from bkuser.apps.tenant.models import TenantUserCustomField, UserBuiltinField
@@ -163,3 +163,33 @@ def validate(self, attrs):
_validate_multi_enum_default(default, opt_ids)
return attrs
+
+
+class NotificationTemplatesInputSLZ(serializers.Serializer):
+ method = serializers.ChoiceField(help_text="通知方式", choices=NotificationMethod.get_choices())
+ scene = serializers.ChoiceField(help_text="通知场景", choices=NotificationScene.get_choices())
+ title = serializers.CharField(help_text="通知标题", allow_null=True)
+ sender = serializers.CharField(help_text="发送人")
+ content = serializers.CharField(help_text="通知内容")
+ content_html = serializers.CharField(help_text="通知内容,页面展示使用")
+
+
+class TenantUserValidityPeriodConfigInputSLZ(serializers.Serializer):
+ enabled = serializers.BooleanField(help_text="是否启用账户有效期")
+ validity_period = serializers.IntegerField(help_text="账户有效期,单位:天")
+ remind_before_expire = serializers.ListField(
+ help_text="临过期提醒时间",
+ child=serializers.IntegerField(min_value=1),
+ )
+ enabled_notification_methods = serializers.ListField(
+ help_text="通知方式",
+ child=serializers.ChoiceField(choices=NotificationMethod.get_choices()),
+ allow_empty=False,
+ )
+ notification_templates = serializers.ListField(
+ help_text="通知模板", child=NotificationTemplatesInputSLZ(), allow_empty=False
+ )
+
+
+class TenantUserValidityPeriodConfigOutputSLZ(TenantUserValidityPeriodConfigInputSLZ):
+ pass
diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/urls.py b/src/bk-user/bkuser/apis/web/tenant_setting/urls.py
index 50275f541..d585b2bd0 100644
--- a/src/bk-user/bkuser/apis/web/tenant_setting/urls.py
+++ b/src/bk-user/bkuser/apis/web/tenant_setting/urls.py
@@ -20,4 +20,9 @@
views.TenantUserCustomFieldUpdateDeleteApi.as_view(),
name="tenant_setting_custom_fields.update_delete",
),
+ path(
+ "settings/tenant-user-validity-period/",
+ views.TenantUserValidityPeriodConfigRetrieveUpdateApi.as_view(),
+ name="tenant_user_validity_period_config.retrieve_update",
+ ),
]
diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py
index 35f31d635..8be22b64c 100644
--- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py
+++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py
@@ -10,6 +10,7 @@
"""
from drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, status
+from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from bkuser.apis.web.mixins import CurrentUserTenantMixin
@@ -18,9 +19,17 @@
TenantUserCustomFieldCreateOutputSLZ,
TenantUserCustomFieldUpdateInputSLZ,
TenantUserFieldOutputSLZ,
+ TenantUserValidityPeriodConfigInputSLZ,
+ TenantUserValidityPeriodConfigOutputSLZ,
)
-from bkuser.apps.tenant.models import TenantUserCustomField, UserBuiltinField
-from bkuser.common.views import ExcludePutAPIViewMixin
+from bkuser.apps.tenant.models import (
+ TenantManager,
+ TenantUserCustomField,
+ TenantUserValidityPeriodConfig,
+ UserBuiltinField,
+)
+from bkuser.common.error_codes import error_codes
+from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin
class TenantUserFieldListApi(CurrentUserTenantMixin, generics.ListAPIView):
@@ -82,8 +91,7 @@ def put(self, request, *args, **kwargs):
tenant_id = self.get_current_tenant_id()
slz = TenantUserCustomFieldUpdateInputSLZ(
- data=request.data,
- context={"tenant_id": tenant_id, "current_custom_field_id": kwargs["id"]},
+ data=request.data, context={"tenant_id": tenant_id, "current_custom_field_id": kwargs["id"]}
)
slz.is_valid(raise_exception=True)
data = slz.validated_data
@@ -104,3 +112,54 @@ def put(self, request, *args, **kwargs):
)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
+
+
+class TenantUserValidityPeriodConfigRetrieveUpdateApi(
+ ExcludePatchAPIViewMixin, CurrentUserTenantMixin, generics.RetrieveUpdateAPIView
+):
+ def get_object(self):
+ queryset = TenantUserValidityPeriodConfig.objects.all()
+ filter_kwargs = {"tenant_id": self.get_current_tenant_id()}
+ return get_object_or_404(queryset, **filter_kwargs)
+
+ @swagger_auto_schema(
+ tags=["tenant-setting"],
+ operation_description="当前租户的账户有效期配置",
+ responses={
+ status.HTTP_200_OK: TenantUserValidityPeriodConfigOutputSLZ(),
+ },
+ )
+ def get(self, request, *args, **kwargs):
+ instance = self.get_object()
+ return Response(TenantUserValidityPeriodConfigOutputSLZ(instance=instance).data)
+
+ @swagger_auto_schema(
+ tags=["tenant-setting"],
+ operation_description="更新当前租户的账户有效期配置",
+ request_body=TenantUserValidityPeriodConfigInputSLZ(),
+ responses={
+ status.HTTP_204_NO_CONTENT: "",
+ },
+ )
+ def put(self, request, *args, **kwargs):
+ instance = self.get_object()
+
+ # TODO (su) 权限调整为 perm_class 当前租户的管理才可做更新操作
+ operator = request.user.username
+ if not TenantManager.objects.filter(tenant_id=instance.tenant_id, tenant_user_id=operator).exists():
+ raise error_codes.NO_PERMISSION
+
+ slz = TenantUserValidityPeriodConfigInputSLZ(data=request.data)
+ slz.is_valid(raise_exception=True)
+ data = slz.validated_data
+
+ instance.enabled = data["enabled"]
+ instance.validity_period = data["validity_period"]
+ instance.remind_before_expire = data["remind_before_expire"]
+ instance.enabled_notification_methods = data["enabled_notification_methods"]
+ instance.notification_templates = data["notification_templates"]
+ instance.updater = operator
+
+ instance.save()
+
+ return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/src/bk-user/bkuser/apps/tenant/constants.py b/src/bk-user/bkuser/apps/tenant/constants.py
index 5bcd3347f..8cb4276bf 100644
--- a/src/bk-user/bkuser/apps/tenant/constants.py
+++ b/src/bk-user/bkuser/apps/tenant/constants.py
@@ -32,3 +32,95 @@ class UserFieldDataType(str, StructuredEnum):
NUMBER = EnumField("number", label=_("数字"))
ENUM = EnumField("enum", label=_("枚举"))
MULTI_ENUM = EnumField("multi_enum", label=_("多选枚举"))
+
+
+class NotificationMethod(str, StructuredEnum):
+ """通知方式"""
+
+ EMAIL = EnumField("email", label=_("邮件通知"))
+ SMS = EnumField("sms", label=_("短信通知"))
+
+
+class NotificationScene(str, StructuredEnum):
+ """通知场景"""
+
+ TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("租户用户即将过期"))
+ TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("租户用户已过期"))
+
+
+DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG = {
+ "enabled": True,
+ "validity_period": 365,
+ "remind_before_expire": [7],
+ "enabled_notification_methods": [NotificationMethod.EMAIL],
+ "notification_templates": [
+ {
+ "method": NotificationMethod.EMAIL,
+ "scene": NotificationScene.TENANT_USER_EXPIRING,
+ "title": "蓝鲸智云 - 账号即将到期提醒!",
+ "sender": "蓝鲸智云",
+ "content": (
+ "{{ username }}, 您好:\n "
+ + "您的蓝鲸智云平台账号将于 {{ remind_before_expire_days }} 天后到期。"
+ + "为避免影响使用,请尽快联系平台管理员进行续期。\n "
+ + "此邮件为系统自动发送,请勿回复。\n "
+ ),
+ "content_html": (
+ "
{{ username }}, 您好:
"
+ + "您的蓝鲸智云平台账号将于 {{ remind_before_expire_days }} 天后到期。"
+ + "为避免影响使用,请尽快联系平台管理员进行续期。
"
+ + "此邮件为系统自动发送,请勿回复。
"
+ ),
+ },
+ {
+ "method": NotificationMethod.EMAIL,
+ "scene": NotificationScene.TENANT_USER_EXPIRED,
+ "title": "蓝鲸智云 - 账号到期提醒!",
+ "sender": "蓝鲸智云",
+ "content": (
+ "{{ username }},您好:\n "
+ + "您的蓝鲸智云平台账号已过期。为避免影响使用,请尽快联系平台管理员进行续期。\n " # noqa: E501
+ + "该邮件为系统自动发送,请勿回复。" # noqa: E501
+ ),
+ "content_html": (
+ "{{ username }},您好:
"
+ + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。
" # noqa: E501
+ + "此邮件为系统自动发送,请勿回复。
"
+ ),
+ },
+ {
+ "method": NotificationMethod.SMS,
+ "scene": NotificationScene.TENANT_USER_EXPIRING,
+ "title": None,
+ "sender": "蓝鲸智云",
+ "content": (
+ "{{ username }},您好:\n "
+ + "您的蓝鲸智云平台账号将于 {{ remind_before_expire_days }} 天后到期。"
+ + "为避免影响使用,请尽快联系平台管理员进行续期。\n "
+ + "该短信为系统自动发送,请勿回复。"
+ ),
+ "content_html": (
+ "{{ username }},您好:
"
+ + "您的蓝鲸智云平台账号将于 {{ remind_before_expire_days }} 天后到期。"
+ + "为避免影响使用,请尽快联系平台管理员进行续期。
"
+ + "该短信为系统自动发送,请勿回复。
"
+ ),
+ },
+ {
+ "method": NotificationMethod.SMS,
+ "scene": NotificationScene.TENANT_USER_EXPIRED,
+ "title": None,
+ "sender": "蓝鲸智云",
+ "content": (
+ "{{ username }}您好:\n "
+ + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。\n " # noqa: E501
+ + "该短信为系统自动发送,请勿回复。" # noqa: E501
+ ),
+ "content_html": (
+ "{{ username }}您好:
"
+ + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。
" # noqa: E501
+ + "该短信为系统自动发送,请勿回复。
"
+ ),
+ },
+ ],
+}
diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0003_auto_20231113_2017.py b/src/bk-user/bkuser/apps/tenant/migrations/0003_auto_20231113_2017.py
new file mode 100644
index 000000000..011a8acbd
--- /dev/null
+++ b/src/bk-user/bkuser/apps/tenant/migrations/0003_auto_20231113_2017.py
@@ -0,0 +1,38 @@
+# Generated by Django 3.2.20 on 2023-11-13 12:17
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tenant', '0002_init_builtin_user_fields'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='tenantuser',
+ name='wx_openid',
+ field=models.CharField(blank=True, default='', max_length=64, null=True, verbose_name='微信公众号 用户OpenID'),
+ ),
+ migrations.CreateModel(
+ name='TenantUserValidityPeriodConfig',
+ 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)),
+ ('creator', models.CharField(blank=True, max_length=128, null=True)),
+ ('updater', models.CharField(blank=True, max_length=128, null=True)),
+ ('enabled', models.BooleanField(default=True, verbose_name='是否启用账户有效期')),
+ ('validity_period', models.IntegerField(default=-1, verbose_name='有效期(单位:天)')),
+ ('remind_before_expire', models.JSONField(default=list, verbose_name='临X天过期发送提醒(单位:天)')),
+ ('enabled_notification_methods', models.JSONField(default=list, verbose_name='通知方式')),
+ ('notification_templates', models.JSONField(default=list, verbose_name='通知模板')),
+ ('tenant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='tenant.tenant')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/src/bk-user/bkuser/apps/tenant/models.py b/src/bk-user/bkuser/apps/tenant/models.py
index f99ada30d..4bca8cc0e 100644
--- a/src/bk-user/bkuser/apps/tenant/models.py
+++ b/src/bk-user/bkuser/apps/tenant/models.py
@@ -12,13 +12,11 @@
from django.db import models
from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser
-from bkuser.apps.tenant.constants import TenantFeatureFlag, UserFieldDataType
+from bkuser.apps.tenant.constants import TIME_ZONE_CHOICES, TenantFeatureFlag, UserFieldDataType
from bkuser.common.constants import PERMANENT_TIME, BkLanguageEnum
-from bkuser.common.models import TimestampedModel
+from bkuser.common.models import AuditedModel, TimestampedModel
from bkuser.common.time import datetime_to_display
-from .constants import TIME_ZONE_CHOICES
-
class Tenant(TimestampedModel):
id = models.CharField("租户唯一标识", primary_key=True, max_length=128)
@@ -56,7 +54,7 @@ class TenantUser(TimestampedModel):
# wx_userid/wx_openid 兼容旧版本迁移
wx_userid = models.CharField("微信ID", null=True, blank=True, default="", max_length=64)
- wx_openid = models.CharField("微信公众号OpenID", null=True, blank=True, default="", max_length=64)
+ wx_openid = models.CharField("微信公众号 用户OpenID", null=True, blank=True, default="", max_length=64)
# 账号有效期相关
account_expired_at = models.DateTimeField("账号过期时间", null=True, blank=True, default=PERMANENT_TIME)
@@ -83,6 +81,14 @@ class Meta:
def account_expired_at_display(self) -> str:
return datetime_to_display(self.account_expired_at)
+ @property
+ def real_phone(self) -> str:
+ return self.data_source_user.phone if self.is_inherited_phone else self.custom_phone
+
+ @property
+ def real_email(self) -> str:
+ return self.data_source_user.email if self.is_inherited_email else self.custom_email
+
class TenantDepartment(TimestampedModel):
"""
@@ -143,21 +149,16 @@ class Meta:
]
-# # TODO: 是否直接定义 TenantCommonConfig 表,AccountValidityPeriod是一个JSON字段?
-# class AccountValidityPeriodConfig:
-# """账号时效配置"""
-#
-# tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, db_index=True, unique=True)
-#
-# enabled = models.BooleanField("是否启用", default=True)
-# # TODO: 定义枚举,设置默认值为永久
-# validity_period_seconds = models.IntegerField("有效期(单位:秒)", default=-1)
-# # TODO: 定义枚举,设置默认值为7天
-# reminder_period_days = models.IntegerField("提醒周期(单位:天)", default=7)
-# # TODO: 定义枚举,同时需要考虑到与企业ESB配置的支持的通知方式有关,是否定义字段?
-# notification_method = models.CharField("通知方式", max_length=32, default="email")
-# # TODO: 需要考虑不同通知方式,可能无法使用相同模板,或者其他设计方式
-# notification_content_template = models.TextField("通知模板", default="")
+class TenantUserValidityPeriodConfig(AuditedModel):
+ """账号有效期-配置"""
+
+ tenant = models.OneToOneField(Tenant, on_delete=models.CASCADE, db_index=True, unique=True)
+
+ enabled = models.BooleanField("是否启用账户有效期", default=True)
+ validity_period = models.IntegerField("有效期(单位:天)", default=-1)
+ remind_before_expire = models.JSONField("临X天过期发送提醒(单位:天)", default=list)
+ enabled_notification_methods = models.JSONField("通知方式", default=list)
+ notification_templates = models.JSONField("通知模板", default=list)
# class TenantUserSocialAccountRelation(TimestampedModel):
diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py
new file mode 100644
index 000000000..1064149e4
--- /dev/null
+++ b/src/bk-user/bkuser/apps/tenant/notifier.py
@@ -0,0 +1,164 @@
+# -*- 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
+from typing import Dict, List, Optional
+
+from django.template import Context, Template
+from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
+from pydantic import BaseModel, model_validator
+
+from bkuser.apps.tenant.constants import NotificationMethod, NotificationScene
+from bkuser.apps.tenant.models import TenantUser, TenantUserValidityPeriodConfig
+from bkuser.component import cmsi
+
+logger = logging.getLogger(__name__)
+
+
+class NotificationTemplate(BaseModel):
+ """通知模板"""
+
+ # 通知方式 如短信,邮件
+ method: NotificationMethod
+ # 通知场景 如将过期,已过期
+ scene: NotificationScene
+ # 模板标题
+ title: Optional[str] = None
+ # 模板发送方
+ sender: str
+ # 模板内容(text)格式
+ content: str
+ # 模板内容(html)格式
+ content_html: str
+
+ @model_validator(mode="after")
+ def validate_attrs(self) -> "NotificationTemplate":
+ if self.method == NotificationMethod.EMAIL and not self.title:
+ raise ValueError(_("邮件通知模板需要提供标题"))
+
+ return self
+
+
+class ValidityPeriodNotificationTmplContextGenerator:
+ """生成通知模板使用的上下文"""
+
+ def __init__(self, user: TenantUser, scene: NotificationScene):
+ self.user = user
+ self.scene = scene
+
+ def gen(self) -> Dict[str, str]:
+ """生成通知模板使用的上下文
+
+ 注:为保证模板渲染准确性,value 值类型需为 str
+ """
+ if self.scene == NotificationScene.TENANT_USER_EXPIRING:
+ return self._gen_tenant_user_expiring_ctx()
+ if self.scene == NotificationScene.TENANT_USER_EXPIRED:
+ return self._gen_tenant_user_expired_ctx()
+ return self._gen_base_ctx()
+
+ def _gen_base_ctx(self) -> Dict[str, str]:
+ """获取基础信息"""
+ return {"username": self.user.data_source_user.username}
+
+ def _gen_tenant_user_expiring_ctx(self) -> Dict[str, str]:
+ """账号有效期-临期通知渲染参数"""
+ remind_before_expire_day = self.user.account_expired_at - timezone.now()
+ return {
+ "remind_before_expire_days": str(remind_before_expire_day.days + 1),
+ **self._gen_base_ctx(),
+ }
+
+ def _gen_tenant_user_expired_ctx(self) -> Dict[str, str]:
+ """账号有效期-过期通知渲染参数"""
+ return self._gen_base_ctx()
+
+
+class TenantUserValidityPeriodNotifier:
+ """租户用户用户通知器,支持批量像用户发送某类信息"""
+
+ def __init__(self, tenant_id: str, scene: NotificationScene):
+ self.tenant_id = tenant_id
+ self.scene = scene
+
+ self.templates = self._get_templates_with_scene(scene)
+
+ def send(self, users: List[TenantUser]) -> None:
+ """根据配置,发送对应的通知信息"""
+ for u in users:
+ try:
+ self._send_notifications(u)
+ # TODO 细化异常处理
+ except Exception: # noqa: PERF203
+ logger.exception(
+ "send notification failed, tenant: %s, scene: %s, tenant_user: %s",
+ self.tenant_id,
+ self.scene,
+ u.id,
+ )
+
+ def _get_templates_with_scene(self, scene: NotificationScene) -> List[NotificationTemplate]:
+ """根据场景以及插件配置中设置的通知方式,获取需要发送通知的模板"""
+
+ if scene not in [NotificationScene.TENANT_USER_EXPIRED, NotificationScene.TENANT_USER_EXPIRING]:
+ raise ValueError(_("通知场景 {} 未被支持".format(scene)))
+
+ # 获取通知配置
+ cfg = TenantUserValidityPeriodConfig.objects.get(tenant_id=self.tenant_id)
+
+ # 返回场景匹配,且被声明启用的模板列表
+ return [
+ NotificationTemplate(**tmpl)
+ for tmpl in cfg.notification_templates
+ if tmpl["scene"] == scene and tmpl["method"] in cfg.enabled_notification_methods
+ ]
+
+ def _send_notifications(self, user: TenantUser):
+ """根据配置的通知模板,逐个用户发送通知"""
+ for tmpl in self.templates:
+ if tmpl.method == NotificationMethod.EMAIL:
+ self._send_email(user, tmpl)
+ elif tmpl.method == NotificationMethod.SMS:
+ self._send_sms(user, tmpl)
+
+ def _send_email(self, user: TenantUser, tmpl: NotificationTemplate):
+ # 根据继承与否,获取真实邮箱
+ logger.info(
+ "send email to user %s, email %s, scene %s, title: %s",
+ user.data_source_user.username,
+ user.real_email,
+ tmpl.scene,
+ tmpl.title,
+ )
+ email = user.real_email
+ if not email:
+ logger.info("user<%s> have no email, not to send_email", user.data_source_user.username)
+ return
+
+ content = self._render_tmpl(user, tmpl.content_html)
+ cmsi.send_mail([email], tmpl.sender, tmpl.title, content) # type: ignore
+
+ def _send_sms(self, user: TenantUser, tmpl: NotificationTemplate):
+ logger.info(
+ "send sms to user %s, phone %s, scene %s", user.data_source_user.username, user.real_phone, tmpl.scene
+ )
+ # 根据继承与否,获取真实手机号
+ phone = user.real_phone
+ if not phone:
+ logger.info("user<%s> have no phone number, not to send_sms", user.data_source_user.username)
+ return
+
+ content = self._render_tmpl(user, tmpl.content)
+ cmsi.send_sms([phone], content)
+
+ def _render_tmpl(self, user: TenantUser, content: str) -> str:
+ ctx = ValidityPeriodNotificationTmplContextGenerator(user=user, scene=self.scene).gen()
+ return Template(content).render(Context(ctx))
diff --git a/src/bk-user/bkuser/apps/tenant/tasks.py b/src/bk-user/bkuser/apps/tenant/tasks.py
new file mode 100644
index 000000000..2dd34d2a2
--- /dev/null
+++ b/src/bk-user/bkuser/apps/tenant/tasks.py
@@ -0,0 +1,101 @@
+# -*- 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 datetime
+import logging
+from typing import List
+
+from django.utils import timezone
+
+from bkuser.apps.tenant.constants import NotificationScene
+from bkuser.apps.tenant.models import Tenant, TenantUser, TenantUserValidityPeriodConfig
+from bkuser.apps.tenant.notifier import TenantUserValidityPeriodNotifier
+from bkuser.celery import app
+from bkuser.common.task import BaseTask
+
+logger = logging.getLogger(__name__)
+
+
+@app.task(base=BaseTask, ignore_result=True)
+def send_notifications(tenant_id: str, scene: NotificationScene, tenant_user_ids: List[str]):
+ # TODO: 后续考虑租户、用户状态等,冻结等非正常状态用户不通知
+ users = TenantUser.objects.filter(id__in=tenant_user_ids)
+ logger.info(
+ "going to send notification for users. user_count=%s tenant=%s, scene=%s", len(users), tenant_id, scene
+ )
+ try:
+ TenantUserValidityPeriodNotifier(tenant_id=tenant_id, scene=scene).send(users)
+ except Exception:
+ logger.exception("send notification failed, please check!")
+
+
+@app.task(base=BaseTask, ignore_result=True)
+def notify_expiring_tenant_user():
+ """扫描全部租户用户,做即将过期通知"""
+ logger.info("[celery] receive period task:send_tenant_user_expiring_notification")
+ now = timezone.now()
+
+ # 获取账号有效期-临期配置
+ tenant_remind_before_expire_list = TenantUserValidityPeriodConfig.objects.all().values(
+ "tenant_id", "remind_before_expire"
+ )
+
+ for item in tenant_remind_before_expire_list:
+ tenant_id, remind_before_expire = item["tenant_id"], item["remind_before_expire"]
+
+ tenant_users = TenantUser.objects.filter(account_expired_at__date__gt=now.date(), tenant_id=tenant_id)
+
+ # 临1/7/15天过期 条件设置, 每个租户的设置都不一样
+ account_expired_date_list = []
+ for remain_days in remind_before_expire:
+ account_expired_at = now + datetime.timedelta(days=int(remain_days))
+ account_expired_date_list.append(account_expired_at.date())
+
+ should_notify_user_ids = list(
+ tenant_users.filter(account_expired_at__date__in=account_expired_date_list).values_list("id", flat=True)
+ )
+ # 发送通知
+ logger.info("going to notify expiring users in tenant{%s}, count: %s", tenant_id, len(should_notify_user_ids))
+
+ if not should_notify_user_ids:
+ continue
+
+ send_notifications.delay(
+ tenant_id=tenant_id, scene=NotificationScene.TENANT_USER_EXPIRING, tenant_user_ids=should_notify_user_ids
+ )
+
+
+@app.task(base=BaseTask, ignore_result=True)
+def notify_expired_tenant_user():
+ """扫描全部租户用户,做过期通知"""
+ logger.info("[celery] receive period task:send_tenant_user_expired_notification")
+
+ # 今日过期, 当前时间转换为实际时区的时间
+ now = timezone.now()
+
+ # 获取 租户-过期用户
+ tenant_ids = Tenant.objects.all().values_list("id", flat=True)
+
+ for tenant_id in tenant_ids:
+ # 发送过期通知
+ should_notify_user_ids = list(
+ TenantUser.objects.filter(
+ account_expired_at__date=now.date(),
+ tenant_id=tenant_id,
+ ).values_list("id", flat=True)
+ )
+
+ logger.info("going to notify expired users in tenant{%s}, count: %s", tenant_id, len(should_notify_user_ids))
+ if not should_notify_user_ids:
+ continue
+
+ send_notifications.delay(
+ tenant_id=tenant_id, scene=NotificationScene.TENANT_USER_EXPIRED, tenant_user_ids=should_notify_user_ids
+ )
diff --git a/src/bk-user/bkuser/biz/data_source_organization.py b/src/bk-user/bkuser/biz/data_source_organization.py
index f7a4691bf..9c4e10fd5 100644
--- a/src/bk-user/bkuser/biz/data_source_organization.py
+++ b/src/bk-user/bkuser/biz/data_source_organization.py
@@ -8,10 +8,12 @@
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 datetime
from collections import defaultdict
from typing import Dict, List
from django.db import transaction
+from django.utils import timezone
from pydantic import BaseModel
from bkuser.apps.data_source.models import (
@@ -21,7 +23,7 @@
DataSourceUser,
DataSourceUserLeaderRelation,
)
-from bkuser.apps.tenant.models import Tenant, TenantUser
+from bkuser.apps.tenant.models import Tenant, TenantUser, TenantUserValidityPeriodConfig
from bkuser.utils.uuid import generate_uuid
@@ -102,14 +104,21 @@ def create_user(
# 查询关联的租户
tenant = Tenant.objects.get(id=data_source.owner_tenant_id)
+
# 创建租户用户
- TenantUser.objects.create(
+ tenant_user = TenantUser(
data_source_user=user,
tenant=tenant,
data_source=data_source,
id=generate_uuid(),
)
+ # 根据配置初始化账号有效期
+ cfg = TenantUserValidityPeriodConfig.objects.get(tenant_id=tenant.id)
+ if cfg.enabled and cfg.validity_period > 0:
+ tenant_user.account_expired_at = timezone.now() + datetime.timedelta(days=cfg.validity_period)
+ # 入库
+ tenant_user.save()
return user.id
@staticmethod
diff --git a/src/bk-user/bkuser/biz/tenant.py b/src/bk-user/bkuser/biz/tenant.py
index 83da08107..af50a85dc 100644
--- a/src/bk-user/bkuser/biz/tenant.py
+++ b/src/bk-user/bkuser/biz/tenant.py
@@ -18,7 +18,14 @@
from pydantic import BaseModel
from bkuser.apps.data_source.models import DataSourceDepartmentRelation, DataSourceUser
-from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantManager, TenantUser
+from bkuser.apps.tenant.constants import DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG
+from bkuser.apps.tenant.models import (
+ Tenant,
+ TenantDepartment,
+ TenantManager,
+ TenantUser,
+ TenantUserValidityPeriodConfig,
+)
from bkuser.biz.data_source import (
DataSourceDepartmentHandler,
DataSourceHandler,
@@ -290,6 +297,9 @@ def create_with_managers(
# 创建租户本身
tenant = Tenant.objects.create(**tenant_info.model_dump())
+ # 创建租户完成后,初始化账号有效期设置
+ TenantUserValidityPeriodConfig.objects.create(tenant=tenant, **DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG)
+
# 创建本地数据源
data_source = DataSourceHandler.create_local_data_source_with_merge_config(
_("{}-本地数据源").format(tenant_info.name), tenant.id, password_initial_config
diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py
index 93789164d..1077573f5 100644
--- a/src/bk-user/bkuser/settings.py
+++ b/src/bk-user/bkuser/settings.py
@@ -16,6 +16,7 @@
import environ
import urllib3
+from celery.schedules import crontab
from django.utils.encoding import force_bytes
# environ
@@ -234,7 +235,16 @@
# CELERY 配置,申明任务的文件路径,即包含有 @task 装饰器的函数文件
# CELERY_IMPORTS = []
# 内置的周期任务
-# CELERYBEAT_SCHEDULE = {}
+CELERYBEAT_SCHEDULE = {
+ "periodic_notify_expiring_tenant_users": {
+ "task": "bkuser.apps.tenant.tasks.notify_expiring_tenant_user",
+ "schedule": crontab(minute="0", hour="10"), # 每天10时执行
+ },
+ "periodic_notify_expired_tenant_users": {
+ "task": "bkuser.apps.tenant.tasks.notify_expired_tenant_user",
+ "schedule": crontab(minute="0", hour="10"), # 每天10时执行
+ },
+}
# Celery 消息队列配置
CELERY_BROKER_URL = env.str("BK_BROKER_URL", default="")
From 049be01ace1b0fa88025563d28f6234fd1837f90 Mon Sep 17 00:00:00 2001
From: schnee
Date: Mon, 13 Nov 2023 20:42:47 +0800
Subject: [PATCH 2/2] feat: rebuild api permission control (#1378)
---
.../bkuser/apis/web/basic/serializers.py | 3 +
src/bk-user/bkuser/apis/web/basic/views.py | 7 +-
.../bkuser/apis/web/data_source/views.py | 14 ++
.../web/data_source_organization/views.py | 7 +
.../bkuser/apis/web/organization/views.py | 24 ++--
.../bkuser/apis/web/personal_center/views.py | 53 ++-----
src/bk-user/bkuser/apis/web/tenant/views.py | 7 +-
.../bkuser/apis/web/tenant_setting/views.py | 34 ++---
.../bkuser/apps/permission/__init__.py | 10 ++
src/bk-user/bkuser/apps/permission/apps.py | 16 +++
.../bkuser/apps/permission/constants.py | 36 +++++
.../bkuser/apps/permission/permissions.py | 134 ++++++++++++++++++
src/bk-user/bkuser/settings.py | 1 +
src/bk-user/pyproject.toml | 2 +-
src/bk-user/tests/conftest.py | 2 +-
src/bk-user/tests/test_utils/auth.py | 18 ++-
16 files changed, 291 insertions(+), 77 deletions(-)
create mode 100644 src/bk-user/bkuser/apps/permission/__init__.py
create mode 100644 src/bk-user/bkuser/apps/permission/apps.py
create mode 100644 src/bk-user/bkuser/apps/permission/constants.py
create mode 100644 src/bk-user/bkuser/apps/permission/permissions.py
diff --git a/src/bk-user/bkuser/apis/web/basic/serializers.py b/src/bk-user/bkuser/apis/web/basic/serializers.py
index d928d6e29..b2a6ad5ab 100644
--- a/src/bk-user/bkuser/apis/web/basic/serializers.py
+++ b/src/bk-user/bkuser/apis/web/basic/serializers.py
@@ -10,7 +10,10 @@
"""
from rest_framework import serializers
+from bkuser.apps.permission.constants import UserRole
+
class CurrentUserRetrieveOutputSLZ(serializers.Serializer):
username = serializers.CharField(help_text="用户名")
tenant_id = serializers.CharField(help_text="租户 ID")
+ role = serializers.ChoiceField(help_text="用户角色", choices=UserRole.get_choices())
diff --git a/src/bk-user/bkuser/apis/web/basic/views.py b/src/bk-user/bkuser/apis/web/basic/views.py
index cf089db04..a58c6da96 100644
--- a/src/bk-user/bkuser/apis/web/basic/views.py
+++ b/src/bk-user/bkuser/apis/web/basic/views.py
@@ -12,6 +12,8 @@
from rest_framework import generics, status
from rest_framework.response import Response
+from bkuser.apps.permission.permissions import get_user_role
+
from .serializers import CurrentUserRetrieveOutputSLZ
@@ -24,9 +26,12 @@ class CurrentUserRetrieveApi(generics.RetrieveAPIView):
def get(self, request, *args, **kwargs):
# FIXME: 待新版登录后重构,return更多信息
current_user = request.user
+ current_tenant_id = current_user.get_property("tenant_id")
+
info = {
"username": current_user.username,
- "tenant_id": current_user.get_property("tenant_id"),
+ "tenant_id": current_tenant_id,
+ "role": get_user_role(current_tenant_id, current_user.username),
}
return Response(CurrentUserRetrieveOutputSLZ(instance=info).data)
diff --git a/src/bk-user/bkuser/apis/web/data_source/views.py b/src/bk-user/bkuser/apis/web/data_source/views.py
index 0b9b0d674..0e57e5cf0 100644
--- a/src/bk-user/bkuser/apis/web/data_source/views.py
+++ b/src/bk-user/bkuser/apis/web/data_source/views.py
@@ -16,6 +16,7 @@
from django.utils.translation import gettext_lazy as _
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.apis.web.data_source.mixins import CurrentUserTenantDataSourceMixin
@@ -42,6 +43,8 @@
from bkuser.apis.web.mixins import CurrentUserTenantMixin
from bkuser.apps.data_source.constants import DataSourceStatus
from bkuser.apps.data_source.models import DataSource, DataSourcePlugin, DataSourceSensitiveInfo
+from bkuser.apps.permission.constants import PermAction
+from bkuser.apps.permission.permissions import perm_class
from bkuser.apps.sync.constants import SyncTaskTrigger
from bkuser.apps.sync.data_models import DataSourceSyncOptions
from bkuser.apps.sync.managers import DataSourceSyncManager
@@ -91,6 +94,7 @@ def get(self, request, *args, **kwargs):
class DataSourceListCreateApi(CurrentUserTenantMixin, generics.ListCreateAPIView):
pagination_class = None
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = DataSourceSearchOutputSLZ
def get_serializer_context(self):
@@ -154,6 +158,7 @@ class DataSourceRetrieveUpdateApi(
CurrentUserTenantDataSourceMixin, ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView
):
pagination_class = None
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = DataSourceRetrieveOutputSLZ
@swagger_auto_schema(
@@ -250,6 +255,7 @@ def post(self, request, *args, **kwargs):
class DataSourceSwitchStatusApi(CurrentUserTenantDataSourceMixin, ExcludePutAPIViewMixin, generics.UpdateAPIView):
"""切换数据源状态(启/停)"""
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = DataSourceSwitchStatusOutputSLZ
@swagger_auto_schema(
@@ -274,6 +280,7 @@ class DataSourceTemplateApi(CurrentUserTenantDataSourceMixin, generics.ListAPIVi
"""获取本地数据源数据导入模板"""
pagination_class = None
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
@swagger_auto_schema(
tags=["data_source"],
@@ -295,6 +302,7 @@ class DataSourceExportApi(CurrentUserTenantDataSourceMixin, generics.ListAPIView
"""本地数据源用户导出"""
pagination_class = None
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
@swagger_auto_schema(
tags=["data_source"],
@@ -317,6 +325,8 @@ def get(self, request, *args, **kwargs):
class DataSourceImportApi(CurrentUserTenantDataSourceMixin, generics.CreateAPIView):
"""从 Excel 导入数据源用户数据"""
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
+
@swagger_auto_schema(
tags=["data_source"],
operation_description="本地数据源用户数据导入",
@@ -371,6 +381,8 @@ def post(self, request, *args, **kwargs):
class DataSourceSyncApi(CurrentUserTenantDataSourceMixin, generics.CreateAPIView):
"""数据源同步"""
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
+
@swagger_auto_schema(
tags=["data_source"],
operation_description="数据源数据同步",
@@ -412,6 +424,7 @@ def post(self, request, *args, **kwargs):
class DataSourceSyncRecordListApi(CurrentUserTenantMixin, generics.ListAPIView):
"""数据源同步记录列表"""
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = DataSourceSyncRecordListOutputSLZ
def get_queryset(self):
@@ -449,6 +462,7 @@ class DataSourceSyncRecordRetrieveApi(CurrentUserTenantMixin, generics.RetrieveA
"""数据源同步记录详情"""
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
def get_queryset(self):
return DataSourceSyncTask.objects.filter(data_source__owner_tenant_id=self.get_current_tenant_id())
diff --git a/src/bk-user/bkuser/apis/web/data_source_organization/views.py b/src/bk-user/bkuser/apis/web/data_source_organization/views.py
index 1c8736d79..d1bf2e055 100644
--- a/src/bk-user/bkuser/apis/web/data_source_organization/views.py
+++ b/src/bk-user/bkuser/apis/web/data_source_organization/views.py
@@ -11,6 +11,7 @@
from django.db.models import Q
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.apis.web.data_source_organization.serializers import (
@@ -26,6 +27,8 @@
UserUpdateInputSLZ,
)
from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser
+from bkuser.apps.permission.constants import PermAction
+from bkuser.apps.permission.permissions import perm_class
from bkuser.biz.data_source_organization import (
DataSourceOrganizationHandler,
DataSourceUserBaseInfo,
@@ -39,6 +42,7 @@
class DataSourceUserListCreateApi(generics.ListCreateAPIView):
serializer_class = UserSearchOutputSLZ
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
def get_queryset(self):
slz = UserSearchInputSLZ(data=self.request.query_params)
@@ -111,6 +115,7 @@ def post(self, request, *args, **kwargs):
class DataSourceLeadersListApi(generics.ListAPIView):
serializer_class = LeaderSearchOutputSLZ
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
def get_queryset(self):
slz = LeaderSearchInputSLZ(data=self.request.query_params)
@@ -139,6 +144,7 @@ def get(self, request, *args, **kwargs):
class DataSourceDepartmentsListApi(generics.ListAPIView):
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = DepartmentSearchOutputSLZ
def get_queryset(self):
@@ -171,6 +177,7 @@ def get(self, request, *args, **kwargs):
class DataSourceUserRetrieveUpdateApi(ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView):
queryset = DataSourceUser.objects.all()
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = UserRetrieveOutputSLZ
def get_serializer_context(self):
diff --git a/src/bk-user/bkuser/apis/web/organization/views.py b/src/bk-user/bkuser/apis/web/organization/views.py
index 1e5bdd725..a2e1e99ca 100644
--- a/src/bk-user/bkuser/apis/web/organization/views.py
+++ b/src/bk-user/bkuser/apis/web/organization/views.py
@@ -13,6 +13,7 @@
from django.db.models import Q
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.apis.web.mixins import CurrentUserTenantMixin
@@ -25,6 +26,8 @@
TenantUserSearchInputSLZ,
)
from bkuser.apis.web.tenant.serializers import TenantRetrieveOutputSLZ, TenantUpdateInputSLZ
+from bkuser.apps.permission.constants import PermAction
+from bkuser.apps.permission.permissions import perm_class
from bkuser.apps.tenant.models import Tenant, TenantUser
from bkuser.biz.tenant import (
TenantDepartmentHandler,
@@ -33,7 +36,6 @@
TenantHandler,
TenantUserHandler,
)
-from bkuser.common.error_codes import error_codes
from bkuser.common.views import ExcludePatchAPIViewMixin
logger = logging.getLogger(__name__)
@@ -42,6 +44,7 @@
class TenantDepartmentUserListApi(CurrentUserTenantMixin, generics.ListAPIView):
queryset = TenantUser.objects.all()
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = TenantUserListOutputSLZ
def get_serializer_context(self):
@@ -100,6 +103,7 @@ def get(self, request, *args, **kwargs):
class TenantUserRetrieveApi(generics.RetrieveAPIView):
queryset = TenantUser.objects.all()
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = TenantUserRetrieveOutputSLZ
@swagger_auto_schema(
@@ -113,6 +117,7 @@ def get(self, request, *args, **kwargs):
class TenantListApi(CurrentUserTenantMixin, generics.ListAPIView):
pagination_class = None
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
@swagger_auto_schema(
tags=["tenant-organization"],
@@ -145,6 +150,7 @@ def get(self, request, *args, **kwargs):
class TenantRetrieveUpdateApi(ExcludePatchAPIViewMixin, CurrentUserTenantMixin, generics.RetrieveUpdateAPIView):
queryset = Tenant.objects.all()
pagination_class = None
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = TenantRetrieveOutputSLZ
lookup_url_kwarg = "id"
@@ -164,28 +170,25 @@ def get(self, request, *args, **kwargs):
tags=["tenant-organization"],
operation_description="更新租户",
request_body=TenantUpdateInputSLZ(),
- responses={status.HTTP_200_OK: ""},
+ responses={status.HTTP_204_NO_CONTENT: ""},
)
def put(self, request, *args, **kwargs):
slz = TenantUpdateInputSLZ(data=request.data)
slz.is_valid(raise_exception=True)
data = slz.validated_data
- instance = self.get_object()
- # NOTE 非当前租户, 无权限做更新操作
- if self.get_current_tenant_id() != instance.id:
- raise error_codes.NO_PERMISSION
-
- should_updated_info = TenantEditableBaseInfo(
+ tenant = self.get_object()
+ tenant_info = TenantEditableBaseInfo(
name=data["name"], logo=data["logo"] or "", feature_flags=TenantFeatureFlag(**data["feature_flags"])
)
- TenantHandler.update_with_managers(instance.id, should_updated_info, data["manager_ids"])
- return Response()
+ TenantHandler.update_with_managers(tenant.id, tenant_info, data["manager_ids"])
+ return Response(status=status.HTTP_204_NO_CONTENT)
class TenantDepartmentChildrenListApi(generics.ListAPIView):
pagination_class = None
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = TenantDepartmentChildrenListOutputSLZ
@swagger_auto_schema(
@@ -204,6 +207,7 @@ def get(self, request, *args, **kwargs):
class TenantUserListApi(CurrentUserTenantMixin, generics.ListAPIView):
queryset = TenantUser.objects.all()
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = TenantUserListOutputSLZ
def get_tenant_user_ids(self, tenant_id):
diff --git a/src/bk-user/bkuser/apis/web/personal_center/views.py b/src/bk-user/bkuser/apis/web/personal_center/views.py
index a96a892c2..e3a8425c0 100644
--- a/src/bk-user/bkuser/apis/web/personal_center/views.py
+++ b/src/bk-user/bkuser/apis/web/personal_center/views.py
@@ -12,6 +12,7 @@
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.apis.web.personal_center.serializers import (
@@ -20,15 +21,17 @@
TenantUserEmailUpdateInputSLZ,
TenantUserPhoneUpdateInputSLZ,
)
+from bkuser.apps.permission.constants import PermAction
+from bkuser.apps.permission.permissions import perm_class
from bkuser.apps.tenant.models import TenantUser
from bkuser.biz.natural_user import NatureUserHandler
from bkuser.biz.tenant import TenantUserEmailInfo, TenantUserHandler, TenantUserPhoneInfo
-from bkuser.common.error_codes import error_codes
from bkuser.common.views import ExcludePutAPIViewMixin
class NaturalUserTenantUserListApi(generics.ListAPIView):
pagination_class = None
+ permission_classes = [IsAuthenticated, perm_class(PermAction.USE_PLATFORM)]
@swagger_auto_schema(
tags=["personal_center"],
@@ -70,6 +73,7 @@ def get(self, request, *args, **kwargs):
class TenantUserRetrieveApi(generics.RetrieveAPIView):
queryset = TenantUser.objects.all()
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.USE_PLATFORM)]
serializer_class = PersonalCenterTenantUserRetrieveOutputSLZ
@swagger_auto_schema(
@@ -78,22 +82,13 @@ class TenantUserRetrieveApi(generics.RetrieveAPIView):
responses={status.HTTP_200_OK: PersonalCenterTenantUserRetrieveOutputSLZ()},
)
def get(self, request, *args, **kwargs):
- instance: TenantUser = self.get_object()
-
- # 获取当前登录的租户用户的自然人
- nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(request.user.username)
-
- # 边界限制
- # 该租户用户的数据源用户,不属于当前自然人
- if instance.data_source_user_id not in nature_user.data_source_user_ids:
- raise error_codes.NO_PERMISSION
-
- return Response(PersonalCenterTenantUserRetrieveOutputSLZ(instance).data)
+ return Response(PersonalCenterTenantUserRetrieveOutputSLZ(self.get_object()).data)
class TenantUserPhoneUpdateApi(ExcludePutAPIViewMixin, generics.UpdateAPIView):
queryset = TenantUser.objects.all()
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.USE_PLATFORM)]
@swagger_auto_schema(
tags=["personal_center"],
@@ -102,33 +97,23 @@ class TenantUserPhoneUpdateApi(ExcludePutAPIViewMixin, generics.UpdateAPIView):
responses={status.HTTP_200_OK: ""},
)
def patch(self, request, *args, **kwargs):
- instance: TenantUser = self.get_object()
-
- # 获取当前登录的租户用户的自然人
- nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(request.user.username)
-
- # 边界限制
- # 该租户用户的数据源用户,不属于当前自然人
- if instance.data_source_user_id not in nature_user.data_source_user_ids:
- raise error_codes.NO_PERMISSION
-
slz = TenantUserPhoneUpdateInputSLZ(data=request.data)
slz.is_valid(raise_exception=True)
-
data = slz.validated_data
+
phone_info = TenantUserPhoneInfo(
is_inherited_phone=data["is_inherited_phone"],
custom_phone=data.get("custom_phone", ""),
custom_phone_country_code=data["custom_phone_country_code"],
)
- TenantUserHandler.update_tenant_user_phone(instance, phone_info)
-
+ TenantUserHandler.update_tenant_user_phone(self.get_object(), phone_info)
return Response()
class TenantUserEmailUpdateApi(ExcludePutAPIViewMixin, generics.UpdateAPIView):
queryset = TenantUser.objects.all()
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.USE_PLATFORM)]
@swagger_auto_schema(
tags=["personal_center"],
@@ -137,23 +122,13 @@ class TenantUserEmailUpdateApi(ExcludePutAPIViewMixin, generics.UpdateAPIView):
responses={status.HTTP_200_OK: ""},
)
def patch(self, request, *args, **kwargs):
- instance: TenantUser = self.get_object()
-
- # 获取当前登录的租户用户的自然人
- nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(request.user.username)
-
- # 边界限制
- # 该租户用户的数据源用户,不属于当前自然人下的
- if instance.data_source_user_id not in nature_user.data_source_user_ids:
- raise error_codes.NO_PERMISSION
-
slz = TenantUserEmailUpdateInputSLZ(data=request.data)
slz.is_valid(raise_exception=True)
-
data = slz.validated_data
+
email_info = TenantUserEmailInfo(
- is_inherited_email=data["is_inherited_email"], custom_email=data.get("custom_email", "")
+ is_inherited_email=data["is_inherited_email"],
+ custom_email=data.get("custom_email", ""),
)
- TenantUserHandler.update_tenant_user_email(instance, email_info)
-
+ TenantUserHandler.update_tenant_user_email(self.get_object(), email_info)
return Response()
diff --git a/src/bk-user/bkuser/apis/web/tenant/views.py b/src/bk-user/bkuser/apis/web/tenant/views.py
index c11c5335d..d0c9165a9 100644
--- a/src/bk-user/bkuser/apis/web/tenant/views.py
+++ b/src/bk-user/bkuser/apis/web/tenant/views.py
@@ -13,6 +13,7 @@
from django.db.models import Q
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.apis.web.tenant.serializers import (
@@ -25,6 +26,8 @@
TenantUserSearchInputSLZ,
TenantUserSearchOutputSLZ,
)
+from bkuser.apps.permission.constants import PermAction
+from bkuser.apps.permission.permissions import perm_class
from bkuser.apps.tenant.models import Tenant, TenantUser
from bkuser.biz.data_source import DataSourceHandler
from bkuser.biz.tenant import (
@@ -42,7 +45,7 @@
class TenantListCreateApi(generics.ListCreateAPIView):
pagination_class = None
-
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_PLATFORM)]
serializer_class = TenantSearchOutputSLZ
def get_serializer_context(self):
@@ -109,6 +112,7 @@ def post(self, request, *args, **kwargs):
class TenantRetrieveUpdateApi(ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView):
queryset = Tenant.objects.all()
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_PLATFORM)]
serializer_class = TenantRetrieveOutputSLZ
def get_serializer_context(self):
@@ -150,6 +154,7 @@ def put(self, request, *args, **kwargs):
class TenantUsersListApi(generics.ListAPIView):
serializer_class = TenantUserSearchOutputSLZ
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_PLATFORM)]
def get_queryset(self):
tenant_id = self.kwargs["tenant_id"]
diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py
index 8be22b64c..4e8bc4c33 100644
--- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py
+++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py
@@ -11,6 +11,7 @@
from drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, status
from rest_framework.generics import get_object_or_404
+from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from bkuser.apis.web.mixins import CurrentUserTenantMixin
@@ -22,18 +23,19 @@
TenantUserValidityPeriodConfigInputSLZ,
TenantUserValidityPeriodConfigOutputSLZ,
)
+from bkuser.apps.permission.constants import PermAction
+from bkuser.apps.permission.permissions import perm_class
from bkuser.apps.tenant.models import (
- TenantManager,
TenantUserCustomField,
TenantUserValidityPeriodConfig,
UserBuiltinField,
)
-from bkuser.common.error_codes import error_codes
from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin
class TenantUserFieldListApi(CurrentUserTenantMixin, generics.ListAPIView):
pagination_class = None
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
serializer_class = TenantUserFieldOutputSLZ
@swagger_auto_schema(
@@ -54,6 +56,8 @@ def get(self, request, *args, **kwargs):
class TenantUserCustomFieldCreateApi(CurrentUserTenantMixin, generics.CreateAPIView):
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
+
@swagger_auto_schema(
tags=["tenant-setting"],
operation_description="新建用户自定义字段",
@@ -77,6 +81,7 @@ class TenantUserCustomFieldUpdateDeleteApi(
CurrentUserTenantMixin, ExcludePutAPIViewMixin, generics.UpdateAPIView, generics.DestroyAPIView
):
lookup_url_kwarg = "id"
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
def get_queryset(self):
return TenantUserCustomField.objects.filter(tenant_id=self.get_current_tenant_id())
@@ -117,6 +122,8 @@ def delete(self, request, *args, **kwargs):
class TenantUserValidityPeriodConfigRetrieveUpdateApi(
ExcludePatchAPIViewMixin, CurrentUserTenantMixin, generics.RetrieveUpdateAPIView
):
+ permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)]
+
def get_object(self):
queryset = TenantUserValidityPeriodConfig.objects.all()
filter_kwargs = {"tenant_id": self.get_current_tenant_id()}
@@ -142,24 +149,17 @@ def get(self, request, *args, **kwargs):
},
)
def put(self, request, *args, **kwargs):
- instance = self.get_object()
-
- # TODO (su) 权限调整为 perm_class 当前租户的管理才可做更新操作
- operator = request.user.username
- if not TenantManager.objects.filter(tenant_id=instance.tenant_id, tenant_user_id=operator).exists():
- raise error_codes.NO_PERMISSION
-
slz = TenantUserValidityPeriodConfigInputSLZ(data=request.data)
slz.is_valid(raise_exception=True)
data = slz.validated_data
- instance.enabled = data["enabled"]
- instance.validity_period = data["validity_period"]
- instance.remind_before_expire = data["remind_before_expire"]
- instance.enabled_notification_methods = data["enabled_notification_methods"]
- instance.notification_templates = data["notification_templates"]
- instance.updater = operator
-
- instance.save()
+ cfg = self.get_object()
+ cfg.enabled = data["enabled"]
+ cfg.validity_period = data["validity_period"]
+ cfg.remind_before_expire = data["remind_before_expire"]
+ cfg.enabled_notification_methods = data["enabled_notification_methods"]
+ cfg.notification_templates = data["notification_templates"]
+ cfg.updater = request.user.username
+ cfg.save()
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/src/bk-user/bkuser/apps/permission/__init__.py b/src/bk-user/bkuser/apps/permission/__init__.py
new file mode 100644
index 000000000..1060b7bf4
--- /dev/null
+++ b/src/bk-user/bkuser/apps/permission/__init__.py
@@ -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.
+"""
diff --git a/src/bk-user/bkuser/apps/permission/apps.py b/src/bk-user/bkuser/apps/permission/apps.py
new file mode 100644
index 000000000..71b90bcb4
--- /dev/null
+++ b/src/bk-user/bkuser/apps/permission/apps.py
@@ -0,0 +1,16 @@
+# -*- 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.apps import AppConfig
+
+
+class PermissionConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "bkuser.apps.permission"
diff --git a/src/bk-user/bkuser/apps/permission/constants.py b/src/bk-user/bkuser/apps/permission/constants.py
new file mode 100644
index 000000000..e06952daf
--- /dev/null
+++ b/src/bk-user/bkuser/apps/permission/constants.py
@@ -0,0 +1,36 @@
+# -*- 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
+
+from blue_krill.data_types.enum import EnumField, StructuredEnum
+from django.utils.translation import gettext_lazy as _
+
+logger = logging.getLogger(__name__)
+
+
+class UserRole(str, StructuredEnum):
+ """用户角色"""
+
+ SUPER_MANAGER = EnumField("super_manager", label=_("超级管理员"))
+ TENANT_MANAGER = EnumField("tenant_manager", label=_("租户管理员"))
+ NATURAL_USER = EnumField("natural_user", label=_("普通用户"))
+
+
+class PermAction(str, StructuredEnum):
+ """权限行为"""
+
+ # TODO (su) 接入 IAM 时,需要评估是否细化 Action
+ # 平台管理 - 超级管理员
+ MANAGE_PLATFORM = EnumField("manage_platform", label=_("平台管理"))
+ # 租户管理 - 租户管理员
+ MANAGE_TENANT = EnumField("manage_tenant", label=_("租户管理"))
+ # 平台使用 - 普通用户/自然人
+ USE_PLATFORM = EnumField("use_platform", label=_("平台使用"))
diff --git a/src/bk-user/bkuser/apps/permission/permissions.py b/src/bk-user/bkuser/apps/permission/permissions.py
new file mode 100644
index 000000000..b599e24b9
--- /dev/null
+++ b/src/bk-user/bkuser/apps/permission/permissions.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+"""
+TencentBlueKing is pleased to support the open source community by making
+蓝鲸智云 - PaaS 平台 (BlueKing - PaaS System) 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.
+
+We undertake not to change the open source license (MIT license) applicable
+to the current version of the project delivered to anyone in the future.
+"""
+import logging
+
+from rest_framework.permissions import BasePermission
+
+from bkuser.apps.data_source.models import DataSource
+from bkuser.apps.natural_user.models import DataSourceUserNaturalUserRelation
+from bkuser.apps.permission.constants import PermAction, UserRole
+from bkuser.apps.tenant.models import Tenant, TenantManager, TenantUser
+
+logger = logging.getLogger(__name__)
+
+
+def perm_class(action: PermAction): # noqa: C901
+ """构建 DRF 可用的应用权限类"""
+
+ class Permission(BasePermission):
+ def has_permission(self, request, view):
+ username = request.user.username
+ cur_tenant_id = request.user.get_property("tenant_id")
+
+ if action == PermAction.MANAGE_PLATFORM:
+ return is_super_manager(cur_tenant_id, username)
+ if action == PermAction.MANAGE_TENANT:
+ return is_tenant_manager(cur_tenant_id, username)
+ if action == PermAction.USE_PLATFORM:
+ # 平台使用的情况,需要用具体的 object 来判断权限
+ return True
+
+ return False
+
+ def has_object_permission(self, request, view, obj):
+ if isinstance(obj, Tenant):
+ tenant_id = obj.id
+ elif hasattr(obj, "tenant_id"):
+ tenant_id = obj.tenant_id
+ elif isinstance(obj, DataSource):
+ # TODO (su) 考虑数据源协同的情况
+ tenant_id = obj.owner_tenant_id
+ elif hasattr(obj, "data_source"):
+ # TODO (su) 考虑数据源协同的情况
+ tenant_id = obj.data_source.owner_tenant_id
+ else:
+ logger.exception("failed to get tenant id, obj: %s", obj)
+ return False
+
+ username = request.user.username
+ cur_tenant_id = request.user.get_property("tenant_id")
+ if action == PermAction.MANAGE_PLATFORM:
+ return is_super_manager(tenant_id, username)
+ if action == PermAction.MANAGE_TENANT:
+ return is_tenant_manager(tenant_id, username)
+ if action == PermAction.USE_PLATFORM:
+ # 当前平台使用(普通用户)能编辑的资源只有 TenantUser
+ if not isinstance(obj, TenantUser):
+ return False
+
+ return is_same_nature_user(obj.id, cur_tenant_id, username)
+
+ return False
+
+ return Permission
+
+
+def is_super_manager(tenant_id: str, username: str) -> bool:
+ """默认租户的管理员,有管理平台的权限(超级管理员)"""
+ tenant = Tenant.objects.get(id=tenant_id)
+ if not tenant.is_default:
+ return False
+
+ return TenantManager.objects.filter(tenant=tenant, tenant_user_id=username).exists()
+
+
+def is_tenant_manager(tenant_id: str, username: str) -> bool:
+ """本租户的管理员,拥有管理当前租户配置的权限"""
+ tenant = Tenant.objects.get(id=tenant_id)
+ return TenantManager.objects.filter(tenant=tenant, tenant_user_id=username).exists()
+
+
+def is_same_nature_user(req_username: str, cur_tenant_id: str, username: str) -> bool:
+ """判断是否同一自然人(可以跨租户访问属于同一自然人/数据源用户的数据)
+
+ :param req_username: 待访问租户用户名
+ :param cur_tenant_id: 当前用户的租户 ID
+ :param username: 当前用户的用户名
+ """
+ cur_tenant_user = TenantUser.objects.filter(tenant_id=cur_tenant_id, id=username).first()
+ # 当前登录的,连租户用户都不是,自然不是自然人
+ if not cur_tenant_user:
+ return False
+
+ # 数据用户源若绑定到自然人,只可能有一条记录
+ relation = DataSourceUserNaturalUserRelation.objects.filter(
+ data_source_user=cur_tenant_user.data_source_user
+ ).first()
+
+ # 如果没有绑定自然人,则将同一数据源用户关联的租户用户,都视作一个自然人
+ if not relation:
+ return TenantUser.objects.filter(id=req_username, data_source_user=cur_tenant_user.data_source_user).exists()
+
+ data_source_user_ids = DataSourceUserNaturalUserRelation.objects.filter(
+ natural_user=relation.natural_user
+ ).values_list("data_source_user_id", flat=True)
+ return TenantUser.objects.filter(id=req_username, data_source_user__in=data_source_user_ids).exists()
+
+
+def get_user_role(tenant_id: str, username: str) -> UserRole:
+ """获取用户角色,因目前超级管理员必定是租户管理员,租户管理员必定是普通用户,因此返回最高级的角色即可"""
+ tenant = Tenant.objects.get(id=tenant_id)
+
+ if TenantManager.objects.filter(tenant=tenant, tenant_user_id=username).exists():
+ if tenant.is_default:
+ return UserRole.SUPER_MANAGER
+
+ return UserRole.TENANT_MANAGER
+
+ return UserRole.NATURAL_USER
diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py
index 1077573f5..7db62f529 100644
--- a/src/bk-user/bkuser/settings.py
+++ b/src/bk-user/bkuser/settings.py
@@ -55,6 +55,7 @@
"bkuser.apps.sync",
"bkuser.apps.idp",
"bkuser.apps.natural_user",
+ "bkuser.apps.permission",
]
MIDDLEWARE = [
diff --git a/src/bk-user/pyproject.toml b/src/bk-user/pyproject.toml
index 02f08f905..4af980417 100644
--- a/src/bk-user/pyproject.toml
+++ b/src/bk-user/pyproject.toml
@@ -158,7 +158,7 @@ layers = [
name = "Apps Layers contract"
type = "layers"
layers = [
- "bkuser.apps.sync",
+ "bkuser.apps.sync | bkuser.apps.permission",
"bkuser.apps.tenant",
"bkuser.apps.idp",
"bkuser.apps.data_source",
diff --git a/src/bk-user/tests/conftest.py b/src/bk-user/tests/conftest.py
index 58371c2f3..c0fa7c675 100644
--- a/src/bk-user/tests/conftest.py
+++ b/src/bk-user/tests/conftest.py
@@ -43,4 +43,4 @@ def random_tenant() -> Tenant:
@pytest.fixture()
def bk_user(default_tenant) -> User:
"""生成随机用户"""
- return create_user()
+ return create_user(default_tenant)
diff --git a/src/bk-user/tests/test_utils/auth.py b/src/bk-user/tests/test_utils/auth.py
index a63c1d2bd..ecbf8f1c5 100644
--- a/src/bk-user/tests/test_utils/auth.py
+++ b/src/bk-user/tests/test_utils/auth.py
@@ -12,20 +12,19 @@
from typing import Optional
from bkuser.apps.data_source.models import DataSource, DataSourceUser
-from bkuser.apps.tenant.models import TenantUser
+from bkuser.apps.tenant.models import Tenant, TenantManager, TenantUser
from bkuser.auth.models import User
from tests.test_utils.helpers import generate_random_string
-from tests.test_utils.tenant import DEFAULT_TENANT
-def create_user(username: Optional[str] = None) -> User:
+def create_user(tenant: Tenant, username: Optional[str] = None) -> User:
"""创建测试用用户"""
username = username or generate_random_string(length=8)
user, _ = User.objects.get_or_create(username=username)
- user.set_property("tenant_id", DEFAULT_TENANT)
+ user.set_property("tenant_id", tenant.id)
# 获取租户默认的本地数据源
- data_source = DataSource.objects.get(owner_tenant_id=DEFAULT_TENANT, name=f"{DEFAULT_TENANT}-default-local")
+ data_source = DataSource.objects.get(owner_tenant_id=tenant.id, name=f"{tenant.id}-default-local")
data_source_user, _ = DataSourceUser.objects.get_or_create(
username=username,
@@ -37,8 +36,13 @@ def create_user(username: Optional[str] = None) -> User:
},
)
- TenantUser.objects.get_or_create(
- tenant_id=DEFAULT_TENANT, id=username, data_source=data_source, data_source_user=data_source_user
+ tenant_user, _ = TenantUser.objects.get_or_create(
+ tenant=tenant,
+ id=username,
+ data_source=data_source,
+ data_source_user=data_source_user,
)
+ TenantManager.objects.get_or_create(tenant=tenant, tenant_user=tenant_user)
+
return user