Skip to content

Commit

Permalink
SQL上线逻辑调整为:工程师提sql->审核人审核->dba执行
Browse files Browse the repository at this point in the history
新增DBA角色,该角色拥有审核、执行权限,去除原审核人、工程师角色的执行权限
仅审核通过状态的工单才可以交由DBA执行
优化邮件发送逻辑,通知DBA走抄送
登录失败默认也发送给DBA,不再单独配置接收人
  • Loading branch information
hhyo authored and lihuanhuan committed May 3, 2018
1 parent 119f7b1 commit ca53c2d
Show file tree
Hide file tree
Showing 7 changed files with 48 additions and 80 deletions.
12 changes: 5 additions & 7 deletions archer/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,11 @@
'handlers': ['default'],
'level': 'DEBUG',
},
'django.db': { # 打印SQL语句到console,方便开发
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
# 'django.db': { # 打印SQL语句到console,方便开发
# 'handlers': ['console'],
# 'level': 'DEBUG',
# 'propagate': True,
# },
'django.request': { # 打印SQL语句到console,方便开发
'handlers': ['console'],
'level': 'DEBUG',
Expand All @@ -238,8 +238,6 @@
MAIL_REVIEW_SMTP_PORT = 25
MAIL_REVIEW_FROM_ADDR = '[email protected]' # 发件人,也是登录SMTP server需要提供的用户名
MAIL_REVIEW_FROM_PASSWORD = '' # 发件人邮箱密码,如果为空则不需要login SMTP server
MAIL_REVIEW_DBA_ADDR = ['[email protected]', '[email protected]'] # DBA地址,执行完毕会发邮件给DBA,以list形式保存
MAIL_REVIEW_SECURE_ADDR = ['[email protected]', '[email protected]'] # 登录失败,等安全相关发送地址
# 是否过滤【DROP DATABASE】|【DROP TABLE】|【TRUNCATE PARTITION】|【TRUNCATE TABLE】等高危DDL操作:
# on是开,会首先用正则表达式匹配sqlContent,如果匹配到高危DDL操作,则判断为“自动审核不通过”;off是关,直接将所有的SQL语句提交给inception,对于上述高危DDL操作,只备份元数据
CRITICAL_DDL_ON_OFF = 'off'
Expand Down
2 changes: 1 addition & 1 deletion sql/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# 2.审核人:可以审核并执行SQL上线单的管理者、高级工程师、系统管理员们。
class users(AbstractUser):
display = models.CharField('显示的中文名', max_length=50)
role = models.CharField('角色', max_length=20, choices=(('工程师', '工程师'), ('审核人', '审核人')), default='工程师')
role = models.CharField('角色', max_length=20, choices=(('工程师', '工程师'), ('审核人', '审核人'), ('DBA', 'DBA')), default='工程师')
is_ldapuser = models.BooleanField('ldap用戶', default=False)

def __str__(self):
Expand Down
18 changes: 12 additions & 6 deletions sql/sendmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ def __init__(self):
self.MAIL_REVIEW_SMTP_PORT = int(getattr(settings, 'MAIL_REVIEW_SMTP_PORT'))
self.MAIL_REVIEW_FROM_ADDR = getattr(settings, 'MAIL_REVIEW_FROM_ADDR')
self.MAIL_REVIEW_FROM_PASSWORD = getattr(settings, 'MAIL_REVIEW_FROM_PASSWORD')
self.MAIL_REVIEW_DBA_ADDR = getattr(settings, 'MAIL_REVIEW_DBA_ADDR')

except AttributeError as a:
print("Error: %s" % a)
Expand All @@ -43,7 +42,7 @@ def _add_attachment(self, filename):

return file_msg

def _send(self, strTitle, strContent, listToAddr, filename_list=None):
def _send(self, strTitle, strContent, listToAddr, **kwargs):
'''''
发送邮件
'''
Expand All @@ -55,14 +54,21 @@ def _send(self, strTitle, strContent, listToAddr, filename_list=None):
main_msg.attach(text_msg)

# 添加附件
filename_list = kwargs.get('filename_list')
if filename_list:
for filename in filename_list:
for filename in kwargs['filename_list']:
file_msg = self._add_attachment(filename)
main_msg.attach(file_msg)

# 收发件人地址和邮件标题:
main_msg['From'] = formataddr(["archer 通知", self.MAIL_REVIEW_FROM_ADDR])
main_msg['To'] = ','.join(listToAddr)
listCcAddr = kwargs.get('listCcAddr')
if listCcAddr:
main_msg['Cc'] = ', '.join(kwargs['listCcAddr'])
listAddr = listToAddr + listCcAddr
else:
listAddr = listToAddr
main_msg['Subject'] = Header(strTitle, "utf-8").encode()
main_msg['Date'] = email.utils.formatdate()

Expand All @@ -72,10 +78,10 @@ def _send(self, strTitle, strContent, listToAddr, filename_list=None):
# 如果提供的密码为空,则不需要登录SMTP server
if self.MAIL_REVIEW_FROM_PASSWORD != '':
server.login(self.MAIL_REVIEW_FROM_ADDR, self.MAIL_REVIEW_FROM_PASSWORD)
sendResult = server.sendmail(self.MAIL_REVIEW_FROM_ADDR, listToAddr, main_msg.as_string())
sendResult = server.sendmail(self.MAIL_REVIEW_FROM_ADDR, listAddr, main_msg.as_string())
server.quit()

# 调用方应该调用此方法,采用子进程方式异步阻塞地发送邮件,避免邮件服务挂掉影响archer主服务
def sendEmail(self, strTitle, strContent, listToAddr, filename_list=None):
p = Process(target=self._send, args=(strTitle, strContent, listToAddr, filename_list))
def sendEmail(self, strTitle, strContent, listToAddr, **kwargs):
p = Process(target=self._send, args=(strTitle, strContent, listToAddr), kwargs=kwargs)
p.start()
12 changes: 5 additions & 7 deletions sql/sqlreview.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,11 @@ def execute_call_back(workflowId, clusterName, url):
objEngineer = users.objects.get(username=engineer)
strTitle = "SQL上线工单执行完毕 # " + str(workflowId)
strContent = "发起人:" + engineer + "\n审核人:" + reviewMen + "\n工单地址:" + url + "\n工单名称: " + workflowName + "\n执行结果:" + workflowStatus
mailSender.sendEmail(strTitle, strContent, [objEngineer.email])
mailSender.sendEmail(strTitle, strContent, getattr(settings, 'MAIL_REVIEW_DBA_ADDR'))
for reviewMan in listAllReviewMen:
if reviewMan == "":
continue
objReviewMan = users.objects.get(username=reviewMan)
mailSender.sendEmail(strTitle, strContent, [objReviewMan.email])
reviewManAddr = [email['email'] for email in
users.objects.filter(username__in=listAllReviewMen).values('email')]
dbaAddr = [email['email'] for email in users.objects.filter(role='DBA').values('email')]
listCcAddr = reviewManAddr + dbaAddr
mailSender.sendEmail(strTitle, strContent, [objEngineer.email], listCcAddr=listCcAddr)


# 给定时任务执行sql
Expand Down
14 changes: 2 additions & 12 deletions sql/static/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -141,22 +141,14 @@ <h4 style="display: inline;">单子名称:<span id="editWorkflowNname">{{ work

{% elif workflowDetail.status == '审核通过' %}
{% if loginUser == workflowDetail.engineer %}
<!--审核通过后提交人也可以执行SQL-->
<form id="from-execute" action="/execute/" method="post" style="display:inline-block;">
{% csrf_token %}
<input type="hidden" name="workflowid" value="{{ workflowDetail.id }}">
<input type="button" id="btnExecute" class="btn btn-danger"
value="立即执行"/>
</form>
<input type="button" class="btn btn-info" data-toggle="modal" data-target="#cronComfirm" value="定时执行"/>
<form id="form-cancel" action="/cancel/" method="post" style="display:inline-block;">
{% csrf_token %}
<input type="hidden" name="workflowid" value="{{ workflowDetail.id }}">
<input type="hidden" id="audit_remark" name="audit_remark" value="">
<input type="submit" onclick="loading(this)" class="btn btn-default" value="终止流程"/>
</form>

{% elif loginUser in listAllReviewMen %}
{% elif loginUserOb.role == 'DBA' %}
<textarea id="remark" name="remark" class="form-control" data-name="审核备注"
placeholder="请填写驳回原因" rows=3></textarea>
<br>
Expand All @@ -178,16 +170,14 @@ <h4 style="display: inline;">单子名称:<span id="editWorkflowNname">{{ work

{% elif workflowDetail.status == '定时执行' %}
{% if loginUser == workflowDetail.engineer %}
<!--审核通过后提交人也可以定时执行SQL-->
<input type="button" class="btn btn-info" data-toggle="modal" data-target="#cronComfirm" value="执行时间变更"/>
<form id="form-cancel" action="/cancel/" method="post" style="display:inline-block;">
{% csrf_token %}
<input type="hidden" name="workflowid" value="{{ workflowDetail.id }}">
<input type="hidden" id="audit_remark" name="audit_remark" value="">
<input type="submit" onclick="loading(this)" class="btn btn-default" value="终止流程"/>
</form>

{% elif loginUser in listAllReviewMen %}
{% elif loginUserOb.role == 'DBA' %}
<textarea id="remark" name="remark" class="form-control" data-name="审核备注"
placeholder="请填写驳回原因" rows=3></textarea>
<br>
Expand Down
62 changes: 18 additions & 44 deletions sql/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .aes_decryptor import Prpcrypt
from .models import users, master_config, AliyunRdsConfig, workflow, slave_config, QueryPrivileges
from .workflow import Workflow
from .permission import role_required, superuser_required
import logging

logger = logging.getLogger('default')
Expand Down Expand Up @@ -78,8 +79,7 @@ def submitSql(request):

# 获取所有审核人,当前登录用户不可以审核
loginUser = request.session.get('login_username', False)
reviewMen = users.objects.filter(role='审核人').exclude(username=loginUser)
listAllReviewMen = [user.username for user in reviewMen]
reviewMen = users.objects.filter(role__in=['审核人', 'DBA']).exclude(username=loginUser)

context = {'currentMenu': 'submitsql', 'dictAllClusterDb': dictAllClusterDb, 'reviewMen': reviewMen}
return render(request, 'submitSql.html', context)
Expand Down Expand Up @@ -157,14 +157,11 @@ def autoreview(request):

# 发一封邮件
strTitle = "新的SQL上线工单提醒 # " + str(workflowId)
objEngineer = users.objects.get(username=engineer)
for reviewMan in listAllReviewMen:
if reviewMan == "":
continue
strContent = "发起人:" + engineer + "\n审核人:" + str(
listAllReviewMen) + "\n工单地址:" + url + "\n工单名称: " + workflowName + "\n具体SQL:" + sqlContent
objReviewMan = users.objects.get(username=reviewMan)
mailSender.sendEmail(strTitle, strContent, [objReviewMan.email])
strContent = "发起人:" + engineer + "\n审核人:" + str(
listAllReviewMen) + "\n工单地址:" + url + "\n工单名称: " + workflowName + "\n具体SQL:" + sqlContent
reviewManAddr = [email['email'] for email in
users.objects.filter(username__in=listAllReviewMen).values('email')]
mailSender.sendEmail(strTitle, strContent, reviewManAddr)
else:
# 不发邮件
pass
Expand Down Expand Up @@ -245,6 +242,7 @@ def detail(request, workflowId):


# 审核通过,不执行
@role_required(('工程师', 'DBA',))
def passed(request):
workflowId = request.POST['workflowid']
if workflowId == '' or workflowId is None:
Expand Down Expand Up @@ -286,18 +284,17 @@ def passed(request):
objEngineer = users.objects.get(username=engineer)
strTitle = "SQL上线工单审核通过 # " + str(workflowId)
strContent = "发起人:" + engineer + "\n审核人:" + reviewMen + "\n工单地址:" + url + "\n工单名称: " + workflowName + "\n审核结果:" + workflowStatus
mailSender.sendEmail(strTitle, strContent, [objEngineer.email])
mailSender.sendEmail(strTitle, strContent, getattr(settings, 'MAIL_REVIEW_DBA_ADDR'))
for reviewMan in listAllReviewMen:
if reviewMan == "":
continue
objReviewMan = users.objects.get(username=reviewMan)
mailSender.sendEmail(strTitle, strContent, [objReviewMan.email])
reviewManAddr = [email['email'] for email in
users.objects.filter(username__in=listAllReviewMen).values('email')]
dbaAddr = [email['email'] for email in users.objects.filter(role='DBA').values('email')]
listCcAddr = reviewManAddr + dbaAddr
mailSender.sendEmail(strTitle, strContent, [objEngineer.email], listCcAddr=listCcAddr)

return HttpResponseRedirect(reverse('sql:detail', args=(workflowId,)))


# 执行SQL
@role_required(('DBA',))
def execute(request):
workflowId = request.POST['workflowid']
if workflowId == '' or workflowId is None:
Expand All @@ -309,17 +306,6 @@ def execute(request):
clusterName = workflowDetail.cluster_name
url = getDetailUrl(request) + str(workflowId) + '/'

try:
listAllReviewMen = json.loads(workflowDetail.review_man)
except ValueError:
listAllReviewMen = (workflowDetail.review_man,)

# 服务器端二次验证,正在执行人工审核动作的当前登录用户必须为审核人或者提交人. 避免攻击或被接口测试工具强行绕过
loginUser = request.session.get('login_username', False)
if loginUser is None or (loginUser not in listAllReviewMen and loginUser != workflowDetail.engineer):
context = {'errMsg': '当前登录用户不是审核人或者提交人,请重新登录.'}
return render(request, 'error.html', context)

# 服务器端二次验证,当前工单状态必须为审核通过状态
if workflowDetail.status != Const.workflowStatus['pass']:
context = {'errMsg': '当前工单状态不是审核通过,请刷新当前页面!'}
Expand Down Expand Up @@ -351,6 +337,7 @@ def execute(request):


# 定时执行SQL
@role_required(('DBA',))
def timingtask(request):
workflowId = request.POST.get('workflowid')
run_date = request.POST.get('run_date')
Expand All @@ -369,17 +356,6 @@ def timingtask(request):
url = getDetailUrl(request) + str(workflowId) + '/'
job_id = Const.workflowJobprefix['sqlreview'] + '-' + str(workflowId)

try:
listAllReviewMen = json.loads(workflowDetail.review_man)
except ValueError:
listAllReviewMen = (workflowDetail.review_man,)

# 服务器端二次验证,正在执行定时执行SQL动作的当前登录用户必须为审核人或者提交人. 避免攻击或被接口测试工具强行绕过
loginUser = request.session.get('login_username', False)
if loginUser is None or (loginUser not in listAllReviewMen and loginUser != workflowDetail.engineer):
context = {'errMsg': '当前登录用户不是审核人或者提交人,请重新登录.'}
return render(request, 'error.html', context)

# 使用事务保持数据一致性
try:
with transaction.atomic():
Expand Down Expand Up @@ -446,11 +422,9 @@ def cancel(request):
if loginUser == engineer:
strTitle = "发起人主动终止SQL上线工单流程 # " + str(workflowId)
strContent = "发起人:" + engineer + "\n审核人:" + reviewMan + "\n工单地址:" + url + "\n工单名称: " + workflowName + "\n执行结果:" + workflowStatus + "\n提醒:发起人主动终止流程"
for reviewMan in listAllReviewMen:
if reviewMan == "":
continue
objReviewMan = users.objects.get(username=reviewMan)
mailSender.sendEmail(strTitle, strContent, [objReviewMan.email])
reviewManAddr = [email['email'] for email in
users.objects.filter(username__in=listAllReviewMen).values('email')]
mailSender.sendEmail(strTitle, strContent, [reviewManAddr])
else:
objEngineer = users.objects.get(username=engineer)
strTitle = "SQL上线工单被拒绝执行 # " + str(workflowId)
Expand Down
8 changes: 5 additions & 3 deletions sql/views_ajax.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@
workflowOb = Workflow()


# 登录失败邮件推送给DBA
def log_mail_record(login_failed_message):
mail_title = 'login inception'
mail_title = 'login archer'
logger.warning(login_failed_message)
dbaAddr = [email['email'] for email in users.objects.filter(role='DBA').values('email')]
if getattr(settings, 'MAIL_ON_OFF') == "on":
mailSender.sendEmail(mail_title, login_failed_message, getattr(settings, 'MAIL_REVIEW_SECURE_ADDR'))
mailSender.sendEmail(mail_title, login_failed_message, dbaAddr)


# ajax接口,登录页面调用,用来验证用户名密码
Expand Down Expand Up @@ -159,7 +161,7 @@ def sqlworkflow(request):

# 全部工单里面包含搜索条件,待审核前置
if navStatus == 'all':
if loginUserOb.is_superuser == 1:
if loginUserOb.is_superuser == 1 or loginUserOb.role == 'DBA':
listWorkflow = workflow.objects.filter(
Q(engineer__contains=search) | Q(workflow_name__contains=search)
).order_by('-create_time')[offset:limit].values("id", "workflow_name", "engineer", "status",
Expand Down

0 comments on commit ca53c2d

Please sign in to comment.