Skip to content

Commit

Permalink
refactor(bklogin): plugin exception
Browse files Browse the repository at this point in the history
  • Loading branch information
nannan00 committed Oct 26, 2023
1 parent f70043a commit dfd9282
Show file tree
Hide file tree
Showing 11 changed files with 83 additions and 81 deletions.
2 changes: 1 addition & 1 deletion src/bk-login/bklogin/authentication/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from bklogin.common.error_codes import error_codes
from bklogin.common.response import APISuccessResponse

from .helper import BkTokenManager
from .manager import BkTokenManager


class CheckTokenApi(View):
Expand Down
1 change: 1 addition & 0 deletions src/bk-login/bklogin/authentication/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

# OpenAPI
urlpatterns += [
# FIXME: 临时兼容,OpenAPI后面接入APIGateway, 还要考虑兼容原有通过ESB和直接调用的
path("api/v1/is_login/", api_views.CheckTokenApi.as_view()),
path("api/v1/get_user/", api_views.GetUserApi.as_view()),
]
80 changes: 59 additions & 21 deletions src/bk-login/bklogin/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
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 typing import Any, Dict, List
import logging
from typing import Any, Callable, Dict, List
from urllib.parse import quote_plus, urljoin

import pydantic
from django.conf import settings
from django.http import HttpResponseNotFound, HttpResponseRedirect
from django.shortcuts import render
Expand All @@ -28,9 +30,10 @@
from bklogin.common.response import APISuccessResponse
from bklogin.idp_plugins.base import BaseCredentialIdpPlugin, BaseFederationIdpPlugin, get_plugin_cls
from bklogin.idp_plugins.constants import AllowedHttpMethodEnum, BuiltinActionEnum
from bklogin.idp_plugins.exceptions import InvalidParamError, ParseRequestBodyError, UnexpectedDataError

from .constants import ALLOWED_SIGN_IN_TENANT_USER_IDS_SESSION_KEY, REDIRECT_FIELD_NAME, SIGN_IN_TENANT_ID_SESSION_KEY
from .helper import BkTokenManager
from .manager import BkTokenManager


# 确保无论何时,响应必然有CSRFToken Cookie
Expand Down Expand Up @@ -175,6 +178,14 @@ def get(self, request, *args, **kwargs):
return APISuccessResponse(data=data)


class PluginErrorContext(pydantic.BaseModel):
"""插件异常上下文,用于打印日志时所需的上下文信息"""

idp: Idp
action: str
http_method: str


# 先对所有请求豁免CSRF校验,由dispatch里根据需要手动执行CSRF校验
@method_decorator(csrf_exempt, name="dispatch")
class IdpPluginDispatchView(View):
Expand All @@ -200,17 +211,24 @@ def dispatch(self, request, *args, **kwargs):
# (1) 获取插件
try:
plugin_cls = get_plugin_cls(idp.plugin_id)
except Exception as error:
raise error_codes.SYSTEM_ERROR.f(
except NotImplementedError as error:
raise error_codes.PLUGIN_SYSTEM_ERROR.f(
_("认证源[{}]获取插件[{}]失败, {}").format(idp.name, idp.plugin.name, error),
)

# (2)初始化插件
try:
plugin_cfg = plugin_cls.config_class(**idp.plugin_config)
plugin = plugin_cls(cfg=plugin_cfg)
except pydantic.ValidationError:
logging.exception("idp(%s) init plugin(%s) config failed", idp.id, idp.plugin.id)
# Note: 不可将error对外,因为配置信息比较敏感
raise error_codes.PLUGIN_SYSTEM_ERROR.f(
_("认证源[{}]初始化插件配置[{}]失败").format(idp.name, idp.plugin.name),
)
except Exception as error:
raise error_codes.SYSTEM_ERROR.f(
logging.exception("idp(%s) load plugin(%s) failed", idp.id, idp.plugin.id)
raise error_codes.PLUGIN_SYSTEM_ERROR.f(
_("认证源[{}]加载插件[{}]失败, {}").format(idp.name, idp.plugin.name, error),
)

Expand All @@ -226,21 +244,40 @@ def dispatch(self, request, *args, **kwargs):

return HttpResponseNotFound()

def wrap_plugin_error(self, context: PluginErrorContext, func: Callable, *func_args, **func_kwargs):
"""统一捕获插件异常"""
try:
return func(*func_args, **func_kwargs)
except ParseRequestBodyError as e:
raise error_codes.INVALID_ARGUMENT.f(str(e))
except InvalidParamError as e:
raise error_codes.VALIDATION_ERROR.f(str(e))
except UnexpectedDataError as e:
raise error_codes.UNEXPECTED_DATA_ERROR.f(str(e))
except Exception:
logging.exception(
"idp(%s) request failed, when dispatch (%s, %s) to credential idp plugin(%s)",
context.idp.id,
context.action,
context.http_method,
context.idp.plugin.id,
)
raise error_codes.PLUGIN_SYSTEM_ERROR.f(
_("认证源[{}]执行插件[{}]失败, {}").format(context.idp.name, context.idp.plugin.name),
)

def _dispatch_credential_idp_plugin(
self, plugin: BaseCredentialIdpPlugin, request, sign_in_tenant_id: str, idp: Idp, action: str, http_method: str
):
"""
身份凭证类的插件执行请求分配
"""
dispatch_cfs = (action, http_method)
plugin_error_context = PluginErrorContext(idp=idp, action=action, http_method=http_method)
# 对凭证进行认证
if dispatch_cfs == (BuiltinActionEnum.AUTHENTICATE, AllowedHttpMethodEnum.POST):
# 认证
try:
user_infos = plugin.authenticate_credentials(request)
except ValueError as error:
# FIXME: 应该定义插件类的异常,某些异常是参数错误,某些异常是系统问题, 下同
raise error_codes.VALIDATION_ERROR.f(str(error))
user_infos = self.wrap_plugin_error(plugin_error_context, plugin.authenticate_credentials, request=request)

# 使用认证源获得的用户信息,匹配认证出对应的租户用户列表
tenant_user_ids = self._auth_backend(request, sign_in_tenant_id, idp, user_infos)
Expand All @@ -249,7 +286,9 @@ def _dispatch_credential_idp_plugin(
# 身份凭证认证直接返回成功即可,由前端重定向路由到用户列表选择页面
return APISuccessResponse()

return plugin.dispatch_extension(action, http_method, request)
return self.wrap_plugin_error(
plugin_error_context, plugin.dispatch_extension, action=action, http_method=http_method, request=request
)

def _dispatch_federation_idp_plugin(
self, plugin: BaseFederationIdpPlugin, request, sign_in_tenant_id: str, idp: Idp, action: str, http_method: str
Expand All @@ -258,13 +297,13 @@ def _dispatch_federation_idp_plugin(
联邦认证类的插件执行请求分配
"""
dispatch_cfs = (action, http_method)
plugin_error_context = PluginErrorContext(idp=idp, action=action, http_method=http_method)
# 跳转到第三方登录
if dispatch_cfs == (BuiltinActionEnum.LOGIN, AllowedHttpMethodEnum.GET):
try:
callback_uri = self._get_complete_action_url(idp.id, BuiltinActionEnum.CALLBACK)
redirect_uri = plugin.build_login_uri(request, callback_uri)
except ValueError as error:
raise error_codes.VALIDATION_ERROR.f(str(error))
callback_uri = self._get_complete_action_url(idp.id, BuiltinActionEnum.CALLBACK)
redirect_uri = self.wrap_plugin_error(
plugin_error_context, plugin.build_login_uri, request=request, callback_uri=callback_uri
)

return HttpResponseRedirect(redirect_uri)

Expand All @@ -276,10 +315,7 @@ def _dispatch_federation_idp_plugin(
(BuiltinActionEnum.CALLBACK, AllowedHttpMethodEnum.POST),
]:
# 认证
try:
user_info = plugin.handle_callback(request)
except ValueError as error:
raise error_codes.VALIDATION_ERROR.f(str(error))
user_info = self.wrap_plugin_error(plugin_error_context, plugin.handle_callback, request=request)

# 使用认证源获得的用户信息,匹配认证出对应的租户用户列表
tenant_user_ids = self._auth_backend(request, sign_in_tenant_id, idp, user_info)
Expand All @@ -288,7 +324,9 @@ def _dispatch_federation_idp_plugin(
# 联邦认证则重定向到前端选择账号页面
return HttpResponseRedirect(redirect_to="pages/users")

return plugin.dispatch_extension(action, http_method, request)
return self.wrap_plugin_error(
plugin_error_context, plugin.dispatch_extension, action=action, http_method=http_method, request=request
)

def _get_complete_action_url(self, idp_id: str, action: str) -> str:
"""获取完整"""
Expand Down
6 changes: 6 additions & 0 deletions src/bk-login/bklogin/common/error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ class ErrorCodes:
status_code=HTTPStatus.NOT_FOUND,
)
VALIDATION_ERROR = ErrorCode(_("参数校验不通过"))
UNEXPECTED_DATA_ERROR = ErrorCode(_("非预期数据"))
PLUGIN_SYSTEM_ERROR = ErrorCode(
_("插件执行异常"),
code_category=ErrorCodeCategoryEnum.INTERNAL,
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
)
SYSTEM_ERROR = ErrorCode(
_("系统异常"),
code_category=ErrorCodeCategoryEnum.INTERNAL,
Expand Down
8 changes: 8 additions & 0 deletions src/idp-plugins/idp_plugins/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ class ParseRequestBodyError(IdpPluginBaseError):

class RequestAPIError(IdpPluginBaseError):
"""请求第三方接口失败"""


class InvalidParamError(IdpPluginBaseError):
"""参数非合法"""


class UnexpectedDataError(IdpPluginBaseError):
"""数据非预期"""
27 changes: 0 additions & 27 deletions src/idp-plugins/idp_plugins/local/exceptions.py

This file was deleted.

10 changes: 5 additions & 5 deletions src/idp-plugins/idp_plugins/local/implement.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from pydantic import BaseModel

from .db_models import LocalDataSourceIdentityInfo
from .exceptions import NotEnabledPasswordLoginError, UserCredentialIncorrectError, UserCredentialRequiredError
from ..exceptions import InvalidParamError, UnexpectedDataError
from ..base import BaseCredentialIdpPlugin
from ..models import TestConnectionResult
from ..tools import parse_request_body_json
Expand Down Expand Up @@ -46,14 +46,14 @@ def authenticate_credentials(self, request: HttpRequest) -> List[Dict[str, Any]]

username = request_body.get("username")
if not username:
raise UserCredentialRequiredError(_("用户名不允许为空"))
raise InvalidParamError(_("用户名不允许为空"))

password = request_body.get("password")
if not password:
raise UserCredentialRequiredError(_("密码不允许为空"))
raise InvalidParamError(_("密码不允许为空"))

if not self.cfg.data_source_ids:
raise NotEnabledPasswordLoginError(_("当前租户没有数据源允许账密登录"))
raise UnexpectedDataError(_("当前租户没有数据源允许账密登录"))

# FIXME (nan): 待用户密码功能改造完成后重新调整校验密码方式
users = LocalDataSourceIdentityInfo.objects.filter(
Expand All @@ -65,6 +65,6 @@ def authenticate_credentials(self, request: HttpRequest) -> List[Dict[str, Any]]

# 判断是否有用户匹配
if len(matched_users) == 0:
raise UserCredentialIncorrectError(_("用户名或密码不正确"))
raise InvalidParamError(_("用户名或密码不正确"))

return [{"id": i.user.id} for i in matched_users]
5 changes: 2 additions & 3 deletions src/idp-plugins/idp_plugins/wecom/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@

from django.utils.translation import gettext_lazy as _

from .exceptions import NotCorpMemberError
from .settings import WECOM_API_BASE_URL
from ..exceptions import RequestAPIError
from ..exceptions import RequestAPIError, UnexpectedDataError
from ..http import http_get

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -97,4 +96,4 @@ def get_user_id_by_code(self, code: str) -> str:
if userid:
return userid

raise NotCorpMemberError(_("非企业成员,登录认证失败"))
raise UnexpectedDataError(_("非企业成员,登录认证失败"))
23 changes: 0 additions & 23 deletions src/idp-plugins/idp_plugins/wecom/exceptions.py

This file was deleted.

2 changes: 1 addition & 1 deletion src/idp-plugins/idp_plugins/wecom/implement.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from pydantic import BaseModel

from .client import WeComAPIClient
from .exceptions import InvalidParamError
from ..exceptions import InvalidParamError
from .settings import WECOM_OAUTH_URL
from ..base import BaseFederationIdpPlugin
from ..models import TestConnectionResult
Expand Down

0 comments on commit dfd9282

Please sign in to comment.