forked from TencentBlueKing/bk-user
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
543 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. | ||
Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. | ||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at http://opensource.org/licenses/MIT | ||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
specific language governing permissions and limitations under the License. | ||
""" | ||
|
||
import base64 | ||
import logging | ||
|
||
import jwt | ||
from django.conf import settings | ||
from django.contrib.auth import get_user_model | ||
from rest_framework import exceptions | ||
from rest_framework.authentication import BaseAuthentication | ||
|
||
from bkuser.common.cache import cachedmethod | ||
from bkuser.common.constants import BKNonEntityUser | ||
from bkuser.component import esb | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class ESBAuthentication(BaseAuthentication): | ||
www_authenticate_realm = "api" | ||
|
||
def authenticate(self, request): | ||
credentials = self.get_credentials(request) | ||
|
||
if not credentials: | ||
return None | ||
|
||
verified, payload = self.verify_credentials(credentials=credentials) | ||
if not verified: | ||
return None | ||
|
||
username = self._get_username_from_jwt_payload(payload) | ||
app_code = self._get_app_code_from_jwt_payload(payload) | ||
|
||
request.bk_app_code = app_code # 获取到调用 app_code | ||
|
||
return self._get_or_create_user(username), None | ||
|
||
def authenticate_header(self, request): | ||
return '{0} realm="{1}"'.format("Bearer", self.www_authenticate_realm) | ||
|
||
def get_credentials(self, request): | ||
credentials = { | ||
"jwt": request.META.get("HTTP_X_BKAPI_JWT"), | ||
"from": request.META.get("HTTP_X_BKAPI_FROM", "esb"), | ||
} | ||
# Return None if some are empty | ||
if all(credentials.values()): | ||
return credentials | ||
|
||
return None | ||
|
||
def verify_credentials(self, credentials): | ||
public_key = self._get_jwt_public_key(credentials["from"]) | ||
# Note: 不从 jwt header 里取 kid 判断是网关还是 ESB 签发的,在不同环境可能不准确 | ||
jwt_payload = self._decode_jwt(credentials["jwt"], public_key) | ||
if not jwt_payload: | ||
return False, None | ||
|
||
return True, jwt_payload | ||
|
||
def _decode_jwt(self, content, public_key): | ||
try: | ||
jwt_header = jwt.get_unverified_header(content) | ||
algorithm = jwt_header.get("alg") or "RS512" | ||
return jwt.decode(content, public_key, algorithms=[algorithm], options={"verify_iss": False}) | ||
except Exception: # pylint: disable=broad-except | ||
logger.exception("decode jwt fail, jwt: %s", content) | ||
return None | ||
|
||
def _get_username_from_jwt_payload(self, jwt_payload): | ||
"""从 jwt payload 里获取 username""" | ||
user = jwt_payload.get("user", {}) | ||
verified = user.get("verified", False) | ||
username = user.get("bk_username", "") or user.get("username", "") | ||
# 如果 user 通过认证,则为实体用户,直接返回 | ||
if verified: | ||
return username | ||
|
||
# 未通过认证有两种可能,(1)username 不可信任(2)username 为空 | ||
# 非空则说明是未认证,不可信任的用户,则统一用不可信任的用户名代替,不使用传递过来的 username | ||
if username: | ||
return BKNonEntityUser.BK__UNVERIFIED_USER.value | ||
|
||
# 匿名用户 | ||
return BKNonEntityUser.BK__ANONYMOUS_USER.value | ||
|
||
def _get_app_code_from_jwt_payload(self, jwt_payload): | ||
"""从 jwt payload 里获取 app_code""" | ||
app = jwt_payload.get("app", {}) | ||
|
||
if not app.get("verified", False): | ||
raise exceptions.AuthenticationFailed("app is not verified") | ||
|
||
# 兼容多版本(企业版/TE版/社区版) 以及兼容 APIGW / ESB | ||
app_code = app.get("bk_app_code", "") or app.get("app_code", "") | ||
|
||
# 虽然 app_code 为空对于后续的鉴权一定是不通过的,但鉴权不通过有很多原因,这里提前log便于问题排查 | ||
if not app_code: | ||
raise exceptions.AuthenticationFailed("could not get app_code from esb/apigateway jwt payload! it's empty") | ||
|
||
return app_code | ||
|
||
def _get_or_create_user(self, username): | ||
user_model = get_user_model() | ||
user, _ = user_model.objects.get_or_create( | ||
username=username, defaults={"is_active": True, "is_staff": False, "is_superuser": False} | ||
) | ||
return user | ||
|
||
def _get_apigw_public_key(self): | ||
""" | ||
获取 APIGW 的 Public Key | ||
由于配置文件里的 public key 是来自环境变量,且使用 base64 编码,因此需要解码 | ||
""" | ||
# 如果 BK_APIGW_PUBLIC_KEY 为空,则直接报错 | ||
if not settings.BK_APIGW_PUBLIC_KEY: | ||
logger.error("BK_APIGW_PUBLIC_KEY can not be empty") | ||
return "" | ||
|
||
try: | ||
public_key = base64.b64decode(settings.BK_APIGW_PUBLIC_KEY).decode("utf-8") | ||
except Exception: # pylint: disable=broad-except | ||
logger.exception("BK_APIGW_PUBLIC_KEY is not the base64 string, base64.b64decode fail") | ||
return "" | ||
|
||
return public_key | ||
|
||
@cachedmethod(timeout=None) # 缓存不过期,除非重新部署服务 | ||
def _get_jwt_public_key(self, request_from): | ||
# TODO 理论上 open_v2 只接 ESB,open_v3 只接 APIGW,后续新增 open_v3 后可以分离该 Auth 类逻辑 | ||
if request_from == "apigw": | ||
return self._get_apigw_public_key() | ||
|
||
return esb.get_api_public_key()["public_key"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. | ||
Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. | ||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at http://opensource.org/licenses/MIT | ||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
specific language governing permissions and limitations under the License. | ||
""" | ||
|
||
from functools import cached_property | ||
|
||
from bkuser.apps.tenant.models import Tenant | ||
|
||
|
||
class DefaultTenantMixin: | ||
"""默认租户 Mixin""" | ||
|
||
@cached_property | ||
def default_tenant(self) -> Tenant: | ||
return Tenant.objects.filter(is_default=True).first() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. | ||
Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. | ||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at http://opensource.org/licenses/MIT | ||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
specific language governing permissions and limitations under the License. | ||
""" | ||
|
||
from rest_framework.permissions import BasePermission | ||
|
||
|
||
class IsAllowedAppCode(BasePermission): | ||
""" | ||
仅允许配置好的 AppCode | ||
需要配合 ESBAuthentication 一起使用 | ||
""" | ||
|
||
def has_permission(self, request, view): | ||
""" | ||
目前只允许桌面访问 | ||
桌面 AppCode: https://github.com/TencentBlueKing/blueking-console/blob/master/backend/components/esb.py#L35 | ||
""" | ||
return hasattr(request, "bk_app_code") and request.bk_app_code in ["bk_paas"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. | ||
Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. | ||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at http://opensource.org/licenses/MIT | ||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
specific language governing permissions and limitations under the License. | ||
""" | ||
|
||
from rest_framework import status | ||
from rest_framework.renderers import JSONRenderer | ||
|
||
|
||
class BkLegacyApiJSONRenderer(JSONRenderer): | ||
"""蓝鲸历史版本 API Json 响应格式化""" | ||
|
||
format = "bk_legacy_json" | ||
|
||
def render(self, data, accepted_media_type=None, renderer_context=None): | ||
# Wrap response data on demand | ||
resp = renderer_context["response"] | ||
if status.is_success(resp.status_code): | ||
data = {"result": True, "code": 0, "message": "", "data": data} | ||
elif status.is_client_error(resp.status_code) or status.is_server_error(resp.status_code): | ||
data = {"result": False, "code": -1, "message": data["message"], "data": {}} | ||
|
||
# For status codes other than (2xx, 4xx, 5xx), do not wrap data | ||
return super().render(data, accepted_media_type=None, renderer_context=None) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. | ||
Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. | ||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at http://opensource.org/licenses/MIT | ||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
specific language governing permissions and limitations under the License. | ||
""" | ||
|
||
from rest_framework import serializers | ||
|
||
from bkuser.common.constants import TIME_ZONE_CHOICES | ||
|
||
|
||
class ProfileUpdateInputSLZ(serializers.Serializer): | ||
username = serializers.CharField(help_text="用户名") | ||
# Note: 只兼容允许修改 language、time_zone、wx_userid 字段 | ||
# display_name = serializers.CharField(help_text="姓名", required=False) | ||
# telephone = serializers.CharField( | ||
# help_text="手机号,仅支持中国大陆", | ||
# required=False, | ||
# min_length=11, | ||
# max_length=11, | ||
# ) | ||
# email = serializers.EmailField(help_text="邮箱", required=False) | ||
language = serializers.ChoiceField(help_text="语言", required=False, choices=["zh-cn", "en"]) | ||
time_zone = serializers.ChoiceField(help_text="时区", required=False, choices=TIME_ZONE_CHOICES) | ||
wx_userid = serializers.CharField(help_text="绑定的微信消息通知的账号 ID", required=False, allow_blank=True) | ||
|
||
|
||
class ProfileBatchQueryInputSLZ(serializers.Serializer): | ||
username_list = serializers.ListField(child=serializers.CharField(help_text="用户名"), max_length=100) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. | ||
Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. | ||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at http://opensource.org/licenses/MIT | ||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
specific language governing permissions and limitations under the License. | ||
""" | ||
|
||
from django.urls import path | ||
|
||
from . import views | ||
|
||
urlpatterns = [ | ||
path("login/profile/", views.ProfileUpdateApi.as_view(), name="open_v1.update_profile"), | ||
path("login/profile/query/", views.ProfileBatchQueryApi.as_view(), name="open_v1.query_profile"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. | ||
Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved. | ||
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at http://opensource.org/licenses/MIT | ||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
specific language governing permissions and limitations under the License. | ||
""" | ||
|
||
import phonenumbers | ||
from django.db.models import Q | ||
from django.http import Http404 | ||
from rest_framework import generics, status | ||
from rest_framework.permissions import IsAuthenticated | ||
from rest_framework.response import Response | ||
|
||
from bkuser.apps.data_source.constants import DataSourceTypeEnum | ||
from bkuser.apps.tenant.models import TenantUser | ||
|
||
from .authentications import ESBAuthentication | ||
from .mixins import DefaultTenantMixin | ||
from .permissions import IsAllowedAppCode | ||
from .renderers import BkLegacyApiJSONRenderer | ||
from .serializers import ProfileBatchQueryInputSLZ, ProfileUpdateInputSLZ | ||
|
||
|
||
class ProfileUpdateApi(DefaultTenantMixin, generics.GenericAPIView): | ||
authentication_classes = [ESBAuthentication] | ||
permission_classes = [IsAuthenticated, IsAllowedAppCode] | ||
renderer_classes = [BkLegacyApiJSONRenderer] | ||
|
||
def post(self, request, *args, **kwargs): | ||
""" | ||
更新用户信息 | ||
Note: | ||
1)在 2.x 中,该接口是 upsert, 而 3.x 中只是 update, 不允许新建用户 | ||
2)在 2.x 中,调用方是 登录 & 桌面,3.x 调整为仅桌面可调用 | ||
""" | ||
slz = ProfileUpdateInputSLZ(data=request.data) | ||
slz.is_valid(raise_exception=True) | ||
data = slz.validated_data | ||
|
||
tenant_user = TenantUser.objects.filter( | ||
Q(id=data["username"]), | ||
Q(tenant=self.default_tenant), | ||
Q(data_source__type=DataSourceTypeEnum.REAL) | ||
| Q(data_source__owner_tenant_id=self.default_tenant.id, data_source__type=DataSourceTypeEnum.VIRTUAL), | ||
).first() | ||
if not tenant_user: | ||
raise Http404(f"user username:{data['username']} not found") | ||
|
||
# 有传入字段参数则更新 | ||
update_fields = [] | ||
for field in ["language", "time_zone", "wx_userid"]: | ||
if field in data: | ||
setattr(tenant_user, field, data[field]) | ||
update_fields.append(field) | ||
|
||
if update_fields: | ||
update_fields.append("updated_at") | ||
tenant_user.save(update_fields=update_fields) | ||
|
||
# Note: 由于调用方是判断非 200 即为异常,所以虽然是更新操作,但是兼容接口不可以返回 204,只能是 200 | ||
return Response(status=status.HTTP_200_OK) | ||
|
||
|
||
class ProfileBatchQueryApi(DefaultTenantMixin, generics.GenericAPIView): | ||
authentication_classes = [ESBAuthentication] | ||
permission_classes = [IsAuthenticated, IsAllowedAppCode] | ||
renderer_classes = [BkLegacyApiJSONRenderer] | ||
|
||
def post(self, request, *args, **kwargs): | ||
""" | ||
根据 username 列表, 批量查询用户信息 | ||
""" | ||
slz = ProfileBatchQueryInputSLZ(data=request.data) | ||
slz.is_valid(raise_exception=True) | ||
data = slz.validated_data | ||
|
||
tenant_users = TenantUser.objects.filter( | ||
Q(id__in=data["username_list"]), | ||
Q(tenant=self.default_tenant), | ||
Q(data_source__type=DataSourceTypeEnum.REAL) | ||
| Q(data_source__owner_tenant_id=self.default_tenant.id, data_source__type=DataSourceTypeEnum.VIRTUAL), | ||
).select_related("data_source_user") | ||
|
||
user_infos = [] | ||
for u in tenant_users: | ||
# 手机号和手机区号 | ||
phone, phone_country_code = u.phone_info | ||
user_infos.append( | ||
{ | ||
# 租户用户 ID 即为对外的 username / bk_username | ||
"username": u.id, | ||
"chname": u.data_source_user.full_name, | ||
"display_name": u.data_source_user.full_name, | ||
"email": u.email, | ||
"phone": phone, | ||
"iso_code": self._phone_country_code_to_iso_code(phone_country_code), | ||
"time_zone": u.time_zone, | ||
"language": u.language, | ||
"wx_userid": u.wx_userid, | ||
"qq": "", | ||
"role": 0, | ||
} | ||
) | ||
|
||
return Response(user_infos) | ||
|
||
@staticmethod | ||
def _phone_country_code_to_iso_code(phone_country_code: str) -> str: | ||
"""将 86 等手机国际区号 转换 CN 等 ISO 代码""" | ||
if phone_country_code and phone_country_code.isdigit(): | ||
return phonenumbers.region_code_for_country_code(int(phone_country_code)) | ||
|
||
return "" |
Oops, something went wrong.