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

feature: 页面操作密码重置,rsa加密 #818

Merged
merged 6 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 0 additions & 1 deletion src/api/bkuser_core/api/web/category/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ def get_queryset(self):
category = get_category(category_id)
namespace = self.kwargs["namespace"]
metas = list_setting_metas(category.type, None, namespace)

return Setting.objects.filter(meta__in=metas, category_id=category_id)

def post(self, request, *args, **kwargs):
Expand Down
14 changes: 14 additions & 0 deletions src/api/bkuser_core/api/web/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# -*- 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.
"""

EXCLUDE_SETTINGS_KEYS = ["rsa_private_key"]

PASSWORD_NAMESPACE = "password"
4 changes: 4 additions & 0 deletions src/api/bkuser_core/api/web/password/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ class PasswordResetByTokenInputSLZ(serializers.Serializer):
class PasswordModifyInputSLZ(serializers.Serializer):
old_password = Base64OrPlainField(required=True, max_length=254)
new_password = Base64OrPlainField(required=True, max_length=254)


class PasswordSettingByTokenInputSLZ(serializers.Serializer):
neronkl marked this conversation as resolved.
Show resolved Hide resolved
token = serializers.CharField(required=True, max_length=254)
5 changes: 5 additions & 0 deletions src/api/bkuser_core/api/web/password/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@
views.PasswordModifyApi.as_view(),
name="password.modify",
),
path(
"settings/by_token/",
views.PasswordSettingsByTokenApi.as_view(),
neronkl marked this conversation as resolved.
Show resolved Hide resolved
name="password.get_settings.by_token",
),
]
52 changes: 48 additions & 4 deletions src/api/bkuser_core/api/web/password/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,21 @@
from rest_framework import generics, status
from rest_framework.response import Response

from .serializers import PasswordModifyInputSLZ, PasswordResetByTokenInputSLZ, PasswordResetSendEmailInputSLZ
from bkuser_core.api.web.utils import get_operator, validate_password
from bkuser_core.api.web.category.serializers import CategorySettingOutputSLZ
from bkuser_core.api.web.constants import PASSWORD_NAMESPACE
from bkuser_core.api.web.password.serializers import (
PasswordModifyInputSLZ,
PasswordResetByTokenInputSLZ,
PasswordResetSendEmailInputSLZ,
PasswordSettingByTokenInputSLZ,
)
from bkuser_core.api.web.utils import (
decrypt_rsa_password,
get_category,
get_operator,
list_setting_metas,
validate_password,
)
from bkuser_core.audit.constants import OperationType
from bkuser_core.audit.utils import create_general_log
from bkuser_core.categories.models import ProfileCategory
Expand All @@ -27,6 +40,7 @@
from bkuser_core.profiles.signals import post_profile_update
from bkuser_core.profiles.tasks import send_password_by_email
from bkuser_core.profiles.utils import parse_username_domain
from bkuser_core.user_settings.models import Setting

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -82,8 +96,10 @@ def post(self, request, *args, **kwargs):
raise error_codes.PROFILE_TOKEN_EXPIRED

profile = token_holder.profile
validate_password(profile, pending_password)
profile.password = make_password(pending_password)

decrypt_password = decrypt_rsa_password(profile.category_id, pending_password)
neronkl marked this conversation as resolved.
Show resolved Hide resolved
validate_password(profile, decrypt_password)
profile.password = make_password(decrypt_password)
profile.password_update_time = now()
profile.save()

Expand Down Expand Up @@ -143,3 +159,31 @@ def post(self, request, *args, **kwargs):
extra_values=modify_summary,
)
return Response(status=status.HTTP_200_OK)


class PasswordSettingsByTokenApi(generics.ListAPIView):
serializer_class = CategorySettingOutputSLZ

def get(self, request, *args, **kwargs):
slz = PasswordSettingByTokenInputSLZ(data=request.query_params)
slz.is_valid(raise_exception=True)

data = slz.validated_data
token = data["token"]

try:
token_holder = ProfileTokenHolder.objects.get(token=token, enabled=True)
except ProfileTokenHolder.DoesNotExist:
logger.info("token<%s> not exist in db", token)
raise error_codes.CANNOT_GET_TOKEN_HOLDER

if token_holder.expired:
raise error_codes.PROFILE_TOKEN_EXPIRED
neronkl marked this conversation as resolved.
Show resolved Hide resolved

profile = token_holder.profile

category = get_category(profile.category_id)
metas = list_setting_metas(category.type, None, PASSWORD_NAMESPACE)
settings = Setting.objects.filter(meta__in=metas, category_id=profile.category_id)

return Response(self.serializer_class(settings, many=True).data)
6 changes: 5 additions & 1 deletion src/api/bkuser_core/api/web/profile/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from rest_framework import serializers

from bkuser_core.api.web.serializers import StringArrayField
from bkuser_core.api.web.utils import get_default_category_id
from bkuser_core.api.web.utils import decrypt_rsa_password, get_default_category_id
from bkuser_core.profiles.models import Profile
from bkuser_core.profiles.validators import validate_username

Expand Down Expand Up @@ -105,12 +105,16 @@ class ProfileSearchOutputSLZ(serializers.Serializer):
class ProfileUpdateInputSLZ(serializers.ModelSerializer):
leader = serializers.ListField(child=serializers.IntegerField(), required=False)
departments = serializers.ListField(child=serializers.IntegerField(), required=False)
password = serializers.CharField(required=False, write_only=True)

class Meta:
model = Profile
# NOTE: 相对原来的api区别, 不支持extras/create_time/update_time更新
exclude = ["category_id", "username", "domain", "extras", "create_time", "update_time"]

def validate_password(self, password):
return decrypt_rsa_password(self.instance.category_id, password)

neronkl marked this conversation as resolved.
Show resolved Hide resolved

class ProfileCreateInputSLZ(serializers.ModelSerializer):
category_id = serializers.IntegerField(required=False)
Expand Down
15 changes: 14 additions & 1 deletion src/api/bkuser_core/api/web/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
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 base64
import logging
from typing import Dict, Union

from django.conf import settings

from bkuser_core.api.web.constants import EXCLUDE_SETTINGS_KEYS
from bkuser_core.categories.models import ProfileCategory
from bkuser_core.common.error_codes import error_codes
from bkuser_core.departments.models import Department
Expand All @@ -23,6 +25,7 @@
from bkuser_core.user_settings.exceptions import SettingHasBeenDisabledError
from bkuser_core.user_settings.loader import ConfigProvider
from bkuser_core.user_settings.models import SettingMeta
from bkuser_global.crypt import rsa_decrypt_password

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -82,7 +85,7 @@ def list_setting_metas(category_type: str, region: str, namespace: str) -> list:
"""
List setting metas.
"""
queryset = SettingMeta.objects.filter(category_type=category_type)
queryset = SettingMeta.objects.filter(category_type=category_type).exclude(key__in=EXCLUDE_SETTINGS_KEYS)
neronkl marked this conversation as resolved.
Show resolved Hide resolved
if region:
queryset = queryset.filter(region=region)
if namespace:
Expand Down Expand Up @@ -152,3 +155,13 @@ def get_extras_with_default_values(extras_from_db: Union[dict, list]) -> dict:

extras.update(formatted_extras)
return extras


neronkl marked this conversation as resolved.
Show resolved Hide resolved
def decrypt_rsa_password(category_id: int, encrypt_password: str) -> str:
neronkl marked this conversation as resolved.
Show resolved Hide resolved
config_loader = ConfigProvider(category_id=category_id)
enable_pwd_rsa_encrypted = config_loader.get("enable_pwd_rsa_encrypted")
neronkl marked this conversation as resolved.
Show resolved Hide resolved
# 未开启,或者未配置rsa
if not enable_pwd_rsa_encrypted:
return encrypt_password
pwd_rsa_private_key = base64.b64decode(config_loader["rsa_private_key"]).decode()
return rsa_decrypt_password(encrypt_password, pwd_rsa_private_key)
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
neronkl marked this conversation as resolved.
Show resolved Hide resolved
"""
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 base64
import logging
import traceback

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric import rsa as crypto_rsa
from django.core.management.base import BaseCommand
from django.db import transaction

from bkuser_core.categories.constants import CategoryType
from bkuser_core.categories.models import ProfileCategory
from bkuser_core.user_settings.models import Setting, SettingMeta

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = "enable category rsa"

def add_arguments(self, parser):
parser.add_argument("--category_id", type=str, help="目录ID", required=True)
parser.add_argument("--random_flag", type=bool, default=True, help="是否随机生成")
parser.add_argument("--key_length", type=int, default=1024, help="随机密钥对的长度")
parser.add_argument("--private_key_file", type=str, default="", help="rsa私钥pem文件目录")
parser.add_argument("--public_key_file", type=str, default="", help="rsa公钥pem文件目录")

def validate_rsa_secret(self, private_key_content: bytes, public_key_content: bytes) -> bool:
testing_msg = "Hello World !!!"

private_key = serialization.load_pem_private_key(private_key_content, password=None, backend=default_backend())
public_key = serialization.load_pem_public_key(public_key_content, backend=default_backend())

common_condition = padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None
)

# 先公钥加密
encrypt_msg = public_key.encrypt(testing_msg.encode(), common_condition)

# 解密
decrypt_msg = private_key.decrypt(encrypt_msg, common_condition)
result_msg = decrypt_msg.decode()

if result_msg != testing_msg:
return False
return True

def create_rsa_secret(self, options: dict):
random_flag = options.get("random_flag")
if not random_flag:
# read the private_key and public key from the file
private_key_file = options.get("private_key_file")
public_key_file = options.get("public_key_file")
with open(private_key_file, "rb") as private_file:
private_key = private_file.read()

with open(public_key_file, "rb") as public_file:
public_key = public_file.read()

if not self.validate_rsa_secret(private_key, private_key):
self.stdout.write("These pem files do not matching")
raise Exception
else:
neronkl marked this conversation as resolved.
Show resolved Hide resolved
self.stdout.write("Private key and public key are creating randomly")
key_length = options.get("key_length")
# 随机生成rsa 秘钥对
private_key_origin = crypto_rsa.generate_private_key(
public_exponent=65537, key_size=key_length, backend=default_backend()
)
public_key_origin = private_key_origin.public_key()

private_key = private_key_origin.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)

public_key = public_key_origin.public_bytes(
encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# base64加密入库
public_key = base64.b64encode(public_key).decode()
private_key = base64.b64encode(private_key).decode()

return public_key, private_key

def handle(self, *args, **options):
category_id = options.get("category_id")
self.stdout.write(f"enable category rsa: category_id={str(category_id)}")

try:
neronkl marked this conversation as resolved.
Show resolved Hide resolved
public_key, private_key = self.create_rsa_secret(options)
category = ProfileCategory.objects.get(id=category_id)
if category.type != CategoryType.LOCAL.value:
self.stdout.write("Rsa setting only support the local category, please check your input")
return

rsa_settings_filters = {
"enable_pwd_rsa_encrypted": True,
"rsa_private_key": private_key,
"rsa_public_key": public_key,
}

meta_combo = {}
for key, value in rsa_settings_filters.items():
meta = SettingMeta.objects.get(key=key)
meta_combo[meta] = value

# 新增或更新该目录的user_setting设置:rsa配置
with transaction.atomic():
rsa_settings = []
for meta, value in meta_combo.items():
instance, _ = Setting.objects.get_or_create(meta=meta, category_id=category.id)
instance.value = value
rsa_settings.append(instance)
Setting.objects.bulk_update(rsa_settings, ["value"])

self.stdout.write(f"Category {category_id} Enable rsa successfully")

except ProfileCategory.DoesNotExist:
self.stdout.write(f"Category is not exist( category_id={category_id} ), please check your input.")
return

except Exception as e:
self.stdout.write(traceback.format_exc())
self.stdout.write(f"Enable rsa failed: {e}")
return
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# -*- 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 __future__ import unicode_literals

from bkuser_core.categories.constants import CategoryType
from bkuser_core.user_settings.constants import SettingsEnableNamespaces
from django.db import migrations


def forwards_func(apps, schema_editor):
"""更新默认目录 密码-rsa加密模板配置"""
SettingMeta = apps.get_model("user_settings", "SettingMeta")

local_password_rsa_settings = [
dict(
key="enable_pwd_rsa_encrypted",
default=False,
example=False
),
dict(
key="rsa_private_key",
default="",
example="LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpkZnNkZmRzZgotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo="
),
dict(
key="rsa_public_key",
default="",
example="LS0tLS1CRUdJTiBSU0EgUFVCTElDS0VZLS0tLS0KZXJ0ZXJ0cmV0Ci0tLS0tRU5EIFJTQSBQVUJMSUNLRVktLS0tLQo="
)
]
neronkl marked this conversation as resolved.
Show resolved Hide resolved

for x in local_password_rsa_settings:

meta, _ = SettingMeta.objects.get_or_create(
namespace=SettingsEnableNamespaces.PASSWORD.value,
category_type=CategoryType.LOCAL.value,
required=False,
**x
)


class Migration(migrations.Migration):
dependencies = [
("user_settings", "0018_alter_local_password_mail_config"),
]

operations = [migrations.RunPython(forwards_func)]
Loading