Skip to content

Commit

Permalink
feat(open api): apply policy support custom ticket (#2685) (#2686)
Browse files Browse the repository at this point in the history
* feat(open api): apply policy support custom ticket (#2686 
* fix: mypy (#2688)
  • Loading branch information
nannan00 authored Jun 4, 2024
1 parent 8e8ebd4 commit 7d1c102
Show file tree
Hide file tree
Showing 21 changed files with 309 additions and 48 deletions.
4 changes: 2 additions & 2 deletions saas/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ repos:
entry: mypy --config-file=saas/pyproject.toml saas
- id: import-linter
name: import-linter
language: python
language: system
pass_filenames: false
entry: cd saas && lint-imports --config=./.importlinter && cd ..
entry: bash -c "cd saas && lint-imports --config=.importlinter"
- id: pytest
name: pytest
language: python
Expand Down
10 changes: 0 additions & 10 deletions saas/__init__.py

This file was deleted.

96 changes: 96 additions & 0 deletions saas/backend/api/application/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,99 @@ 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 len(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 = [
str(ind + 1) 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_with_custom_ticket",
),
]
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(AccessSystemApplicationCustomPolicyResultSLZ(applications, many=True).data)
2 changes: 1 addition & 1 deletion saas/backend/biz/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def list_by_subject(self, system_id: str, role, subject: Subject, hidden: bool =
action_list = ActionBeanList(actions)

if hidden:
action_list = ActionList(action_list.list_not_hidden())
action_list = ActionBeanList(action_list.list_not_hidden())

policies = self.policy_svc.list_by_subject(system_id, subject)
action_expired_at = {policy.action_id: policy.expired_at for policy in policies}
Expand Down
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: 7 additions & 1 deletion saas/backend/biz/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,12 @@ 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)

return p and p.has_resource_group_list(policy.resource_groups)

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

Expand Down Expand Up @@ -1471,7 +1477,7 @@ def list_expired(self, subject: Subject, expired_at: int) -> List[ExpiredPolicy]

# 查询saas policy id
all_action_id = {p.action_id for p in backend_policies}
action_id_dict = self.svc.get_action_id_dict(subject, all_action_id)
action_id_dict = self.svc.get_action_id_dict(subject, list(all_action_id))

# 取策略详情
system_ids = defaultdict(list)
Expand Down
2 changes: 1 addition & 1 deletion saas/backend/biz/related_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ def _check_path_by_instance_selection(
不同类型的实例视图匹配使用前缀匹配
"""
path = list(path.copy())
path = list(path.copy()) # type: ignore
for selection in instance_selections:
# 资源类型不同时, 截取视图长度的拓扑
if len(path) > len(selection.resource_type_chain):
Expand Down
10 changes: 5 additions & 5 deletions saas/backend/biz/subject_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def get_subject_template_group_count(
params = [subject.type, subject.id]
if id:
where_conditions.append("a.id = %s")
params.append(id)
params.append(id) # type: ignore
if name:
where_conditions.append("a.name LIKE %s")
params.append("%" + name + "%")
Expand All @@ -283,7 +283,7 @@ def get_subject_template_group_count(
where_conditions.append("a.hidden = 0")
if group_ids:
where_conditions.append("a.id IN %s")
params.append(tuple(group_ids))
params.append(tuple(group_ids)) # type: ignore
if system_id:
where_conditions.append("a.source_system_id = %s")
params.append(system_id)
Expand Down Expand Up @@ -397,7 +397,7 @@ def list_paging_subject_template_group(
params = [subject.type, subject.id]
if id:
where_conditions.append("a.id = %s")
params.append(id)
params.append(id) # type: ignore
if name:
where_conditions.append("a.name LIKE %s")
params.append("%" + name + "%")
Expand All @@ -408,12 +408,12 @@ def list_paging_subject_template_group(
where_conditions.append("a.hidden = 0")
if group_ids:
where_conditions.append("a.id IN %s")
params.append(tuple(group_ids))
params.append(tuple(group_ids)) # type: ignore
if system_id:
where_conditions.append("a.source_system_id = %s")
params.append(system_id)

params.extend([limit, offset])
params.extend([limit, offset]) # type: ignore

sql_query = """
SELECT
Expand Down
6 changes: 4 additions & 2 deletions saas/backend/component/iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union

from django.conf import settings

Expand Down Expand Up @@ -478,7 +478,9 @@ def list_exist_subjects_before_expired_at(subjects: List[Dict], expired_at: int)
return _call_iam_api(http_post, url_path, data=data)


def list_group_subject_before_expired_at(expired_at: int, limit: int = 10, offset: int = 0) -> List:
def list_group_subject_before_expired_at(
expired_at: int, limit: int = 10, offset: int = 0
) -> Dict[str, Union[List, int]]:
"""
查询已过期的关系
"""
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_prefix: 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
Loading

0 comments on commit 7d1c102

Please sign in to comment.