mirror of https://github.com/jumpserver/jumpserver
perf: add account status action
parent
46962e035a
commit
4db4a6dce7
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -7,8 +8,9 @@ from rest_framework.response import Response
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.filters import GatheredAccountFilterSet
|
from accounts.filters import GatheredAccountFilterSet
|
||||||
from accounts.models import GatherAccountsAutomation
|
from accounts.models import GatherAccountsAutomation, AutomationExecution
|
||||||
from accounts.models import GatheredAccount
|
from accounts.models import GatheredAccount
|
||||||
|
from assets.models import Asset
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from .base import AutomationExecutionViewSet
|
from .base import AutomationExecutionViewSet
|
||||||
|
|
||||||
|
@ -49,8 +51,29 @@ class GatheredAccountViewSet(OrgBulkModelViewSet):
|
||||||
}
|
}
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'sync_accounts': 'assets.add_gatheredaccount',
|
'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')
|
@action(methods=['post'], detail=False, url_path='sync-accounts')
|
||||||
def sync_accounts(self, request, *args, **kwargs):
|
def sync_accounts(self, request, *args, **kwargs):
|
||||||
gathered_account_ids = request.data.get('gathered_account_ids')
|
gathered_account_ids = request.data.get('gathered_account_ids')
|
||||||
|
|
|
@ -3,6 +3,7 @@ from collections import defaultdict
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.models import GatheredAccount
|
from accounts.models import GatheredAccount
|
||||||
from assets.models import Asset
|
from assets.models import Asset
|
||||||
|
from common.const import ConfirmOrIgnore
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from orgs.utils import tmp_to_org
|
from orgs.utils import tmp_to_org
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
@ -70,8 +71,9 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||||
|
|
||||||
def update_or_create_accounts(self):
|
def update_or_create_accounts(self):
|
||||||
for asset, data in self.asset_account_info.items():
|
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 = []
|
gathered_accounts = []
|
||||||
|
# 把所有的设置为 present = False, 创建的时候如果有就会更新
|
||||||
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
||||||
for d in data:
|
for d in data:
|
||||||
username = d['username']
|
username = d['username']
|
||||||
|
@ -79,10 +81,16 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||||
defaults=d, asset=asset, username=username,
|
defaults=d, asset=asset, username=username,
|
||||||
)
|
)
|
||||||
gathered_accounts.append(gathered_account)
|
gathered_accounts.append(gathered_account)
|
||||||
|
# 不存在的标识为待处理
|
||||||
|
GatheredAccount.objects \
|
||||||
|
.filter(asset=asset, present=False) \
|
||||||
|
.exclude(status=ConfirmOrIgnore.ignored) \
|
||||||
|
.update(status='')
|
||||||
if not self.is_sync_account:
|
if not self.is_sync_account:
|
||||||
continue
|
continue
|
||||||
GatheredAccount.sync_accounts(gathered_accounts)
|
GatheredAccount.sync_accounts(gathered_accounts)
|
||||||
|
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
super().run(*args, **kwargs)
|
super().run(*args, **kwargs)
|
||||||
users, change_info = self.generate_send_users_and_change_info()
|
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'])
|
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
|
||||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
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'))
|
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'))
|
date_last_login = 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'))
|
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'))
|
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'))
|
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 import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes, Source
|
from accounts.const import AutomationTypes, Source
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
from common.const import ConfirmOrIgnore
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
from .base import AccountBaseAutomation
|
from .base import AccountBaseAutomation
|
||||||
|
|
||||||
|
@ -11,19 +13,46 @@ __all__ = ['GatherAccountsAutomation', 'GatheredAccount']
|
||||||
|
|
||||||
|
|
||||||
class GatheredAccount(JMSOrgBaseModel):
|
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"))
|
date_last_login = models.DateTimeField(null=True, verbose_name=_("Date login"))
|
||||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset"))
|
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'))
|
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"))
|
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
|
@property
|
||||||
def address(self):
|
def address(self):
|
||||||
return self.asset.address
|
return self.asset.address
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def sync_accounts(gathered_accounts):
|
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 = []
|
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:
|
for gathered_account in gathered_accounts:
|
||||||
asset_id = gathered_account.asset_id
|
asset_id = gathered_account.asset_id
|
||||||
username = gathered_account.username
|
username = gathered_account.username
|
||||||
|
@ -32,13 +61,12 @@ class GatheredAccount(JMSOrgBaseModel):
|
||||||
Q(asset_id=asset_id, name=username)
|
Q(asset_id=asset_id, name=username)
|
||||||
)
|
)
|
||||||
if accounts.exists():
|
if accounts.exists():
|
||||||
continue
|
cls.update_exists_accounts(gathered_account, accounts)
|
||||||
account = Account(
|
else:
|
||||||
asset_id=asset_id, username=username,
|
cls.create_accounts(gathered_account, accounts)
|
||||||
name=username, source=Source.COLLECTED
|
|
||||||
)
|
gathered_account.status = ConfirmOrIgnore.confirmed
|
||||||
account_objs.append(account)
|
gathered_account.save(update_fields=['action'])
|
||||||
Account.objects.bulk_create(account_objs)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Gather asset accounts")
|
verbose_name = _("Gather asset accounts")
|
||||||
|
|
|
@ -236,7 +236,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||||
class Meta(BaseAccountSerializer.Meta):
|
class Meta(BaseAccountSerializer.Meta):
|
||||||
model = Account
|
model = Account
|
||||||
automation_fields = [
|
automation_fields = [
|
||||||
'date_last_access', 'access_by', 'date_verified', 'connectivity',
|
'date_last_login', 'login_by', 'date_verified', 'connectivity',
|
||||||
'date_change_secret', 'change_secret_status'
|
'date_change_secret', 'change_secret_status'
|
||||||
]
|
]
|
||||||
fields = BaseAccountSerializer.Meta.fields + [
|
fields = BaseAccountSerializer.Meta.fields + [
|
||||||
|
|
|
@ -2,10 +2,15 @@ from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.models import GatheredAccount
|
from accounts.models import GatheredAccount
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from .account import AccountAssetSerializer
|
from .account import AccountAssetSerializer as _AccountAssetSerializer
|
||||||
from .base import BaseAccountSerializer
|
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):
|
class GatheredAccountSerializer(BulkOrgResourceModelSerializer):
|
||||||
asset = AccountAssetSerializer(label=_('Asset'))
|
asset = AccountAssetSerializer(label=_('Asset'))
|
||||||
|
|
||||||
|
@ -13,7 +18,8 @@ class GatheredAccountSerializer(BulkOrgResourceModelSerializer):
|
||||||
model = GatheredAccount
|
model = GatheredAccount
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'present', 'asset', 'username',
|
'id', 'present', 'asset', 'username',
|
||||||
'date_updated', 'address_last_login', 'date_last_login'
|
'date_updated', 'address_last_login',
|
||||||
|
'date_last_login', 'status'
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -1,39 +1,33 @@
|
||||||
# ~*~ coding: utf-8 ~*~
|
# ~*~ 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 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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
#
|
||||||
@org_aware_func("nodes")
|
# @org_aware_func("nodes")
|
||||||
def gather_asset_accounts_util(nodes, task_name):
|
# def gather_asset_accounts_util(nodes, task_name):
|
||||||
from accounts.models import GatherAccountsAutomation
|
# from accounts.models import GatherAccountsAutomation
|
||||||
task_name = GatherAccountsAutomation.generate_unique_name(task_name)
|
# task_name = GatherAccountsAutomation.generate_unique_name(task_name)
|
||||||
|
#
|
||||||
task_snapshot = {
|
# task_snapshot = {
|
||||||
'nodes': [str(node.id) for node in nodes],
|
# 'nodes': [str(node.id) for node in nodes],
|
||||||
}
|
# }
|
||||||
tp = AutomationTypes.verify_account
|
# tp = AutomationTypes.verify_account
|
||||||
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
# quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||||
|
#
|
||||||
|
#
|
||||||
@shared_task(
|
# @shared_task(
|
||||||
queue="ansible",
|
# queue="ansible",
|
||||||
verbose_name=_('Gather asset accounts'),
|
# verbose_name=_('Gather asset accounts'),
|
||||||
activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
|
# activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
|
||||||
description=_("Unused")
|
# description=_("Unused")
|
||||||
)
|
# )
|
||||||
def gather_asset_accounts_task(node_ids, task_name=None):
|
# def gather_asset_accounts_task(node_ids, task_name=None):
|
||||||
if task_name is None:
|
# if task_name is None:
|
||||||
task_name = gettext_noop("Gather assets accounts")
|
# task_name = gettext_noop("Gather assets accounts")
|
||||||
|
#
|
||||||
nodes = Node.objects.filter(id__in=node_ids)
|
# nodes = Node.objects.filter(id__in=node_ids)
|
||||||
gather_asset_accounts_util(nodes=nodes, task_name=task_name)
|
# gather_asset_accounts_util(nodes=nodes, task_name=task_name)
|
||||||
|
#
|
||||||
|
|
|
@ -131,8 +131,8 @@ class AutomationExecution(OrgModelMixin):
|
||||||
return self.snapshot['type']
|
return self.snapshot['type']
|
||||||
|
|
||||||
def get_all_asset_ids(self):
|
def get_all_asset_ids(self):
|
||||||
node_ids = self.snapshot['nodes']
|
node_ids = self.snapshot.get('nodes', [])
|
||||||
asset_ids = self.snapshot['assets']
|
asset_ids = self.snapshot.get('assets', [])
|
||||||
nodes = Node.objects.filter(id__in=node_ids)
|
nodes = Node.objects.filter(id__in=node_ids)
|
||||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||||
asset_ids = set(list(asset_ids) + list(node_asset_ids))
|
asset_ids = set(list(asset_ids) + list(node_asset_ids))
|
||||||
|
|
|
@ -75,4 +75,9 @@ class Language(models.TextChoices):
|
||||||
jp = 'ja', '日本語',
|
jp = 'ja', '日本語',
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmOrIgnore(models.TextChoices):
|
||||||
|
confirmed = 'confirmed', _('Confirmed')
|
||||||
|
ignored = 'ignored', _('Ignored')
|
||||||
|
|
||||||
|
|
||||||
COUNTRY_CALLING_CODES = get_country_phone_choices()
|
COUNTRY_CALLING_CODES = get_country_phone_choices()
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<!-- css file -->
|
<!-- css file -->
|
||||||
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
|
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'css/font-awesome.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/style.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue