mirror of https://github.com/jumpserver/jumpserver
perf: add account status action
parent
46962e035a
commit
4db4a6dce7
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
@ -7,8 +8,9 @@ from rest_framework.response import Response
|
|||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.filters import GatheredAccountFilterSet
|
||||
from accounts.models import GatherAccountsAutomation
|
||||
from accounts.models import GatherAccountsAutomation, AutomationExecution
|
||||
from accounts.models import GatheredAccount
|
||||
from assets.models import Asset
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from .base import AutomationExecutionViewSet
|
||||
|
||||
|
@ -49,8 +51,29 @@ class GatheredAccountViewSet(OrgBulkModelViewSet):
|
|||
}
|
||||
rbac_perms = {
|
||||
'sync_accounts': 'assets.add_gatheredaccount',
|
||||
'discover': 'assets.add_gatheredaccount',
|
||||
}
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='discover')
|
||||
def discover(self, request, *args, **kwargs):
|
||||
asset_id = request.query_params.get('asset_id')
|
||||
if not asset_id:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={'asset_id': 'This field is required.'})
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
execution = AutomationExecution()
|
||||
execution.snapshot = {
|
||||
'assets': [asset_id],
|
||||
'nodes': [],
|
||||
'type': 'gather_accounts',
|
||||
'is_sync_account': True,
|
||||
'name': 'Adhoc gather accounts: {}'.format(asset_id),
|
||||
}
|
||||
execution.save()
|
||||
execution.start()
|
||||
accounts = self.model.objects.filter(asset=asset)
|
||||
serializer = self.get_serializer(accounts, many=True)
|
||||
return Response(status=status.HTTP_200_OK, data=serializer.data)
|
||||
|
||||
@action(methods=['post'], detail=False, url_path='sync-accounts')
|
||||
def sync_accounts(self, request, *args, **kwargs):
|
||||
gathered_account_ids = request.data.get('gathered_account_ids')
|
||||
|
|
|
@ -3,6 +3,7 @@ from collections import defaultdict
|
|||
from accounts.const import AutomationTypes
|
||||
from accounts.models import GatheredAccount
|
||||
from assets.models import Asset
|
||||
from common.const import ConfirmOrIgnore
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import tmp_to_org
|
||||
from users.models import User
|
||||
|
@ -70,8 +71,9 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
|||
|
||||
def update_or_create_accounts(self):
|
||||
for asset, data in self.asset_account_info.items():
|
||||
with tmp_to_org(asset.org_id):
|
||||
with (tmp_to_org(asset.org_id)):
|
||||
gathered_accounts = []
|
||||
# 把所有的设置为 present = False, 创建的时候如果有就会更新
|
||||
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
||||
for d in data:
|
||||
username = d['username']
|
||||
|
@ -79,10 +81,16 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
|||
defaults=d, asset=asset, username=username,
|
||||
)
|
||||
gathered_accounts.append(gathered_account)
|
||||
# 不存在的标识为待处理
|
||||
GatheredAccount.objects \
|
||||
.filter(asset=asset, present=False) \
|
||||
.exclude(status=ConfirmOrIgnore.ignored) \
|
||||
.update(status='')
|
||||
if not self.is_sync_account:
|
||||
continue
|
||||
GatheredAccount.sync_accounts(gathered_accounts)
|
||||
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
super().run(*args, **kwargs)
|
||||
users, change_info = self.generate_send_users_and_change_info()
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 4.1.13 on 2024-10-28 08:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0009_remove_account_date_discovery_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="accountrisk",
|
||||
options={"verbose_name": "Account risk"},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="account",
|
||||
old_name="date_last_access",
|
||||
new_name="date_last_login",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="account",
|
||||
old_name="access_by",
|
||||
new_name="login_by",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="gatheredaccount",
|
||||
name="action",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("pending", "Pending"),
|
||||
("confirm", "Confirm"),
|
||||
("ignore", "Ignore"),
|
||||
],
|
||||
default="pending",
|
||||
max_length=32,
|
||||
verbose_name="Action",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.1.13 on 2024-10-28 08:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0010_alter_accountrisk_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="gatheredaccount",
|
||||
name="action",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="gatheredaccount",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[("confirmed", "Confirmed"), ("ignored", "Ignored")],
|
||||
default="",
|
||||
max_length=32,
|
||||
verbose_name="Action",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -57,8 +57,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
|
|||
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
|
||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
||||
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
||||
date_last_access = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last access'))
|
||||
access_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Access by'))
|
||||
date_last_login = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last access'))
|
||||
login_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Access by'))
|
||||
date_change_secret = models.DateTimeField(null=True, blank=True, verbose_name=_('Date change secret'))
|
||||
change_secret_status = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('Change secret status'))
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes, Source
|
||||
from accounts.models import Account
|
||||
from common.const import ConfirmOrIgnore
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
from .base import AccountBaseAutomation
|
||||
|
||||
|
@ -11,19 +13,46 @@ __all__ = ['GatherAccountsAutomation', 'GatheredAccount']
|
|||
|
||||
|
||||
class GatheredAccount(JMSOrgBaseModel):
|
||||
present = models.BooleanField(default=True, verbose_name=_("Present"))
|
||||
present = models.BooleanField(default=True, verbose_name=_("Present")) # 资产上是否还存在
|
||||
date_last_login = models.DateTimeField(null=True, verbose_name=_("Date login"))
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset"))
|
||||
username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username'))
|
||||
address_last_login = models.CharField(max_length=39, default='', verbose_name=_("Address login"))
|
||||
status = models.CharField(max_length=32, default='', blank=True, choices=ConfirmOrIgnore.choices, verbose_name=_("Action"))
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
return self.asset.address
|
||||
|
||||
@staticmethod
|
||||
def sync_accounts(gathered_accounts):
|
||||
@classmethod
|
||||
def update_exists_accounts(cls, gathered_account, accounts):
|
||||
if not gathered_account.date_last_login:
|
||||
return
|
||||
|
||||
for account in accounts:
|
||||
if (not account.date_last_login or
|
||||
account.date_last_login - gathered_account.date_last_login > timezone.timedelta(minutes=5)):
|
||||
account.date_last_login = gathered_account.date_last_login
|
||||
account.login_by = '{}({})'.format('unknown', gathered_account.address_last_login)
|
||||
account.save(update_fields=['date_last_login', 'login_by'])
|
||||
|
||||
@classmethod
|
||||
def create_accounts(cls, gathered_account, accounts):
|
||||
account_objs = []
|
||||
asset_id = gathered_account.asset_id
|
||||
username = gathered_account.username
|
||||
access_by = '{}({})'.format('unknown', gathered_account.address_last_login)
|
||||
account = Account(
|
||||
asset_id=asset_id, username=username,
|
||||
name=username, source=Source.COLLECTED,
|
||||
date_last_access=gathered_account.date_last_login,
|
||||
access_by=access_by
|
||||
)
|
||||
account_objs.append(account)
|
||||
Account.objects.bulk_create(account_objs)
|
||||
|
||||
@classmethod
|
||||
def sync_accounts(cls, gathered_accounts):
|
||||
for gathered_account in gathered_accounts:
|
||||
asset_id = gathered_account.asset_id
|
||||
username = gathered_account.username
|
||||
|
@ -32,13 +61,12 @@ class GatheredAccount(JMSOrgBaseModel):
|
|||
Q(asset_id=asset_id, name=username)
|
||||
)
|
||||
if accounts.exists():
|
||||
continue
|
||||
account = Account(
|
||||
asset_id=asset_id, username=username,
|
||||
name=username, source=Source.COLLECTED
|
||||
)
|
||||
account_objs.append(account)
|
||||
Account.objects.bulk_create(account_objs)
|
||||
cls.update_exists_accounts(gathered_account, accounts)
|
||||
else:
|
||||
cls.create_accounts(gathered_account, accounts)
|
||||
|
||||
gathered_account.status = ConfirmOrIgnore.confirmed
|
||||
gathered_account.save(update_fields=['action'])
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Gather asset accounts")
|
||||
|
|
|
@ -236,7 +236,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
|||
class Meta(BaseAccountSerializer.Meta):
|
||||
model = Account
|
||||
automation_fields = [
|
||||
'date_last_access', 'access_by', 'date_verified', 'connectivity',
|
||||
'date_last_login', 'login_by', 'date_verified', 'connectivity',
|
||||
'date_change_secret', 'change_secret_status'
|
||||
]
|
||||
fields = BaseAccountSerializer.Meta.fields + [
|
||||
|
|
|
@ -2,10 +2,15 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from accounts.models import GatheredAccount
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .account import AccountAssetSerializer
|
||||
from .account import AccountAssetSerializer as _AccountAssetSerializer
|
||||
from .base import BaseAccountSerializer
|
||||
|
||||
|
||||
class AccountAssetSerializer(_AccountAssetSerializer):
|
||||
class Meta(_AccountAssetSerializer.Meta):
|
||||
fields = [f for f in _AccountAssetSerializer.Meta.fields if f != 'auto_config']
|
||||
|
||||
|
||||
class GatheredAccountSerializer(BulkOrgResourceModelSerializer):
|
||||
asset = AccountAssetSerializer(label=_('Asset'))
|
||||
|
||||
|
@ -13,7 +18,8 @@ class GatheredAccountSerializer(BulkOrgResourceModelSerializer):
|
|||
model = GatheredAccount
|
||||
fields = [
|
||||
'id', 'present', 'asset', 'username',
|
||||
'date_updated', 'address_last_login', 'date_last_login'
|
||||
'date_updated', 'address_last_login',
|
||||
'date_last_login', 'status'
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -1,39 +1,33 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from celery import shared_task
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext_noop
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||
from assets.models import Node
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import org_aware_func
|
||||
|
||||
__all__ = ['gather_asset_accounts_task']
|
||||
# __all__ = ['gather_asset_accounts_task']
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@org_aware_func("nodes")
|
||||
def gather_asset_accounts_util(nodes, task_name):
|
||||
from accounts.models import GatherAccountsAutomation
|
||||
task_name = GatherAccountsAutomation.generate_unique_name(task_name)
|
||||
|
||||
task_snapshot = {
|
||||
'nodes': [str(node.id) for node in nodes],
|
||||
}
|
||||
tp = AutomationTypes.verify_account
|
||||
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible",
|
||||
verbose_name=_('Gather asset accounts'),
|
||||
activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
|
||||
description=_("Unused")
|
||||
)
|
||||
def gather_asset_accounts_task(node_ids, task_name=None):
|
||||
if task_name is None:
|
||||
task_name = gettext_noop("Gather assets accounts")
|
||||
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
gather_asset_accounts_util(nodes=nodes, task_name=task_name)
|
||||
#
|
||||
# @org_aware_func("nodes")
|
||||
# def gather_asset_accounts_util(nodes, task_name):
|
||||
# from accounts.models import GatherAccountsAutomation
|
||||
# task_name = GatherAccountsAutomation.generate_unique_name(task_name)
|
||||
#
|
||||
# task_snapshot = {
|
||||
# 'nodes': [str(node.id) for node in nodes],
|
||||
# }
|
||||
# tp = AutomationTypes.verify_account
|
||||
# quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||
#
|
||||
#
|
||||
# @shared_task(
|
||||
# queue="ansible",
|
||||
# verbose_name=_('Gather asset accounts'),
|
||||
# activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
|
||||
# description=_("Unused")
|
||||
# )
|
||||
# def gather_asset_accounts_task(node_ids, task_name=None):
|
||||
# if task_name is None:
|
||||
# task_name = gettext_noop("Gather assets accounts")
|
||||
#
|
||||
# nodes = Node.objects.filter(id__in=node_ids)
|
||||
# gather_asset_accounts_util(nodes=nodes, task_name=task_name)
|
||||
#
|
||||
|
|
|
@ -131,8 +131,8 @@ class AutomationExecution(OrgModelMixin):
|
|||
return self.snapshot['type']
|
||||
|
||||
def get_all_asset_ids(self):
|
||||
node_ids = self.snapshot['nodes']
|
||||
asset_ids = self.snapshot['assets']
|
||||
node_ids = self.snapshot.get('nodes', [])
|
||||
asset_ids = self.snapshot.get('assets', [])
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||
asset_ids = set(list(asset_ids) + list(node_asset_ids))
|
||||
|
|
|
@ -75,4 +75,9 @@ class Language(models.TextChoices):
|
|||
jp = 'ja', '日本語',
|
||||
|
||||
|
||||
class ConfirmOrIgnore(models.TextChoices):
|
||||
confirmed = 'confirmed', _('Confirmed')
|
||||
ignored = 'ignored', _('Ignored')
|
||||
|
||||
|
||||
COUNTRY_CALLING_CODES = get_country_phone_choices()
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<!-- css file -->
|
||||
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/font-awesome.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/plugins/toastr/toastr.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/style.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
|
||||
|
|
Loading…
Reference in New Issue