Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update local data source plugin config notification templates #1221

Merged
merged 3 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/bk-user/bkuser/apps/data_source/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# -*- 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.dispatch import receiver

from bkuser.apps.data_source.models import DataSource
from bkuser.apps.data_source.signals import post_update_data_source


@receiver(post_update_data_source)
def initial_local_data_source_user_identity_info(sender, data_source: DataSource, **kwargs):
"""
TODO (su) 数据源更新后,需要检查是否是本地数据源,若是本地数据源且启用账密登录,
则需要对没有账密信息的用户,进行密码的初始化 & 发送通知
"""
pass
12 changes: 10 additions & 2 deletions src/bk-user/bkuser/apps/data_source/plugins/local/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
# 可选最长锁定时间:10年
MAX_LOCK_TIME = 10 * 365 * ONE_DAY_SECONDS

# 连续性限制上下限
MIN_NOT_CONTINUOUS_COUNT = 5
# 连续性限制上限
MAX_NOT_CONTINUOUS_COUNT = 10

# 重试密码次数上限
Expand All @@ -50,3 +49,12 @@ class NotificationMethod(str, StructuredEnum):

EMAIL = EnumField("email", label=_("邮件通知"))
SMS = EnumField("sms", label=_("短信通知"))


class NotificationScene(str, StructuredEnum):
"""通知场景"""

USER_INITIALIZE = EnumField("user_initialize", label=_("用户初始化"))
RESET_PASSWORD = EnumField("reset_password", label=_("重置密码"))
PASSWORD_EXPIRING = EnumField("password_expiring", label=_("密码即将过期"))
PASSWORD_EXPIRED = EnumField("password_expired", label=_("密码过期"))
62 changes: 52 additions & 10 deletions src/bk-user/bkuser/apps/data_source/plugins/local/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
MAX_PASSWORD_LENGTH,
MAX_PASSWORD_VALID_TIME,
MAX_RESERVED_PREVIOUS_PASSWORD_COUNT,
MIN_NOT_CONTINUOUS_COUNT,
NEVER_EXPIRE_TIME,
PASSWORD_MAX_RETRIES,
NotificationMethod,
NotificationScene,
PasswordGenerateMethod,
)
from bkuser.common.passwd import PasswordRule, PasswordValidator
from bkuser.common.passwd import PasswordGenerateError, PasswordGenerator, PasswordRule, PasswordValidator
from bkuser.utils.pydantic import stringify_pydantic_error


Expand All @@ -47,7 +47,7 @@ class PasswordRuleConfig(BaseModel):

# --- 连续性限制类 ---
# 不允许连续出现位数
not_continuous_count: int = Field(ge=MIN_NOT_CONTINUOUS_COUNT, le=MAX_NOT_CONTINUOUS_COUNT)
not_continuous_count: int = Field(default=0, ge=0, le=MAX_NOT_CONTINUOUS_COUNT)
# 不允许键盘序
not_keyboard_order: bool
# 不允许连续字母序
Expand Down Expand Up @@ -84,12 +84,40 @@ def to_rule(self) -> PasswordRule:
)


class NotificationTemplate(BaseModel):
"""通知模板"""

# 通知方式 如短信,邮件
method: NotificationMethod
# 通知场景 如将过期,已过期
scene: NotificationScene
# 模板标题
title: Optional[str] = None
# 模板发送方
sender: str
# 模板内容(text)格式
content: str
# 模板内容(html)格式
content_html: Optional[str] = None

@model_validator(mode="after")
def validate_attrs(self) -> "NotificationTemplate":
if self.method == NotificationMethod.EMAIL:
if not self.title:
raise ValueError(_("邮件通知模板需要提供标题"))

if not self.content_html:
raise ValueError(_("邮件通知模板需要提供 HTML 格式内容"))

return self


class NotificationConfig(BaseModel):
"""通知相关配置"""

methods: List[NotificationMethod]
enabled_methods: List[NotificationMethod]
# 通知模板
template: str
templates: List[NotificationTemplate]


class PasswordInitialConfig(BaseModel):
Expand Down Expand Up @@ -122,17 +150,25 @@ class LocalDataSourcePluginConfig(BaseModel):
"""本地数据源插件配置"""

# 是否允许使用账密登录
enable_login_by_password: bool
enable_account_password_login: bool
# 密码生成规则
password_rule: PasswordRuleConfig
password_rule: Optional[PasswordRuleConfig] = None
# 密码初始化/修改规则
password_initial: PasswordInitialConfig
password_initial: Optional[PasswordInitialConfig] = None
# 密码到期规则
password_expire: PasswordExpireConfig
password_expire: Optional[PasswordExpireConfig] = None

@model_validator(mode="after")
def validate_attrs(self) -> "LocalDataSourcePluginConfig":
"""插件配置合法性检查"""
# 如果没有开启账密登录,则不需要检查配置
if not self.enable_account_password_login:
return self

# 若启用账密登录,则各字段都需要配置上
if not (self.password_rule and self.password_initial and self.password_expire):
raise ValueError(_("密码生成规则、初始密码设置、密码到期设置均不能为空"))

try:
rule = self.password_rule.to_rule()
except ValidationError as e:
Expand All @@ -146,6 +182,12 @@ def validate_attrs(self) -> "LocalDataSourcePluginConfig":
# 若配置固定密码,则需要检查是否符合定义的密码强度规则
ret = PasswordValidator(rule).validate(self.password_initial.fixed_password)
if not ret.ok:
raise ValueError("固定密码的值不符合密码规则:{}".format(ret.exception_message))
raise ValueError(_("固定密码的值不符合密码规则:{}").format(ret.exception_message))
else:
# 随机生成密码的,校验下能否在有限次数内成功生成
try:
PasswordGenerator(rule).generate()
except PasswordGenerateError:
raise ValueError(_("无法根据预设规则生成符合条件的密码,请调整规则"))

return self
123 changes: 111 additions & 12 deletions src/bk-user/tests/apis/web/data_source/test_data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
import pytest
from bkuser.apps.data_source.constants import DataSourcePluginEnum, DataSourceStatus
from bkuser.apps.data_source.models import DataSource, DataSourcePlugin
from bkuser.apps.data_source.plugins.local.constants import NotificationMethod, PasswordGenerateMethod
from bkuser.apps.data_source.plugins.local.constants import (
NotificationMethod,
NotificationScene,
PasswordGenerateMethod,
)
from django.urls import reverse
from rest_framework import status

Expand All @@ -26,7 +30,7 @@
@pytest.fixture()
def local_ds_plugin_config() -> Dict[str, Any]:
return {
"enable_login_by_password": True,
"enable_account_password_login": True,
"password_rule": {
"min_length": 12,
"contain_lowercase": True,
Expand All @@ -49,15 +53,73 @@ def local_ds_plugin_config() -> Dict[str, Any]:
"generate_method": PasswordGenerateMethod.RANDOM,
"fixed_password": None,
"notification": {
"methods": [NotificationMethod.EMAIL, NotificationMethod.SMS],
"template": "你的密码是 xxx",
"enabled_methods": [NotificationMethod.EMAIL, NotificationMethod.SMS],
"templates": [
{
"method": NotificationMethod.EMAIL,
"scene": NotificationScene.USER_INITIALIZE,
"title": "您的账户已经成功创建",
"sender": "蓝鲸智云",
"content": "您的账户已经成功创建,请尽快修改密码",
"content_html": "<p>您的账户已经成功创建,请尽快修改密码</p>",
},
{
"method": NotificationMethod.EMAIL,
"scene": NotificationScene.RESET_PASSWORD,
"title": "登录密码重置",
"sender": "蓝鲸智云",
"content": "点击以下链接以重置代码",
"content_html": "<p>点击以下链接以重置代码</p>",
},
{
"method": NotificationMethod.SMS,
"scene": NotificationScene.USER_INITIALIZE,
"sender": "蓝鲸智云",
"content": "您的账户已经成功创建,请尽快修改密码",
},
{
"method": NotificationMethod.SMS,
"scene": NotificationScene.RESET_PASSWORD,
"sender": "蓝鲸智云",
"content": "点击以下链接以重置代码",
},
],
},
},
"password_expire": {
"remind_before_expire": [3600, 7200],
"notification": {
"methods": [NotificationMethod.EMAIL, NotificationMethod.SMS],
"template": "密码即将过期,请尽快修改",
"enabled_methods": [NotificationMethod.EMAIL, NotificationMethod.SMS],
"templates": [
{
"method": NotificationMethod.EMAIL,
"scene": NotificationScene.PASSWORD_EXPIRING,
"title": "【蓝鲸智云】密码即将到期提醒!",
"sender": "蓝鲸智云",
"content": "您的密码即将到期!",
"content_html": "<p>您的密码即将到期!</p>",
},
{
"method": NotificationMethod.EMAIL,
"scene": NotificationScene.PASSWORD_EXPIRED,
"title": "【蓝鲸智云】密码到期提醒!",
"sender": "蓝鲸智云",
"content": "点击以下链接以重置代码",
"content_html": "<p>您的密码已到期!</p>",
},
{
"method": NotificationMethod.SMS,
"scene": NotificationScene.PASSWORD_EXPIRING,
"sender": "蓝鲸智云",
"content": "您的密码即将到期!",
},
{
"method": NotificationMethod.SMS,
"scene": NotificationScene.PASSWORD_EXPIRED,
"sender": "蓝鲸智云",
"content": "您的密码已到期!",
},
],
},
},
}
Expand Down Expand Up @@ -104,6 +166,17 @@ def test_create_local_data_source(self, api_client, local_ds_plugin_config):
)
assert resp.status_code == status.HTTP_201_CREATED

def test_create_with_minimal_plugin_config(self, api_client):
resp = api_client.post(
reverse("data_source.list_create"),
data={
"name": generate_random_string(),
"plugin_id": DataSourcePluginEnum.LOCAL,
"plugin_config": {"enable_account_password_login": False},
},
)
assert resp.status_code == status.HTTP_201_CREATED

def test_create_with_not_exist_plugin(self, api_client):
resp = api_client.post(
reverse("data_source.list_create"),
Expand All @@ -124,8 +197,34 @@ def test_create_without_plugin_config(self, api_client):
assert resp.status_code == status.HTTP_400_BAD_REQUEST
assert "plugin_config: 该字段是必填项。" in resp.data["message"]

def test_create_with_broken_plugin_config(self, api_client, local_ds_plugin_config):
local_ds_plugin_config["password_initial"] = None
resp = api_client.post(
reverse("data_source.list_create"),
data={
"name": generate_random_string(),
"plugin_id": DataSourcePluginEnum.LOCAL,
"plugin_config": local_ds_plugin_config,
},
)
assert resp.status_code == status.HTTP_400_BAD_REQUEST
assert "密码生成规则、初始密码设置、密码到期设置均不能为空" in resp.data["message"]

def test_create_with_invalid_notification_template(self, api_client, local_ds_plugin_config):
local_ds_plugin_config["password_expire"]["notification"]["templates"][0]["title"] = None
resp = api_client.post(
reverse("data_source.list_create"),
data={
"name": generate_random_string(),
"plugin_id": DataSourcePluginEnum.LOCAL,
"plugin_config": local_ds_plugin_config,
},
)
assert resp.status_code == status.HTTP_400_BAD_REQUEST
assert "邮件通知模板需要提供标题" in resp.data["message"]

def test_create_with_invalid_plugin_config(self, api_client, local_ds_plugin_config):
local_ds_plugin_config.pop("enable_login_by_password")
local_ds_plugin_config.pop("enable_account_password_login")
resp = api_client.post(
reverse("data_source.list_create"),
data={
Expand All @@ -135,7 +234,7 @@ def test_create_with_invalid_plugin_config(self, api_client, local_ds_plugin_con
},
)
assert resp.status_code == status.HTTP_400_BAD_REQUEST
assert "插件配置不合法:enable_login_by_password: Field required" in resp.data["message"]
assert "插件配置不合法:enable_account_password_login: Field required" in resp.data["message"]

def test_create_without_required_field_mapping(self, api_client):
"""非本地数据源,需要字段映射配置"""
Expand Down Expand Up @@ -167,24 +266,24 @@ def test_list_other_tenant_data_source(self, api_client, random_tenant, data_sou

class TestDataSourceUpdateApi:
def test_update_local_data_source(self, api_client, data_source, local_ds_plugin_config):
local_ds_plugin_config["enable_login_by_password"] = False
local_ds_plugin_config["enable_account_password_login"] = False
resp = api_client.put(
reverse("data_source.retrieve_update", kwargs={"id": data_source.id}),
data={"plugin_config": local_ds_plugin_config},
)
assert resp.status_code == status.HTTP_204_NO_CONTENT

resp = api_client.get(reverse("data_source.retrieve_update", kwargs={"id": data_source.id}))
assert resp.data["plugin_config"]["enable_login_by_password"] is False
assert resp.data["plugin_config"]["enable_account_password_login"] is False

def test_update_with_invalid_plugin_config(self, api_client, data_source, local_ds_plugin_config):
local_ds_plugin_config.pop("enable_login_by_password")
local_ds_plugin_config.pop("enable_account_password_login")
resp = api_client.put(
reverse("data_source.retrieve_update", kwargs={"id": data_source.id}),
data={"plugin_config": local_ds_plugin_config},
)
assert resp.status_code == status.HTTP_400_BAD_REQUEST
assert "插件配置不合法:enable_login_by_password: Field required" in resp.data["message"]
assert "插件配置不合法:enable_account_password_login: Field required" in resp.data["message"]

def test_update_without_required_field_mapping(self, api_client):
"""非本地数据源,需要字段映射配置"""
Expand Down
Loading