diff --git a/saas/VERSION b/saas/VERSION index df808199c..41336a1c0 100644 --- a/saas/VERSION +++ b/saas/VERSION @@ -1 +1 @@ -1.5.11 +1.5.12 diff --git a/saas/backend/apps/template/serializers.py b/saas/backend/apps/template/serializers.py index d6389b230..d7644d91f 100644 --- a/saas/backend/apps/template/serializers.py +++ b/saas/backend/apps/template/serializers.py @@ -21,6 +21,7 @@ from backend.apps.policy.serializers import BasePolicyActionSLZ from backend.apps.template.models import PermTemplate, PermTemplatePolicyAuthorized, PermTemplatePreUpdateLock from backend.biz.policy import PolicyBean, PolicyBeanList +from backend.biz.role import RoleScopeSystemActions from backend.biz.system import SystemBiz from backend.service.constants import SubjectType @@ -52,9 +53,12 @@ class TemplateListSLZ(serializers.ModelSerializer): system = serializers.SerializerMethodField(label="系统信息") tag = serializers.SerializerMethodField(label="标签") is_lock = serializers.SerializerMethodField(label="是否锁定") + need_to_update = serializers.SerializerMethodField(label="是否需要更新") def __init__(self, *args, **kwargs): self.authorized_template = kwargs.pop("authorized_template", set()) + self.role_system_actions: RoleScopeSystemActions = kwargs.pop("role_system_actions") # NOTE: 必须要传 + assert self.role_system_actions super().__init__(*args, **kwargs) self._system_list = SystemBiz().new_system_list() @@ -77,6 +81,7 @@ class Meta: "subject_count", "tag", "is_lock", + "need_to_update", "updater", "updated_time", "creator", @@ -99,15 +104,40 @@ def get_tag(self, obj): def get_is_lock(self, obj): return obj.id in self._lock_ids + def get_need_to_update(self, obj): + # 如果系统不在授权范围内, 说明整个系统的操作都被删除了, 这个模板只能被删除 + if not self.role_system_actions.has_system(obj.system_id): + return True + + # 如果role的范围时任意, 模板不需要更新 + if self.role_system_actions.is_all_action(obj.system_id): + return False + + # template 的 action set 减去 role 的action set, 还有剩下的说明模板需要更新 + rest_action = set(obj.action_ids) - set(self.role_system_actions.list_action_id(obj.system_id)) + return len(rest_action) != 0 + class TemplateListSchemaSLZ(serializers.ModelSerializer): system = SystemInfoSLZ(label="系统信息", required=True) tag = serializers.CharField(label="标签") is_lock = serializers.BooleanField(label="是否锁定") + need_to_update = serializers.BooleanField(label="是否需要更新") class Meta: model = PermTemplate - fields = ("id", "system", "name", "description", "subject_count", "tag", "is_lock", "updater", "updated_time") + fields = ( + "id", + "system", + "name", + "description", + "subject_count", + "tag", + "is_lock", + "need_to_update", + "updater", + "updated_time", + ) class TemplateRetrieveSchemaSLZ(TemplateListSchemaSLZ): diff --git a/saas/backend/apps/template/views.py b/saas/backend/apps/template/views.py index e9e51e6cc..cc1ce2919 100644 --- a/saas/backend/apps/template/views.py +++ b/saas/backend/apps/template/views.py @@ -129,16 +129,22 @@ def list(self, request, *args, **kwargs): group_id = request.query_params.get("group_id", "") queryset = self.filter_queryset(self.get_queryset()) + # 查询role的system-actions set + role_system_actions = RoleListQuery(request.role).get_scope_system_actions() page = self.paginate_queryset(queryset) if page is not None: # 查询模板中对group_id中有授权的 exists_template_set = self._query_group_exists_template_set(group_id, page) - serializer = TemplateListSLZ(page, many=True, authorized_template=exists_template_set) + serializer = TemplateListSLZ( + page, many=True, authorized_template=exists_template_set, role_system_actions=role_system_actions + ) return self.get_paginated_response(serializer.data) # 查询模板中对group_id中有授权的 exists_template_set = self._query_group_exists_template_set(group_id, queryset) - serializer = TemplateListSLZ(queryset, many=True, authorized_template=exists_template_set) + serializer = TemplateListSLZ( + queryset, many=True, authorized_template=exists_template_set, role_system_actions=role_system_actions + ) return Response(serializer.data) def _query_group_exists_template_set(self, group_id: str, queryset) -> Set[int]: @@ -197,12 +203,16 @@ def retrieve(self, request, *args, **kwargs): slz.is_valid(raise_exception=True) grouping = slz.validated_data["grouping"] + # 查询role的system-actions set + role_system_actions = RoleListQuery(request.role).get_scope_system_actions() template = get_object_or_404(self.queryset, pk=kwargs["id"]) - serializer = TemplateListSLZ(instance=template) + serializer = TemplateListSLZ(instance=template, role_system_actions=role_system_actions) data = serializer.data template_action_set = set(template.action_ids) - actions = self.action_biz.list_checked_action_by_role(template.system_id, request.role, template_action_set) + actions = self.action_biz.list_template_tagged_action_by_role( + template.system_id, request.role, template_action_set + ) if grouping: action_groups = self.action_group_biz.list_by_actions(template.system_id, actions) data["actions"] = [one.dict() for one in action_groups] diff --git a/saas/backend/biz/action.py b/saas/backend/biz/action.py index c11c77fa1..7d8754f57 100644 --- a/saas/backend/biz/action.py +++ b/saas/backend/biz/action.py @@ -107,14 +107,38 @@ def list_by_role(self, system_id: str, role) -> List[ActionBean]: actions = action_list.filter_by_scope_action_ids(scope_action_ids) return actions - def list_checked_action_by_role(self, system_id: str, role, checked_action_set: Set[str]) -> List[ActionBean]: + def list_template_tagged_action_by_role( + self, system_id: str, role, template_action_set: Set[str] + ) -> List[ActionBean]: """ - 查询角色相关的操作列表, 并上标签 + 查询角色相关的操作列表, 加上标签 + + 返回的操作是role范围+template操作的合集 + 对role范围外的操作要打上delete标签 """ - actions = self.list_by_role(system_id, role) - for action in actions: - action.tag = ActionTag.CHECKED.value if action.id in checked_action_set else ActionTag.UNCHECKED.value - return actions + actions = self.list(system_id).actions + scope_action_ids = RoleListQuery(role).list_scope_action_id(system_id) + if ACTION_ALL in scope_action_ids: + for action in actions: + action.tag = ActionTag.CHECKED.value if action.id in template_action_set else ActionTag.UNCHECKED.value + return actions + + # 筛选出在role范围内+在模板操作的操作集合 + # 存量模板的action范围可能会超过role的范围, 为了展示完整, 需要补全role范围中没有的action + filter_actions = [ + action for action in actions if action.id in scope_action_ids or action.id in template_action_set + ] + for action in filter_actions: + # 如果操作在模板内且不在role范围内, 操作已被删除 + if action.id in template_action_set and action.id not in scope_action_ids: + action.tag = ActionTag.DELETE.value + # 如果操作同时在role范围内与模板内, 操作已勾选 + elif action.id in template_action_set and action.id in scope_action_ids: + action.tag = ActionTag.CHECKED.value + # 如果操作不在模板内, 操作未勾选 + else: + action.tag = ActionTag.UNCHECKED.value + return filter_actions def list_by_subject(self, system_id: str, role, subject: Subject) -> List[ActionBean]: """ diff --git a/saas/backend/biz/constants.py b/saas/backend/biz/constants.py index 14d6d1dfd..4a2c7cfc9 100644 --- a/saas/backend/biz/constants.py +++ b/saas/backend/biz/constants.py @@ -33,3 +33,4 @@ class ActionTag(LowerStrEnum): READONLY = auto() CHECKED = auto() UNCHECKED = auto() + DELETE = auto() diff --git a/saas/backend/biz/role.py b/saas/backend/biz/role.py index bf9ac1267..1d171d35c 100644 --- a/saas/backend/biz/role.py +++ b/saas/backend/biz/role.py @@ -10,7 +10,7 @@ """ import logging from collections import defaultdict -from typing import List, Optional +from typing import Dict, List, Optional, Set from django.db.models import Q from django.utils.functional import cached_property @@ -60,6 +60,38 @@ class AuthScopeSystemBean(BaseModel): actions: List[PolicyBean] +class RoleScopeSystemActions(BaseModel): + """ + role的授权范围系统-操作列表 + """ + + systems: Dict[str, Set[str]] # key: system_id, value: action_id_set + + def is_all_action(self, system_id: str) -> bool: + """ + 是否是全操作列表 + """ + if not self.has_system(system_id): + return False + + if SYSTEM_ALL in self.systems or ACTION_ALL in self.systems[system_id]: + return True + + return False + + def has_system(self, system_id: str) -> bool: + return SYSTEM_ALL in self.systems or system_id in self.systems + + def list_action_id(self, system_id: str) -> List[str]: + """ + 返回role范围内的所有action_id, 如果为all_action, 应该事先判断 + """ + if system_id in self.systems: + return list(self.systems[system_id]) + + return [] + + class RoleBiz: svc = RoleService() system_svc = SystemService() @@ -280,15 +312,22 @@ def list_scope_action_id(self, system_id: str) -> List[str]: if self.role.type == RoleType.STAFF.value: return [ACTION_ALL] - scopes = self.role_svc.list_auth_scope(self.role.id) - systems = {s.system_id: {a.id for a in s.actions} for s in scopes} - if system_id not in systems and SYSTEM_ALL not in systems: + system_actions = self.get_scope_system_actions() + if not system_actions.has_system(system_id): return [] - if SYSTEM_ALL in systems or ACTION_ALL in systems[system_id]: + if system_actions.is_all_action(system_id): return [ACTION_ALL] - return list(systems[system_id]) + return system_actions.list_action_id(system_id) + + def get_scope_system_actions(self) -> RoleScopeSystemActions: + """ + 获取授权范围的系统-操作字典 + """ + scopes = self.role_svc.list_auth_scope(self.role.id) + systems = {s.system_id: {a.id for a in s.actions} for s in scopes} + return RoleScopeSystemActions(systems=systems) def query_template(self): """ diff --git a/saas/backend/common/debug.py b/saas/backend/common/debug.py index 1a94ffec9..ad4eae306 100644 --- a/saas/backend/common/debug.py +++ b/saas/backend/common/debug.py @@ -214,7 +214,7 @@ def _clean(self, data: Dict[str, Any]): self._clean(one) elif isinstance(value, str): for sk in self.sensitive_keys: - if key.endswith(sk): + if str(key).endswith(sk): data[key] = "***" if key in self.sensitive_key_func: diff --git a/saas/backend/common/vue.py b/saas/backend/common/vue.py index 91b280469..e748ed360 100644 --- a/saas/backend/common/vue.py +++ b/saas/backend/common/vue.py @@ -32,7 +32,7 @@ def get(self, request): # csrftoken name "CSRF_COOKIE_NAME": settings.CSRF_COOKIE_NAME, # BK_ITSM - "BK_ITSM_APP_URL": settings.BK_ITSM_APP_URL, + "BK_ITSM_APP_URL": settings.BK_ITSM_APP_URL.rstrip("/"), } # 添加前端功能启用开关 diff --git a/saas/resources/version_log/V1.5.12_2021-11-11.md b/saas/resources/version_log/V1.5.12_2021-11-11.md new file mode 100644 index 000000000..2725c65cc --- /dev/null +++ b/saas/resources/version_log/V1.5.12_2021-11-11.md @@ -0,0 +1,6 @@ +# V1.5.12 版本更新日志 + +### 功能优化 +* 增加用户同步记录 +* 分级管理员修改操作范围后, 范围不一致的模板不能授权 + diff --git a/saas/resources/version_log/V1.5.12_2021-11-11_en.md b/saas/resources/version_log/V1.5.12_2021-11-11_en.md new file mode 100644 index 000000000..a6d220261 --- /dev/null +++ b/saas/resources/version_log/V1.5.12_2021-11-11_en.md @@ -0,0 +1,5 @@ +# V1.5.12 ChangeLog + +### Optimization Updates +* Add user synchronization records +* After the rating manager modifies the action scope, templates with inconsistent scopes cannot be authorized diff --git a/saas/tests/biz/role_tests.py b/saas/tests/biz/role_tests.py index 5a6a2e5c9..5f2087ee6 100644 --- a/saas/tests/biz/role_tests.py +++ b/saas/tests/biz/role_tests.py @@ -8,10 +8,15 @@ 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 unittest import mock + +import pytest from django.test import TestCase +from backend.apps.role.models import Role from backend.biz.policy import InstanceBean -from backend.biz.role import ActionScopeDiffer +from backend.biz.role import ActionScopeDiffer, RoleListQuery, RoleScopeSystemActions +from backend.service.constants import ACTION_ALL, SYSTEM_ALL, RoleType class TestInstanceDiff(TestCase): @@ -60,3 +65,68 @@ def test_false(self): ] self.assertFalse(ActionScopeDiffer(None, None)._diff_instances(template_instances, scope_instances)) + + +@pytest.fixture() +def role_scope_system_action_system_all(): + return RoleScopeSystemActions(systems={SYSTEM_ALL: set()}) + + +@pytest.fixture() +def role_scope_system_action_action_all(): + return RoleScopeSystemActions(systems={"system": {ACTION_ALL}}) + + +@pytest.fixture() +def role_scope_system_action_normal(): + return RoleScopeSystemActions(systems={"system": {"action"}}) + + +class TestRoleScopeSystemActions: + def test_has_system(self, role_scope_system_action_system_all, role_scope_system_action_action_all): + assert role_scope_system_action_system_all.has_system("system") + + assert role_scope_system_action_action_all.has_system("system") + assert not role_scope_system_action_action_all.has_system("test") + + def test_is_all_action( + self, role_scope_system_action_system_all, role_scope_system_action_action_all, role_scope_system_action_normal + ): + assert role_scope_system_action_system_all.is_all_action("system") + + assert role_scope_system_action_action_all.is_all_action("system") + assert not role_scope_system_action_action_all.is_all_action("test") + + assert not role_scope_system_action_normal.is_all_action("system") + + def test_list_action_id( + self, role_scope_system_action_system_all, role_scope_system_action_action_all, role_scope_system_action_normal + ): + assert role_scope_system_action_system_all.list_action_id("system") == [] + + assert role_scope_system_action_action_all.list_action_id("system") == [ACTION_ALL] + assert role_scope_system_action_action_all.list_action_id("test") == [] + + assert role_scope_system_action_normal.list_action_id("system") == ["action"] + + +class TestRoleListQuery: + def test_list_scope_action_id(self, role_scope_system_action_system_all, role_scope_system_action_normal): + role = Role(type=RoleType.STAFF.value) + q = RoleListQuery(role=role) + assert q.list_scope_action_id("system") == [ACTION_ALL] + + role = Role(type=RoleType.RATING_MANAGER.value) + q = RoleListQuery(role=role) + q.get_scope_system_actions = mock.Mock(return_value=role_scope_system_action_normal) + assert q.list_scope_action_id("test") == [] + + role = Role(type=RoleType.RATING_MANAGER.value) + q = RoleListQuery(role=role) + q.get_scope_system_actions = mock.Mock(return_value=role_scope_system_action_system_all) + assert q.list_scope_action_id("system") == [ACTION_ALL] + + role = Role(type=RoleType.RATING_MANAGER.value) + q = RoleListQuery(role=role) + q.get_scope_system_actions = mock.Mock(return_value=role_scope_system_action_normal) + assert q.list_scope_action_id("system") == ["action"]