1
0
mirror of https://github.com/jumpserver/jumpserver.git synced 2025-05-05 22:56:26 +00:00

Merge branch 'dev' into pr@dev@license

This commit is contained in:
feng626 2025-03-18 18:56:41 +08:00 committed by GitHub
commit 28d6f2f9ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 2838 additions and 2587 deletions
apps
accounts
acls/models
authentication/templates/authentication
common/management/commands/services/services
i18n
core
en/LC_MESSAGES
es/LC_MESSAGES
ja/LC_MESSAGES
pt_BR/LC_MESSAGES
ru/LC_MESSAGES
zh/LC_MESSAGES
zh_Hant/LC_MESSAGES
lina
ops/ansible/runners
tickets
users/models/user

View File

@ -133,11 +133,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

@ -106,11 +106,9 @@ class CommandFilterACL(UserAssetAccountBaseACL):
return self.name
def create_command_review_ticket(self, run_command, session, cmd_filter_acl, org_id):
from tickets.const import TicketType
from tickets.models import ApplyCommandTicket
data = {
'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketType.command_confirm,
'applicant': session.user_obj,
'apply_run_user_id': session.user_id,
'apply_run_asset': str(session.asset),

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

@ -295,7 +295,7 @@
"ClearSuccessMsg": "Clear successful",
"ClickCopy": "Click to copy",
"ClientCertificate": "Client certificate",
"Clipboard ": "Clipboard",
"Clipboard": "Clipboard",
"ClipboardCopyPaste": "Clipboard copy and paste",
"Clone": "Clone",
"Close": "Close",
@ -1523,5 +1523,7 @@
"IpGroup": "IP group",
"PublicIP": "Public IP",
"PrivateIP": "Private IP",
"ExecuteAfterSaving": "Execute after saving"
"ExecuteAfterSaving": "Execute after saving",
"PleaseEnterReason": "Please enter a reason",
"Processing": "Processing"
}

View File

@ -280,7 +280,7 @@
"ClearSelection": "Limpiar selección",
"ClickCopy": "Hacer clic para copiar",
"ClientCertificate": "Certificado de cliente",
"Clipboard ": "Portapapeles",
"Clipboard": "Portapapeles",
"ClipboardCopyPaste": "Copiar y pegar del portapapeles.",
"Clone": "Clonar",
"Close": "cerrar",

View File

@ -298,7 +298,7 @@
"ClearSuccessMsg": "クリアに成功",
"ClickCopy": "クリックでコピー",
"ClientCertificate": "クライアント証明書",
"Clipboard ": "クリップボード",
"Clipboard": "クリップボード",
"ClipboardCopyPaste": "クリップボードのコピーペースト",
"Clone": "クローン",
"Close": "閉じる",

View File

@ -295,7 +295,7 @@
"ClearSuccessMsg": "Limpeza bem-sucedida",
"ClickCopy": "Clicar para copiar",
"ClientCertificate": "Certificado do cliente",
"Clipboard ": "Área de transferência",
"Clipboard": "Área de transferência",
"ClipboardCopyPaste": "Copiar e colar da área de transferência",
"Clone": "Clonar",
"Close": "Desativar",

View File

@ -288,7 +288,7 @@
"ClearSuccessMsg": "Очистка выполнена успешно",
"ClickCopy": "Нажмите для копирования",
"ClientCertificate": "Сертификат клиента",
"Clipboard ": "Буфер обмена",
"Clipboard": "Буфер обмена",
"ClipboardCopyPaste": "Копирование и вставка в буфер обмена",
"Clone": "Клонировать",
"Close": "закрыть",

View File

@ -294,7 +294,7 @@
"ClearSuccessMsg": "清除成功",
"ClickCopy": "点击复制",
"ClientCertificate": "客户端证书",
"Clipboard ": "剪贴板",
"Clipboard": "剪贴板",
"ClipboardCopyPaste": "剪贴板复制粘贴",
"Clone": "克隆",
"Close": "关闭",
@ -914,7 +914,7 @@
"PasswordAndSSHKey": "认证设置",
"PasswordChangeLog": "改密日志",
"PasswordError": "密码错误",
"PasswordExpired": "密码过期",
"PasswordExpired": "密码过期",
"PasswordPlaceholder": "请输入密码",
"PasswordRecord": "密码记录",
"PasswordRule": "密码规则",
@ -1525,5 +1525,7 @@
"IpGroup": "IP 组",
"PublicIP": "公有 IP",
"PrivateIP": "私有 IP",
"ExecuteAfterSaving": "保存后执行"
"ExecuteAfterSaving": "保存后执行",
"PleaseEnterReason": "请输入原因",
"Processing": "处理中"
}

View File

@ -298,7 +298,7 @@
"ClearSuccessMsg": "清除成功",
"ClickCopy": "點擊複製",
"ClientCertificate": "用戶端證書",
"Clipboard ": "剪貼簿",
"Clipboard": "剪貼簿",
"ClipboardCopyPaste": "剪貼簿複製貼上",
"Clone": "複製",
"Close": "關閉",

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)

View File

@ -2,6 +2,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from perms.const import ActionChoices
from tickets.const import TicketType
from .general import Ticket
__all__ = ['ApplyAssetTicket']
@ -19,6 +20,8 @@ class ApplyAssetTicket(Ticket):
apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True)
apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True)
TICKET_TYPE = TicketType.apply_asset
def get_apply_actions_display(self):
return ActionChoices.display(self.apply_actions)

View File

@ -2,6 +2,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from .general import Ticket
from ...const import TicketType
class ApplyCommandTicket(Ticket):
@ -19,5 +20,7 @@ class ApplyCommandTicket(Ticket):
null=True, verbose_name=_('Command filter acl')
)
TICKET_TYPE = TicketType.command_confirm
class Meta:
verbose_name = _('Apply Command Ticket')

View File

@ -303,6 +303,8 @@ class Ticket(StatusMixin, JMSBaseModel):
max_length=36, blank=True, default='', verbose_name=_('Organization'), db_index=True
)
TICKET_TYPE = TicketType.general
class Meta:
ordering = ('-date_created',)
verbose_name = _('Ticket')
@ -313,11 +315,23 @@ class Ticket(StatusMixin, JMSBaseModel):
def __str__(self):
return '{}({})'.format(self.title, self.applicant)
def save(self, *args, **kwargs):
self.type = self.TICKET_TYPE
super().save(*args, **kwargs)
@property
def spec_ticket(self):
attr = self.type.replace('_', '') + 'ticket'
return getattr(self, attr)
@property
def name(self):
return self.title
@name.setter
def name(self, value):
self.title = value
# TODO 先单独处理一下
@property
def org_name(self):

View File

@ -5,6 +5,8 @@ from .general import Ticket
__all__ = ['ApplyLoginAssetTicket']
from ...const import TicketType
class ApplyLoginAssetTicket(Ticket):
apply_login_user = models.ForeignKey(
@ -17,6 +19,8 @@ class ApplyLoginAssetTicket(Ticket):
max_length=128, default='', verbose_name=_('Login account')
)
TICKET_TYPE = TicketType.login_asset_confirm
def activate_connection_token_if_need(self):
if not self.connection_token:
return

View File

@ -5,11 +5,15 @@ from .general import Ticket
__all__ = ['ApplyLoginTicket']
from ...const import TicketType
class ApplyLoginTicket(Ticket):
apply_login_ip = models.GenericIPAddressField(verbose_name=_('Login IP'), null=True)
apply_login_city = models.CharField(max_length=64, verbose_name=_('Login city'), null=True)
apply_login_datetime = models.DateTimeField(verbose_name=_('Login Date'), null=True)
TICKET_TYPE = TicketType.login_confirm
class Meta:
verbose_name = _('Apply Login Ticket')

View File

@ -25,8 +25,9 @@ class TicketSerializer(OrgResourceModelSerializerMixin):
fields_mini = ['id', 'title']
fields_small = fields_mini + ['org_id', 'comment']
read_only_fields = [
'serial_num', 'process_map', 'approval_step', 'type', 'state', 'applicant',
'status', 'date_created', 'date_updated', 'org_name', 'rel_snapshot'
'serial_num', 'process_map', 'approval_step', 'type',
'state', 'applicant', 'status', 'date_created',
'date_updated', 'org_name', 'rel_snapshot'
]
fields = fields_small + read_only_fields
extra_kwargs = {}

View File

@ -137,8 +137,8 @@ class AuthMixin:
if self.can_update_ssh_key():
from authentication.models import SSHKey
SSHKey.objects.create(
public_key=public_key, private_key=private_key, user=self, name=kwargs.get('name'),
comment=kwargs.get('comment'), is_active=kwargs.get('is_active')
public_key=public_key, private_key=private_key, user=self, name=kwargs.get('name', ''),
comment=kwargs.get('comment', ''), is_active=kwargs.get('is_active')
)
post_user_change_password.send(self.__class__, user=self)