diff --git a/conftest.py b/conftest.py index 00c2ea42a7..11a8ba3c12 100644 --- a/conftest.py +++ b/conftest.py @@ -190,3 +190,9 @@ def create_audit_workflow(normal_user, create_resource_group): ) yield audit_wf audit_wf.delete() + + +@pytest.fixture +def clean_auth_group(db): + yield + Group.objects.all().delete() diff --git a/sql/engines/mssql.py b/sql/engines/mssql.py index 6d930b3f59..b7c16d5391 100644 --- a/sql/engines/mssql.py +++ b/sql/engines/mssql.py @@ -316,10 +316,6 @@ def filter_sql(self, sql="", limit_num=0): # 对查询sql增加limit限制 if re.match(r"^select", sql_lower): if sql_lower.find(" top ") == -1: - if sql_lower.find(" distinct ") > 0: - return sql_lower.replace( - "distinct", "distinct top {}".format(limit_num) - ) return sql_lower.replace("select", "select top {}".format(limit_num)) return sql.strip() diff --git a/sql/engines/tests.py b/sql/engines/tests.py index 88cea4d093..50939b2d2a 100644 --- a/sql/engines/tests.py +++ b/sql/engines/tests.py @@ -192,13 +192,6 @@ def test_filter_sql(self): check_result = new_engine.filter_sql(sql=banned_sql, limit_num=10) self.assertEqual(check_result, "select top 10 user from user_table") - def test_filter_sql_with_distinct(self): - new_engine = MssqlEngine(instance=self.ins1) - # 只抽查一个函数 - banned_sql = "select distinct * from user_table" - check_result = new_engine.filter_sql(sql=banned_sql, limit_num=10) - self.assertEqual(check_result, "select distinct top 10 * from user_table") - def test_execute_check(self): new_engine = MssqlEngine(instance=self.ins1) test_sql = ( diff --git a/sql/notify.py b/sql/notify.py index c68618ca52..1ac8c0b784 100755 --- a/sql/notify.py +++ b/sql/notify.py @@ -26,7 +26,7 @@ SqlWorkflowContent, ) from sql.utils.resource_group import auth_group_users -from sql.utils.workflow_audit import Audit +from sql.utils.workflow_audit import Audit, AuditV2 from sql_api.serializers import ( WorkflowContentSerializer, WorkflowAuditListSerializer, @@ -156,9 +156,8 @@ def render_audit(self): workflow_from = self.audit.create_user_display group_name = self.audit.group_name # 获取当前审批和审批流程 - workflow_auditors, current_workflow_auditors = Audit.review_info( - self.audit.workflow_id, self.audit.workflow_type - ) + audit_handler = AuditV2(workflow=self.workflow, audit=self.audit) + review_info = audit_handler.get_review_info() # workflow content, 即申请通过后要执行什么东西 # 执行的 SQL 语句, 授权的范围 if workflow_type == WorkflowType.QUERY: @@ -221,8 +220,8 @@ def render_audit(self): group_name, instance, db_name, - workflow_auditors, - current_workflow_auditors, + review_info.readable_info, + review_info.current_node.group.name, workflow_title, workflow_url, workflow_content, @@ -238,7 +237,7 @@ def render_audit(self): group_name, instance, db_name, - workflow_auditors, + review_info.readable_info, workflow_title, workflow_url, workflow_content, @@ -284,9 +283,8 @@ def render_execute(self): base_url = self.sys_config.get( "archery_base_url", "http://127.0.0.1:8000" ).rstrip("/") - audit_auth_group, current_audit_auth_group = Audit.review_info( - self.workflow.id, 2 - ) + audit_handler = AuditV2(workflow=self.workflow, audit=self.audit) + review_info = audit_handler.get_review_info() audit_id = Audit.detail_by_workflow_id(self.workflow.id, 2).audit_id url = "{base_url}/workflow/{audit_id}".format( base_url=base_url, audit_id=audit_id @@ -305,7 +303,7 @@ def render_execute(self): 组:{self.workflow.group_name} 目标实例:{self.workflow.instance.instance_name} 数据库:{self.workflow.db_name} -审批流程:{audit_auth_group} +审批流程:{review_info.readable_info} 工单名称:{self.workflow.workflow_name} 工单地址:{url} 工单详情预览:{preview}""" diff --git a/sql/templates/archivedetail.html b/sql/templates/archivedetail.html index 441867ce22..8e6fecc3b4 100644 --- a/sql/templates/archivedetail.html +++ b/sql/templates/archivedetail.html @@ -21,6 +21,14 @@

工单名称:{{ archive_config.title }}
+

+ 审批流 +

+
+ {% include "workflow_display.html" %} +

+ 其他信息 +

@@ -28,12 +36,6 @@

工单名称:{{ archive_config.title }} 申请人 -

- @@ -65,12 +67,6 @@

工单名称:{{ archive_config.title }} {{ archive_config.user_display }} -

- @@ -240,7 +236,7 @@

归档条件


{% endif %} {% if archive_config.status == 0 %} - {% if is_can_review %} + {% if can_review %}
diff --git a/sql/templates/detail.html b/sql/templates/detail.html index 9fa999b4c7..95e258d993 100644 --- a/sql/templates/detail.html +++ b/sql/templates/detail.html @@ -18,6 +18,14 @@


+

+ 审批流 +

+
+ {% include "workflow_display.html" %} +

+ 其他信息 +

- 审批流程 - - 当前审批 - 实例 - {{ audit_auth_group }} - - {{ current_audit_auth_group }} - {{ archive_config.src_instance }}
@@ -25,12 +33,6 @@

- - @@ -65,12 +67,6 @@

- - diff --git a/sql/templates/queryapplydetail.html b/sql/templates/queryapplydetail.html index 324b20bf55..402a8e4a40 100644 --- a/sql/templates/queryapplydetail.html +++ b/sql/templates/queryapplydetail.html @@ -10,6 +10,14 @@

工单名称:{{ workflow_detail.title }}
+

+ 审批流 +

+
+ {% include "workflow_display.html" %} +

+ 其他信息 +

发起人 - 审批流程 - - 当前审批 - 目标实例 {{ workflow_detail.engineer_display }} - {{ audit_auth_group }} - - {{ current_audit_auth_group }} - {{ workflow_detail.instance.instance_name }}
@@ -17,12 +25,6 @@

工单名称:{{ workflow_detail.title }} 申请人 -

- @@ -54,12 +56,6 @@

工单名称:{{ workflow_detail.title }} {{ workflow_detail.user_display }} -

- diff --git a/sql/test_archiver.py b/sql/test_archiver.py index 33e444e0f3..6f5025fc1a 100644 --- a/sql/test_archiver.py +++ b/sql/test_archiver.py @@ -4,10 +4,11 @@ from django.conf import settings from django.contrib.auth.models import Permission from django.test import TestCase, Client +from pytest_django.asserts import assertTemplateUsed from common.config import SysConfig from common.utils.const import WorkflowStatus, WorkflowType -from sql.utils.workflow_audit import AuditSetting +from sql.utils.workflow_audit import AuditSetting, AuditV2 from sql.archiver import add_archive_task, archive from sql.models import ( Instance, @@ -299,3 +300,23 @@ def test_archive_log(self, _async_task): self.client.force_login(self.superuser) r = self.client.post(path="/archive/log/", data=data) self.assertDictEqual(json.loads(r.content), {"total": 0, "rows": []}) + + +def test_archive_detail_view( + archive_apply, + resource_group, + admin_client, + fake_generate_audit_setting, + create_auth_group, +): + audit = AuditV2(workflow=archive_apply, resource_group=resource_group.group_name) + audit.create_audit() + audit.workflow.save() + response = admin_client.get(f"/archive/{archive_apply.id}/") + assert response.status_code == 200 + assertTemplateUsed(response, "archivedetail.html") + review_info = response.context["review_info"] + assert len(review_info.nodes) == len( + fake_generate_audit_setting.return_value.audit_auth_groups + ) + assert review_info.nodes[0].group.name == create_auth_group.name diff --git a/sql/test_notify.py b/sql/test_notify.py index 8cc9412595..71d36ec474 100644 --- a/sql/test_notify.py +++ b/sql/test_notify.py @@ -131,9 +131,9 @@ def setUp(self): workflow_type=1, workflow_title="申请标题", workflow_remark="申请备注", - audit_auth_groups="1,2,3", - current_audit="1", - next_audit="2", + audit_auth_groups=",".join([str(self.aug.id)]), + current_audit=str(self.aug.id), + next_audit="-1", current_status=0, ) self.audit_query_detail = WorkflowAuditDetail.objects.create( @@ -167,9 +167,9 @@ def setUp(self): workflow_type=3, workflow_title=self.archive_apply.title, workflow_remark="申请备注", - audit_auth_groups="1,2,3", - current_audit="1", - next_audit="2", + audit_auth_groups=",".join([str(self.aug.id)]), + current_audit=str(self.aug.id), + next_audit="-1", current_status=0, ) diff --git a/sql/test_workflow.py b/sql/test_workflow.py index a36d145ad0..87702e3eee 100644 --- a/sql/test_workflow.py +++ b/sql/test_workflow.py @@ -19,7 +19,8 @@ def test_get_sql_workflow( assert response.status_code == 200 assertTemplateUsed(response, "detail.html") # 展示审批人用户名 - assert ( - response.context["current_audit_auth_group"] - == f"{create_auth_group.name}: {super_user.username}" + review_info = response.context["review_info"] + assert len(review_info.nodes) == len( + fake_generate_audit_setting.return_value.audit_auth_groups ) + assert review_info.nodes[0].group.name == create_auth_group.name diff --git a/sql/tests.py b/sql/tests.py index d96dab16d7..f72ff2e58e 100644 --- a/sql/tests.py +++ b/sql/tests.py @@ -825,52 +825,6 @@ def test_alter_run_date(self, _can_review): r, f"/detail/{self.wf1.id}/", fetch_redirect_response=False ) - @patch("sql.utils.workflow_audit.Audit.logs") - @patch("sql.utils.workflow_audit.Audit.detail_by_workflow_id") - @patch("sql.utils.workflow_audit.Audit.review_info") - @patch("sql.utils.workflow_audit.Audit.can_review") - def testWorkflowDetailView(self, _can_review, _review_info, _detail_by_id, _logs): - """测试工单详情""" - _review_info.return_value = ("some_auth_group", "current_auth_group") - _can_review.return_value = False - _detail_by_id.return_value.audit_id = 123 - _logs.return_value.latest("id").operation_info = "" - c = Client() - c.force_login(self.u1) - r = c.get("/detail/{}/".format(self.wf1.id)) - expected_status_display = r"""id="workflow_detail_disaply">已正常结束""" - self.assertContains(r, expected_status_display) - exepcted_status = r"""id="workflow_detail_status">workflow_finish""" - self.assertContains(r, exepcted_status) - - # 测试执行详情解析失败 - self.wfc1.execute_result = "cannotbedecode:1,:" - self.wfc1.save() - r = c.get("/detail/{}/".format(self.wf1.id)) - self.assertContains(r, expected_status_display) - self.assertContains(r, exepcted_status) - - # 执行详情为空 - self.wfc1.review_content = [ - { - "id": 1, - "stage": "CHECKED", - "errlevel": 0, - "stagestatus": "Audit completed", - "errormessage": "None", - "sql": "use archery", - "affected_rows": 0, - "sequence": "'0_0_0'", - "backup_dbname": "None", - "execute_time": "0", - "sqlsha1": "", - "actual_affected_rows": "", - } - ] - self.wfc1.execute_result = "" - self.wfc1.save() - r = c.get("/detail/{}/".format(self.wf1.id)) - def testWorkflowListView(self): """测试工单列表""" c = Client() diff --git a/sql/utils/test_workflow_audit.py b/sql/utils/test_workflow_audit.py index a8d2628b7e..29315d9384 100644 --- a/sql/utils/test_workflow_audit.py +++ b/sql/utils/test_workflow_audit.py @@ -302,34 +302,6 @@ def test_can_review_no_prem_exception( self.user, self.audit.workflow_id, self.audit.workflow_type ) - def test_review_info_no_review(self): - """测试获取当前工单审批流程和当前审核组,无需审批""" - self.audit.workflow_type = WorkflowType.SQL_REVIEW - self.audit.workflow_id = self.wf.id - self.audit.audit_auth_groups = "" - self.audit.current_audit = "-1" - self.audit.save() - audit_auth_group, current_audit_auth_group = Audit.review_info( - self.audit.workflow_id, self.audit.workflow_type - ) - self.assertEqual(audit_auth_group, "无需审批") - self.assertEqual(current_audit_auth_group, None) - - def test_review_info(self): - """测试获取当前工单审批流程和当前审核组,无需审批""" - aug = Group.objects.create(name="DBA") - self.user.groups.add(aug) - self.audit.workflow_type = WorkflowType.SQL_REVIEW - self.audit.workflow_id = self.wf.id - self.audit.audit_auth_groups = str(aug.id) - self.audit.current_audit = str(aug.id) - self.audit.save() - audit_auth_group, current_audit_auth_group = Audit.review_info( - self.audit.workflow_id, self.audit.workflow_type - ) - self.assertEqual(audit_auth_group, "DBA") - self.assertEqual(current_audit_auth_group, f"DBA: {self.user.username}") - def test_logs(self): """测试获取工单日志""" r = Audit.logs(self.audit.audit_id).first() @@ -543,3 +515,34 @@ def test_auto_review_not_applicable( audit.sys_config.set("auto_review_max_update_rows", 1000) # 全部条件满足, 自动审核通过 assert audit.is_auto_review() is True + + +def test_get_review_info( + sql_query_apply, + resource_group, + create_auth_group, + fake_generate_audit_setting, + clean_auth_group, +): + g2 = Group.objects.create(name="g2") + g3 = Group.objects.create(name="g3") + fake_generate_audit_setting.return_value = AuditSetting( + auto_pass=False, audit_auth_groups=[create_auth_group.id, g2.id, g3.id] + ) + audit = AuditV2(workflow=sql_query_apply) + audit.create_audit() + review_info = audit.get_review_info() + assert review_info.nodes[0].group.name == create_auth_group.name + assert review_info.nodes[0].is_current_node is True + assert review_info.nodes[0].is_passed_node is False + assert review_info.nodes[1].is_current_node is False + assert review_info.nodes[1].is_passed_node is False + + # 将当前节点设置为第二个节点, 重新生成 + audit.audit.current_audit = str(g2.id) + audit.audit.save() + review_info = audit.get_review_info() + assert review_info.nodes[0].is_current_node is False + assert review_info.nodes[0].is_passed_node is True + assert review_info.nodes[1].is_current_node is True + assert review_info.nodes[1].is_passed_node is False diff --git a/sql/utils/workflow_audit.py b/sql/utils/workflow_audit.py index 261d64b313..aa6cc25e0f 100644 --- a/sql/utils/workflow_audit.py +++ b/sql/utils/workflow_audit.py @@ -35,6 +35,45 @@ class AuditException(Exception): pass +@dataclass +class ReviewNode: + group: Group + is_current_node: bool = False + is_passed_node: bool = False + + +@dataclass +class ReviewInfo: + nodes: List[ReviewNode] = field(default_factory=list) + current_node_index: int = None + + @property + def readable_info(self) -> str: + """生成可读的工作流, 形如 g1(passed) -> g2(current) -> g3 + 一般用途是渲染消息 + """ + steps = [] + for index, n in enumerate(self.nodes): + if n.is_current_node: + self.current_node_index = index + steps.append(f"{n.group.name}(current)") + continue + if n.is_passed_node: + steps.append(f"{n.group.name}(passed)") + continue + steps.append(n.group.name) + return " -> ".join(steps) + + @property + def current_node(self) -> ReviewNode: + if self.current_node_index: + return self.nodes[self.current_node_index] + for index, n in enumerate(self.nodes): + if n.is_current_node: + self.current_node_index = n + return n + + @dataclass class AuditSetting: """ @@ -283,7 +322,7 @@ def create_audit(self) -> str: return "工单已正常提交" def can_operate(self, action: WorkflowAction, actor: Users): - """检查用户是否有权限做相关操作, 默认不返回, 如有权限问题, raise AuditException""" + """检查用户是否有权限做相关操作, 如有权限问题, raise AuditException, 无问题返回 True""" # 首先检查工单状态和相关操作是否匹配, 如已通过的工单不能再通过 allowed_actions = SUPPORTED_OPERATION_GRID.get(self.audit.current_status) if not allowed_actions: @@ -306,12 +345,12 @@ def can_operate(self, action: WorkflowAction, actor: Users): if action == WorkflowAction.ABORT: if actor.username != self.audit.create_user: raise AuditException(f"只有工单提交者可以撤回工单") - return + return True if action in [WorkflowAction.PASS, WorkflowAction.REJECT]: # 需要检查权限 # 超级用户可以审批所有工单 if actor.is_superuser: - return + return True # 看是否本人审核 if actor.username == self.audit.create_user and self.sys_config.get( "ban_self_audit" @@ -328,14 +367,14 @@ def can_operate(self, action: WorkflowAction, actor: Users): raise AuditException("当前审批权限组不存在, 请联系管理员检查并清洗错误数据") if not auth_group_users([audit_auth_group.name], self.resource_group_id): raise AuditException("用户不在当前审批审批节点的用户组内, 无权限审核") - return + return True if action in [ WorkflowAction.EXECUTE_START, WorkflowAction.EXECUTE_END, WorkflowAction.EXECUTE_SET_TIME, ]: # 一般是系统自动流转, 自动通过 - return + return True raise AuditException(f"不支持的操作, 无法判断权限") @@ -465,6 +504,55 @@ def operate_abort(self, actor: Users, remark: str) -> WorkflowAuditDetail: ) return workflow_audit_detail + def get_review_info(self) -> ReviewInfo: + """提供审批流各节点的状态 + 如果总体是待审核状态, 当前节点之前为已通过, 当前节点为当前节点, 未通过, 当前节点之后为未通过 + 如果总体为其他状态, 节点的属性都不设置, 均为默认值 + """ + self.get_audit_info() + review_nodes = [] + has_met_current_node = False + current_node_group_id = int(self.audit.current_audit) + for g in self.audit.audit_auth_groups.split(","): + g = int(g) + group_in_db = Group.objects.get(id=g) + if self.audit.current_status != WorkflowStatus.WAITING: + # 总体状态不是待审核, 不设置详细的属性 + review_nodes.append( + ReviewNode( + group=group_in_db, + ) + ) + continue + if current_node_group_id == g: + # 当前节点, 一定为未通过 + has_met_current_node = True + review_nodes.append( + ReviewNode( + group=group_in_db, + is_current_node=True, + is_passed_node=False, + ) + ) + continue + if has_met_current_node: + # 当前节点之后的节点, 一定为未通过 + review_nodes.append( + ReviewNode( + group=group_in_db, + is_passed_node=False, + ) + ) + continue + # 以上情况之外的情况, 一定为已经通过的节点 + review_nodes.append( + ReviewNode( + group=group_in_db, + is_passed_node=True, + ) + ) + return ReviewInfo(nodes=review_nodes) + class Audit(object): """老版 Audit, 建议不再更新新内容, 转而使用 AuditV2""" @@ -588,36 +676,6 @@ def get_workflow_applicant(workflow_id, workflow_type): result = True return result - # 获取当前工单审批流程和当前审核组 - @staticmethod - def review_info(workflow_id, workflow_type): - audit_info = WorkflowAudit.objects.get( - workflow_id=workflow_id, workflow_type=workflow_type - ) - if audit_info.audit_auth_groups == "": - audit_auth_group = "无需审批" - else: - try: - audit_auth_group = "->".join( - [ - Group.objects.get(id=auth_group_id).name - for auth_group_id in audit_info.audit_auth_groups.split(",") - ] - ) - except Exception: - audit_auth_group = audit_info.audit_auth_groups - if audit_info.current_audit == "-1": - current_audit_auth_group = None - else: - try: - auth_group_in_db = Group.objects.get(id=audit_info.current_audit) - users = auth_group_in_db.user_set.all() - users_display = ",".join(x.username for x in users) or "组内无用户, 请联系管理员" - current_audit_auth_group = f"{auth_group_in_db.name}: {users_display}" - except Exception: - current_audit_auth_group = audit_info.current_audit - return audit_auth_group, current_audit_auth_group - # 新增工单日志 @staticmethod def add_log( diff --git a/sql/views.py b/sql/views.py index ca3d69efe7..f6f9712caa 100644 --- a/sql/views.py +++ b/sql/views.py @@ -32,7 +32,7 @@ AuditEntry, TwoFactorAuthConfig, ) -from sql.utils.workflow_audit import Audit +from sql.utils.workflow_audit import Audit, AuditV2, AuditException from sql.utils.sql_review import ( can_execute, can_timingtask, @@ -40,7 +40,7 @@ can_view, can_rollback, ) -from common.utils.const import Const, WorkflowType +from common.utils.const import Const, WorkflowType, WorkflowAction from sql.utils.resource_group import user_groups, user_instances, auth_group_users import logging @@ -185,13 +185,12 @@ def submit_sql(request): def detail(request, workflow_id): """展示SQL工单详细页面""" workflow_detail = get_object_or_404(SqlWorkflow, pk=workflow_id) + audit_handler = AuditV2(workflow=workflow_detail) if not can_view(request.user, workflow_id): raise PermissionDenied + review_info = audit_handler.get_review_info() # 自动审批不通过的不需要获取下列信息 if workflow_detail.status != "workflow_autoreviewwrong": - # 获取当前审批和审批流程 - audit_auth_group, current_audit_auth_group = Audit.review_info(workflow_id, 2) - # 是否可审核 is_can_review = Audit.can_review(request.user, workflow_id, 2) # 是否可执行 TODO 这几个判断方法入参都修改为workflow对象,可减少多次数据库交互 @@ -213,18 +212,10 @@ def detail(request, workflow_id): last_operation_info = ( Audit.logs(audit_id=audit_id).latest("id").operation_info ) - # 等待审批的展示当前全部审批人 - if workflow_detail.status == "workflow_manreviewing": - _, current_audit_users_display = Audit.review_info( - workflow_id, WorkflowType.SQL_REVIEW - ) - last_operation_info += f",当前审批节点:{current_audit_users_display}" except Exception as e: logger.debug(f"无审核日志记录,错误信息{e}") last_operation_info = "" else: - audit_auth_group = "系统自动驳回" - current_audit_auth_group = "系统自动驳回" is_can_review = False is_can_execute = False is_can_timingtask = False @@ -254,9 +245,8 @@ def detail(request, workflow_id): "is_can_timingtask": is_can_timingtask, "is_can_cancel": is_can_cancel, "is_can_rollback": is_can_rollback, - "audit_auth_group": audit_auth_group, + "review_info": review_info, "manual": manual, - "current_audit_auth_group": current_audit_auth_group, "run_date": run_date, } return render(request, "detail.html", context) @@ -348,7 +338,8 @@ def queryapplydetail(request, apply_id): """查询权限申请详情页面""" workflow_detail = QueryPrivilegesApply.objects.get(apply_id=apply_id) # 获取当前审批和审批流程 - audit_auth_group, current_audit_auth_group = Audit.review_info(apply_id, 1) + audit_handler = AuditV2(workflow=workflow_detail) + review_info = audit_handler.get_review_info() # 是否可审核 is_can_review = Audit.can_review(request.user, apply_id, 1) @@ -369,9 +360,8 @@ def queryapplydetail(request, apply_id): context = { "workflow_detail": workflow_detail, - "audit_auth_group": audit_auth_group, + "review_info": review_info, "last_operation_info": last_operation_info, - "current_audit_auth_group": current_audit_auth_group, "is_can_review": is_can_review, } return render(request, "queryapplydetail.html", context) @@ -469,13 +459,15 @@ def archive_detail(request, id): """归档详情页面""" archive_config = ArchiveConfig.objects.get(pk=id) # 获取当前审批和审批流程、是否可审核 + audit_handler = AuditV2( + workflow=archive_config, resource_group=archive_config.resource_group + ) + review_info = audit_handler.get_review_info() try: - audit_auth_group, current_audit_auth_group = Audit.review_info(id, 3) - is_can_review = Audit.can_review(request.user, id, 3) - except Exception as e: - logger.debug(f"归档配置{id}无审核信息,{e}") - audit_auth_group, current_audit_auth_group = None, None - is_can_review = False + audit_handler.can_operate(WorkflowAction.PASS, request.user) + can_review = True + except AuditException: + can_review = False # 获取审核日志 if archive_config.status == 2: try: @@ -493,10 +485,9 @@ def archive_detail(request, id): context = { "archive_config": archive_config, - "audit_auth_group": audit_auth_group, + "review_info": review_info, "last_operation_info": last_operation_info, - "current_audit_auth_group": current_audit_auth_group, - "is_can_review": is_can_review, + "can_review": can_review, } return render(request, "archivedetail.html", context)
- 审批流程 - - 当前审批 - 实例 - {{ audit_auth_group }} - - {{ current_audit_auth_group }} - {{ workflow_detail.instance.instance_name }}