Skip to content

Commit

Permalink
feat(open api): apply policy support custom ticket (#2685)
Browse files Browse the repository at this point in the history
  • Loading branch information
nannan00 authored Jun 3, 2024
1 parent 8e8ebd4 commit c68fac6
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 25 deletions.
10 changes: 0 additions & 10 deletions saas/__init__.py

This file was deleted.

94 changes: 94 additions & 0 deletions saas/backend/api/application/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,97 @@ class ApprovalBotRoleCallbackSLZ(serializers.Serializer):
expired_at_before = serializers.IntegerField(label="过期时间")
expired_at_after = serializers.IntegerField(label="过期时间")
month = serializers.IntegerField(label="续期月数")


class ASResourceTypeWithCustomTicketSLZ(serializers.Serializer):
"""
接入系统申请操作的资源类型
"""

system = serializers.CharField(label="系统ID")
type = serializers.CharField(label="资源类型")
instance = serializers.ListField(
label="资源拓扑", child=ASInstanceSLZ(label="资源实例"), required=False, allow_empty=True, default=list
)
attributes = serializers.ListField(
label="属性", default=list, required=False, child=AttributeSLZ(label="属性"), allow_empty=True
)


class ASActionWithCustomTicketSLZ(serializers.Serializer):
id = serializers.CharField(label="操作ID")
related_resource_types = serializers.ListField(
label="关联资源类型", child=ASResourceTypeWithCustomTicketSLZ(label="资源类型"), allow_empty=True, default=list
)

ticket_content = serializers.DictField(label="单条权限的审批单内容", required=False, allow_empty=True, default=dict)


class ASApplicationCustomPolicyWithCustomTicketSLZ(serializers.Serializer):
"""接入系统自定义权限申请单据创建"""

applicant = serializers.CharField(label="申请者的用户名", max_length=32)
reason = serializers.CharField(label="申请理由", max_length=255)
expired_at = serializers.IntegerField(
label="过期时间", required=False, default=0, min_value=0, max_value=PERMANENT_SECONDS
)

ticket_title_prefix = serializers.CharField(label="审批单标题前缀", required=False, allow_blank=True, default="")
ticket_content_template = serializers.DictField(label="审批单内容模板", required=False, allow_empty=True, default=dict)

system = serializers.CharField(label="系统ID")
actions = serializers.ListField(label="申请操作", child=ASActionWithCustomTicketSLZ(label="操作"), allow_empty=False)

def validate_expired_at(self, value):
"""
验证过期时间
"""
if 0 < value <= (time.time()):
raise serializers.ValidationError("greater than now timestamp")
return value

def validate(self, data):
# 自定义 ITSM 单据展示内容
content_template = data["ticket_content_template"]
if content_template:
# 必须满足 ITSM 的单据数据结构
if "schemes" not in content_template or "form_data" not in content_template:
raise serializers.ValidationError(
{"ticket_content_template": ["ticket_content_template 中必须包含 schemes 和 form_data "]}
)

if not isinstance(content_template["form_data"], list) or len(content_template["form_data"]) == 0:
raise serializers.ValidationError(
{"ticket_content_template": ["ticket_content_template 中必须包含 form_data,且 form_data 必须为非空数组"]}
)

# IAM 所需的策略 Form (索引)
policy_forms = [
i
for i in content_template["form_data"]
if isinstance(i, dict)
and i.get("scheme") == "policy_table_scheme"
and isinstance(i.get("value"), list)
]
if policy_forms != 1:
raise serializers.ValidationError(
{
"ticket_content_template": [
"ticket_content_template['form_data'] 必须"
"包含 IAM 指定 scheme 为 iam_policy_table_scheme 且 value 为列表的项,"
]
},
)
# 必须每条权限都有配置单据所需渲染内容
empty_ticket_content_actions = [ind for ind, a in enumerate(data["actions"]) if not a["ticket_content"]]
if len(empty_ticket_content_actions) > 0:
raise serializers.ValidationError(
{
"actions": [
f"当 ticket_content_template 不为空时,所有权限的 ticket_content 都必须非空,当前请求中,"
f"第 {','.join(empty_ticket_content_actions)} 条权限的 ticket_content 为空"
]
}
)

return data
5 changes: 5 additions & 0 deletions saas/backend/api/application/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@
path("approval_bot/user/", views.ApprovalBotUserCallbackView.as_view(), name="open.approval_bot_user"),
path("approval_bot/role/", views.ApprovalBotRoleCallbackView.as_view(), name="open.approval_bot_role"),
path("<str:sn>/", views.ApplicationDetailView.as_view({"get": "retrieve"}), name="open.application_detail"),
path(
"policies/with-custom-ticket/",
views.ApplicationCustomPolicyWithCustomTicketView.as_view(),
name="open.application_policy",
),
]
42 changes: 42 additions & 0 deletions saas/backend/api/application/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
AccessSystemApplicationUrlSLZ,
ApprovalBotRoleCallbackSLZ,
ApprovalBotUserCallbackSLZ,
ASApplicationCustomPolicyWithCustomTicketSLZ,
)


Expand Down Expand Up @@ -283,3 +284,44 @@ def post(self, request):
)

return Response({})


class ApplicationCustomPolicyWithCustomTicketView(views.APIView):
"""
创建自定义权限申请单 - 允许单据自定义审批内容
"""

authentication_classes = [ESBAuthentication]
permission_classes = [IsAuthenticated]

access_system_application_trans = AccessSystemApplicationTrans()
application_biz = ApplicationBiz()

@swagger_auto_schema(
operation_description="创建自定义权限申请单-允许单据自定义审批内容",
request_body=ASApplicationCustomPolicyWithCustomTicketSLZ(),
responses={status.HTTP_200_OK: AccessSystemApplicationCustomPolicyResultSLZ(label="申请单信息", many=True)},
tags=["open"],
)
def post(self, request):
serializer = ASApplicationCustomPolicyWithCustomTicketSLZ(data=request.data)
serializer.is_valid(raise_exception=True)

data = serializer.validated_data
username = data["applicant"]

# 将Dict数据转换为创建单据所需的数据结构
(
application_data,
policy_ticket_contents,
) = self.access_system_application_trans.from_grant_policy_with_custom_ticket_application(username, data)
# 创建单据
applications = self.application_biz.create_for_policy(
ApplicationType.GRANT_ACTION.value,
application_data,
data["ticket_content_template"] or None,
policy_ticket_contents,
data["ticket_title_prefix"],
)

return Response([{"id": a.id, "sn": a.sn} for a in applications])
32 changes: 28 additions & 4 deletions saas/backend/biz/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,12 @@ def _gen_application_system(self, system_id: str) -> ApplicationSystem:
return parse_obj_as(ApplicationSystem, system)

def create_for_policy(
self, application_type: ApplicationType, data: ActionApplicationDataBean
self,
application_type: ApplicationType,
data: ActionApplicationDataBean,
content_template: Optional[Dict[str, Any]] = None,
policy_contents: Optional[List[Tuple[PolicyBean, Any]]] = None,
title_prefix: str = "",
) -> List[Application]:
"""自定义权限"""
# 1. 提前查询部分信息
Expand Down Expand Up @@ -540,12 +545,31 @@ def create_for_policy(
applicants=data.applicants,
),
)
new_data_list.append((application_data, process))

# 组装外部传入的 itsm 单据数据
content: Optional[Dict[str, Any]] = None
if content_template and policy_contents:
content = deepcopy(content_template)
policy_form_value = [c for p, c in policy_contents if policy_list.contains_policy(p)]
for c in content["form_data"]:
if (
isinstance(c, dict)
and c.get("scheme") == "policy_table_scheme"
and isinstance(c.get("value"), list)
):
c["value"] = policy_form_value

new_data_list.append((application_data, process, content))

# 8. 循环创建申请单
applications = []
for _data, _process in new_data_list:
application = self.svc.create_for_policy(_data, _process)
for _data, _process, _content in new_data_list:
application = self.svc.create_for_policy(
_data,
_process,
approval_content=_content,
approval_title_prefix=title_prefix,
)
applications.append(application)

return applications
Expand Down
8 changes: 8 additions & 0 deletions saas/backend/biz/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,14 @@ def sub(self, policy_list: "PolicyBeanList") -> "PolicyBeanList":
pass
return PolicyBeanList(self.system_id, subtraction)

def contains_policy(self, policy: PolicyBean):
"""是否包含策略"""
p = self.get(policy.action_id)
if p and p.has_resource_group_list(policy.resource_groups):
return True

return False

def to_svc_policies(self):
return parse_obj_as(List[Policy], self.policies)

Expand Down
7 changes: 6 additions & 1 deletion saas/backend/plugins/application_ticket/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ def get_ticket(self, sn: str) -> ApplicationTicket:

@abc.abstractmethod
def create_for_policy(
self, data: GrantActionApplicationData, process: ApprovalProcessWithNodeProcessor, callback_url: str
self,
data: GrantActionApplicationData,
process: ApprovalProcessWithNodeProcessor,
callback_url: str,
approval_title: str = "",
approval_content: Optional[Dict] = None,
) -> str:
"""创建 - 申请或续期自定义权限单据"""
pass
Expand Down
25 changes: 19 additions & 6 deletions saas/backend/plugins/application_ticket/itsm/itsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,28 @@ def _generate_ticket_common_params(
}

def create_for_policy(
self, data: GrantActionApplicationData, process: ApprovalProcessWithNodeProcessor, callback_url: str
self,
data: GrantActionApplicationData,
process: ApprovalProcessWithNodeProcessor,
callback_url: str,
approval_title_prefix: str = "",
approval_content: Optional[Dict] = None,
) -> str:
"""创建 - 申请或续期自定义权限单据"""
params = self._generate_ticket_common_params(data, process, callback_url)
params["title"] = f"申请{data.content.system.name}{len(data.content.policies)}个操作权限"
params["content"] = {
"schemes": FORM_SCHEMES,
"form_data": [ActionTable.from_application(data.content).dict()],
} # 真正生成申请内容的核心入口点

if approval_title_prefix:
params["title"] = f"{approval_title_prefix} {len(data.content.policies)} 个操作权限"
else:
params["title"] = f"申请{data.content.system.name}{len(data.content.policies)}个操作权限"

if approval_content:
params["content"] = approval_content
else:
params["content"] = {
"schemes": FORM_SCHEMES,
"form_data": [ActionTable.from_application(data.content).dict()],
} # 真正生成申请内容的核心入口点

# 如果审批流程中包含资源审批人, 并且资源审批人不为空
# 增加 has_instance_approver 字段, 用于itsm审批流程走分支
Expand Down
17 changes: 15 additions & 2 deletions saas/backend/service/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,23 @@ def _create(
return application

def create_for_policy(
self, data: GrantActionApplicationData, process: ApprovalProcessWithNodeProcessor
self,
data: GrantActionApplicationData,
process: ApprovalProcessWithNodeProcessor,
approval_title_prefix: str = "",
approval_content: Optional[Dict] = None,
) -> Application:
"""创建或续期自定义权限申请单"""
return self._create(data, lambda callback_url: self.provider.create_for_policy(data, process, callback_url))
return self._create(
data,
lambda callback_url: self.provider.create_for_policy(
data,
process,
callback_url,
approval_title_prefix=approval_title_prefix,
approval_content=approval_content,
),
)

def create_for_group(
self,
Expand Down
59 changes: 57 additions & 2 deletions saas/backend/trans/open_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
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 Dict, List
from typing import Any, Dict, List, Optional, Tuple

from pydantic.tools import parse_obj_as

from backend.apps.organization.models import User as UserModel
from backend.biz.application import ActionApplicationDataBean
from backend.biz.policy import PolicyBeanList
from backend.biz.policy import PolicyBean, PolicyBeanList
from backend.service.constants import SubjectType
from backend.service.models import Applicant
from backend.trans.application import ApplicationDataTrans
Expand Down Expand Up @@ -107,3 +107,58 @@ def from_grant_policy_application(self, applicant: str, data: Dict) -> ActionApp
)

return application_data

def from_grant_policy_with_custom_ticket_application(
self, applicant: str, data: Dict
) -> Tuple[ActionApplicationDataBean, Optional[List[Tuple[PolicyBean, Any]]]]:
"""来着带自定义审批内容的自定义权限申请的数据转换"""

# 1. 将多条策略按 Action 合并为标准的策略数据
policy_list = PolicyBeanList(system_id=data["system"], policies=[])
policy_ticket_contents = []
for action in data["actions"]:
one_policy_data = {
"system": data["system"],
"actions": [
{
"id": action["id"],
"related_resource_types": [
{
"system": rrt["system"],
"type": rrt["type"],
# 实际上是将单个实例转换为标准的多实例结构
"instances": [rrt["instance"]] if "instance" in rrt else [],
"attributes": rrt.get("attributes", []),
}
for rrt in action["related_resource_types"]
],
}
],
}
if "expired_at" in data:
one_policy_data["expired_at"] = data["expired_at"]

# 转换数据结构
one_policy_list = self.to_policy_list(one_policy_data)

# 添加
policy_list.add(one_policy_list)

# 每条原始策略(未合并) 对应的审批单据内容
if data["ticket_content_template"]:
policy_ticket_contents.append((policy_list.policies[0], action["ticket_content"]))

# 2. 只对新增的策略进行申请,所以需要移除掉已有的权限
application_policy_list = self._gen_need_apply_policy_list(applicant, data["system"], policy_list)

# 3. 转换为ApplicationBiz创建申请单所需数据结构
user = UserModel.objects.get(username=applicant)

application_data = ActionApplicationDataBean(
applicant=applicant,
policy_list=application_policy_list,
applicants=[Applicant(type=SubjectType.USER.value, id=user.username, display_name=user.display_name)],
reason=data["reason"],
)

return application_data, policy_ticket_contents or None

0 comments on commit c68fac6

Please sign in to comment.