perf: merge with remote

pull/14774/head^2
ibuler 2025-01-03 11:23:42 +08:00
commit 738465c02d
21 changed files with 6783 additions and 6607 deletions

View File

@ -5,14 +5,16 @@ from rest_framework.response import Response
from rest_framework.status import HTTP_200_OK
from accounts import serializers
from accounts.const import ChangeSecretRecordStatusChoice
from accounts.filters import AccountFilterSet
from accounts.mixins import AccountRecordViewLogMixin
from accounts.models import Account
from accounts.models import Account, ChangeSecretRecord
from assets.models import Asset, Node
from authentication.permissions import UserConfirmation, ConfirmType
from common.api.mixin import ExtraFilterFieldsMixin
from common.drf.filters import AttrRulesFilterBackend
from common.permissions import IsValidUser
from common.utils import lazyproperty
from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission
@ -35,6 +37,8 @@ class AccountViewSet(OrgBulkModelViewSet):
'partial_update': ['accounts.change_account'],
'su_from_accounts': 'accounts.view_account',
'clear_secret': 'accounts.change_account',
'move_to_assets': 'accounts.create_account',
'copy_to_assets': 'accounts.create_account',
}
export_as_zip = True
@ -88,6 +92,43 @@ class AccountViewSet(OrgBulkModelViewSet):
self.model.objects.filter(id__in=account_ids).update(secret=None)
return Response(status=HTTP_200_OK)
def _copy_or_move_to_assets(self, request, move=False):
account = self.get_object()
asset_ids = request.data.get('assets', [])
assets = Asset.objects.filter(id__in=asset_ids)
field_names = [
'name', 'username', 'secret_type', 'secret',
'privileged', 'is_active', 'source', 'source_id', 'comment'
]
account_data = {field: getattr(account, field) for field in field_names}
creation_results = {}
success_count = 0
for asset in assets:
account_data['asset'] = asset
creation_results[asset] = {'state': 'created'}
try:
self.model.objects.create(**account_data)
success_count += 1
except Exception as e:
creation_results[asset] = {'error': str(e), 'state': 'error'}
results = [{'asset': str(asset), **res} for asset, res in creation_results.items()]
if move and success_count > 0:
account.delete()
return Response(results, status=HTTP_200_OK)
@action(methods=['post'], detail=True, url_path='move-to-assets')
def move_to_assets(self, request, *args, **kwargs):
return self._copy_or_move_to_assets(request, move=True)
@action(methods=['post'], detail=True, url_path='copy-to-assets')
def copy_to_assets(self, request, *args, **kwargs):
return self._copy_or_move_to_assets(request, move=False)
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
"""
@ -127,17 +168,31 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
'GET': 'accounts.view_accountsecret',
}
def get_object(self):
@lazyproperty
def account(self) -> Account:
return get_object_or_404(Account, pk=self.kwargs.get('pk'))
def get_object(self):
return self.account
@lazyproperty
def latest_history(self):
return self.account.history.first()
@property
def latest_change_secret_record(self) -> ChangeSecretRecord:
return self.account.change_secret_records.filter(
status=ChangeSecretRecordStatusChoice.pending
).order_by('-date_created').first()
@staticmethod
def filter_spm_queryset(resource_ids, queryset):
return queryset.filter(history_id__in=resource_ids)
def get_queryset(self):
account = self.get_object()
account = self.account
histories = account.history.all()
latest_history = account.history.first()
latest_history = self.latest_history
if not latest_history:
return histories
if account.secret != latest_history.secret:
@ -146,3 +201,25 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
return histories
histories = histories.exclude(history_id=latest_history.history_id)
return histories
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = list(queryset)
latest_history = self.latest_history
if not latest_history:
return queryset
latest_change_secret_record = self.latest_change_secret_record
if not latest_change_secret_record:
return queryset
if latest_change_secret_record.date_created > latest_history.history_date:
temp_history = self.model(
secret=latest_change_secret_record.new_secret,
secret_type=self.account.secret_type,
version=latest_history.version,
history_date=latest_change_secret_record.date_created,
)
queryset = [temp_history] + queryset
return queryset

View File

@ -9,9 +9,10 @@ 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, AutomationExecution
from accounts.models import GatherAccountsAutomation, AutomationExecution, Account
from accounts.models import GatheredAccount
from assets.models import Asset
from common.utils.http import is_true
from orgs.mixins.api import OrgBulkModelViewSet
from .base import AutomationExecutionViewSet
@ -80,30 +81,39 @@ class GatheredAccountViewSet(OrgBulkModelViewSet):
"details": serializers.GatheredAccountDetailsSerializer
}
rbac_perms = {
"sync_accounts": "assets.add_gatheredaccount",
"status": "assets.change_gatheredaccount",
"details": "assets.view_gatheredaccount"
}
@action(methods=["put"], detail=True, url_path="status")
@action(methods=["put"], detail=False, url_path="status")
def status(self, request, *args, **kwargs):
instance = self.get_object()
instance.status = request.data.get("status")
instance.save(update_fields=["status"])
if instance.status == "confirmed":
GatheredAccount.sync_accounts([instance])
ids = request.data.get('ids', [])
new_status = request.data.get("status")
updated_instances = GatheredAccount.objects.filter(id__in=ids)
updated_instances.update(status=new_status)
if new_status == "confirmed":
GatheredAccount.sync_accounts(updated_instances)
return Response(status=status.HTTP_200_OK)
@action(methods=["post"], detail=False, url_path="delete-remote")
def delete_remote(self, request, *args, **kwargs):
asset_id = request.data.get("asset_id")
username = request.data.get("username")
def perform_destroy(self, instance):
request = self.request
params = request.query_params
is_delete_remote = params.get("is_delete_remote")
is_delete_account = params.get("is_delete_account")
asset_id = params.get("asset")
username = params.get("username")
if is_true(is_delete_remote):
self._delete_remote(asset_id, username)
if is_true(is_delete_account):
account = get_object_or_404(Account, username=username, asset_id=asset_id)
account.delete()
super().perform_destroy(instance)
def _delete_remote(self, asset_id, username):
asset = get_object_or_404(Asset, pk=asset_id)
handler = RiskHandler(asset, username, request=self.request)
handler.handle_delete_remote()
return Response(status=status.HTTP_200_OK)
@action(methods=["get"], detail=True, url_path="details")
def details(self, request, *args, **kwargs):

View File

@ -33,18 +33,19 @@ class GatherAccountsFilter:
@staticmethod
def mysql_filter(info):
result = {}
for username, user_info in info.items():
password_last_changed = parse_date(user_info.get('password_last_changed'))
password_lifetime = user_info.get('password_lifetime')
user = {
'username': username,
'date_password_change': password_last_changed,
'date_password_expired': password_last_changed + timezone.timedelta(
days=password_lifetime) if password_last_changed and password_lifetime else None,
'date_last_login': None,
'groups': '',
}
result[username] = user
for host, user_dict in info.items():
for username, user_info in user_dict.items():
password_last_changed = parse_date(user_info.get('password_last_changed'))
password_lifetime = user_info.get('password_lifetime')
user = {
'username': username,
'date_password_change': password_last_changed,
'date_password_expired': password_last_changed + timezone.timedelta(
days=password_lifetime) if password_last_changed and password_lifetime else None,
'date_last_login': None,
'groups': '',
}
result[username] = user
return result
@staticmethod
@ -59,7 +60,7 @@ class GatherAccountsFilter:
'groups': '',
}
detail = {
'canlogin': user_info.get('canlogin'),
'can_login': user_info.get('canlogin'),
'superuser': user_info.get('superuser'),
}
user['detail'] = detail
@ -87,7 +88,6 @@ class GatherAccountsFilter:
'is_disabled': user_info.get('is_disabled', ''),
'default_database_name': user_info.get('default_database_name', ''),
}
print(user)
user['detail'] = detail
result[user['username']] = user
return result

View File

@ -16,7 +16,7 @@ DEFAULT_PASSWORD_RULES = {
__all__ = [
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice', 'GatherAccountDetailField'
]
@ -114,3 +114,20 @@ class ChangeSecretRecordStatusChoice(models.TextChoices):
failed = 'failed', _('Failed')
success = 'success', _('Success')
pending = 'pending', _('Pending')
class GatherAccountDetailField(models.TextChoices):
can_login = 'can_login', _('Can login')
superuser = 'superuser', _('Superuser')
create_date = 'create_date', _('Create date')
is_disabled = 'is_disabled', _('Is disabled')
default_database_name = 'default_database_name', _('Default database name')
uid = 'uid', _('UID')
account_status = 'account_status', _('Account status')
default_tablespace = 'default_tablespace', _('Default tablespace')
roles = 'roles', _('Roles')
privileges = 'privileges', _('Privileges')
groups = 'groups', _('Groups')
sudoers = 'sudoers', 'sudoers'
authorized_keys = 'authorized_keys', _('Authorized keys')
db = 'db', _('DB')

View File

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
#
from django.db.models import Q, F
from django.db.models import Q, F, Value, CharField
from django.db.models.functions import Concat
from django.utils import timezone
from django_filters import rest_framework as drf_filters
from assets.models import Node
from common.drf.filters import BaseFilterSet
from common.utils.timezone import local_zero_hour, local_now
from .models import Account, GatheredAccount, ChangeSecretRecord
from .models import Account, GatheredAccount, ChangeSecretRecord, AccountRisk
class AccountFilterSet(BaseFilterSet):
@ -70,11 +71,20 @@ class AccountFilterSet(BaseFilterSet):
if not value:
return queryset
queryset = (
queryset.prefetch_related("risks")
.annotate(risk=F("risks__risk"), confirmed=F("risks__confirmed"))
.filter(risk=value, confirmed=False)
asset_usernames = AccountRisk.objects.filter(risk=value). \
values_list(
Concat(
F('asset_id'), Value('-'), F('username'),
output_field=CharField()
), flat=True
)
queryset = queryset.annotate(
asset_username=Concat(
F('asset_id'), Value('-'), F('username'),
output_field=CharField()
)
).filter(asset_username__in=asset_usernames)
return queryset
@staticmethod

View File

@ -0,0 +1,42 @@
# Generated by Django 4.1.13 on 2024-12-24 05:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0011_auto_20241204_1516'),
('accounts', '0023_alter_changesecretrecord_options'),
]
operations = [
migrations.RemoveField(
model_name='changesecretrecord',
name='date_started',
),
migrations.AlterField(
model_name='changesecretrecord',
name='account',
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='change_secret_records', to='accounts.account'
),
),
migrations.AlterField(
model_name='changesecretrecord',
name='asset',
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='asset_change_secret_records', to='assets.asset'
),
),
migrations.AlterField(
model_name='changesecretrecord',
name='execution',
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='execution_change_secret_records', to='accounts.automationexecution'
),
),
]

View File

@ -3,12 +3,14 @@ from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords
from assets.models.base import AbsConnectivity
from common.utils import lazyproperty
from common.utils import lazyproperty, get_logger
from labels.mixins import LabeledMixin
from .base import BaseAccount
from .mixins import VaultModelMixin
from ..const import Source
logger = get_logger(__file__)
__all__ = ['Account', 'AccountHistoricalRecords']
@ -26,7 +28,7 @@ class AccountHistoricalRecords(HistoricalRecords):
history_account = instance.history.first()
if history_account is None:
self.updated_version = 1
self.updated_version = 0
return super().post_save(instance, created, using=using, **kwargs)
history_attrs = {field: getattr(history_account, field) for field in check_fields}
@ -38,12 +40,13 @@ class AccountHistoricalRecords(HistoricalRecords):
if not diff:
return
self.updated_version = history_account.version + 1
instance.version = self.updated_version
return super().post_save(instance, created, using=using, **kwargs)
def create_historical_record(self, instance, history_type, using=None):
super().create_historical_record(instance, history_type, using=using)
if self.updated_version is not None:
instance.version = self.updated_version
# Ignore deletion history_type: -
if self.updated_version is not None and history_type != '-':
instance.save(update_fields=['version'])
def create_history_model(self, model, inherited):

View File

@ -31,12 +31,20 @@ class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
class ChangeSecretRecord(JMSBaseModel):
execution = models.ForeignKey('accounts.AutomationExecution', on_delete=models.SET_NULL, null=True)
asset = models.ForeignKey('assets.Asset', on_delete=models.SET_NULL, null=True)
account = models.ForeignKey('accounts.Account', on_delete=models.SET_NULL, null=True)
account = models.ForeignKey(
'accounts.Account', on_delete=models.SET_NULL,
null=True, related_name='change_secret_records'
)
asset = models.ForeignKey(
'assets.Asset', on_delete=models.SET_NULL,
null=True, related_name='asset_change_secret_records'
)
execution = models.ForeignKey(
'accounts.AutomationExecution', on_delete=models.SET_NULL,
null=True, related_name='execution_change_secret_records',
)
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'), db_index=True)
ignore_fail = models.BooleanField(default=False, verbose_name=_('Ignore fail'))
status = models.CharField(

View File

@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from accounts.const import AutomationTypes
from accounts.const import AutomationTypes, GatherAccountDetailField
from accounts.models import GatherAccountsAutomation
from accounts.models import GatheredAccount
from accounts.serializers.account.account import AccountAssetSerializer as _AccountAssetSerializer
@ -82,7 +82,9 @@ class GatheredAccountDetailsSerializer(serializers.Serializer):
obj = get_object_or_404(GatheredAccount, pk=pk)
details = obj.detail
for key, value in details.items():
field_dict = GatherAccountDetailField._member_map_
label = field_dict[key].label if key in field_dict else key
if isinstance(value, bool):
self.fields[key] = serializers.BooleanField(label=key, read_only=True)
self.fields[key] = serializers.BooleanField(label=label, read_only=True)
else:
self.fields[key] = serializers.CharField(label=key, read_only=True)
self.fields[key] = serializers.CharField(label=label, read_only=True)

View File

@ -6,9 +6,7 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from assets.const import Connectivity
from common.utils import (
get_logger
)
from common.utils import get_logger
logger = get_logger(__file__)

View File

@ -230,7 +230,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
.prefetch_related('platform', 'platform__automation') \
.annotate(category=F("platform__category")) \
.annotate(type=F("platform__type")) \
.annotate(assets_amount=Count('accounts'))
.annotate(accounts_amount=Count('accounts'))
if queryset.model is Asset:
queryset = queryset.prefetch_related('labels__label', 'labels')
else:

View File

@ -6,7 +6,8 @@ from rest_framework.validators import UniqueValidator
from assets.models import Asset
from common.serializers import (
WritableNestedModelSerializer, type_field_map, MethodSerializer,
DictSerializer, create_serializer_class, ResourceLabelsMixin
DictSerializer, create_serializer_class, ResourceLabelsMixin,
CommonSerializerMixin
)
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import lazyproperty
@ -158,7 +159,7 @@ class PlatformCustomField(serializers.Serializer):
choices = serializers.ListField(default=list, label=_("Choices"), required=False)
class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
class PlatformSerializer(ResourceLabelsMixin, CommonSerializerMixin, WritableNestedModelSerializer):
id = serializers.IntegerField(
label='ID', required=False,
validators=[UniqueValidator(queryset=Platform.objects.all())]

View File

@ -5254,7 +5254,7 @@ msgstr ""
#: ops/models/job.py:148
msgid "Timeout (Seconds)"
msgstr ""
msgstr "Timeout (Sec)"
#: ops/models/job.py:153
msgid "Use Parameter Define"
@ -5334,8 +5334,8 @@ msgid "Next execution time"
msgstr ""
#: ops/serializers/job.py:15
msgid "Execute after saving"
msgstr "Execute after saving"
msgid "Run on save"
msgstr "Run on save"
#: ops/serializers/job.py:72
msgid "Job type"

View File

@ -5527,7 +5527,7 @@ msgid "Next execution time"
msgstr "最後の実行"
#: ops/serializers/job.py:15
msgid "Execute after saving"
msgid "Run on save"
msgstr "保存後に実行"
#: ops/serializers/job.py:72

View File

@ -5478,7 +5478,7 @@ msgid "Next execution time"
msgstr "下次执行时间"
#: ops/serializers/job.py:15
msgid "Execute after saving"
msgid "Run on save"
msgstr "保存后执行"
#: ops/serializers/job.py:72

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

@ -12,7 +12,7 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer
class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin):
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
run_after_save = serializers.BooleanField(label=_("Execute after saving"), default=False, required=False)
run_after_save = serializers.BooleanField(label=_("Run on save"), default=False, required=False)
nodes = serializers.ListField(required=False, child=serializers.CharField())
date_last_run = serializers.DateTimeField(label=_('Date last run'), read_only=True)
name = serializers.CharField(label=_('Name'), max_length=128, allow_blank=True, required=False)

View File

@ -1,12 +1,14 @@
from collections import defaultdict
from django.utils import timezone
from accounts.const import AliasAccount
from accounts.models import VirtualAccount
from assets.models import Asset, MyAsset
from common.utils import lazyproperty
from orgs.utils import tmp_to_org, tmp_to_root_org
from .permission import AssetPermissionUtil
from perms.const import ActionChoices
from .permission import AssetPermissionUtil
__all__ = ['PermAssetDetailUtil']
@ -40,6 +42,12 @@ class PermAssetDetailUtil:
def validate_permission(self, account_name, protocol):
with tmp_to_org(self.asset.org):
if self.user.is_superuser:
account = self.asset.accounts.all().active().get(name=account_name)
account.actions = ActionChoices.all()
account.date_expired = timezone.now() + timezone.timedelta(days=365)
return account
protocols = self.get_permed_protocols_for_user(only_name=True)
if 'all' not in protocols and protocol not in protocols:
return None