From 049be01ace1b0fa88025563d28f6234fd1837f90 Mon Sep 17 00:00:00 2001 From: schnee Date: Mon, 13 Nov 2023 20:42:47 +0800 Subject: [PATCH] feat: rebuild api permission control (#1378) --- .../bkuser/apis/web/basic/serializers.py | 3 + src/bk-user/bkuser/apis/web/basic/views.py | 7 +- .../bkuser/apis/web/data_source/views.py | 14 ++ .../web/data_source_organization/views.py | 7 + .../bkuser/apis/web/organization/views.py | 24 ++-- .../bkuser/apis/web/personal_center/views.py | 53 ++----- src/bk-user/bkuser/apis/web/tenant/views.py | 7 +- .../bkuser/apis/web/tenant_setting/views.py | 34 ++--- .../bkuser/apps/permission/__init__.py | 10 ++ src/bk-user/bkuser/apps/permission/apps.py | 16 +++ .../bkuser/apps/permission/constants.py | 36 +++++ .../bkuser/apps/permission/permissions.py | 134 ++++++++++++++++++ src/bk-user/bkuser/settings.py | 1 + src/bk-user/pyproject.toml | 2 +- src/bk-user/tests/conftest.py | 2 +- src/bk-user/tests/test_utils/auth.py | 18 ++- 16 files changed, 291 insertions(+), 77 deletions(-) create mode 100644 src/bk-user/bkuser/apps/permission/__init__.py create mode 100644 src/bk-user/bkuser/apps/permission/apps.py create mode 100644 src/bk-user/bkuser/apps/permission/constants.py create mode 100644 src/bk-user/bkuser/apps/permission/permissions.py diff --git a/src/bk-user/bkuser/apis/web/basic/serializers.py b/src/bk-user/bkuser/apis/web/basic/serializers.py index d928d6e29..b2a6ad5ab 100644 --- a/src/bk-user/bkuser/apis/web/basic/serializers.py +++ b/src/bk-user/bkuser/apis/web/basic/serializers.py @@ -10,7 +10,10 @@ """ from rest_framework import serializers +from bkuser.apps.permission.constants import UserRole + class CurrentUserRetrieveOutputSLZ(serializers.Serializer): username = serializers.CharField(help_text="用户名") tenant_id = serializers.CharField(help_text="租户 ID") + role = serializers.ChoiceField(help_text="用户角色", choices=UserRole.get_choices()) diff --git a/src/bk-user/bkuser/apis/web/basic/views.py b/src/bk-user/bkuser/apis/web/basic/views.py index cf089db04..a58c6da96 100644 --- a/src/bk-user/bkuser/apis/web/basic/views.py +++ b/src/bk-user/bkuser/apis/web/basic/views.py @@ -12,6 +12,8 @@ from rest_framework import generics, status from rest_framework.response import Response +from bkuser.apps.permission.permissions import get_user_role + from .serializers import CurrentUserRetrieveOutputSLZ @@ -24,9 +26,12 @@ class CurrentUserRetrieveApi(generics.RetrieveAPIView): def get(self, request, *args, **kwargs): # FIXME: 待新版登录后重构,return更多信息 current_user = request.user + current_tenant_id = current_user.get_property("tenant_id") + info = { "username": current_user.username, - "tenant_id": current_user.get_property("tenant_id"), + "tenant_id": current_tenant_id, + "role": get_user_role(current_tenant_id, current_user.username), } return Response(CurrentUserRetrieveOutputSLZ(instance=info).data) diff --git a/src/bk-user/bkuser/apis/web/data_source/views.py b/src/bk-user/bkuser/apis/web/data_source/views.py index 0b9b0d674..0e57e5cf0 100644 --- a/src/bk-user/bkuser/apis/web/data_source/views.py +++ b/src/bk-user/bkuser/apis/web/data_source/views.py @@ -16,6 +16,7 @@ from django.utils.translation import gettext_lazy as _ from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from bkuser.apis.web.data_source.mixins import CurrentUserTenantDataSourceMixin @@ -42,6 +43,8 @@ from bkuser.apis.web.mixins import CurrentUserTenantMixin from bkuser.apps.data_source.constants import DataSourceStatus from bkuser.apps.data_source.models import DataSource, DataSourcePlugin, DataSourceSensitiveInfo +from bkuser.apps.permission.constants import PermAction +from bkuser.apps.permission.permissions import perm_class from bkuser.apps.sync.constants import SyncTaskTrigger from bkuser.apps.sync.data_models import DataSourceSyncOptions from bkuser.apps.sync.managers import DataSourceSyncManager @@ -91,6 +94,7 @@ def get(self, request, *args, **kwargs): class DataSourceListCreateApi(CurrentUserTenantMixin, generics.ListCreateAPIView): pagination_class = None + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = DataSourceSearchOutputSLZ def get_serializer_context(self): @@ -154,6 +158,7 @@ class DataSourceRetrieveUpdateApi( CurrentUserTenantDataSourceMixin, ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView ): pagination_class = None + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = DataSourceRetrieveOutputSLZ @swagger_auto_schema( @@ -250,6 +255,7 @@ def post(self, request, *args, **kwargs): class DataSourceSwitchStatusApi(CurrentUserTenantDataSourceMixin, ExcludePutAPIViewMixin, generics.UpdateAPIView): """切换数据源状态(启/停)""" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = DataSourceSwitchStatusOutputSLZ @swagger_auto_schema( @@ -274,6 +280,7 @@ class DataSourceTemplateApi(CurrentUserTenantDataSourceMixin, generics.ListAPIVi """获取本地数据源数据导入模板""" pagination_class = None + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] @swagger_auto_schema( tags=["data_source"], @@ -295,6 +302,7 @@ class DataSourceExportApi(CurrentUserTenantDataSourceMixin, generics.ListAPIView """本地数据源用户导出""" pagination_class = None + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] @swagger_auto_schema( tags=["data_source"], @@ -317,6 +325,8 @@ def get(self, request, *args, **kwargs): class DataSourceImportApi(CurrentUserTenantDataSourceMixin, generics.CreateAPIView): """从 Excel 导入数据源用户数据""" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] + @swagger_auto_schema( tags=["data_source"], operation_description="本地数据源用户数据导入", @@ -371,6 +381,8 @@ def post(self, request, *args, **kwargs): class DataSourceSyncApi(CurrentUserTenantDataSourceMixin, generics.CreateAPIView): """数据源同步""" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] + @swagger_auto_schema( tags=["data_source"], operation_description="数据源数据同步", @@ -412,6 +424,7 @@ def post(self, request, *args, **kwargs): class DataSourceSyncRecordListApi(CurrentUserTenantMixin, generics.ListAPIView): """数据源同步记录列表""" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = DataSourceSyncRecordListOutputSLZ def get_queryset(self): @@ -449,6 +462,7 @@ class DataSourceSyncRecordRetrieveApi(CurrentUserTenantMixin, generics.RetrieveA """数据源同步记录详情""" lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] def get_queryset(self): return DataSourceSyncTask.objects.filter(data_source__owner_tenant_id=self.get_current_tenant_id()) diff --git a/src/bk-user/bkuser/apis/web/data_source_organization/views.py b/src/bk-user/bkuser/apis/web/data_source_organization/views.py index 1c8736d79..d1bf2e055 100644 --- a/src/bk-user/bkuser/apis/web/data_source_organization/views.py +++ b/src/bk-user/bkuser/apis/web/data_source_organization/views.py @@ -11,6 +11,7 @@ from django.db.models import Q from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from bkuser.apis.web.data_source_organization.serializers import ( @@ -26,6 +27,8 @@ UserUpdateInputSLZ, ) from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser +from bkuser.apps.permission.constants import PermAction +from bkuser.apps.permission.permissions import perm_class from bkuser.biz.data_source_organization import ( DataSourceOrganizationHandler, DataSourceUserBaseInfo, @@ -39,6 +42,7 @@ class DataSourceUserListCreateApi(generics.ListCreateAPIView): serializer_class = UserSearchOutputSLZ lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] def get_queryset(self): slz = UserSearchInputSLZ(data=self.request.query_params) @@ -111,6 +115,7 @@ def post(self, request, *args, **kwargs): class DataSourceLeadersListApi(generics.ListAPIView): serializer_class = LeaderSearchOutputSLZ + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] def get_queryset(self): slz = LeaderSearchInputSLZ(data=self.request.query_params) @@ -139,6 +144,7 @@ def get(self, request, *args, **kwargs): class DataSourceDepartmentsListApi(generics.ListAPIView): + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = DepartmentSearchOutputSLZ def get_queryset(self): @@ -171,6 +177,7 @@ def get(self, request, *args, **kwargs): class DataSourceUserRetrieveUpdateApi(ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView): queryset = DataSourceUser.objects.all() lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = UserRetrieveOutputSLZ def get_serializer_context(self): diff --git a/src/bk-user/bkuser/apis/web/organization/views.py b/src/bk-user/bkuser/apis/web/organization/views.py index 1e5bdd725..a2e1e99ca 100644 --- a/src/bk-user/bkuser/apis/web/organization/views.py +++ b/src/bk-user/bkuser/apis/web/organization/views.py @@ -13,6 +13,7 @@ from django.db.models import Q from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from bkuser.apis.web.mixins import CurrentUserTenantMixin @@ -25,6 +26,8 @@ TenantUserSearchInputSLZ, ) from bkuser.apis.web.tenant.serializers import TenantRetrieveOutputSLZ, TenantUpdateInputSLZ +from bkuser.apps.permission.constants import PermAction +from bkuser.apps.permission.permissions import perm_class from bkuser.apps.tenant.models import Tenant, TenantUser from bkuser.biz.tenant import ( TenantDepartmentHandler, @@ -33,7 +36,6 @@ TenantHandler, TenantUserHandler, ) -from bkuser.common.error_codes import error_codes from bkuser.common.views import ExcludePatchAPIViewMixin logger = logging.getLogger(__name__) @@ -42,6 +44,7 @@ class TenantDepartmentUserListApi(CurrentUserTenantMixin, generics.ListAPIView): queryset = TenantUser.objects.all() lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = TenantUserListOutputSLZ def get_serializer_context(self): @@ -100,6 +103,7 @@ def get(self, request, *args, **kwargs): class TenantUserRetrieveApi(generics.RetrieveAPIView): queryset = TenantUser.objects.all() lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = TenantUserRetrieveOutputSLZ @swagger_auto_schema( @@ -113,6 +117,7 @@ def get(self, request, *args, **kwargs): class TenantListApi(CurrentUserTenantMixin, generics.ListAPIView): pagination_class = None + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] @swagger_auto_schema( tags=["tenant-organization"], @@ -145,6 +150,7 @@ def get(self, request, *args, **kwargs): class TenantRetrieveUpdateApi(ExcludePatchAPIViewMixin, CurrentUserTenantMixin, generics.RetrieveUpdateAPIView): queryset = Tenant.objects.all() pagination_class = None + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = TenantRetrieveOutputSLZ lookup_url_kwarg = "id" @@ -164,28 +170,25 @@ def get(self, request, *args, **kwargs): tags=["tenant-organization"], operation_description="更新租户", request_body=TenantUpdateInputSLZ(), - responses={status.HTTP_200_OK: ""}, + responses={status.HTTP_204_NO_CONTENT: ""}, ) def put(self, request, *args, **kwargs): slz = TenantUpdateInputSLZ(data=request.data) slz.is_valid(raise_exception=True) data = slz.validated_data - instance = self.get_object() - # NOTE 非当前租户, 无权限做更新操作 - if self.get_current_tenant_id() != instance.id: - raise error_codes.NO_PERMISSION - - should_updated_info = TenantEditableBaseInfo( + tenant = self.get_object() + tenant_info = TenantEditableBaseInfo( name=data["name"], logo=data["logo"] or "", feature_flags=TenantFeatureFlag(**data["feature_flags"]) ) - TenantHandler.update_with_managers(instance.id, should_updated_info, data["manager_ids"]) - return Response() + TenantHandler.update_with_managers(tenant.id, tenant_info, data["manager_ids"]) + return Response(status=status.HTTP_204_NO_CONTENT) class TenantDepartmentChildrenListApi(generics.ListAPIView): pagination_class = None + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = TenantDepartmentChildrenListOutputSLZ @swagger_auto_schema( @@ -204,6 +207,7 @@ def get(self, request, *args, **kwargs): class TenantUserListApi(CurrentUserTenantMixin, generics.ListAPIView): queryset = TenantUser.objects.all() lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = TenantUserListOutputSLZ def get_tenant_user_ids(self, tenant_id): diff --git a/src/bk-user/bkuser/apis/web/personal_center/views.py b/src/bk-user/bkuser/apis/web/personal_center/views.py index a96a892c2..e3a8425c0 100644 --- a/src/bk-user/bkuser/apis/web/personal_center/views.py +++ b/src/bk-user/bkuser/apis/web/personal_center/views.py @@ -12,6 +12,7 @@ from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from bkuser.apis.web.personal_center.serializers import ( @@ -20,15 +21,17 @@ TenantUserEmailUpdateInputSLZ, TenantUserPhoneUpdateInputSLZ, ) +from bkuser.apps.permission.constants import PermAction +from bkuser.apps.permission.permissions import perm_class from bkuser.apps.tenant.models import TenantUser from bkuser.biz.natural_user import NatureUserHandler from bkuser.biz.tenant import TenantUserEmailInfo, TenantUserHandler, TenantUserPhoneInfo -from bkuser.common.error_codes import error_codes from bkuser.common.views import ExcludePutAPIViewMixin class NaturalUserTenantUserListApi(generics.ListAPIView): pagination_class = None + permission_classes = [IsAuthenticated, perm_class(PermAction.USE_PLATFORM)] @swagger_auto_schema( tags=["personal_center"], @@ -70,6 +73,7 @@ def get(self, request, *args, **kwargs): class TenantUserRetrieveApi(generics.RetrieveAPIView): queryset = TenantUser.objects.all() lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.USE_PLATFORM)] serializer_class = PersonalCenterTenantUserRetrieveOutputSLZ @swagger_auto_schema( @@ -78,22 +82,13 @@ class TenantUserRetrieveApi(generics.RetrieveAPIView): responses={status.HTTP_200_OK: PersonalCenterTenantUserRetrieveOutputSLZ()}, ) def get(self, request, *args, **kwargs): - instance: TenantUser = self.get_object() - - # 获取当前登录的租户用户的自然人 - nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(request.user.username) - - # 边界限制 - # 该租户用户的数据源用户,不属于当前自然人 - if instance.data_source_user_id not in nature_user.data_source_user_ids: - raise error_codes.NO_PERMISSION - - return Response(PersonalCenterTenantUserRetrieveOutputSLZ(instance).data) + return Response(PersonalCenterTenantUserRetrieveOutputSLZ(self.get_object()).data) class TenantUserPhoneUpdateApi(ExcludePutAPIViewMixin, generics.UpdateAPIView): queryset = TenantUser.objects.all() lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.USE_PLATFORM)] @swagger_auto_schema( tags=["personal_center"], @@ -102,33 +97,23 @@ class TenantUserPhoneUpdateApi(ExcludePutAPIViewMixin, generics.UpdateAPIView): responses={status.HTTP_200_OK: ""}, ) def patch(self, request, *args, **kwargs): - instance: TenantUser = self.get_object() - - # 获取当前登录的租户用户的自然人 - nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(request.user.username) - - # 边界限制 - # 该租户用户的数据源用户,不属于当前自然人 - if instance.data_source_user_id not in nature_user.data_source_user_ids: - raise error_codes.NO_PERMISSION - slz = TenantUserPhoneUpdateInputSLZ(data=request.data) slz.is_valid(raise_exception=True) - data = slz.validated_data + phone_info = TenantUserPhoneInfo( is_inherited_phone=data["is_inherited_phone"], custom_phone=data.get("custom_phone", ""), custom_phone_country_code=data["custom_phone_country_code"], ) - TenantUserHandler.update_tenant_user_phone(instance, phone_info) - + TenantUserHandler.update_tenant_user_phone(self.get_object(), phone_info) return Response() class TenantUserEmailUpdateApi(ExcludePutAPIViewMixin, generics.UpdateAPIView): queryset = TenantUser.objects.all() lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.USE_PLATFORM)] @swagger_auto_schema( tags=["personal_center"], @@ -137,23 +122,13 @@ class TenantUserEmailUpdateApi(ExcludePutAPIViewMixin, generics.UpdateAPIView): responses={status.HTTP_200_OK: ""}, ) def patch(self, request, *args, **kwargs): - instance: TenantUser = self.get_object() - - # 获取当前登录的租户用户的自然人 - nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(request.user.username) - - # 边界限制 - # 该租户用户的数据源用户,不属于当前自然人下的 - if instance.data_source_user_id not in nature_user.data_source_user_ids: - raise error_codes.NO_PERMISSION - slz = TenantUserEmailUpdateInputSLZ(data=request.data) slz.is_valid(raise_exception=True) - data = slz.validated_data + email_info = TenantUserEmailInfo( - is_inherited_email=data["is_inherited_email"], custom_email=data.get("custom_email", "") + is_inherited_email=data["is_inherited_email"], + custom_email=data.get("custom_email", ""), ) - TenantUserHandler.update_tenant_user_email(instance, email_info) - + TenantUserHandler.update_tenant_user_email(self.get_object(), email_info) return Response() diff --git a/src/bk-user/bkuser/apis/web/tenant/views.py b/src/bk-user/bkuser/apis/web/tenant/views.py index c11c5335d..d0c9165a9 100644 --- a/src/bk-user/bkuser/apis/web/tenant/views.py +++ b/src/bk-user/bkuser/apis/web/tenant/views.py @@ -13,6 +13,7 @@ from django.db.models import Q from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from bkuser.apis.web.tenant.serializers import ( @@ -25,6 +26,8 @@ TenantUserSearchInputSLZ, TenantUserSearchOutputSLZ, ) +from bkuser.apps.permission.constants import PermAction +from bkuser.apps.permission.permissions import perm_class from bkuser.apps.tenant.models import Tenant, TenantUser from bkuser.biz.data_source import DataSourceHandler from bkuser.biz.tenant import ( @@ -42,7 +45,7 @@ class TenantListCreateApi(generics.ListCreateAPIView): pagination_class = None - + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_PLATFORM)] serializer_class = TenantSearchOutputSLZ def get_serializer_context(self): @@ -109,6 +112,7 @@ def post(self, request, *args, **kwargs): class TenantRetrieveUpdateApi(ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView): queryset = Tenant.objects.all() lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_PLATFORM)] serializer_class = TenantRetrieveOutputSLZ def get_serializer_context(self): @@ -150,6 +154,7 @@ def put(self, request, *args, **kwargs): class TenantUsersListApi(generics.ListAPIView): serializer_class = TenantUserSearchOutputSLZ + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_PLATFORM)] def get_queryset(self): tenant_id = self.kwargs["tenant_id"] diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py index 8be22b64c..4e8bc4c33 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py @@ -11,6 +11,7 @@ from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status from rest_framework.generics import get_object_or_404 +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from bkuser.apis.web.mixins import CurrentUserTenantMixin @@ -22,18 +23,19 @@ TenantUserValidityPeriodConfigInputSLZ, TenantUserValidityPeriodConfigOutputSLZ, ) +from bkuser.apps.permission.constants import PermAction +from bkuser.apps.permission.permissions import perm_class from bkuser.apps.tenant.models import ( - TenantManager, TenantUserCustomField, TenantUserValidityPeriodConfig, UserBuiltinField, ) -from bkuser.common.error_codes import error_codes from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin class TenantUserFieldListApi(CurrentUserTenantMixin, generics.ListAPIView): pagination_class = None + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] serializer_class = TenantUserFieldOutputSLZ @swagger_auto_schema( @@ -54,6 +56,8 @@ def get(self, request, *args, **kwargs): class TenantUserCustomFieldCreateApi(CurrentUserTenantMixin, generics.CreateAPIView): + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] + @swagger_auto_schema( tags=["tenant-setting"], operation_description="新建用户自定义字段", @@ -77,6 +81,7 @@ class TenantUserCustomFieldUpdateDeleteApi( CurrentUserTenantMixin, ExcludePutAPIViewMixin, generics.UpdateAPIView, generics.DestroyAPIView ): lookup_url_kwarg = "id" + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] def get_queryset(self): return TenantUserCustomField.objects.filter(tenant_id=self.get_current_tenant_id()) @@ -117,6 +122,8 @@ def delete(self, request, *args, **kwargs): class TenantUserValidityPeriodConfigRetrieveUpdateApi( ExcludePatchAPIViewMixin, CurrentUserTenantMixin, generics.RetrieveUpdateAPIView ): + permission_classes = [IsAuthenticated, perm_class(PermAction.MANAGE_TENANT)] + def get_object(self): queryset = TenantUserValidityPeriodConfig.objects.all() filter_kwargs = {"tenant_id": self.get_current_tenant_id()} @@ -142,24 +149,17 @@ def get(self, request, *args, **kwargs): }, ) def put(self, request, *args, **kwargs): - instance = self.get_object() - - # TODO (su) 权限调整为 perm_class 当前租户的管理才可做更新操作 - operator = request.user.username - if not TenantManager.objects.filter(tenant_id=instance.tenant_id, tenant_user_id=operator).exists(): - raise error_codes.NO_PERMISSION - slz = TenantUserValidityPeriodConfigInputSLZ(data=request.data) slz.is_valid(raise_exception=True) data = slz.validated_data - instance.enabled = data["enabled"] - instance.validity_period = data["validity_period"] - instance.remind_before_expire = data["remind_before_expire"] - instance.enabled_notification_methods = data["enabled_notification_methods"] - instance.notification_templates = data["notification_templates"] - instance.updater = operator - - instance.save() + cfg = self.get_object() + cfg.enabled = data["enabled"] + cfg.validity_period = data["validity_period"] + cfg.remind_before_expire = data["remind_before_expire"] + cfg.enabled_notification_methods = data["enabled_notification_methods"] + cfg.notification_templates = data["notification_templates"] + cfg.updater = request.user.username + cfg.save() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/bk-user/bkuser/apps/permission/__init__.py b/src/bk-user/bkuser/apps/permission/__init__.py new file mode 100644 index 000000000..1060b7bf4 --- /dev/null +++ b/src/bk-user/bkuser/apps/permission/__init__.py @@ -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. +""" diff --git a/src/bk-user/bkuser/apps/permission/apps.py b/src/bk-user/bkuser/apps/permission/apps.py new file mode 100644 index 000000000..71b90bcb4 --- /dev/null +++ b/src/bk-user/bkuser/apps/permission/apps.py @@ -0,0 +1,16 @@ +# -*- 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.apps import AppConfig + + +class PermissionConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "bkuser.apps.permission" diff --git a/src/bk-user/bkuser/apps/permission/constants.py b/src/bk-user/bkuser/apps/permission/constants.py new file mode 100644 index 000000000..e06952daf --- /dev/null +++ b/src/bk-user/bkuser/apps/permission/constants.py @@ -0,0 +1,36 @@ +# -*- 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 logging + +from blue_krill.data_types.enum import EnumField, StructuredEnum +from django.utils.translation import gettext_lazy as _ + +logger = logging.getLogger(__name__) + + +class UserRole(str, StructuredEnum): + """用户角色""" + + SUPER_MANAGER = EnumField("super_manager", label=_("超级管理员")) + TENANT_MANAGER = EnumField("tenant_manager", label=_("租户管理员")) + NATURAL_USER = EnumField("natural_user", label=_("普通用户")) + + +class PermAction(str, StructuredEnum): + """权限行为""" + + # TODO (su) 接入 IAM 时,需要评估是否细化 Action + # 平台管理 - 超级管理员 + MANAGE_PLATFORM = EnumField("manage_platform", label=_("平台管理")) + # 租户管理 - 租户管理员 + MANAGE_TENANT = EnumField("manage_tenant", label=_("租户管理")) + # 平台使用 - 普通用户/自然人 + USE_PLATFORM = EnumField("use_platform", label=_("平台使用")) diff --git a/src/bk-user/bkuser/apps/permission/permissions.py b/src/bk-user/bkuser/apps/permission/permissions.py new file mode 100644 index 000000000..b599e24b9 --- /dev/null +++ b/src/bk-user/bkuser/apps/permission/permissions.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making +蓝鲸智云 - PaaS 平台 (BlueKing - PaaS System) 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. + +We undertake not to change the open source license (MIT license) applicable +to the current version of the project delivered to anyone in the future. +""" +import logging + +from rest_framework.permissions import BasePermission + +from bkuser.apps.data_source.models import DataSource +from bkuser.apps.natural_user.models import DataSourceUserNaturalUserRelation +from bkuser.apps.permission.constants import PermAction, UserRole +from bkuser.apps.tenant.models import Tenant, TenantManager, TenantUser + +logger = logging.getLogger(__name__) + + +def perm_class(action: PermAction): # noqa: C901 + """构建 DRF 可用的应用权限类""" + + class Permission(BasePermission): + def has_permission(self, request, view): + username = request.user.username + cur_tenant_id = request.user.get_property("tenant_id") + + if action == PermAction.MANAGE_PLATFORM: + return is_super_manager(cur_tenant_id, username) + if action == PermAction.MANAGE_TENANT: + return is_tenant_manager(cur_tenant_id, username) + if action == PermAction.USE_PLATFORM: + # 平台使用的情况,需要用具体的 object 来判断权限 + return True + + return False + + def has_object_permission(self, request, view, obj): + if isinstance(obj, Tenant): + tenant_id = obj.id + elif hasattr(obj, "tenant_id"): + tenant_id = obj.tenant_id + elif isinstance(obj, DataSource): + # TODO (su) 考虑数据源协同的情况 + tenant_id = obj.owner_tenant_id + elif hasattr(obj, "data_source"): + # TODO (su) 考虑数据源协同的情况 + tenant_id = obj.data_source.owner_tenant_id + else: + logger.exception("failed to get tenant id, obj: %s", obj) + return False + + username = request.user.username + cur_tenant_id = request.user.get_property("tenant_id") + if action == PermAction.MANAGE_PLATFORM: + return is_super_manager(tenant_id, username) + if action == PermAction.MANAGE_TENANT: + return is_tenant_manager(tenant_id, username) + if action == PermAction.USE_PLATFORM: + # 当前平台使用(普通用户)能编辑的资源只有 TenantUser + if not isinstance(obj, TenantUser): + return False + + return is_same_nature_user(obj.id, cur_tenant_id, username) + + return False + + return Permission + + +def is_super_manager(tenant_id: str, username: str) -> bool: + """默认租户的管理员,有管理平台的权限(超级管理员)""" + tenant = Tenant.objects.get(id=tenant_id) + if not tenant.is_default: + return False + + return TenantManager.objects.filter(tenant=tenant, tenant_user_id=username).exists() + + +def is_tenant_manager(tenant_id: str, username: str) -> bool: + """本租户的管理员,拥有管理当前租户配置的权限""" + tenant = Tenant.objects.get(id=tenant_id) + return TenantManager.objects.filter(tenant=tenant, tenant_user_id=username).exists() + + +def is_same_nature_user(req_username: str, cur_tenant_id: str, username: str) -> bool: + """判断是否同一自然人(可以跨租户访问属于同一自然人/数据源用户的数据) + + :param req_username: 待访问租户用户名 + :param cur_tenant_id: 当前用户的租户 ID + :param username: 当前用户的用户名 + """ + cur_tenant_user = TenantUser.objects.filter(tenant_id=cur_tenant_id, id=username).first() + # 当前登录的,连租户用户都不是,自然不是自然人 + if not cur_tenant_user: + return False + + # 数据用户源若绑定到自然人,只可能有一条记录 + relation = DataSourceUserNaturalUserRelation.objects.filter( + data_source_user=cur_tenant_user.data_source_user + ).first() + + # 如果没有绑定自然人,则将同一数据源用户关联的租户用户,都视作一个自然人 + if not relation: + return TenantUser.objects.filter(id=req_username, data_source_user=cur_tenant_user.data_source_user).exists() + + data_source_user_ids = DataSourceUserNaturalUserRelation.objects.filter( + natural_user=relation.natural_user + ).values_list("data_source_user_id", flat=True) + return TenantUser.objects.filter(id=req_username, data_source_user__in=data_source_user_ids).exists() + + +def get_user_role(tenant_id: str, username: str) -> UserRole: + """获取用户角色,因目前超级管理员必定是租户管理员,租户管理员必定是普通用户,因此返回最高级的角色即可""" + tenant = Tenant.objects.get(id=tenant_id) + + if TenantManager.objects.filter(tenant=tenant, tenant_user_id=username).exists(): + if tenant.is_default: + return UserRole.SUPER_MANAGER + + return UserRole.TENANT_MANAGER + + return UserRole.NATURAL_USER diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index 1077573f5..7db62f529 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -55,6 +55,7 @@ "bkuser.apps.sync", "bkuser.apps.idp", "bkuser.apps.natural_user", + "bkuser.apps.permission", ] MIDDLEWARE = [ diff --git a/src/bk-user/pyproject.toml b/src/bk-user/pyproject.toml index 02f08f905..4af980417 100644 --- a/src/bk-user/pyproject.toml +++ b/src/bk-user/pyproject.toml @@ -158,7 +158,7 @@ layers = [ name = "Apps Layers contract" type = "layers" layers = [ - "bkuser.apps.sync", + "bkuser.apps.sync | bkuser.apps.permission", "bkuser.apps.tenant", "bkuser.apps.idp", "bkuser.apps.data_source", diff --git a/src/bk-user/tests/conftest.py b/src/bk-user/tests/conftest.py index 58371c2f3..c0fa7c675 100644 --- a/src/bk-user/tests/conftest.py +++ b/src/bk-user/tests/conftest.py @@ -43,4 +43,4 @@ def random_tenant() -> Tenant: @pytest.fixture() def bk_user(default_tenant) -> User: """生成随机用户""" - return create_user() + return create_user(default_tenant) diff --git a/src/bk-user/tests/test_utils/auth.py b/src/bk-user/tests/test_utils/auth.py index a63c1d2bd..ecbf8f1c5 100644 --- a/src/bk-user/tests/test_utils/auth.py +++ b/src/bk-user/tests/test_utils/auth.py @@ -12,20 +12,19 @@ from typing import Optional from bkuser.apps.data_source.models import DataSource, DataSourceUser -from bkuser.apps.tenant.models import TenantUser +from bkuser.apps.tenant.models import Tenant, TenantManager, TenantUser from bkuser.auth.models import User from tests.test_utils.helpers import generate_random_string -from tests.test_utils.tenant import DEFAULT_TENANT -def create_user(username: Optional[str] = None) -> User: +def create_user(tenant: Tenant, username: Optional[str] = None) -> User: """创建测试用用户""" username = username or generate_random_string(length=8) user, _ = User.objects.get_or_create(username=username) - user.set_property("tenant_id", DEFAULT_TENANT) + user.set_property("tenant_id", tenant.id) # 获取租户默认的本地数据源 - data_source = DataSource.objects.get(owner_tenant_id=DEFAULT_TENANT, name=f"{DEFAULT_TENANT}-default-local") + data_source = DataSource.objects.get(owner_tenant_id=tenant.id, name=f"{tenant.id}-default-local") data_source_user, _ = DataSourceUser.objects.get_or_create( username=username, @@ -37,8 +36,13 @@ def create_user(username: Optional[str] = None) -> User: }, ) - TenantUser.objects.get_or_create( - tenant_id=DEFAULT_TENANT, id=username, data_source=data_source, data_source_user=data_source_user + tenant_user, _ = TenantUser.objects.get_or_create( + tenant=tenant, + id=username, + data_source=data_source, + data_source_user=data_source_user, ) + TenantManager.objects.get_or_create(tenant=tenant, tenant_user=tenant_user) + return user