From ff3b04f7e5d17b841d225654fdf38d1060f63fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E4=BD=A9=E7=8F=8A=5Bshiisa=5D?= Date: Thu, 21 Mar 2024 16:20:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=B1=BB=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81=E8=B6=85?= =?UTF-8?q?=E7=AE=A1=E8=A7=92=E8=89=B2=20#2264?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- saas/backend/api/admin/constants.py | 25 ++- saas/backend/api/admin/filters.py | 12 +- .../migrations/0002_auto_20240321_1557.py | 36 ++++ saas/backend/api/admin/mixins.py | 52 +++++ saas/backend/api/admin/models.py | 21 ++ saas/backend/api/admin/serializers.py | 30 ++- saas/backend/api/admin/urls.py | 20 +- saas/backend/api/admin/views/__init__.py | 2 + saas/backend/api/admin/views/grade_manager.py | 186 ++++++++++++++++++ 9 files changed, 379 insertions(+), 5 deletions(-) create mode 100644 saas/backend/api/admin/migrations/0002_auto_20240321_1557.py create mode 100644 saas/backend/api/admin/mixins.py create mode 100644 saas/backend/api/admin/views/grade_manager.py diff --git a/saas/backend/api/admin/constants.py b/saas/backend/api/admin/constants.py index 1e8f8cbfc..45e27166f 100644 --- a/saas/backend/api/admin/constants.py +++ b/saas/backend/api/admin/constants.py @@ -8,9 +8,10 @@ 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 aenum import auto, skip +from aenum import auto, skip, LowerStrEnum from backend.api.constants import BaseAPIEnum +from backend.util.enum import ChoicesEnum class AdminAPIEnum(BaseAPIEnum): @@ -44,6 +45,11 @@ class AdminAPIEnum(BaseAPIEnum): # 是否有权限数据 SUBJECT_PERMISSION_EXISTS = auto() + # 分级管理员 + GRADE_MANAGER_LIST = auto() + GRADE_MANAGER_CREATE = auto() + GRADE_MANAGER_UPDATE = auto() + _choices_labels = skip( ( (SYSTEM_LIST, "获取系统列表"), @@ -57,5 +63,22 @@ class AdminAPIEnum(BaseAPIEnum): (SUBJECT_FREEZE_UNFREEZE, "冻结/解冻Subject"), (SUBJECT_PERMISSION_CLEANUP, "权限清理"), (SUBJECT_PERMISSION_EXISTS, "权限是否存在"), + (GRADE_MANAGER_LIST, "获取分级管理员列表"), + (GRADE_MANAGER_CREATE, "新建分级管理员"), + (GRADE_MANAGER_UPDATE, "更新分级管理员"), + ) + ) + + +class VerifyApiParamLocationEnum(ChoicesEnum, LowerStrEnum): + ROLE_IN_PATH = auto() + SYSTEM_IN_BODY = auto() + SYSTEM_IN_QUERY = auto() + + _choices_labels = skip( + ( + (ROLE_IN_PATH, "在URL里的role id参数"), + (SYSTEM_IN_BODY, "在body data里的system参数"), + (SYSTEM_IN_QUERY, "在get请求query里的system参数"), ) ) diff --git a/saas/backend/api/admin/filters.py b/saas/backend/api/admin/filters.py index 4510f613c..0cbee45b7 100644 --- a/saas/backend/api/admin/filters.py +++ b/saas/backend/api/admin/filters.py @@ -11,7 +11,7 @@ from django_filters import rest_framework as filters from backend.apps.group.models import Group -from backend.apps.role.models import RoleRelatedObject +from backend.apps.role.models import RoleRelatedObject, Role from backend.service.constants import RoleRelatedObjectType @@ -28,3 +28,13 @@ class Meta: def grade_manager_id_filter(self, queryset, name, value): group_ids = RoleRelatedObject.objects.list_role_object_ids(value, RoleRelatedObjectType.GROUP.value) return queryset.filter(id__in=group_ids) + + +class GradeManagerFilter(filters.FilterSet): + name = filters.CharFilter(label="名字", lookup_expr="icontains") + + class Meta: + model = Role + fields = [ + "name", + ] diff --git a/saas/backend/api/admin/migrations/0002_auto_20240321_1557.py b/saas/backend/api/admin/migrations/0002_auto_20240321_1557.py new file mode 100644 index 000000000..43b9cee59 --- /dev/null +++ b/saas/backend/api/admin/migrations/0002_auto_20240321_1557.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.16 on 2024-03-21 07:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api_admin', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='adminapiallowlistconfig', + name='api', + field=models.CharField(choices=[('system_list', '获取系统列表'), ('group_list', '获取用户组列表'), ('group_member_list', '获取用户组成员列表'), ('subject_joined_group_list', '获取Subject加入的用户组列表'), ('subject_role_list', '获取Subject角色列表'), ('role_super_manager_member_list', '获取超级管理员成员列表'), ('role_system_manager_member_list', '获取系统管理员及成员列表'), ('audit_event_list', '获取审计事件列表'), ('subject_freeze_unfreeze', '冻结/解冻Subject'), ('subject_permission_cleanup', '权限清理'), ('subject_permission_exists', '权限是否存在'), ('grade_manager_list', '获取分级管理员列表'), ('grade_manager_create', '新建分级管理员'), ('grade_manager_update', '更新分级管理员')], help_text='*代表任意', max_length=32, verbose_name='API'), + ), + migrations.CreateModel( + name='SystemAllowAuthSystem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('creator', models.CharField(max_length=64, verbose_name='创建者')), + ('updater', models.CharField(max_length=64, verbose_name='更新者')), + ('created_time', models.DateTimeField(auto_now_add=True)), + ('updated_time', models.DateTimeField(auto_now=True)), + ('system_id', models.CharField(max_length=32, verbose_name='接入系统')), + ('auth_system_id', models.CharField(help_text='*代表任意', max_length=32, verbose_name='接入系统')), + ], + options={ + 'verbose_name': '系统允许授权的系统', + 'verbose_name_plural': '系统允许授权的系统', + 'ordering': ['-id'], + 'index_together': {('system_id', 'auth_system_id')}, + }, + ), + ] diff --git a/saas/backend/api/admin/mixins.py b/saas/backend/api/admin/mixins.py new file mode 100644 index 000000000..31b2cad96 --- /dev/null +++ b/saas/backend/api/admin/mixins.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-权限中心(BlueKing-IAM) 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 typing import List + +from rest_framework import exceptions + +from backend.api.constants import ALLOW_ANY +from backend.api.mixins import SystemClientCheckMixin + +from backend.api.admin.models import SystemAllowAuthSystem + + +class SuperManagerAPIPermissionCheckMixin(SystemClientCheckMixin): + """ + 管理类-超管角色API认证与鉴权 + """ + def verify_system_scope(self, system_id: str, auth_system_ids: List[str]): + """ + API数据鉴权:查询系统可管控的授权系统表,校验接入系统是否越权提交了其他系统的数据 + 主要是针对分级管理员创建和更新时的可授权范围 + """ + # 加速判断:大部分系统都只是管理自身系统的权限,即可授权范围里的每个系统都只能等于自身 + if all([sys_id == system_id for sys_id in auth_system_ids]): + return + + # 接入系统可管控的系统表[system_id/auth_system_id]来实现管控更多接入系统权限 + allowed_auth_system = SystemAllowAuthSystem.list_auth_system_id(system_id) + allowed_system_ids = set(allowed_auth_system) + # 任何系统都允许访问自身 + allowed_system_ids.add(system_id) + + # 如果允许管理的系统存在任意,则说明该系统可管理所有系统,鉴权直接通过 + if ALLOW_ANY in allowed_system_ids: + return + + # 遍历校验 + for sys_id in auth_system_ids: + if sys_id not in allowed_system_ids: + raise exceptions.PermissionDenied( + detail=( + f"system[{system_id}] is not allow to operate system[{sys_id}]'s permission data, " + "please contact the developer to add a whitelist" + ) + ) diff --git a/saas/backend/api/admin/models.py b/saas/backend/api/admin/models.py index 043e2f88a..2d8647170 100644 --- a/saas/backend/api/admin/models.py +++ b/saas/backend/api/admin/models.py @@ -11,6 +11,7 @@ from django.db import models from backend.api.constants import ALLOW_ANY +from backend.common.cache import cachedmethod from backend.common.models import BaseModel from .constants import AdminAPIEnum @@ -36,3 +37,23 @@ def is_allowed(cls, app_code: str, api: str): 由于支持配置任意,所以判断还需要判断是否包含了任意 """ return cls.objects.filter(app_code=app_code, api__in=[ALLOW_ANY, api]).exists() + + +class SystemAllowAuthSystem(BaseModel): + """系统允许授权的系统 + 即可配置某个系统管理其他系统的权限 + """ + + system_id = models.CharField("接入系统", max_length=32) + auth_system_id = models.CharField("接入系统", max_length=32, help_text="*代表任意") + + class Meta: + verbose_name = "系统允许授权的系统" + verbose_name_plural = "系统允许授权的系统" + ordering = ["-id"] + index_together = ["system_id", "auth_system_id"] + + @classmethod + @cachedmethod(timeout=5 * 60) # 缓存5分钟 + def list_auth_system_id(cls, system_id: str): + return list(cls.objects.filter(system_id=system_id).values_list("auth_system_id", flat=True)) diff --git a/saas/backend/api/admin/serializers.py b/saas/backend/api/admin/serializers.py index 33f515e41..2d5b9cd59 100644 --- a/saas/backend/api/admin/serializers.py +++ b/saas/backend/api/admin/serializers.py @@ -10,9 +10,17 @@ """ from rest_framework import serializers +from backend.api.management.v1.serializers import ( + ManagementGradeManagerBasicInfoSLZ, + ManagementGradeManagerUpdateSLZ, + ManagementGradeManagerCreateSLZ, + ManagementSourceSystemSLZ, +) from backend.apps.group.models import Group from backend.apps.role.models import Role -from backend.apps.role.serializers import BaseGradeMangerSLZ +from backend.apps.role.serializers import ( + BaseGradeMangerSLZ, +) from backend.service.constants import GroupMemberType, RoleType @@ -73,3 +81,23 @@ class SubjectSLZ(serializers.Serializer): class FreezeSubjectResponseSLZ(serializers.Serializer): type = serializers.CharField(label="SubjectType") id = serializers.CharField(label="SubjectID") + + +class SuperManagerSourceSystemSLZ(ManagementSourceSystemSLZ): + class Meta: + ref_name = "V1SuperManagerSourceSystemSLZ" + + +class SuperManagerGradeManagerCreateSLZ(ManagementGradeManagerCreateSLZ): + class Meta: + ref_name = "V1SuperManagerGradeManagerCreateSLZ" + + +class SuperManagerGradeManagerUpdateSLZ(ManagementGradeManagerUpdateSLZ): + class Meta: + ref_name = "V1SuperManagerGradeManagerUpdateSLZ" + + +class SuperManagerGradeManagerBasicInfoSLZ(ManagementGradeManagerBasicInfoSLZ): + class Meta: + ref_name = "V1SuperManagerGradeManagerBasicInfoSLZ" diff --git a/saas/backend/api/admin/urls.py b/saas/backend/api/admin/urls.py index cb623bbb9..b9d8ae39d 100644 --- a/saas/backend/api/admin/urls.py +++ b/saas/backend/api/admin/urls.py @@ -8,12 +8,28 @@ 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 django.urls import path, include from . import views urlpatterns = [ - # 用户组 + path( + "super_managers/", + include( + [ + path( + "grade_managers/", + views.SuperManagerGradeManagerViewSet.as_view({"get": "list", "post": "create"}), + name="open.admin.super_manager.grade_manager", + ), + path( + "grade_managers//", + views.SuperManagerGradeManagerViewSet.as_view({"put": "update"}), + name="open.admin.super_manager.grade_manager", + ), + ] + ), + ), path("groups/", views.AdminGroupViewSet.as_view({"get": "list"}), name="open.admin.group"), # 用户组成员 path( diff --git a/saas/backend/api/admin/views/__init__.py b/saas/backend/api/admin/views/__init__.py index a6c8f3fcb..79dedb6b8 100644 --- a/saas/backend/api/admin/views/__init__.py +++ b/saas/backend/api/admin/views/__init__.py @@ -9,6 +9,7 @@ specific language governing permissions and limitations under the License. """ from .audit import AdminAuditEventViewSet +from .grade_manager import SuperManagerGradeManagerViewSet from .group import AdminGroupMemberViewSet, AdminGroupViewSet from .role import AdminSuperManagerMemberViewSet, AdminSystemManagerMemberViewSet from .subject import ( @@ -32,4 +33,5 @@ "AdminSubjectFreezeViewSet", "AdminSubjectPermissionCleanupViewSet", "AdminSubjectPermissionExistsViewSet", + "SuperManagerGradeManagerViewSet" ] diff --git a/saas/backend/api/admin/views/grade_manager.py b/saas/backend/api/admin/views/grade_manager.py new file mode 100644 index 000000000..3cc8b5876 --- /dev/null +++ b/saas/backend/api/admin/views/grade_manager.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-权限中心(BlueKing-IAM) 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.db import transaction +from drf_yasg.utils import swagger_auto_schema +from rest_framework import serializers, status +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from backend.api.admin.constants import VerifyApiParamLocationEnum, AdminAPIEnum +from backend.api.admin.mixins import SuperManagerAPIPermissionCheckMixin +from backend.api.admin.permissions import AdminAPIPermission +from backend.api.admin.serializers import ( + SuperManagerGradeManagerBasicInfoSLZ, + SuperManagerSourceSystemSLZ, + SuperManagerGradeManagerUpdateSLZ, + SuperManagerGradeManagerCreateSLZ, +) +from backend.api.authentication import ESBAuthentication +from backend.api.admin.filters import GradeManagerFilter + +from backend.apps.role.audit import ( + RoleCreateAuditProvider, + RoleUpdateAuditProvider, +) +from backend.apps.role.models import Role, RoleSource +from backend.apps.role.serializers import RoleIdSLZ +from backend.apps.role.tasks import sync_subset_manager_subject_scope +from backend.audit.audit import audit_context_setter, view_audit_decorator +from backend.biz.role import RoleBiz, RoleCheckBiz + +from backend.common.lock import gen_role_upsert_lock +from backend.common.pagination import CustomPageNumberPagination +from backend.service.constants import RoleSourceType, RoleType +from backend.trans.open_management import GradeManagerTrans + + +class SuperManagerGradeManagerViewSet(SuperManagerAPIPermissionCheckMixin, GenericViewSet): + """分级管理员""" + + authentication_classes = [ESBAuthentication] + permission_classes = [AdminAPIPermission] + admin_api_permission = { + "create": (VerifyApiParamLocationEnum.SYSTEM_IN_BODY.value, AdminAPIEnum.GRADE_MANAGER_CREATE.value), + "list": (VerifyApiParamLocationEnum.SYSTEM_IN_QUERY.value, AdminAPIEnum.GRADE_MANAGER_LIST.value), + "update": (VerifyApiParamLocationEnum.ROLE_IN_PATH.value, AdminAPIEnum.GRADE_MANAGER_UPDATE.value), + } + + lookup_field = "id" + queryset = Role.objects.filter(type=RoleType.GRADE_MANAGER.value).order_by("-updated_time") + pagination_class = CustomPageNumberPagination + filterset_class = GradeManagerFilter + + biz = RoleBiz() + role_check_biz = RoleCheckBiz() + trans = GradeManagerTrans() + + @swagger_auto_schema( + operation_description="创建分级管理员", + request_body=SuperManagerGradeManagerCreateSLZ(label="创建分级管理员"), + responses={status.HTTP_201_CREATED: RoleIdSLZ(label="分级管理员ID")}, + tags=["management.role"], + ) + @view_audit_decorator(RoleCreateAuditProvider) + def create(self, request, *args, **kwargs): + """ + 创建分级管理员 + """ + serializer = SuperManagerGradeManagerCreateSLZ(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + + # API里数据鉴权: 不可超过接入系统可管控的授权系统范围 + source_system_id = data["system"] + auth_system_ids = list({i["system"] for i in data["authorization_scopes"]}) + self.verify_system_scope(source_system_id, auth_system_ids) + + # 检查该系统可创建的分级管理员数量是否超限 + self.role_check_biz.check_grade_manager_of_system_limit(source_system_id) + + # 兼容member格式 + data["members"] = [{"username": username} for username in data["members"]] + + # 转换为RoleInfoBean,用于创建时使用 + role_info = self.trans.to_role_info(data, source_system_id=source_system_id) + + with gen_role_upsert_lock(data["name"]): + # 名称唯一性检查 + self.role_check_biz.check_grade_manager_unique_name(data["name"]) + + with transaction.atomic(): + # 创建角色 + role = self.biz.create_grade_manager(role_info, request.user.username) + + # 记录role创建来源信息 + RoleSource.objects.create( + role_id=role.id, source_type=RoleSourceType.API.value, source_system_id=source_system_id + ) + + # 审计 + audit_context_setter(role=role) + + return Response({"id": role.id}) + + @swagger_auto_schema( + operation_description="更新分级管理员", + request_body=SuperManagerGradeManagerUpdateSLZ(label="更新分级管理员"), + responses={status.HTTP_200_OK: serializers.Serializer()}, + tags=["super_manager.role"], + ) + @view_audit_decorator(RoleUpdateAuditProvider) + def update(self, request, *args, **kwargs): + """ + 更新分级管理员 + Note: 这里可授权范围和可授权人员范围均是全覆盖的,只对body里传入的字段进行更新 + """ + role = self.get_object() + + serializer = SuperManagerGradeManagerUpdateSLZ(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + + if "authorization_scopes" in data: + # API里数据鉴权: 不可超过接入系统可管控的授权系统范围 + role_source = RoleSource.objects.get(source_type=RoleSourceType.API.value, role_id=role.id) + auth_system_ids = list({i["system"] for i in data["authorization_scopes"]}) + self.verify_system_scope(role_source.source_system_id, auth_system_ids) + + # 转换为RoleInfoBean + role_info = self.trans.to_role_info_for_update(data) + + # 数据校验 + if "name" in data: + with gen_role_upsert_lock(data["name"]): + # 名称唯一性检查 + self.role_check_biz.check_grade_manager_unique_name(data["name"], role.name) + + # 更新 + self.biz.update(role, role_info, request.user.username) + else: + # 更新 + self.biz.update(role, role_info, request.user.username) + + if role.type == RoleType.GRADE_MANAGER.value and "subject_scopes" in role_info.get_partial_fields(): + sync_subset_manager_subject_scope.delay(role.id) + + # 审计 + audit_context_setter(role=role) + + return Response({}) + + @swagger_auto_schema( + operation_description="分级管理员列表", + query_serializer=SuperManagerSourceSystemSLZ(), + responses={status.HTTP_200_OK: SuperManagerGradeManagerBasicInfoSLZ(many=True)}, + tags=["super_manager.role.member"], + ) + def list(self, request, *args, **kwargs): + serializer = SuperManagerSourceSystemSLZ(data=request.query_params) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + + role_ids = list( + RoleSource.objects.filter( + source_system_id=data["system"], + source_type=RoleSourceType.API.value, + ).values_list("role_id", flat=True) + ) + + queryset = self.queryset.filter(id__in=role_ids) + queryset = self.filter_queryset(queryset) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = SuperManagerGradeManagerBasicInfoSLZ(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = SuperManagerGradeManagerBasicInfoSLZ(queryset, many=True) + return Response(serializer.data) From 0825619516a34dadd398c46ac412209ddf999a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E4=BD=A9=E7=8F=8A=5Bshiisa=5D?= Date: Thu, 21 Mar 2024 16:32:27 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=B1=BB=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81=E8=B6=85?= =?UTF-8?q?=E7=AE=A1=E8=A7=92=E8=89=B2=20#2264?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- saas/backend/api/admin/urls.py | 2 +- saas/backend/api/admin/views/grade_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/saas/backend/api/admin/urls.py b/saas/backend/api/admin/urls.py index b9d8ae39d..9d6e370d5 100644 --- a/saas/backend/api/admin/urls.py +++ b/saas/backend/api/admin/urls.py @@ -26,7 +26,7 @@ "grade_managers//", views.SuperManagerGradeManagerViewSet.as_view({"put": "update"}), name="open.admin.super_manager.grade_manager", - ), + ), ] ), ), diff --git a/saas/backend/api/admin/views/grade_manager.py b/saas/backend/api/admin/views/grade_manager.py index 3cc8b5876..111156ac6 100644 --- a/saas/backend/api/admin/views/grade_manager.py +++ b/saas/backend/api/admin/views/grade_manager.py @@ -49,7 +49,7 @@ class SuperManagerGradeManagerViewSet(SuperManagerAPIPermissionCheckMixin, Gener permission_classes = [AdminAPIPermission] admin_api_permission = { "create": (VerifyApiParamLocationEnum.SYSTEM_IN_BODY.value, AdminAPIEnum.GRADE_MANAGER_CREATE.value), - "list": (VerifyApiParamLocationEnum.SYSTEM_IN_QUERY.value, AdminAPIEnum.GRADE_MANAGER_LIST.value), + "list": (VerifyApiParamLocationEnum.SYSTEM_IN_QUERY.value, AdminAPIEnum.GRADE_MANAGER_LIST.value), "update": (VerifyApiParamLocationEnum.ROLE_IN_PATH.value, AdminAPIEnum.GRADE_MANAGER_UPDATE.value), }