Merge branch 'pam' of github.com:jumpserver/jumpserver into pam

pull/14578/head
ibuler 2024-12-03 17:49:27 +08:00
commit 707a83ec19
9 changed files with 390 additions and 200 deletions

View File

@ -125,6 +125,7 @@ class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
("list", "accounts.view_changesecretexecution"), ("list", "accounts.view_changesecretexecution"),
("retrieve", "accounts.view_changesecretexecution"), ("retrieve", "accounts.view_changesecretexecution"),
("create", "accounts.add_changesecretexecution"), ("create", "accounts.add_changesecretexecution"),
("report", "accounts.view_changesecretexecution"),
) )
tp = AutomationTypes.change_secret tp = AutomationTypes.change_secret

View File

@ -11,7 +11,7 @@ from accounts.const import (
AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice
) )
from accounts.models import ChangeSecretRecord, BaseAccountQuerySet from accounts.models import ChangeSecretRecord, BaseAccountQuerySet
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
from accounts.serializers import ChangeSecretRecordBackUpSerializer from accounts.serializers import ChangeSecretRecordBackUpSerializer
from assets.const import HostTypes from assets.const import HostTypes
from common.db.utils import safe_db_connection from common.db.utils import safe_db_connection
@ -183,6 +183,14 @@ class ChangeSecretManager(AccountBasePlaybookManager):
with safe_db_connection(): with safe_db_connection():
recorder.save(update_fields=['status', 'date_finished']) recorder.save(update_fields=['status', 'date_finished'])
account.save(update_fields=['secret', 'version', 'date_updated']) account.save(update_fields=['secret', 'version', 'date_updated'])
self.summary['ok_accounts'] += 1
self.result['ok_accounts'].append(
{
"asset": str(account.asset),
"username": account.username,
}
)
super().on_host_success(host, result)
def on_host_error(self, host, error, result): def on_host_error(self, host, error, result):
recorder = self.name_recorder_mapper.get(host) recorder = self.name_recorder_mapper.get(host)
@ -195,9 +203,14 @@ class ChangeSecretManager(AccountBasePlaybookManager):
recorder.save() recorder.save()
except Exception as e: except Exception as e:
print(f"\033[31m Save {host} recorder error: {e} \033[0m\n") print(f"\033[31m Save {host} recorder error: {e} \033[0m\n")
self.summary['fail_accounts'] += 1
def on_runner_failed(self, runner, e, **kwargs): self.result['fail_accounts'].append(
logger.error("Account error: ", e) {
"asset": str(recorder.asset),
"username": recorder.account.username,
}
)
super().on_host_success(host, result)
def check_secret(self): def check_secret(self):
if self.secret_strategy == SecretStrategy.custom \ if self.secret_strategy == SecretStrategy.custom \
@ -236,24 +249,13 @@ class ChangeSecretManager(AccountBasePlaybookManager):
if self.record_map: if self.record_map:
return return
failed_recorders = [
r for r in recorders
if r.status == ChangeSecretRecordStatusChoice.failed.value
]
recipients = self.execution.recipients recipients = self.execution.recipients
if not recipients: if not recipients:
return return
if failed_recorders: context = self.get_report_context()
name = self.execution.snapshot.get('name') for user in recipients:
execution_id = str(self.execution.id) ChangeSecretReportMsg(user, context).publish()
_ids = [r.id for r in failed_recorders]
asset_account_errors = ChangeSecretRecord.objects.filter(
id__in=_ids).values_list('asset__name', 'account__username', 'error')
for user in recipients:
ChangeSecretFailedMsg(name, execution_id, user, asset_account_errors).publish()
if not recorders: if not recorders:
return return
@ -295,3 +297,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
ws.write_string(row_index, col_index, col_data) ws.write_string(row_index, col_index, col_data)
wb.close() wb.close()
return True return True
def get_report_template(self):
return "accounts/change_secret_report.html"

View File

@ -1,8 +1,21 @@
from django.utils import timezone from django.utils import timezone
from datetime import datetime
__all__ = ['GatherAccountsFilter'] __all__ = ['GatherAccountsFilter']
def parse_date(date_str, default=''):
if not date_str:
return default
if date_str == 'Never':
return None
try:
dt = datetime.strptime(date_str, '%Y/%m/%d %H:%M:%S')
return timezone.make_aware(dt, timezone.get_current_timezone())
except ValueError:
return default
# TODO 后期会挪到 playbook 中 # TODO 后期会挪到 playbook 中
class GatherAccountsFilter: class GatherAccountsFilter:
def __init__(self, tp): def __init__(self, tp):
@ -101,11 +114,26 @@ class GatherAccountsFilter:
@staticmethod @staticmethod
def windows_filter(info): def windows_filter(info):
info = info[4:-2]
result = {} result = {}
for i in info: for user_details in info['user_details']:
for username in i.split(): user_info = {}
result[username] = {} lines = user_details['stdout_lines']
for line in lines:
if not line.strip():
continue
parts = line.split(' ', 1)
if len(parts) == 2:
key, value = parts
user_info[key.strip()] = value.strip()
user = {
'username': user_info.get('User name', ''),
'groups': user_info.get('Global Group memberships', ''),
'date_password_change': parse_date(user_info.get('Password last set', '')),
'date_password_expired': parse_date(user_info.get('Password expires', '')),
'date_last_login': parse_date(user_info.get('Last logon', '')),
'can_change_password': user_info.get('User may change password', 'Yes')
}
result[user['username']] = user
return result return result
def run(self, method_id_meta_mapper, info): def run(self, method_id_meta_mapper, info):

View File

@ -1,14 +1,32 @@
- hosts: demo - hosts: demo
gather_facts: no gather_facts: no
tasks: tasks:
- name: Gather windows account - name: Run net user command to get all users
ansible.builtin.win_shell: net user win_shell: net user
register: result register: user_list_output
ignore_errors: true
- name: Define info by set_fact - name: Parse all users from net user command
ansible.builtin.set_fact: set_fact:
info: "{{ result.stdout_lines }}" all_users: >-
{%- set users = [] -%}
{%- for line in user_list_output.stdout_lines -%}
{%- if loop.index > 4 and line.strip() != "" and not line.startswith("The command completed") -%}
{%- for user in line.split() -%}
{%- set _ = users.append(user) -%}
{%- endfor -%}
{%- endif -%}
{%- endfor -%}
{{ users }}
- name: Run net user command for each user to get details
win_shell: net user {{ item }}
loop: "{{ all_users }}"
register: user_details
ignore_errors: yes
- set_fact:
info:
user_details: "{{ user_details.results }}"
- debug: - debug:
var: info var: info

View File

@ -1,5 +1,6 @@
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from premailer import transform
from accounts.models import ChangeSecretRecord from accounts.models import ChangeSecretRecord
from common.tasks import send_mail_attachment_async, upload_backup_to_obj_storage from common.tasks import send_mail_attachment_async, upload_backup_to_obj_storage
@ -101,24 +102,19 @@ class GatherAccountChangeMsg(UserMessage):
return cls(user, {}) return cls(user, {})
class ChangeSecretFailedMsg(UserMessage): class ChangeSecretReportMsg(UserMessage):
subject = _('Change secret or push account failed information') subject = _('Change secret or push account failed information')
def __init__(self, name, execution_id, user, asset_account_errors: list): def __init__(self, user, context: dict):
self.name = name self.context = context
self.execution_id = execution_id
self.asset_account_errors = asset_account_errors
super().__init__(user) super().__init__(user)
def get_html_msg(self) -> dict: def get_html_msg(self) -> dict:
context = { report = render_to_string(
'name': self.name, 'accounts/change_secret_report.html',
'recipient': self.user, self.context
'execution_id': self.execution_id, )
'asset_account_errors': self.asset_account_errors message = transform(report)
}
message = render_to_string('accounts/change_secret_failed_info.html', context)
return { return {
'subject': str(self.subject), 'subject': str(self.subject),
'message': message 'message': message
@ -130,4 +126,4 @@ class ChangeSecretFailedMsg(UserMessage):
user = User.objects.first() user = User.objects.first()
record = ChangeSecretRecord.objects.first() record = ChangeSecretRecord.objects.first()
execution_id = str(record.execution_id) execution_id = str(record.execution_id)
return cls(name, execution_id, user, []) return cls(user, {})

View File

@ -0,0 +1,134 @@
{% load i18n %}
<div class='summary'>
<p>{% trans 'The following is a summary of account change secret tasks, please review and handle them' %}</p>
<table>
<caption></caption>
<thead>
<tr>
<th colspan='2'>任务汇总:</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans 'Task name' %}:</td>
<td>{{ execution.automation.name }} </td>
</tr>
<tr>
<td>{% trans 'Date start' %}:</td>
<td>{{ execution.date_start | date:"Y/m/d H:i:s" }}</td>
</tr>
<tr>
<td>{% trans 'Date end' %}:</td>
<td>{{ execution.date_finished | date:"Y/m/d H:i:s" }}</td>
</tr>
<tr>
<td>{% trans 'Time using' %}:</td>
<td>{{ execution.duration }}s</td>
</tr>
<tr>
<td>{% trans 'Assets count' %}:</td>
<td>{{ summary.total_assets }}</td>
</tr>
<tr>
<td>{% trans 'Asset success count' %}:</td>
<td>{{ summary.ok_assets }}</td>
</tr>
<tr>
<td>{% trans 'Asset failed count' %}:</td>
<td>{{ summary.fail_assets }}</td>
</tr>
<tr>
<td>{% trans 'Asset not support count' %}:</td>
<td>{{ summary.error_assets }}</td>
</tr>
</tbody>
</table>
</div>
<div class='result'>
{% if summary.ok_accounts %}
<p>{% trans 'Success accounts' %}: {{ summary.ok_accounts }}</p>
<table>
<caption></caption>
<thead>
<tr>
<th>{% trans 'No.' %}</th>
<th>{% trans 'Asset' %}</th>
<th>{% trans 'Username' %}</th>
</tr>
</thead>
<tbody>
{% for account in result.ok_accounts %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ account.asset }}</td>
<td>{{ account.username }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<div class='result'>
{% if summary.lost_accounts %}
<p>{% trans 'Failed accounts' %}: {{ summary.fail_accounts }}</p>
<table>
<caption></caption>
<thead>
<tr>
<th>{% trans 'No.' %}</th>
<th>{% trans 'Asset' %}</th>
<th>{% trans 'Username' %}</th>
</tr>
</thead>
<tbody>
{% for account in result.fail_accounts %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ account.asset }}</td>
<td>{{ account.username }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<style>
table {
width: 100%;
border-collapse: collapse;
max-width: 100%;
text-align: left;
margin-top: 10px;
padding: 20px;
}
th {
background: #f2f2f2;
font-size: 14px;
padding: 5px;
border: 1px solid #ddd;
}
tr :first-child {
width: 30%;
}
td {
border: 1px solid #ddd;
padding: 5px;
font-size: 12px;
}
.result {
margin-top: 20px;
}
.result tr :first-child {
width: 10%;
}
</style>

View File

@ -3,74 +3,88 @@
<div class='summary'> <div class='summary'>
<p>{% trans 'The following is a summary of the account check tasks. Please review and handle them' %}</p> <p>{% trans 'The following is a summary of the account check tasks. Please review and handle them' %}</p>
<table> <table>
<caption></caption>
<thead> <thead>
<tr> <tr>
<th colspan='2'>任务汇总: </th> <th colspan='2'>任务汇总:</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{% trans 'Task name' %}: </td> <td>{% trans 'Task name' %}:</td>
<td>{{ execution.automation.name }} </td> <td>{{ execution.automation.name }} </td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Date start' %}: </td> <td>{% trans 'Date start' %}:</td>
<td>{{ execution.date_start }}</td> <td>{{ execution.date_start | date:"Y/m/d H:i:s" }}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Date end' %}: </td> <td>{% trans 'Date end' %}:</td>
<td>{{ execution.date_finished }}</td> <td>{{ execution.date_finished | date:"Y/m/d H:i:s" }}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Time using' %}: </td> <td>{% trans 'Time using' %}:</td>
<td>{{ execution.duration }}s</td> <td>{{ execution.duration }}s</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Assets count' %}: </td> <td>{% trans 'Assets count' %}:</td>
<td>{{ summary.assets }}</td> <td>{{ summary.assets }}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Account count' %}: </td> <td>{% trans 'Asset success count' %}:</td>
<td>{{ summary.accounts }}</td> <td>{{ summary.ok_assets }}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Week password count' %}:</td> <td>{% trans 'Asset failed count' %}:</td>
<td> <span> {{ summary.weak_password }}</span></td> <td>{{ summary.fail_assets }}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Ok count' %}: </td> <td>{% trans 'Asset not support count' %}:</td>
<td>{{ summary.ok }}</td> <td>{{ summary.error_assets }}</td>
</tr> </tr>
<tr>
<td>{% trans 'No password count' %}: </td> <tr>
<td>{{ summary.no_secret }}</td> <td>{% trans 'Account count' %}:</td>
</tr> <td>{{ summary.accounts }}</td>
</tr>
<tr>
<td>{% trans 'Ok count' %}:</td>
<td>{{ summary.ok }}</td>
</tr>
<tr>
<td>{% trans 'No password count' %}:</td>
<td>{{ summary.no_secret }}</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class='result'> <div class='result'>
<p>{% trans 'Account check details' %}:</p> {% if summary.weak_password %}
<table style=""> <p>{% trans 'Week password' %}: {{ summary.weak_password }}</p>
<thead> <p>{% trans 'Account check details' %}:</p>
<tr> <table>
<th>{% trans 'No.' %}</th> <caption></caption>
<th>{% trans 'Asset' %}</th> <thead>
<th>{% trans 'Username' %}</th>
<th>{% trans 'Result' %}</th>
</tr>
</thead>
<tbody>
{% for account in result.weak_password %}
<tr> <tr>
<td>{{ forloop.counter }}</td> <th>{% trans 'No.' %}</th>
<td>{{ account.asset }}</td> <th>{% trans 'Asset' %}</th>
<td>{{ account.username }}</td> <th>{% trans 'Username' %}</th>
<td style="color: red">{% trans 'Week password' %}</td> <th>{% trans 'Result' %}</th>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for account in result.weak_password %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ account.asset }}</td>
<td>{{ account.username }}</td>
<td style="color: red">{% trans 'Week password' %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div> </div>
<style> <style>

View File

@ -3,129 +3,123 @@
<div class='summary'> <div class='summary'>
<p>{% trans 'The following is a summary of the account check tasks. Please review and handle them' %}</p> <p>{% trans 'The following is a summary of the account check tasks. Please review and handle them' %}</p>
<table> <table>
<caption></caption>
<thead> <thead>
<tr> <tr>
<th colspan='2'>任务汇总: </th> <th colspan='2'>任务汇总:</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{% trans 'Task name' %}: </td> <td>{% trans 'Task name' %}:</td>
<td>{{ execution.automation.name }} </td> <td>{{ execution.automation.name }} </td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Date start' %}: </td> <td>{% trans 'Date start' %}:</td>
<td>{{ execution.date_start }}</td> <td>{{ execution.date_start | date:"Y/m/d H:i:s" }}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Date end' %}: </td> <td>{% trans 'Date end' %}:</td>
<td>{{ execution.date_finished }}</td> <td>{{ execution.date_finished | date:"Y/m/d H:i:s" }}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Time using' %}: </td> <td>{% trans 'Time using' %}:</td>
<td>{{ execution.duration }}s</td> <td>{{ execution.duration }}s</td>
</tr> </tr>
<tr>
<tr> <td>{% trans 'Assets count' %}:</td>
<td>{% trans 'Assets count' %}: </td> <td>{{ summary.total_assets }}</td>
<td>{{ summary.total_assets }}</td> </tr>
</tr> <tr>
<tr> <td>{% trans 'Asset success count' %}:</td>
<td>{% trans 'Asset success count' %}: </td> <td>{{ summary.ok_assets }}</td>
<td>{{ summary.ok_assets }}</td> </tr>
</tr> <tr>
<tr> <td>{% trans 'Asset failed count' %}:</td>
<td>{% trans 'Asset failed count' %}: </td> <td>{{ summary.fail_assets }}</td>
<td>{{ summary.fail_assets }}</td> </tr>
</tr> <tr>
<tr> <td>{% trans 'Asset not support count' %}:</td>
<td>{% trans 'Asset not support count' %}: </td> <td>{{ summary.error_assets }}</td>
<td>{{ summary.error_assets }}</td> </tr>
</tr>
<tr>
<td>{% trans 'Account new found count' %}: </td>
<td>{{ summary.new_accounts }}</td>
</tr>
<tr>
<td>{% trans 'Account lost count' %}: </td>
<td>{{ summary.lost_accounts }}</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class='result'> <div class='result'>
<p>{% trans 'New found accounts' %}: {{ summary.new_accounts }}</p>
{% if summary.new_accounts %} {% if summary.new_accounts %}
<table style=""> <p>{% trans 'New found accounts' %}: {{ summary.new_accounts }}</p>
<thead> <table>
<tr> <caption></caption>
<th>{% trans 'No.' %}</th> <thead>
<th>{% trans 'Asset' %}</th>
<th>{% trans 'Username' %}</th>
</tr>
</thead>
<tbody>
{% for account in result.new_accounts %}
<tr> <tr>
<td>{{ forloop.counter }}</td> <th>{% trans 'No.' %}</th>
<td>{{ account.asset }}</td> <th>{% trans 'Asset' %}</th>
<td>{{ account.username }}</td> <th>{% trans 'Username' %}</th>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for account in result.new_accounts %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ account.asset }}</td>
<td>{{ account.username }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %} {% endif %}
</div> </div>
<div class='result'> <div class='result'>
<p>{% trans 'Lost accounts' %}: {{ summary.lost_accounts }}</p>
{% if summary.lost_accounts %} {% if summary.lost_accounts %}
<table style=""> <p>{% trans 'Lost accounts' %}: {{ summary.lost_accounts }}</p>
<thead> <table>
<tr> <caption></caption>
<th>{% trans 'No.' %}</th> <thead>
<th>{% trans 'Asset' %}</th>
<th>{% trans 'Username' %}</th>
</tr>
</thead>
<tbody>
{% for account in result.lost_accounts %}
<tr> <tr>
<td>{{ forloop.counter }}</td> <th>{% trans 'No.' %}</th>
<td>{{ account.asset }}</td> <th>{% trans 'Asset' %}</th>
<td>{{ account.username }}</td> <th>{% trans 'Username' %}</th>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for account in result.lost_accounts %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ account.asset }}</td>
<td>{{ account.username }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %} {% endif %}
</div> </div>
<div class='result'> <div class='result'>
<p>{% trans 'New found risks' %}: {{ summary.new_risks }}</p> <p>{% trans 'New found risks' %}: {{ summary.new_risks }}</p>
{% if summary.new_risks %} {% if summary.new_risks %}
<table style=""> <table>
<thead> <caption></caption>
<tr> <thead>
<th>{% trans 'No.' %}</th>
<th>{% trans 'Asset' %}</th>
<th>{% trans 'Username' %}</th>
<th>{% trans 'Result' %}</th>
</tr>
</thead>
<tbody>
{% for risk in result.risks %}
<tr> <tr>
<td>{{ forloop.counter }}</td> <th>{% trans 'No.' %}</th>
<td>{{ risk.asset }}</td> <th>{% trans 'Asset' %}</th>
<td>{{ risk.username }}</td> <th>{% trans 'Username' %}</th>
<td>{{ risk.risk }}</td> <th>{% trans 'Result' %}</th>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for risk in result.risks %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ risk.asset }}</td>
<td>{{ risk.username }}</td>
<td>{{ risk.risk }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %} {% endif %}
</div> </div>

View File

@ -143,7 +143,7 @@ class BaseManager:
recipients = self.execution.recipients recipients = self.execution.recipients
if not recipients: if not recipients:
return return
print("Send report to: ", ",".join(recipients)) print("Send report to: ", ",".join([str(u) for u in recipients]))
report = self.gen_report() report = self.gen_report()
report = transform(report) report = transform(report)