Skip to content

Commit

Permalink
feat(login): opt when only enable auth tenant (#1418)
Browse files Browse the repository at this point in the history
  • Loading branch information
nannan00 authored Nov 24, 2023
1 parent 90bf05d commit 578db85
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 26 deletions.
6 changes: 4 additions & 2 deletions src/bk-login/bklogin/authentication/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
# 前端页面(选择登录的用户)
path("page/users/", TemplateView.as_view(template_name="index.html")),
# ------------------------------------------ 租户 & 登录方式选择 ------------------------------------------
# 租户配置
path("tenant-global-settings/", views.TenantGlobalSettingRetrieveApi.as_view()),
# FIXME: 待联调tenant-global-infos完成后删除tenant-global-settings
path("tenant-global-settings/", views.TenantGlobalInfoRetrieveApi.as_view()),
# 租户全局信息
path("tenant-global-infos/", views.TenantGlobalInfoRetrieveApi.as_view()),
# 租户信息
path("tenants/", views.TenantListApi.as_view()),
path("tenants/<str:tenant_id>/", views.TenantRetrieveApi.as_view()),
Expand Down
38 changes: 28 additions & 10 deletions src/bk-login/bklogin/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
from bklogin.common.response import APISuccessResponse
from bklogin.component.bk_user import api as bk_user_api
from bklogin.component.bk_user.constants import IdpStatus
from bklogin.idp_plugins.base import BaseCredentialIdpPlugin, BaseFederationIdpPlugin, get_plugin_cls
from bklogin.idp_plugins.constants import AllowedHttpMethodEnum, BuiltinActionEnum
from bklogin.idp_plugins.base import BaseCredentialIdpPlugin, BaseFederationIdpPlugin, get_plugin_cls, get_plugin_type
from bklogin.idp_plugins.constants import AllowedHttpMethodEnum, BuiltinActionEnum, PluginTypeEnum
from bklogin.idp_plugins.exceptions import (
InvalidParamError,
ParseRequestBodyError,
Expand Down Expand Up @@ -79,18 +79,36 @@ def get(self, request, *args, **kwargs):
# 存储到当前session里,待认证成功后取出后重定向
request.session["redirect_uri"] = redirect_url

# TODO: 【优化】当只有一个租户且该租户有且仅有一种登录方式,且该登录方式为联邦登录,则直接重定向到第三方登录
# 当只有一个租户且该租户有且仅有一种登录方式,且该登录方式为联邦登录,则直接重定向到第三方登录
global_info = bk_user_api.get_global_info()
if (
global_info.enabled_auth_tenant_number == 1
and global_info.only_enabled_auth_tenant
and len(global_info.only_enabled_auth_tenant.enabled_idps) == 1
):
idp = global_info.only_enabled_auth_tenant.enabled_idps[0]
# 判断是否联邦登录
if get_plugin_type(idp.plugin_id) == PluginTypeEnum.FEDERATION:
# session记录登录的租户
request.session[SIGN_IN_TENANT_ID_SESSION_KEY] = global_info.only_enabled_auth_tenant.id
# 联邦登录,则直接重定向到第三方登录
return HttpResponseRedirect(f"/auth/idps/{idp.id}/actions/{BuiltinActionEnum.LOGIN}/")

# 返回登录页面
return render(request, self.template_name)


class TenantGlobalSettingRetrieveApi(View):
class TenantGlobalInfoRetrieveApi(View):
def get(self, request, *args, **kwargs):
"""
租户的全局配置,即所有租户的公共配置
租户的全局信息
"""
global_setting = bk_user_api.get_global_setting()
return APISuccessResponse(data=global_setting.model_dump(include={"tenant_visible"}))
global_info = bk_user_api.get_global_info()
return APISuccessResponse(
data=global_info.model_dump(
include={"tenant_visible", "enabled_auth_tenant_number", "only_enabled_auth_tenant"}
)
)


class TenantListApi(View):
Expand All @@ -103,7 +121,7 @@ def get(self, request, *args, **kwargs):
tenant_ids = [i for i in tenant_ids_str.split(",") if i]

# 无tenant_ids表示需要获取全部租户,这时候需要检查租户是否可见
global_setting = bk_user_api.get_global_setting()
global_setting = bk_user_api.get_global_info()
if not tenant_ids and not global_setting.tenant_visible:
raise error_codes.NO_PERMISSION.f(_("租户信息不可见"))

Expand Down Expand Up @@ -273,7 +291,7 @@ def wrap_plugin_error(self, context: PluginErrorContext, func: Callable, *func_a
context.idp.plugin_id,
)
raise error_codes.PLUGIN_SYSTEM_ERROR.f(
_("认证源[{}]执行插件[{}]失败, {}").format(context.idp.name, context.idp.plugin_name),
_("认证源[{}]执行插件[{}]失败").format(context.idp.name, context.idp.plugin_name),
)

def _dispatch_credential_idp_plugin(
Expand Down Expand Up @@ -344,7 +362,7 @@ def _dispatch_federation_idp_plugin(
# 记录支持登录的租户用户
request.session[ALLOWED_SIGN_IN_TENANT_USERS_SESSION_KEY] = tenant_users
# 联邦认证则重定向到前端选择账号页面
return HttpResponseRedirect(redirect_to="page/users/")
return HttpResponseRedirect(redirect_to="/page/users/")

return self.wrap_plugin_error(
plugin_error_context, plugin.dispatch_extension, action=action, http_method=http_method, request=request
Expand Down
10 changes: 5 additions & 5 deletions src/bk-login/bklogin/component/bk_user/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from bklogin.component.http import HttpStatusCode, http_get, http_post
from bklogin.utils.url import urljoin

from .models import GlobalSetting, IdpDetailInfo, IdpInfo, TenantInfo, TenantUserDetailInfo, TenantUserInfo
from .models import GlobalInfo, IdpDetailInfo, IdpInfo, TenantInfo, TenantUserDetailInfo, TenantUserInfo

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -54,10 +54,10 @@ def _call_bk_user_api_20x(http_func, url_path: str, **kwargs):
return _call_bk_user_api(http_func, url_path, allow_error_status_func=lambda s: False, **kwargs)["data"]


def get_global_setting() -> GlobalSetting:
"""获取全局配置"""
data = _call_bk_user_api_20x(http_get, "/api/v1/login/global-settings/")
return GlobalSetting(**data)
def get_global_info() -> GlobalInfo:
"""获取全局信息"""
data = _call_bk_user_api_20x(http_get, "/api/v1/login/global-infos/")
return GlobalInfo(**data)


def list_tenant(tenant_ids: List[str] | None = None) -> List[TenantInfo]:
Expand Down
21 changes: 18 additions & 3 deletions src/bk-login/bklogin/component/bk_user/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,32 @@
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
from typing import Any, Dict, List

from pydantic import BaseModel

from .constants import IdpStatus


class GlobalSetting(BaseModel):
"""全局配置"""
class EnabledIdp(BaseModel):
id: str
plugin_id: str


class OnlyEnabledAuthTenant(BaseModel):
id: str
name: str
logo: str = ""
enabled_idps: List[EnabledIdp]


class GlobalInfo(BaseModel):
"""全局信息"""

tenant_visible: bool
enabled_auth_tenant_number: int
# 当且仅当只有一个租户认证可用时候才有值,即 enabled_auth_tenant_number = 1 时才有值
only_enabled_auth_tenant: OnlyEnabledAuthTenant | None


class TenantInfo(BaseModel):
Expand Down
18 changes: 18 additions & 0 deletions src/bk-user/bkuser/apis/login/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,24 @@ class TenantRetrieveOutputSLZ(TenantListOutputSLZ):
...


class EnabledIdpOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="认证源 ID")
plugin_id = serializers.CharField(help_text="认证源插件 ID")


class OnlyEnabledAuthTenantOutputSLZ(TenantListOutputSLZ):
enabled_idps = serializers.ListField(child=EnabledIdpOutputSLZ(help_text="认证源插件"))


class GlobalInfoRetrieveOutputSLZ(serializers.Serializer):
tenant_visible = serializers.BooleanField(help_text="租户可见性")
enabled_auth_tenant_number = serializers.IntegerField(help_text="启用用户认证的租户数量")
only_enabled_auth_tenant = OnlyEnabledAuthTenantOutputSLZ(
help_text="唯一启动用户认证的租户数量,当 enabled_auth_tenant_number 不是一个时,该值为空",
allow_null=True,
)


class IdpPluginOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="认证源插件 ID")
name = serializers.CharField(help_text="认证源插件名称")
Expand Down
4 changes: 2 additions & 2 deletions src/bk-user/bkuser/apis/login/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
views.LocalUserCredentialAuthenticateApi.as_view(),
name="login.local_user_credentials.authenticate",
),
# 全局配置
path("global-settings/", views.GlobalSettingRetrieveApi.as_view(), name="login.global_setting.retrieve"),
# 全局信息
path("global-infos/", views.GlobalInfoRetrieveApi.as_view(), name="login.global_info.retrieve"),
# 租户列表
path("tenants/", views.TenantListApi.as_view(), name="login.tenant.list"),
# 单个租户
Expand Down
39 changes: 35 additions & 4 deletions src/bk-user/bkuser/apis/login/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@
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 collections import defaultdict
from typing import Any, Dict

from django.utils.translation import gettext_lazy as _
from rest_framework import generics
from rest_framework.response import Response

from bkuser.apps.data_source.models import LocalDataSourceIdentityInfo
from bkuser.apps.idp.constants import IdpStatus
from bkuser.apps.idp.models import Idp
from bkuser.apps.tenant.models import Tenant, TenantUser
from bkuser.biz.idp import AuthenticationMatcher
from bkuser.common.error_codes import error_codes

from .mixins import LoginApiAccessControlMixin
from .serializers import (
GlobalSettingRetrieveOutputSLZ,
GlobalInfoRetrieveOutputSLZ,
IdpListOutputSLZ,
IdpRetrieveOutputSLZ,
LocalUserCredentialAuthenticateInputSLZ,
Expand Down Expand Up @@ -66,10 +69,38 @@ def post(self, request, *args, **kwargs):
return Response(LocalUserCredentialAuthenticateOutputSLZ(instance=matched_users, many=True).data)


class GlobalSettingRetrieveApi(LoginApiAccessControlMixin, generics.RetrieveAPIView):
class GlobalInfoRetrieveApi(LoginApiAccessControlMixin, generics.RetrieveAPIView):
def get(self, request, *args, **kwargs):
# TODO: 待实现全局配置管理功能后调整
return Response(GlobalSettingRetrieveOutputSLZ(instance={"tenant_visible": False}).data)
# 查询租户启用的认证源
enabled_idp_map = defaultdict(list)
for idp in Idp.objects.filter(status=IdpStatus.ENABLED).values("owner_tenant_id", "id", "plugin_id"):
enabled_idp_map[idp["owner_tenant_id"]].append({"id": idp["id"], "plugin_id": idp["plugin_id"]})

# 启用认证源的租户数量
enabled_auth_tenant_number = len(enabled_idp_map)

# 唯一启用认证的租户信息
only_enabled_auth_tenant: Dict[str, Any] | None = None
if enabled_auth_tenant_number == 1:
owner_tenant_id, enabled_idps = next(iter(enabled_idp_map.items()))
tenant = Tenant.objects.get(id=owner_tenant_id)
only_enabled_auth_tenant = {
"id": tenant.id,
"name": tenant.name,
"logo": tenant.logo,
"enabled_idps": enabled_idps,
}

return Response(
GlobalInfoRetrieveOutputSLZ(
instance={
# FIXME (nan): 待实现全局配置管理功能后调整
"tenant_visible": False,
"enabled_auth_tenant_number": enabled_auth_tenant_number,
"only_enabled_auth_tenant": only_enabled_auth_tenant,
}
).data
)


class TenantListApi(LoginApiAccessControlMixin, generics.ListAPIView):
Expand Down
10 changes: 10 additions & 0 deletions src/bk-user/tests/apis/login/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
"""
44 changes: 44 additions & 0 deletions src/bk-user/tests/apis/login/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- 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 pytest
from bkuser.apps.idp.models import Idp, IdpPlugin
from bkuser.idp_plugins.constants import BuiltinIdpPluginEnum
from django.urls import reverse

from tests.test_utils.helpers import generate_random_string

pytestmark = pytest.mark.django_db


class TestGlobalInfoRetrieveApi:
def test_retrieve_with_only_tenant(self, api_client, default_tenant):
resp = api_client.get(reverse("login.global_info.retrieve"))
assert not resp.data["tenant_visible"]
assert resp.data["enabled_auth_tenant_number"] == 1
assert resp.data["only_enabled_auth_tenant"] is not None
assert resp.data["only_enabled_auth_tenant"]["id"] == default_tenant.id
assert len(resp.data["only_enabled_auth_tenant"]["enabled_idps"]) == 1
assert resp.data["only_enabled_auth_tenant"]["enabled_idps"][0]["plugin_id"] == BuiltinIdpPluginEnum.LOCAL

def test_retrieve_with_mult_tenant_but_not_mult_idp(self, api_client, default_tenant, random_tenant):
self.test_retrieve_with_only_tenant(api_client, default_tenant)

def test_retrieve_with_mult_tenant_and_mult_idp(self, api_client, default_tenant, random_tenant):
Idp.objects.create(
name=generate_random_string(),
owner_tenant_id=random_tenant.id,
plugin=IdpPlugin.objects.get(id=BuiltinIdpPluginEnum.LOCAL),
)
resp = api_client.get(reverse("login.global_info.retrieve"))

assert not resp.data["tenant_visible"]
assert resp.data["enabled_auth_tenant_number"] > 1
assert resp.data["only_enabled_auth_tenant"] is None

0 comments on commit 578db85

Please sign in to comment.