diff --git a/apps/accounts/api/account/__init__.py b/apps/accounts/api/account/__init__.py index 2b1f578e7..c2221a379 100644 --- a/apps/accounts/api/account/__init__.py +++ b/apps/accounts/api/account/__init__.py @@ -1,5 +1,6 @@ from .account import * +from .application import * +from .pam_dashboard import * from .task import * from .template import * from .virtual import * -from .application import * diff --git a/apps/accounts/api/account/account.py b/apps/accounts/api/account/account.py index a9edc25c2..6aa3d330e 100644 --- a/apps/accounts/api/account/account.py +++ b/apps/accounts/api/account/account.py @@ -1,4 +1,6 @@ +from django.db import transaction from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ from rest_framework.decorators import action from rest_framework.generics import ListAPIView, CreateAPIView from rest_framework.response import Response @@ -14,10 +16,12 @@ 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 common.utils import lazyproperty, get_logger from orgs.mixins.api import OrgBulkModelViewSet from rbac.permissions import RBACPermission +logger = get_logger(__file__) + __all__ = [ 'AccountViewSet', 'AccountSecretsViewSet', 'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi', @@ -109,10 +113,12 @@ class AccountViewSet(OrgBulkModelViewSet): account_data['asset'] = asset creation_results[asset] = {'state': 'created'} try: - self.model.objects.create(**account_data) - success_count += 1 + with transaction.atomic(): + self.model.objects.create(**account_data) + success_count += 1 except Exception as e: - creation_results[asset] = {'error': str(e), 'state': 'error'} + logger.debug(f'{ "Move" if move else "Copy" } to assets error: {e}') + creation_results[asset] = {'error': _('Account already exists'), 'state': 'error'} results = [{'asset': str(asset), **res} for asset, res in creation_results.items()] diff --git a/apps/accounts/api/account/pam_dashboard.py b/apps/accounts/api/account/pam_dashboard.py new file mode 100644 index 000000000..b857b56ee --- /dev/null +++ b/apps/accounts/api/account/pam_dashboard.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# +from django.http.response import JsonResponse +from rest_framework.views import APIView + +from accounts.models import Account, RiskChoice +from common.utils.timezone import local_monday + +__all__ = ['PamDashboardApi'] + + +class PamDashboardApi(APIView): + http_method_names = ['get'] + rbac_perms = { + 'GET': 'accounts.view_account', + } + + def get(self, request, *args, **kwargs): + query_params = self.request.query_params + data = {} + + account_count = Account.objects.count() + privileged_account_count = Account.objects.filter(privileged=True).count() + + if query_params.get('total_accounts'): + data['total_accounts'] = account_count + + if query_params.get('total_week_add_accounts'): + monday_time = local_monday() + data['total_week_add_accounts'] = Account.objects.filter(date_created__gte=monday_time).count() + + if query_params.get('total_privileged_accounts'): + data['total_privileged_accounts'] = privileged_account_count + + if query_params.get('total_ordinary_accounts'): + data['total_ordinary_accounts'] = account_count - privileged_account_count + + if query_params.get('total_unmanaged_accounts'): + data['total_unmanaged_accounts'] = Account.get_risks(risk_type=RiskChoice.new_found).count() + + if query_params.get('total_unavailable_accounts'): + data['total_unavailable_accounts'] = Account.objects.filter(is_active=False).count() + + if query_params.get('total_long_time_no_login_accounts'): + data['total_long_time_no_login_accounts'] = Account.get_risks(risk_type=RiskChoice.long_time_no_login).count() + + if query_params.get('total_weak_password_accounts'): + data['total_weak_password_accounts'] = Account.get_risks(risk_type=RiskChoice.weak_password).count() + + if query_params.get('total_long_time_change_password_accounts'): + data['total_long_time_change_password_accounts'] = Account.get_risks(risk_type=RiskChoice.long_time_password).count() + + return JsonResponse(data, status=200) diff --git a/apps/accounts/automations/base/manager.py b/apps/accounts/automations/base/manager.py index b86963e7c..1e261a89b 100644 --- a/apps/accounts/automations/base/manager.py +++ b/apps/accounts/automations/base/manager.py @@ -54,7 +54,7 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager): asset = privilege_account.asset accounts = asset.accounts.all() - accounts = accounts.filter(id__in=self.account_ids) + accounts = accounts.filter(id__in=self.account_ids, secret_reset=True) if self.secret_type: accounts = accounts.filter(secret_type=self.secret_type) diff --git a/apps/accounts/automations/check_account/manager.py b/apps/accounts/automations/check_account/manager.py index 9d802bd3e..162f40b2c 100644 --- a/apps/accounts/automations/check_account/manager.py +++ b/apps/accounts/automations/check_account/manager.py @@ -3,7 +3,7 @@ from collections import defaultdict from django.utils import timezone -from accounts.models import Account, AccountRisk +from accounts.models import Account, AccountRisk, RiskChoice from assets.automations.base.manager import BaseManager from common.decorators import bulk_create_decorator, bulk_update_decorator from common.utils.strings import color_fmt @@ -69,12 +69,12 @@ def check_account_secrets(accounts, assets): if is_weak_password(account.secret): print(tmpl % (account, color_fmt("weak", "red"))) - summary["weak_password"] += 1 - result["weak_password"].append(result_item) + summary[RiskChoice.weak_password] += 1 + result[RiskChoice.weak_password].append(result_item) risks.append( { "account": account, - "risk": "weak_password", + "risk": RiskChoice.weak_password, } ) else: @@ -143,7 +143,7 @@ class CheckAccountManager(BaseManager): "\n---\nSummary: \nok: %s, weak password: %s, no secret: %s, using time: %ss" % ( self.summary["ok"], - self.summary["weak_password"], + self.summary[RiskChoice.weak_password], self.summary["no_secret"], int(self.duration), ) diff --git a/apps/accounts/automations/gather_account/manager.py b/apps/accounts/automations/gather_account/manager.py index fb0c819fa..af085926b 100644 --- a/apps/accounts/automations/gather_account/manager.py +++ b/apps/accounts/automations/gather_account/manager.py @@ -4,7 +4,7 @@ from collections import defaultdict from django.utils import timezone from accounts.const import AutomationTypes -from accounts.models import GatheredAccount, Account, AccountRisk +from accounts.models import GatheredAccount, Account, AccountRisk, RiskChoice from common.const import ConfirmOrIgnore from common.decorators import bulk_create_decorator, bulk_update_decorator from common.utils import get_logger @@ -68,7 +68,7 @@ class AnalyseAccountRisk: {"field": "date_last_login", "risk": "long_time_no_login", "delta": long_time}, { "field": "date_password_change", - "risk": "long_time_password", + "risk": RiskChoice.long_time_password, "delta": long_time, }, { @@ -164,7 +164,7 @@ class AnalyseAccountRisk: self._create_risk( dict( **basic, - risk="new_found", + risk=RiskChoice.new_found, details=[{"datetime": self.now.isoformat()}], ) ) @@ -358,7 +358,6 @@ class GatherAccountsManager(AccountBasePlaybookManager): for asset, accounts_data in self.asset_account_info.items(): ori_users = self.ori_asset_usernames[str(asset.id)] with tmp_to_org(asset.org_id): - gathered_accounts = [] for d in accounts_data: username = d["username"] ori_account = self.ori_gathered_accounts_mapper.get( @@ -374,6 +373,9 @@ class GatherAccountsManager(AccountBasePlaybookManager): self.create_gathered_account.finish() self.update_gathered_account.finish() self.update_gather_accounts_status(asset) + if not self.is_sync_account: + continue + gathered_accounts = GatheredAccount.objects.filter(asset=asset) GatheredAccount.sync_accounts(gathered_accounts, self.is_sync_account) # 因为有 bulk create, bulk update, 所以这里需要 sleep 一下,等待数据同步 time.sleep(0.5) diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py index d3db70649..84dd82452 100644 --- a/apps/accounts/filters.py +++ b/apps/accounts/filters.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # -from django.db.models import Q, F, Value, CharField -from django.db.models.functions import Concat +from django.db.models import Q from django.utils import timezone from django_filters import rest_framework as drf_filters diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py index b5cd382fa..f7c759625 100644 --- a/apps/accounts/models/account.py +++ b/apps/accounts/models/account.py @@ -1,4 +1,5 @@ from django.db import models +from django.db.models import Exists, OuterRef from django.utils.translation import gettext_lazy as _ from simple_history.models import HistoricalRecords @@ -165,6 +166,23 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount): return escape(value) + @classmethod + def get_risks(cls, queryset=None, risk_type=None): + # TODO 数据量大时,子查询性能不佳,考虑用原生sql或者在模型层面做出改动 + from accounts.models import AccountRisk + subquery = AccountRisk.objects.filter( + asset_id=OuterRef('asset_id'), + username=OuterRef('username') + ) + + if risk_type: + subquery = subquery.filter(risk=risk_type) + + if queryset is None: + queryset = cls.objects.all() + + return queryset.filter(Exists(subquery)) + def replace_history_model_with_mixin(): """ diff --git a/apps/accounts/risk_handlers.py b/apps/accounts/risk_handlers.py index e2911da7e..2dd8c8d53 100644 --- a/apps/accounts/risk_handlers.py +++ b/apps/accounts/risk_handlers.py @@ -1,15 +1,13 @@ +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from accounts.const import AutomationTypes -from common.const import ConfirmOrIgnore from accounts.models import ( GatheredAccount, AccountRisk, SecretType, - AutomationExecution, + AutomationExecution, RiskChoice, ) -from django.utils import timezone - from common.const import ConfirmOrIgnore TYPE_CHOICES = [ @@ -83,7 +81,7 @@ class RiskHandler: GatheredAccount.objects.filter(asset=self.asset, username=self.username).update( present=True, status=ConfirmOrIgnore.confirmed ) - self.risk = "new_found" + self.risk = RiskChoice.new_found def handle_disable_remote(self): pass diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index b9cfea710..f196e1146 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -49,6 +49,7 @@ urlpatterns = [ path('push-account//nodes/', api.PushAccountNodeAddRemoveApi.as_view(), name='push-account-add-or-remove-node'), path('push-account//assets/', api.PushAccountAssetsListApi.as_view(), name='push-account-assets'), + path('pam-dashboard/', api.PamDashboardApi.as_view(), name='pam-dashboard'), path('change-secret-dashboard/', api.ChangeSecretDashboardApi.as_view(), name='change-secret-dashboard'), ]