perf: add delete account action (#15059)

pull/15064/head^2
fit2bot 2025-03-18 16:57:27 +08:00 committed by GitHub
parent e802e145af
commit cdebfd8121
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 2792 additions and 2573 deletions

View File

@ -30,7 +30,6 @@ __all__ = [
]
from ...filters import NodeFilterBackend
from ...risk_handlers import RiskHandler
@ -130,11 +129,13 @@ class AccountRiskViewSet(OrgBulkModelViewSet):
s.validated_data, ("asset", "username", "action", "risk")
)
handler = RiskHandler(asset=asset, username=username, request=self.request)
data = handler.handle(act, risk)
if not data:
return Response(data={"message": "Success"})
s = serializers.AccountRiskSerializer(instance=data)
return Response(data=s.data)
try:
risk = handler.handle(act, risk)
s = serializers.AccountRiskSerializer(instance=risk)
return Response(data=s.data)
except Exception as e:
return Response(status=400, data=str(e))
class CheckAccountEngineViewSet(JMSModelViewSet):

View File

@ -155,6 +155,19 @@ class AnalyseAccountRisk:
def _update_risk(self, account):
return account
def lost_accounts(self, asset, lost_users):
if not self.check_risk:
return
for user in lost_users:
self._create_risk(
dict(
asset_id=str(asset.id),
username=user,
risk=RiskChoice.account_deleted,
details=[{"datetime": self.now.isoformat()}],
)
)
def analyse_risk(self, asset, ga, d, sys_found):
if not self.check_risk:
return
@ -289,6 +302,8 @@ class GatherAccountsManager(AccountBasePlaybookManager):
"username": username,
}
)
risk_analyser = AnalyseAccountRisk(self.check_risk)
risk_analyser.lost_accounts(asset, lost_users)
# 收集的账号 比 账号列表多的, 有可能是账号中删掉了, 但这时候状态已经是 confirm 了
# 标识状态为 待处理, 让管理员去确认

View File

@ -139,6 +139,7 @@ class Migration(migrations.Migration):
choices=[
("long_time_no_login", "Long time no login"),
("new_found", "New found"),
("account_deleted", "Account deleted"),
("groups_changed", "Groups change"),
("sudoers_changed", "Sudo changed"),
("authorized_keys_changed", "Authorized keys changed"),

View File

@ -1,8 +1,9 @@
from itertools import islice
from django.db import models
from django.db.models import TextChoices
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from itertools import islice
from common.const import ConfirmOrIgnore
from common.db.models import JMSBaseModel
@ -41,6 +42,7 @@ class RiskChoice(TextChoices):
# 依赖自动发现的
long_time_no_login = 'long_time_no_login', _('Long time no login') # 好久没登录的账号, 禁用、删除
new_found = 'new_found', _('New found') # 未被纳管的账号, 纳管, 删除, 禁用
account_deleted = 'account_deleted', _('Account deleted') # 账号被删除, 纳管, 删除, 禁用
group_changed = 'groups_changed', _('Groups change') # 组变更, 确认
sudo_changed = 'sudoers_changed', _('Sudo changed') # sudo 变更, 确认
authorized_keys_changed = 'authorized_keys_changed', _('Authorized keys changed') # authorized_keys 变更, 确认

View File

@ -8,7 +8,7 @@ from accounts.models import (
AccountRisk,
SecretType,
AutomationExecution,
RiskChoice
RiskChoice, Account
)
from common.const import ConfirmOrIgnore
from common.utils import random_string
@ -19,10 +19,11 @@ TYPE_CHOICES = [
("close", _("Close")),
("disable_remote", _("Disable remote")),
("delete_remote", _("Delete remote")),
("delete_account", _("Delete account")),
("delete_both", _("Delete remote")),
("add_account", _("Add account")),
("change_password_add", _("Change password and Add")),
("change_password", _("Change password"))
("change_password", _("Change password")),
]
@ -73,6 +74,10 @@ class RiskHandler:
def handle_reopen(self):
pass
def handle_delete_account(self):
Account.objects.filter(asset=self.asset, username=self.username).delete()
GatheredAccount.objects.filter(asset=self.asset, username=self.username).delete()
def handle_close(self):
pass
@ -102,7 +107,7 @@ class RiskHandler:
present=True, status=ConfirmOrIgnore.confirmed
)
self.risk = RiskChoice.new_found
risk = self.get_risk()
risk.account = account
risk.save()
@ -113,6 +118,15 @@ class RiskHandler:
def handle_delete_remote(self):
self._handle_delete(delete="remote")
@staticmethod
def start_execution(execution):
execution.save()
execution.start()
if execution.status != "success":
msg = _("Execution failed: {}").format(execution.status)
raise ValidationError(msg)
def _handle_delete(self, delete="both"):
asset = self.asset
execution = AutomationExecution()
@ -124,9 +138,7 @@ class RiskHandler:
"delete": delete,
"risk": self.risk
}
execution.save()
execution.start()
return execution.summary
self.start_execution(execution)
def handle_delete_both(self):
self._handle_delete(delete="both")
@ -134,7 +146,11 @@ class RiskHandler:
def handle_change_password(self):
asset = self.asset
execution = AutomationExecution()
account = self.asset.accounts.get(username=self.username)
account = self.asset.accounts.filter(username=self.username, secret_type=SecretType.PASSWORD).first()
if not account:
raise ValidationError("Account not found")
execution.snapshot = {
"assets": [str(asset.id)],
"accounts": [str(account.id)],
@ -143,9 +159,7 @@ class RiskHandler:
"secret_strategy": "random",
"name": "Change account password: {}@{}".format(self.username, asset.name),
}
execution.save()
execution.start()
return execution.summary
self.start_execution(execution)
def handle_change_password_add(self):
asset = self.asset
@ -174,10 +188,10 @@ class RiskHandler:
'check_conn_after_change': True,
"name": "Push account password: {}@{}".format(self.username, asset.name),
}
execution.save()
execution.start()
self.start_execution(execution)
GatheredAccount.objects.filter(asset=self.asset, username=self.username).update(
present=True
(
GatheredAccount.objects
.filter(asset=self.asset, username=self.username)
.update(present=True)
)
return execution.summary

View File

@ -11,6 +11,11 @@
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script src="{% static "js/jumpserver.js" %}?_=9"></script>
<style>
.btn-sm i {
margin-right: 6px;
}
</style>
</head>
@ -20,19 +25,18 @@
<div class="col-md-12">
<div class="ibox-content">
<div>
<img src="{{ INTERFACE.logo_logout }}" style="margin: auto" width="82" height="82">
<img src="{{ INTERFACE.logo_logout }}" style="margin: auto" width="62" height="62" alt="logo">
<h2 style="display: inline">
{{ INTERFACE.login_title }}
</h2>
</div>
<p></p>
<div class="alert alert-success info-messages" >
<div class="alert alert-success info-messages">
{{ msg|safe }}
</div>
<div class="alert alert-danger error-messages" style="display: none">
</div>
<div class="alert alert-danger error-messages" style="display: none"></div>
<div class="progress progress-bar-default progress-striped active">
<div aria-valuemax="3600" aria-valuemin="0" aria-valuenow="43" role="progressbar" class="progress-bar">
<div aria-valuemax="3600" aria-valuemin="0" aria-valuenow="43" role="progressbar"
class="progress-bar">
</div>
</div>
<div class="row">
@ -66,107 +70,111 @@
{% include '_foot_js.html' %}
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
<script>
var errorMsgShow = false;
var errorMsgRef = $(".error-messages");
var infoMsgRef = $(".info-messages");
var timestamp = '{{ timestamp }}';
var progressBarRef = $(".progress-bar");
var interval, checkInterval;
var url = "{% url 'api-auth:login-confirm-ticket-status' %}";
var successUrl = "{% url 'authentication:login-guard' %}";
var errorMsgShow = false;
var errorMsgRef = $(".error-messages");
var infoMsgRef = $(".info-messages");
var timestamp = '{{ timestamp }}';
var progressBarRef = $(".progress-bar");
var interval, checkInterval;
var url = "{% url 'api-auth:login-confirm-ticket-status' %}";
var successUrl = "{% url 'authentication:login-guard' %}";
function doRequestAuth() {
requestApi({
url: url,
method: "GET",
headers: {
"X-JMS-LOGIN-TYPE": "W"
},
success: function (data) {
if (!data.error && data.msg === 'ok') {
window.onbeforeunload = function(){};
window.location = "{% url 'authentication:login-guard' %}"
} else if (data.error !== "login_confirm_wait") {
if (!errorMsgShow) {
infoMsgRef.hide();
errorMsgRef.show();
progressBarRef.addClass('progress-bar-danger');
errorMsgShow = true;
function doRequestAuth() {
requestApi({
url: url,
method: "GET",
headers: {
"X-JMS-LOGIN-TYPE": "W"
},
success: function (data) {
if (!data.error && data.msg === 'ok') {
window.onbeforeunload = function () {
};
window.location = "{% url 'authentication:login-guard' %}"
} else if (data.error !== "login_confirm_wait") {
if (!errorMsgShow) {
infoMsgRef.hide();
errorMsgRef.show();
progressBarRef.addClass('progress-bar-danger');
errorMsgShow = true;
}
clearInterval(interval);
clearInterval(checkInterval);
cancelTicket();
$(".copy-btn").attr('disabled', 'disabled');
errorMsgRef.html(data.msg)
}
clearInterval(interval);
clearInterval(checkInterval);
cancelTicket();
$(".copy-btn").attr('disabled', 'disabled');
errorMsgRef.html(data.msg)
},
error: function (text, data) {
},
flash_message: false, // 是否显示flash消息
})
}
function initClipboard() {
var clipboard = new Clipboard('.btn-copy', {
text: function (trigger) {
var origin = window.location.origin;
var link = origin + $(".btn-copy").data('link');
return link
}
},
error: function (text, data) {
},
flash_message: false, // 是否显示flash消息
})
}
function initClipboard() {
var clipboard = new Clipboard('.btn-copy', {
text: function (trigger) {
var origin = window.location.origin;
var link = origin + $(".btn-copy").data('link');
return link
});
clipboard.on("success", function (e) {
toastr.success("{% trans "Copy success" %}")
})
}
function handleProgressBar() {
var now = new Date().getTime() / 1000;
var offset = now - timestamp;
var percent = offset / 3600 * 100;
if (percent > 100) {
percent = 100
}
});
clipboard.on("success", function (e) {
toastr.success("{% trans "Copy success" %}")
})
}
function handleProgressBar() {
var now = new Date().getTime() / 1000;
var offset = now - timestamp;
var percent = offset / 3600 * 100;
if (percent > 100) {
percent = 100
progressBarRef.css("width", percent + '%');
progressBarRef.attr('aria-valuenow', offset);
}
progressBarRef.css("width", percent + '%');
progressBarRef.attr('aria-valuenow', offset);
}
function cancelTicket() {
requestApi({
url: url,
method: "DELETE",
flash_message: false
})
}
function cancelTicket() {
requestApi({
url: url,
method: "DELETE",
flash_message: false
})
}
function cancelCloseConfirm() {
window.onbeforeunload = function() {};
window.onunload = function(){};
}
function cancelCloseConfirm() {
window.onbeforeunload = function () {
};
window.onunload = function () {
};
}
function setCloseConfirm() {
window.onbeforeunload = function (e) {
return 'Confirm';
};
window.onunload = function (e) {
function setCloseConfirm() {
window.onbeforeunload = function (e) {
return 'Confirm';
};
window.onunload = function (e) {
cancelTicket();
}
}
$(document).ready(function () {
interval = setInterval(handleProgressBar, 1000);
checkInterval = setInterval(doRequestAuth, 5000);
doRequestAuth();
initClipboard();
setCloseConfirm();
}).on('click', '.btn-refresh', function () {
cancelCloseConfirm();
window.location.reload();
}).on('click', '.btn-return', function () {
cancelTicket();
}
}
$(document).ready(function () {
interval = setInterval(handleProgressBar, 1000);
checkInterval = setInterval(doRequestAuth, 5000);
doRequestAuth();
initClipboard();
setCloseConfirm();
}).on('click', '.btn-refresh', function () {
cancelCloseConfirm();
window.location.reload();
}).on('click', '.btn-return', function () {
cancelTicket();
cancelCloseConfirm();
clearInterval(interval);
clearInterval(checkInterval);
window.location = "{% url 'authentication:logout' %}"
})
cancelCloseConfirm();
clearInterval(interval);
clearInterval(checkInterval);
window.location = "{% url 'authentication:logout' %}"
})
</script>
</html>

View File

@ -12,15 +12,10 @@ class CeleryBaseService(BaseService):
@property
def cmd(self):
print('\n- Start Celery as Distributed Task Queue: {}'.format(self.queue.capitalize()))
ansible_config_path = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'ansible.cfg')
ansible_modules_path = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'modules')
os.environ.setdefault('PYTHONPATH', settings.APPS_DIR)
os.environ.setdefault('LC_ALL', 'en_US.UTF-8')
os.environ.setdefault('LANG', 'en_US.UTF-8')
os.environ.setdefault('PYTHONOPTIMIZE', '1')
os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True')
os.environ.setdefault('ANSIBLE_CONFIG', ansible_config_path)
os.environ.setdefault('ANSIBLE_LIBRARY', ansible_modules_path)
os.environ.setdefault('PYTHONPATH', settings.APPS_DIR)
if os.getuid() == 0:
os.environ.setdefault('C_FORCE_ROOT', '1')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -914,7 +914,7 @@
"PasswordAndSSHKey": "认证设置",
"PasswordChangeLog": "改密日志",
"PasswordError": "密码错误",
"PasswordExpired": "密码过期",
"PasswordExpired": "密码过期",
"PasswordPlaceholder": "请输入密码",
"PasswordRecord": "密码记录",
"PasswordRule": "密码规则",

View File

@ -1,11 +1,23 @@
from ops.ansible.cleaner import WorkPostRunCleaner, cleanup_post_run
import os
from django.conf import settings
from ops.ansible.cleaner import WorkPostRunCleaner
class BaseRunner(WorkPostRunCleaner):
def __init__(self, **kwargs):
self.runner_params = kwargs
self.clean_workspace = kwargs.pop("clean_workspace", True)
self.setup_env()
@staticmethod
def setup_env():
ansible_config_path = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'ansible.cfg')
ansible_modules_path = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'modules')
os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True')
os.environ.setdefault('ANSIBLE_CONFIG', ansible_config_path)
os.environ.setdefault('ANSIBLE_LIBRARY', ansible_modules_path)
@classmethod
def kill_precess(cls, pid):

View File

@ -8,9 +8,6 @@ __all__ = ['AnsibleNativeRunner']
class AnsibleNativeRunner(BaseRunner):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@classmethod
def kill_precess(cls, pid):
return kill_ansible_ssh_process(pid)