mirror of https://github.com/jumpserver/jumpserver
merge: with pam (#14911)
* perf: change i18n
* perf: pam
* perf: change translate
* perf: add check account
* perf: add date field
* perf: add account filter
* perf: remove some js
* perf: add account status action
* perf: update pam
* perf: 修改 discover account
* perf: update filter
* perf: update gathered account
* perf: 修改账号同步
* perf: squash migrations
* perf: update pam
* perf: change i18n
* perf: update account risk
* perf: 更新风险发现
* perf: remove css
* perf: Admin connection token
* perf: Add a switch to check connectivity after changing the password, and add a custom ssh command for push tasks
* perf: Modify account migration files
* perf: update pam
* perf: remove to check account dir
* perf: Admin connection token
* perf: update check account
* perf: 优化发送结果
* perf: update pam
* perf: update bulk update create
* perf: prepaire using thread timer for bulk_create_decorator
* perf: update bulk create decorator
* perf: 优化 playbook manager
* perf: 优化收集账号的报表
* perf: Update poetry
* perf: Update Dockerfile with new base image tag
* fix: Account migrate 0012 file
* perf: 修改备份
* perf: update pam
* fix: Expand resource_type filter to include raw type
* feat: PAM Service (#14552)
* feat: PAM Service
* perf: import package name
---------
Co-authored-by: jiangweidong <1053570670@qq.com>
* perf: Change secret dashboard (#14551)
Co-authored-by: feng <1304903146@qq.com>
* perf: update migrations
* perf: 修改支持 pam
* perf: Change secret record table dashboard
* perf: update status
* fix: Automation send report
* perf: Change secret report
* feat: windows accounts gather
* perf: update change status
* perf: Account backup
* perf: Account backup report
* perf: Account migrate
* perf: update service to application
* perf: update migrations
* perf: update logo
* feat: oracle accounts gather (#14571)
* feat: oracle accounts gather
* feat: sqlserver accounts gather
* feat: postgresql accounts gather
* feat: mysql accounts gather
---------
Co-authored-by: wangruidong <940853815@qq.com>
* feat: mongodb accounts gather
* perf: Change secret
* perf: Migrate
* perf: Merge conflicting migration files
* perf: Change secret
* perf: Automation filter org
* perf: Account push
* perf: Random secret string
* perf: Enhance SQL query and update risk handling in accounts
* perf: Ticket filter assignee_id
* perf: 修改 account remote
* perf: 修改一些 adhoc 任务
* perf: Change secret
* perf: Remove push account extra api
* perf: update status
* perf: The entire organization can view activity log
* fix: risk field check
* perf: add account details api
* perf: add demo mode
* perf: Delete gather_account
* perf: Perfect solution to account version problem
* perf: Update status action to handle multiple accounts
* perf: Add GatherAccountDetailField and update serializers
* perf: Display account history in combination with password change records
* perf: Lina translate
* fix: Update mysql_filter to handle nested user info
* perf: Admin connection token validate_permission account
* perf: copy move account
* perf: account filter risk
* perf: account risk filter
* perf: Copy move account failed message
* fix: gather account sync account to asset
* perf: Pam dashboard
* perf: Account dashboard total accounts
* perf: Pam dashboard
* perf: Change secret filter account secret_reset
* perf: 修改 risk filter
* perf: pam translate
* feat: Check for leaked duplicate passwords. (#14711)
* feat: Check for leaked duplicate passwords.
* perf: Use SQLite instead of txt as leak password database
---------
Co-authored-by: jiangweidong <1053570670@qq.com>
Co-authored-by: 老广 <ibuler@qq.com>
* perf: merge with remote
* perf: Add risk change_password_add handle
* perf: Pam dashboard
* perf: check account manager import
* perf: 重构扫描
* perf: 修改 db
* perf: Gather account manager
* perf: update change db lib
* perf: dashboard
* perf: Account gather
* perf: 修改 asset get queryset
* perf: automation report
* perf: Pam account
* perf: Pam dashboard api
* perf: risk add account
* perf: 修改 risk check
* perf: Risk account
* perf: update risk add reopen action
* perf: add pylintrc
* Revert "perf: automation report"
This reverts commit 22aee54207
.
* perf: check account engine
* perf: Perf: Optimism Gather Report Style
* Perf: Remove unuser actions
* Perf: Perf push account
* perf: perf gather account
* perf: Automation report
* perf: Push account recorder
* perf: Push account record
* perf: Pam dashboard
* perf: perf
* perf: update intergration
* perf: integrations application detail add account tab page
* feat: Custom change password supports configuration of interactive items
* perf: Go and Python demo code
* perf: Custom secret change
* perf: add user filter
* perf: translate
* perf: Add demo code docs
* perf: update some i18n
* perf: update some i18n
* perf: Add Java, Node, Go, and cURL demo code
* perf: Translate
* perf: Change secret translate
* perf: Translate
* perf: update some i18n
* perf: translate
* perf: Ansible playbook
* perf: update some choice
* perf: update some choice
* perf: update account serializer remote unused code
* perf: conflict
* perf: update import
---------
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: feng <1304903146@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: wangruidong <940853815@qq.com>
Co-authored-by: jiangweidong <1053570670@qq.com>
Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>
Co-authored-by: zhaojisen <1301338853@qq.com>
pull/14912/head
parent
d516349a68
commit
3f4141ca0b
|
@ -1,4 +1,4 @@
|
||||||
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
||||||
*.mo filter=lfs diff=lfs merge=lfs -text
|
*.mo filter=lfs diff=lfs merge=lfs -text
|
||||||
*.ipdb filter=lfs diff=lfs merge=lfs -text
|
*.ipdb filter=lfs diff=lfs merge=lfs -text
|
||||||
|
leak_passwords.db filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,too-many-ancestors
|
|
@ -1,4 +1,6 @@
|
||||||
from .account import *
|
from .account import *
|
||||||
|
from .application import *
|
||||||
|
from .pam_dashboard import *
|
||||||
from .task import *
|
from .task import *
|
||||||
from .template import *
|
from .template import *
|
||||||
from .virtual import *
|
from .virtual import *
|
||||||
|
|
|
@ -1,20 +1,27 @@
|
||||||
|
from django.db import transaction
|
||||||
from django.shortcuts import get_object_or_404
|
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.decorators import action
|
||||||
from rest_framework.generics import ListAPIView, CreateAPIView
|
from rest_framework.generics import ListAPIView, CreateAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.status import HTTP_200_OK
|
from rest_framework.status import HTTP_200_OK
|
||||||
|
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
|
from accounts.const import ChangeSecretRecordStatusChoice
|
||||||
from accounts.filters import AccountFilterSet
|
from accounts.filters import AccountFilterSet
|
||||||
from accounts.mixins import AccountRecordViewLogMixin
|
from accounts.mixins import AccountRecordViewLogMixin
|
||||||
from accounts.models import Account
|
from accounts.models import Account, ChangeSecretRecord
|
||||||
from assets.models import Asset, Node
|
from assets.models import Asset, Node
|
||||||
from authentication.permissions import UserConfirmation, ConfirmType
|
from authentication.permissions import UserConfirmation, ConfirmType
|
||||||
from common.api.mixin import ExtraFilterFieldsMixin
|
from common.api.mixin import ExtraFilterFieldsMixin
|
||||||
|
from common.drf.filters import AttrRulesFilterBackend
|
||||||
from common.permissions import IsValidUser
|
from common.permissions import IsValidUser
|
||||||
|
from common.utils import lazyproperty, get_logger
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AccountViewSet', 'AccountSecretsViewSet',
|
'AccountViewSet', 'AccountSecretsViewSet',
|
||||||
'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
|
'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
|
||||||
|
@ -24,6 +31,7 @@ __all__ = [
|
||||||
class AccountViewSet(OrgBulkModelViewSet):
|
class AccountViewSet(OrgBulkModelViewSet):
|
||||||
model = Account
|
model = Account
|
||||||
search_fields = ('username', 'name', 'asset__name', 'asset__address', 'comment')
|
search_fields = ('username', 'name', 'asset__name', 'asset__address', 'comment')
|
||||||
|
extra_filter_backends = [AttrRulesFilterBackend]
|
||||||
filterset_class = AccountFilterSet
|
filterset_class = AccountFilterSet
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': serializers.AccountSerializer,
|
'default': serializers.AccountSerializer,
|
||||||
|
@ -33,6 +41,8 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||||
'partial_update': ['accounts.change_account'],
|
'partial_update': ['accounts.change_account'],
|
||||||
'su_from_accounts': 'accounts.view_account',
|
'su_from_accounts': 'accounts.view_account',
|
||||||
'clear_secret': 'accounts.change_account',
|
'clear_secret': 'accounts.change_account',
|
||||||
|
'move_to_assets': 'accounts.create_account',
|
||||||
|
'copy_to_assets': 'accounts.create_account',
|
||||||
}
|
}
|
||||||
export_as_zip = True
|
export_as_zip = True
|
||||||
|
|
||||||
|
@ -86,6 +96,45 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||||
self.model.objects.filter(id__in=account_ids).update(secret=None)
|
self.model.objects.filter(id__in=account_ids).update(secret=None)
|
||||||
return Response(status=HTTP_200_OK)
|
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:
|
||||||
|
with transaction.atomic():
|
||||||
|
self.model.objects.create(**account_data)
|
||||||
|
success_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
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()]
|
||||||
|
|
||||||
|
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):
|
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -125,17 +174,31 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
|
||||||
'GET': 'accounts.view_accountsecret',
|
'GET': 'accounts.view_accountsecret',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_object(self):
|
@lazyproperty
|
||||||
|
def account(self) -> Account:
|
||||||
return get_object_or_404(Account, pk=self.kwargs.get('pk'))
|
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.changesecretrecords.filter(
|
||||||
|
status=ChangeSecretRecordStatusChoice.pending
|
||||||
|
).order_by('-date_created').first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_spm_queryset(resource_ids, queryset):
|
def filter_spm_queryset(resource_ids, queryset):
|
||||||
return queryset.filter(history_id__in=resource_ids)
|
return queryset.filter(history_id__in=resource_ids)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
account = self.get_object()
|
account = self.account
|
||||||
histories = account.history.all()
|
histories = account.history.all()
|
||||||
latest_history = account.history.first()
|
latest_history = self.latest_history
|
||||||
if not latest_history:
|
if not latest_history:
|
||||||
return histories
|
return histories
|
||||||
if account.secret != latest_history.secret:
|
if account.secret != latest_history.secret:
|
||||||
|
@ -144,3 +207,25 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
|
||||||
return histories
|
return histories
|
||||||
histories = histories.exclude(history_id=latest_history.history_id)
|
histories = histories.exclude(history_id=latest_history.history_id)
|
||||||
return histories
|
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
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import os
|
||||||
|
from django.utils.translation import gettext_lazy as _, get_language
|
||||||
|
from django.conf import settings
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from accounts import serializers
|
||||||
|
from accounts.models import IntegrationApplication
|
||||||
|
from audits.models import IntegrationApplicationLog
|
||||||
|
from authentication.permissions import UserConfirmation, ConfirmType
|
||||||
|
from common.exceptions import JMSException
|
||||||
|
from common.permissions import IsValidUser
|
||||||
|
from common.utils import get_request_ip
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
from rbac.permissions import RBACPermission
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationApplicationViewSet(OrgBulkModelViewSet):
|
||||||
|
model = IntegrationApplication
|
||||||
|
search_fields = ('name', 'comment')
|
||||||
|
serializer_classes = {
|
||||||
|
'default': serializers.IntegrationApplicationSerializer,
|
||||||
|
'get_account_secret': serializers.IntegrationAccountSecretSerializer
|
||||||
|
}
|
||||||
|
rbac_perms = {
|
||||||
|
'get_once_secret': 'accounts.change_integrationapplication',
|
||||||
|
'get_account_secret': 'accounts.view_integrationapplication'
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_file(self, path):
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, 'r', encoding='utf-8') as file:
|
||||||
|
return file.read()
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@action(
|
||||||
|
['GET'], detail=False, url_path='sdks',
|
||||||
|
permission_classes=[IsValidUser]
|
||||||
|
)
|
||||||
|
def get_sdks_info(self, request, *args, **kwargs):
|
||||||
|
code_suffix_mapper = {
|
||||||
|
'python': 'py',
|
||||||
|
'java': 'java',
|
||||||
|
'go': 'go',
|
||||||
|
'node': 'js',
|
||||||
|
'curl': 'sh',
|
||||||
|
}
|
||||||
|
sdk_language = request.query_params.get('language','python')
|
||||||
|
sdk_path = os.path.join(settings.APPS_DIR, 'accounts', 'demos', sdk_language)
|
||||||
|
readme_path = os.path.join(sdk_path, f'readme.{get_language()}.md')
|
||||||
|
demo_path = os.path.join(sdk_path, f'demo.{code_suffix_mapper[sdk_language]}')
|
||||||
|
|
||||||
|
readme_content = self.read_file(readme_path)
|
||||||
|
demo_content = self.read_file(demo_path)
|
||||||
|
|
||||||
|
return Response(data={'readme': readme_content, 'code': demo_content})
|
||||||
|
|
||||||
|
@action(
|
||||||
|
['GET'], detail=True, url_path='secret',
|
||||||
|
permission_classes=[RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
|
||||||
|
)
|
||||||
|
def get_once_secret(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
secret = instance.get_secret()
|
||||||
|
return Response(data={'id': instance.id, 'secret': secret})
|
||||||
|
|
||||||
|
@action(['GET'], detail=False, url_path='account-secret',
|
||||||
|
permission_classes=[RBACPermission])
|
||||||
|
def get_account_secret(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_serializer(data=request.query_params)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response({'error': serializer.errors}, status=400)
|
||||||
|
|
||||||
|
service = request.user
|
||||||
|
account = service.get_account(**serializer.data)
|
||||||
|
if not account:
|
||||||
|
msg = _('Account not found')
|
||||||
|
raise JMSException(code='Not found', detail='%s' % msg)
|
||||||
|
asset = account.asset
|
||||||
|
IntegrationApplicationLog.objects.create(
|
||||||
|
remote_addr=get_request_ip(request), service=service.name, service_id=service.id,
|
||||||
|
account=f'{account.name}({account.username})', asset=f'{asset.name}({asset.address})',
|
||||||
|
)
|
||||||
|
return Response(data={'id': request.user.id, 'secret': account.secret})
|
|
@ -0,0 +1,132 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django.db.models import Count, F, Q
|
||||||
|
from django.http.response import JsonResponse
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from accounts.models import (
|
||||||
|
Account, GatherAccountsAutomation,
|
||||||
|
PushAccountAutomation, BackupAccountAutomation,
|
||||||
|
AccountRisk, IntegrationApplication, ChangeSecretAutomation
|
||||||
|
)
|
||||||
|
from assets.const import AllTypes
|
||||||
|
from common.utils.timezone import local_monday
|
||||||
|
|
||||||
|
__all__ = ['PamDashboardApi']
|
||||||
|
|
||||||
|
|
||||||
|
class PamDashboardApi(APIView):
|
||||||
|
http_method_names = ['get']
|
||||||
|
rbac_perms = {
|
||||||
|
'GET': 'accounts.view_account',
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_type_to_accounts():
|
||||||
|
result = Account.objects.annotate(type=F('asset__platform__type')) \
|
||||||
|
.values('type').order_by('type').annotate(total=Count(1))
|
||||||
|
all_types_dict = dict(AllTypes.choices())
|
||||||
|
|
||||||
|
return [
|
||||||
|
{**i, 'label': all_types_dict.get(i['type'], i['type'])}
|
||||||
|
for i in result
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_account_risk_data(_all, query_params):
|
||||||
|
agg_map = {
|
||||||
|
'total_long_time_no_login_accounts': ('long_time_no_login_count', Q(risk='long_time_no_login')),
|
||||||
|
'total_new_found_accounts': ('new_found_count', Q(risk='new_found')),
|
||||||
|
'total_group_changed_accounts': ('group_changed_count', Q(risk='group_changed')),
|
||||||
|
'total_sudo_changed_accounts': ('sudo_changed_count', Q(risk='sudo_changed')),
|
||||||
|
'total_authorized_keys_changed_accounts': (
|
||||||
|
'authorized_keys_changed_count', Q(risk='authorized_keys_changed')),
|
||||||
|
'total_account_deleted_accounts': ('account_deleted_count', Q(risk='account_deleted')),
|
||||||
|
'total_password_expired_accounts': ('password_expired_count', Q(risk='password_expired')),
|
||||||
|
'total_long_time_password_accounts': ('long_time_password_count', Q(risk='long_time_password')),
|
||||||
|
'total_weak_password_accounts': ('weak_password_count', Q(risk='weak_password')),
|
||||||
|
'total_leaked_password_accounts': ('leaked_password_count', Q(risk='leaked_password')),
|
||||||
|
'total_repeated_password_accounts': ('repeated_password_count', Q(risk='repeated_password')),
|
||||||
|
'total_password_error_accounts': ('password_error_count', Q(risk='password_error')),
|
||||||
|
'total_no_admin_account_accounts': ('no_admin_account_count', Q(risk='no_admin_account')),
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregations = {
|
||||||
|
agg_key: Count('account_id', distinct=True, filter=agg_filter)
|
||||||
|
for param_key, (agg_key, agg_filter) in agg_map.items()
|
||||||
|
if _all or query_params.get(param_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
if aggregations:
|
||||||
|
account_stats = AccountRisk.objects.filter(account__isnull=False).aggregate(**aggregations)
|
||||||
|
data = {param_key: account_stats.get(agg_key) for param_key, (agg_key, _) in agg_map.items() if
|
||||||
|
agg_key in account_stats}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_account_data(_all, query_params):
|
||||||
|
agg_map = {
|
||||||
|
'total_accounts': ('total_count', Count('id')),
|
||||||
|
'total_privileged_accounts': ('privileged_count', Count('id', filter=Q(privileged=True))),
|
||||||
|
'total_connectivity_ok_accounts': ('connectivity_ok_count', Count('id', filter=Q(connectivity='ok'))),
|
||||||
|
'total_secret_reset_accounts': ('secret_reset_count', Count('id', filter=Q(secret_reset=True))),
|
||||||
|
'total_valid_accounts': ('valid_count', Count('id', filter=Q(is_active=True))),
|
||||||
|
'total_week_add_accounts': ('week_add_count', Count('id', filter=Q(date_created__gte=local_monday()))),
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregations = {
|
||||||
|
agg_key: agg_expr
|
||||||
|
for param_key, (agg_key, agg_expr) in agg_map.items()
|
||||||
|
if _all or query_params.get(param_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
account_stats = Account.objects.aggregate(**aggregations)
|
||||||
|
for param_key, (agg_key, __) in agg_map.items():
|
||||||
|
if agg_key in account_stats:
|
||||||
|
data[param_key] = account_stats[agg_key]
|
||||||
|
|
||||||
|
if _all or query_params.get('total_ordinary_accounts'):
|
||||||
|
if 'total_count' in account_stats and 'privileged_count' in account_stats:
|
||||||
|
data['total_ordinary_accounts'] = \
|
||||||
|
account_stats['total_count'] - account_stats['privileged_count']
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_automation_counts(_all, query_params):
|
||||||
|
automation_counts = defaultdict(int)
|
||||||
|
automation_models = {
|
||||||
|
'total_count_change_secret_automation': ChangeSecretAutomation,
|
||||||
|
'total_count_gathered_account_automation': GatherAccountsAutomation,
|
||||||
|
'total_count_push_account_automation': PushAccountAutomation,
|
||||||
|
'total_count_backup_account_automation': BackupAccountAutomation,
|
||||||
|
'total_count_integration_application': IntegrationApplication,
|
||||||
|
}
|
||||||
|
|
||||||
|
for param_key, model in automation_models.items():
|
||||||
|
if _all or query_params.get(param_key):
|
||||||
|
automation_counts[param_key] = model.objects.count()
|
||||||
|
|
||||||
|
return automation_counts
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
query_params = self.request.query_params
|
||||||
|
|
||||||
|
_all = query_params.get('all')
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
data.update(self.get_account_data(_all, query_params))
|
||||||
|
data.update(self.get_account_risk_data(_all, query_params))
|
||||||
|
data.update(self.get_automation_counts(_all, query_params))
|
||||||
|
|
||||||
|
if _all or query_params.get('total_count_type_to_accounts'):
|
||||||
|
data.update({
|
||||||
|
'total_count_type_to_accounts': self.get_type_to_accounts(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse(data, status=200)
|
|
@ -1,5 +1,7 @@
|
||||||
from .backup import *
|
from .backup import *
|
||||||
from .base import *
|
from .base import *
|
||||||
from .change_secret import *
|
from .change_secret import *
|
||||||
from .gather_accounts import *
|
from .change_secret_dashboard import *
|
||||||
|
from .check_account import *
|
||||||
|
from .gather_account import *
|
||||||
from .push_account import *
|
from .push_account import *
|
||||||
|
|
|
@ -1,41 +1,36 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from rest_framework import status, viewsets
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
|
from accounts.const import AutomationTypes
|
||||||
from accounts.models import (
|
from accounts.models import (
|
||||||
AccountBackupAutomation, AccountBackupExecution
|
BackupAccountAutomation
|
||||||
)
|
)
|
||||||
from accounts.tasks import execute_account_backup_task
|
|
||||||
from common.const.choices import Trigger
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
from .base import AutomationExecutionViewSet
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AccountBackupPlanViewSet', 'AccountBackupPlanExecutionViewSet'
|
'BackupAccountViewSet', 'BackupAccountExecutionViewSet'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class AccountBackupPlanViewSet(OrgBulkModelViewSet):
|
class BackupAccountViewSet(OrgBulkModelViewSet):
|
||||||
model = AccountBackupAutomation
|
model = BackupAccountAutomation
|
||||||
filterset_fields = ('name',)
|
filterset_fields = ('name',)
|
||||||
search_fields = filterset_fields
|
search_fields = filterset_fields
|
||||||
serializer_class = serializers.AccountBackupSerializer
|
serializer_class = serializers.BackupAccountSerializer
|
||||||
|
|
||||||
|
|
||||||
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
|
class BackupAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||||
serializer_class = serializers.AccountBackupPlanExecutionSerializer
|
rbac_perms = (
|
||||||
search_fields = ('trigger', 'plan__name')
|
("list", "accounts.view_backupaccountexecution"),
|
||||||
filterset_fields = ('trigger', 'plan_id', 'plan__name')
|
("retrieve", "accounts.view_backupaccountexecution"),
|
||||||
http_method_names = ['get', 'post', 'options']
|
("create", "accounts.add_backupaccountexecution"),
|
||||||
|
("report", "accounts.view_backupaccountexecution"),
|
||||||
|
)
|
||||||
|
|
||||||
|
tp = AutomationTypes.backup_account
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = AccountBackupExecution.objects.all()
|
queryset = super().get_queryset()
|
||||||
|
queryset = queryset.filter(automation__type=self.tp)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
pid = serializer.data.get('plan')
|
|
||||||
task = execute_account_backup_task.delay(pid=str(pid), trigger=Trigger.manual)
|
|
||||||
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import status, mixins, viewsets
|
from rest_framework import status, mixins, viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from accounts.models import AutomationExecution
|
from accounts.models import AutomationExecution
|
||||||
|
@ -98,7 +100,6 @@ class AutomationExecutionViewSet(
|
||||||
search_fields = ('trigger', 'automation__name')
|
search_fields = ('trigger', 'automation__name')
|
||||||
filterset_fields = ('trigger', 'automation_id', 'automation__name')
|
filterset_fields = ('trigger', 'automation_id', 'automation__name')
|
||||||
serializer_class = serializers.AutomationExecutionSerializer
|
serializer_class = serializers.AutomationExecutionSerializer
|
||||||
|
|
||||||
tp: str
|
tp: str
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -113,3 +114,10 @@ class AutomationExecutionViewSet(
|
||||||
pid=str(automation.pk), trigger=Trigger.manual, tp=self.tp
|
pid=str(automation.pk), trigger=Trigger.manual, tp=self.tp
|
||||||
)
|
)
|
||||||
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=True, url_path='report')
|
||||||
|
def report(self, request, *args, **kwargs):
|
||||||
|
execution = self.get_object()
|
||||||
|
report = execution.manager.gen_report()
|
||||||
|
return HttpResponse(report)
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
from django.db.models import Max, Q, Subquery, OuterRef
|
||||||
from rest_framework import status, mixins
|
from rest_framework import status, mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice
|
||||||
from accounts.filters import ChangeSecretRecordFilterSet
|
from accounts.filters import ChangeSecretRecordFilterSet
|
||||||
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
|
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
|
||||||
from accounts.tasks import execute_automation_record_task
|
from accounts.tasks import execute_automation_record_task
|
||||||
|
@ -34,7 +35,8 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
|
||||||
|
|
||||||
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||||
filterset_class = ChangeSecretRecordFilterSet
|
filterset_class = ChangeSecretRecordFilterSet
|
||||||
search_fields = ('asset__address',)
|
search_fields = ('asset__address', 'account_username')
|
||||||
|
ordering_fields = ('date_finished',)
|
||||||
tp = AutomationTypes.change_secret
|
tp = AutomationTypes.change_secret
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': serializers.ChangeSecretRecordSerializer,
|
'default': serializers.ChangeSecretRecordSerializer,
|
||||||
|
@ -43,6 +45,8 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'execute': 'accounts.add_changesecretexecution',
|
'execute': 'accounts.add_changesecretexecution',
|
||||||
'secret': 'accounts.view_changesecretrecord',
|
'secret': 'accounts.view_changesecretrecord',
|
||||||
|
'dashboard': 'accounts.view_changesecretrecord',
|
||||||
|
'ignore_fail': 'accounts.view_changesecretrecord',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
|
@ -53,8 +57,37 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||||
]
|
]
|
||||||
return super().get_permissions()
|
return super().get_permissions()
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
if self.action == 'dashboard':
|
||||||
|
return self.get_dashboard_queryset(queryset)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_dashboard_queryset(queryset):
|
||||||
|
recent_dates = queryset.values('account').annotate(
|
||||||
|
max_date_finished=Max('date_finished')
|
||||||
|
)
|
||||||
|
|
||||||
|
recent_success_accounts = queryset.filter(
|
||||||
|
account=OuterRef('account'),
|
||||||
|
date_finished=Subquery(
|
||||||
|
recent_dates.filter(account=OuterRef('account')).values('max_date_finished')[:1]
|
||||||
|
)
|
||||||
|
).filter(Q(status=ChangeSecretRecordStatusChoice.success) | Q(ignore_fail=True))
|
||||||
|
|
||||||
|
failed_records = queryset.filter(
|
||||||
|
~Q(account__in=Subquery(recent_success_accounts.values('account'))),
|
||||||
|
status=ChangeSecretRecordStatusChoice.failed
|
||||||
|
)
|
||||||
|
return failed_records
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return ChangeSecretRecord.objects.all()
|
qs = ChangeSecretRecord.get_valid_records()
|
||||||
|
return qs.filter(
|
||||||
|
execution__automation__type=self.tp
|
||||||
|
)
|
||||||
|
|
||||||
@action(methods=['post'], detail=False, url_path='execute')
|
@action(methods=['post'], detail=False, url_path='execute')
|
||||||
def execute(self, request, *args, **kwargs):
|
def execute(self, request, *args, **kwargs):
|
||||||
|
@ -75,12 +108,24 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||||
serializer = self.get_serializer(instance)
|
serializer = self.get_serializer(instance)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=False, url_path='dashboard')
|
||||||
|
def dashboard(self, request, *args, **kwargs):
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@action(methods=['patch'], detail=True, url_path='ignore-fail')
|
||||||
|
def ignore_fail(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
instance.ignore_fail = True
|
||||||
|
instance.save(update_fields=['ignore_fail'])
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
|
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
|
||||||
rbac_perms = (
|
rbac_perms = (
|
||||||
("list", "accounts.view_changesecretexecution"),
|
("list", "accounts.view_changesecretexecution"),
|
||||||
("retrieve", "accounts.view_changesecretexecution"),
|
("retrieve", "accounts.view_changesecretexecution"),
|
||||||
("create", "accounts.add_changesecretexecution"),
|
("create", "accounts.add_changesecretexecution"),
|
||||||
|
("report", "accounts.view_changesecretexecution"),
|
||||||
)
|
)
|
||||||
|
|
||||||
tp = AutomationTypes.change_secret
|
tp = AutomationTypes.change_secret
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django.http.response import JsonResponse
|
||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice
|
||||||
|
from accounts.models import ChangeSecretAutomation, AutomationExecution, ChangeSecretRecord
|
||||||
|
from assets.models import Node, Asset
|
||||||
|
from common.utils import lazyproperty
|
||||||
|
from common.utils.timezone import local_zero_hour, local_now
|
||||||
|
from ops.celery import app
|
||||||
|
|
||||||
|
__all__ = ['ChangeSecretDashboardApi']
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSecretDashboardApi(APIView):
|
||||||
|
http_method_names = ['get']
|
||||||
|
rbac_perms = {
|
||||||
|
'GET': 'accounts.view_changesecretautomation',
|
||||||
|
}
|
||||||
|
|
||||||
|
tp = AutomationTypes.change_secret
|
||||||
|
task_name = 'accounts.tasks.automation.execute_account_automation_task'
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def days(self):
|
||||||
|
count = self.request.query_params.get('days', 1)
|
||||||
|
return int(count)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def days_to_datetime(self):
|
||||||
|
if self.days == 1:
|
||||||
|
return local_zero_hour()
|
||||||
|
return local_now() - timezone.timedelta(days=self.days)
|
||||||
|
|
||||||
|
def get_queryset_date_filter(self, qs, query_field='date_updated'):
|
||||||
|
return qs.filter(**{f'{query_field}__gte': self.days_to_datetime})
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def date_range_list(self):
|
||||||
|
return [
|
||||||
|
(local_now() - timezone.timedelta(days=i)).date()
|
||||||
|
for i in range(self.days - 1, -1, -1)
|
||||||
|
]
|
||||||
|
|
||||||
|
def filter_by_date_range(self, queryset, field_name):
|
||||||
|
date_range_bounds = self.days_to_datetime.date(), (local_now() + timezone.timedelta(days=1)).date()
|
||||||
|
return queryset.filter(**{f'{field_name}__range': date_range_bounds})
|
||||||
|
|
||||||
|
def calculate_daily_metrics(self, queryset, date_field):
|
||||||
|
filtered_queryset = self.filter_by_date_range(queryset, date_field)
|
||||||
|
results = filtered_queryset.values_list(date_field, 'status')
|
||||||
|
|
||||||
|
status_counts = defaultdict(lambda: defaultdict(int))
|
||||||
|
|
||||||
|
for date_finished, status in results:
|
||||||
|
date_str = str(date_finished.date())
|
||||||
|
if status == ChangeSecretRecordStatusChoice.failed:
|
||||||
|
status_counts[date_str]['failed'] += 1
|
||||||
|
elif status == ChangeSecretRecordStatusChoice.success:
|
||||||
|
status_counts[date_str]['success'] += 1
|
||||||
|
|
||||||
|
metrics = defaultdict(list)
|
||||||
|
for date in self.date_range_list:
|
||||||
|
date_str = str(date)
|
||||||
|
for status in ['success', 'failed']:
|
||||||
|
metrics[status].append(status_counts[date_str].get(status, 0))
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
def get_daily_success_and_failure_metrics(self):
|
||||||
|
metrics = self.calculate_daily_metrics(self.change_secret_records_queryset, 'date_finished')
|
||||||
|
return metrics.get('success', []), metrics.get('failed', [])
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def change_secrets_queryset(self):
|
||||||
|
return ChangeSecretAutomation.objects.all()
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def change_secret_executions_queryset(self):
|
||||||
|
return AutomationExecution.objects.filter(automation__type=self.tp)
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def change_secret_records_queryset(self):
|
||||||
|
return ChangeSecretRecord.get_valid_records().filter(execution__automation__type=self.tp)
|
||||||
|
|
||||||
|
def get_change_secret_asset_queryset(self):
|
||||||
|
qs = self.get_queryset_date_filter(self.change_secrets_queryset)
|
||||||
|
node_ids = qs.filter(nodes__isnull=False).values_list('nodes', flat=True).distinct()
|
||||||
|
nodes = Node.objects.filter(id__in=node_ids)
|
||||||
|
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||||
|
direct_asset_ids = qs.filter(assets__isnull=False).values_list('assets', flat=True).distinct()
|
||||||
|
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
|
||||||
|
return Asset.objects.filter(id__in=asset_ids)
|
||||||
|
|
||||||
|
def get_filtered_counts(self, qs, field):
|
||||||
|
return self.get_queryset_date_filter(qs, field).count()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_status_counts(records):
|
||||||
|
pending = ChangeSecretRecordStatusChoice.pending
|
||||||
|
failed = ChangeSecretRecordStatusChoice.failed
|
||||||
|
total_ids = {str(i) for i in records.exclude(status=pending).values('execution_id').distinct()}
|
||||||
|
failed_ids = {str(i) for i in records.filter(status=failed).values('execution_id').distinct()}
|
||||||
|
total = len(total_ids)
|
||||||
|
failed = len(total_ids & failed_ids)
|
||||||
|
return {
|
||||||
|
'total_count_change_secret_executions': total,
|
||||||
|
'total_count_success_change_secret_executions': total - failed,
|
||||||
|
'total_count_failed_change_secret_executions': failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
query_params = self.request.query_params
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
_all = query_params.get('all')
|
||||||
|
|
||||||
|
if _all or query_params.get('total_count_change_secrets'):
|
||||||
|
data['total_count_change_secrets'] = self.get_filtered_counts(
|
||||||
|
self.change_secrets_queryset, 'date_updated'
|
||||||
|
)
|
||||||
|
|
||||||
|
if _all or query_params.get('total_count_periodic_change_secrets'):
|
||||||
|
data['total_count_periodic_change_secrets'] = self.get_filtered_counts(
|
||||||
|
self.change_secrets_queryset.filter(is_periodic=True), 'date_updated'
|
||||||
|
)
|
||||||
|
|
||||||
|
if _all or query_params.get('total_count_change_secret_assets'):
|
||||||
|
data['total_count_change_secret_assets'] = self.get_change_secret_asset_queryset().count()
|
||||||
|
|
||||||
|
if _all or query_params.get('total_count_change_secret_status'):
|
||||||
|
records = self.get_queryset_date_filter(self.change_secret_records_queryset, 'date_finished')
|
||||||
|
data.update(self.get_status_counts(records))
|
||||||
|
|
||||||
|
if _all or query_params.get('daily_success_and_failure_metrics'):
|
||||||
|
success, failed = self.get_daily_success_and_failure_metrics()
|
||||||
|
data.update({
|
||||||
|
'dates_metrics_date': [date.strftime('%m-%d') for date in self.date_range_list] or ['0'],
|
||||||
|
'dates_metrics_total_count_success': success,
|
||||||
|
'dates_metrics_total_count_failed': failed,
|
||||||
|
})
|
||||||
|
|
||||||
|
if _all or query_params.get('total_count_ongoing_change_secret'):
|
||||||
|
execution_ids = []
|
||||||
|
inspect = app.control.inspect()
|
||||||
|
active_tasks = inspect.active()
|
||||||
|
if active_tasks:
|
||||||
|
for tasks in active_tasks.values():
|
||||||
|
for task in tasks:
|
||||||
|
_id = task.get('id')
|
||||||
|
name = task.get('name')
|
||||||
|
tp = task.kwargs.get('tp')
|
||||||
|
if name == self.task_name and tp == self.tp:
|
||||||
|
execution_ids.append(_id)
|
||||||
|
|
||||||
|
snapshots = self.change_secret_executions_queryset.filter(
|
||||||
|
id__in=execution_ids).values_list('id', 'snapshot')
|
||||||
|
|
||||||
|
asset_ids = {asset for i in snapshots for asset in i.get('assets', [])}
|
||||||
|
account_ids = {account for i in snapshots for account in i.get('accounts', [])}
|
||||||
|
data['total_count_ongoing_change_secret'] = len(execution_ids)
|
||||||
|
data['total_count_ongoing_change_secret_assets'] = len(asset_ids)
|
||||||
|
data['total_count_ongoing_change_secret_accounts'] = len(account_ids)
|
||||||
|
|
||||||
|
return JsonResponse(data, status=200)
|
|
@ -0,0 +1,153 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from django.db.models import Q, Count
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import MethodNotAllowed
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from accounts import serializers
|
||||||
|
from accounts.const import AutomationTypes
|
||||||
|
from accounts.models import (
|
||||||
|
CheckAccountAutomation,
|
||||||
|
AccountRisk,
|
||||||
|
RiskChoice,
|
||||||
|
CheckAccountEngine,
|
||||||
|
AutomationExecution,
|
||||||
|
)
|
||||||
|
from assets.models import Asset
|
||||||
|
from common.api import JMSModelViewSet
|
||||||
|
from common.utils import many_get
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
from .base import AutomationExecutionViewSet
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CheckAccountAutomationViewSet",
|
||||||
|
"CheckAccountExecutionViewSet",
|
||||||
|
"AccountRiskViewSet",
|
||||||
|
"CheckAccountEngineViewSet",
|
||||||
|
]
|
||||||
|
|
||||||
|
from ...risk_handlers import RiskHandler
|
||||||
|
|
||||||
|
|
||||||
|
class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
|
||||||
|
model = CheckAccountAutomation
|
||||||
|
filterset_fields = ("name",)
|
||||||
|
search_fields = filterset_fields
|
||||||
|
serializer_class = serializers.CheckAccountAutomationSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||||
|
rbac_perms = (
|
||||||
|
("list", "accounts.view_checkaccountexecution"),
|
||||||
|
("retrieve", "accounts.view_checkaccountsexecution"),
|
||||||
|
("create", "accounts.add_checkaccountexecution"),
|
||||||
|
("adhoc", "accounts.add_checkaccountexecution"),
|
||||||
|
("report", "accounts.view_checkaccountsexecution"),
|
||||||
|
)
|
||||||
|
ordering = ("-date_created",)
|
||||||
|
tp = AutomationTypes.check_account
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
queryset = queryset.filter(automation__type=self.tp)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
@action(methods=["get"], detail=False, url_path="adhoc")
|
||||||
|
def adhoc(self, request, *args, **kwargs):
|
||||||
|
asset_id = request.query_params.get("asset_id")
|
||||||
|
if not asset_id:
|
||||||
|
return Response(status=400, data={"asset_id": "This field is required."})
|
||||||
|
|
||||||
|
get_object_or_404(Asset, pk=asset_id)
|
||||||
|
execution = AutomationExecution()
|
||||||
|
execution.snapshot = {
|
||||||
|
"assets": [asset_id],
|
||||||
|
"nodes": [],
|
||||||
|
"type": AutomationTypes.check_account,
|
||||||
|
"engines": ["check_account_secret"],
|
||||||
|
"name": "Check asset risk: {} {}".format(asset_id, timezone.now()),
|
||||||
|
}
|
||||||
|
execution.save()
|
||||||
|
execution.start()
|
||||||
|
report = execution.manager.gen_report()
|
||||||
|
return HttpResponse(report)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountRiskViewSet(OrgBulkModelViewSet):
|
||||||
|
model = AccountRisk
|
||||||
|
search_fields = ("username", "asset")
|
||||||
|
filterset_fields = ("risk", "status", "asset")
|
||||||
|
serializer_classes = {
|
||||||
|
"default": serializers.AccountRiskSerializer,
|
||||||
|
"assets": serializers.AssetRiskSerializer,
|
||||||
|
"handle": serializers.HandleRiskSerializer,
|
||||||
|
}
|
||||||
|
ordering_fields = ("asset", "risk", "status", "username", "date_created")
|
||||||
|
ordering = ("status", "asset", "date_created")
|
||||||
|
rbac_perms = {
|
||||||
|
"sync_accounts": "assets.add_accountrisk",
|
||||||
|
"assets": "accounts.view_accountrisk",
|
||||||
|
"handle": "accounts.change_accountrisk",
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
raise MethodNotAllowed("PUT")
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
raise MethodNotAllowed("POST")
|
||||||
|
|
||||||
|
@action(methods=["get"], detail=False, url_path="assets")
|
||||||
|
def assets(self, request, *args, **kwargs):
|
||||||
|
annotations = {
|
||||||
|
f"{risk[0]}_count": Count("id", filter=Q(risk=risk[0]))
|
||||||
|
for risk in RiskChoice.choices
|
||||||
|
}
|
||||||
|
queryset = (
|
||||||
|
AccountRisk.objects.select_related(
|
||||||
|
"asset", "asset__platform"
|
||||||
|
) # 使用 select_related 来优化 asset 和 asset__platform 的查询
|
||||||
|
.values(
|
||||||
|
"asset__id", "asset__name", "asset__address", "asset__platform__name"
|
||||||
|
) # 添加需要的字段
|
||||||
|
.annotate(risk_total=Count("id")) # 计算风险总数
|
||||||
|
.annotate(**annotations) # 使用上面定义的 annotations 进行计数
|
||||||
|
)
|
||||||
|
return self.get_paginated_response_from_queryset(queryset)
|
||||||
|
|
||||||
|
@action(methods=["post"], detail=False, url_path="handle")
|
||||||
|
def handle(self, request, *args, **kwargs):
|
||||||
|
s = self.get_serializer(data=request.data)
|
||||||
|
s.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
asset, username, act, risk = many_get(
|
||||||
|
s.validated_data, ("asset", "username", "action", "risk")
|
||||||
|
)
|
||||||
|
handler = RiskHandler(asset=asset, username=username, request=self.request)
|
||||||
|
data = handler.handle(act, risk)
|
||||||
|
if not data:
|
||||||
|
data = {"message": "Success"}
|
||||||
|
s = serializers.AccountRiskSerializer(instance=data)
|
||||||
|
return Response(data=s.data)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckAccountEngineViewSet(JMSModelViewSet):
|
||||||
|
search_fields = ("name",)
|
||||||
|
serializer_class = serializers.CheckAccountEngineSerializer
|
||||||
|
|
||||||
|
perm_model = CheckAccountEngine
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return CheckAccountEngine.get_default_engines()
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset: list):
|
||||||
|
search = self.request.GET.get('search')
|
||||||
|
if search is not None:
|
||||||
|
queryset = [
|
||||||
|
item for item in queryset
|
||||||
|
if search in item['name']
|
||||||
|
]
|
||||||
|
return queryset
|
|
@ -0,0 +1,123 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from django.http import HttpResponse
|
||||||
|
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
|
||||||
|
|
||||||
|
from accounts import serializers
|
||||||
|
from accounts.const import AutomationTypes
|
||||||
|
from accounts.filters import GatheredAccountFilterSet
|
||||||
|
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
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DiscoverAccountsAutomationViewSet",
|
||||||
|
"DiscoverAccountsExecutionViewSet",
|
||||||
|
"GatheredAccountViewSet",
|
||||||
|
]
|
||||||
|
|
||||||
|
from ...risk_handlers import RiskHandler
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoverAccountsAutomationViewSet(OrgBulkModelViewSet):
|
||||||
|
model = GatherAccountsAutomation
|
||||||
|
filterset_fields = ("name",)
|
||||||
|
search_fields = filterset_fields
|
||||||
|
serializer_class = serializers.DiscoverAccountAutomationSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoverAccountsExecutionViewSet(AutomationExecutionViewSet):
|
||||||
|
rbac_perms = (
|
||||||
|
("list", "accounts.view_gatheraccountsexecution"),
|
||||||
|
("retrieve", "accounts.view_gatheraccountsexecution"),
|
||||||
|
("create", "accounts.add_gatheraccountsexecution"),
|
||||||
|
("adhoc", "accounts.add_gatheraccountsexecution"),
|
||||||
|
("report", "accounts.view_gatheraccountsexecution"),
|
||||||
|
)
|
||||||
|
|
||||||
|
tp = AutomationTypes.gather_accounts
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
queryset = queryset.filter(automation__type=self.tp)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
@action(methods=["get"], detail=False, url_path="adhoc")
|
||||||
|
def adhoc(self, request, *args, **kwargs):
|
||||||
|
asset_id = request.query_params.get("asset_id")
|
||||||
|
if not asset_id:
|
||||||
|
return Response(status=400, data={"asset_id": "This field is required."})
|
||||||
|
|
||||||
|
get_object_or_404(Asset, pk=asset_id)
|
||||||
|
execution = AutomationExecution()
|
||||||
|
execution.snapshot = {
|
||||||
|
"assets": [asset_id],
|
||||||
|
"nodes": [],
|
||||||
|
"type": "gather_accounts",
|
||||||
|
"is_sync_account": False,
|
||||||
|
"check_risk": True,
|
||||||
|
"name": "Adhoc gather accounts: {}".format(asset_id),
|
||||||
|
}
|
||||||
|
execution.save()
|
||||||
|
execution.start()
|
||||||
|
report = execution.manager.gen_report()
|
||||||
|
return HttpResponse(report)
|
||||||
|
|
||||||
|
|
||||||
|
class GatheredAccountViewSet(OrgBulkModelViewSet):
|
||||||
|
model = GatheredAccount
|
||||||
|
search_fields = ("username",)
|
||||||
|
filterset_class = GatheredAccountFilterSet
|
||||||
|
ordering = ("status",)
|
||||||
|
serializer_classes = {
|
||||||
|
"default": serializers.DiscoverAccountSerializer,
|
||||||
|
"status": serializers.DiscoverAccountActionSerializer,
|
||||||
|
"details": serializers.DiscoverAccountDetailsSerializer
|
||||||
|
}
|
||||||
|
rbac_perms = {
|
||||||
|
"status": "assets.change_gatheredaccount",
|
||||||
|
"details": "assets.view_gatheredaccount"
|
||||||
|
}
|
||||||
|
|
||||||
|
@action(methods=["put"], detail=False, url_path="status")
|
||||||
|
def status(self, request, *args, **kwargs):
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
@action(methods=["get"], detail=True, url_path="details")
|
||||||
|
def details(self, request, *args, **kwargs):
|
||||||
|
pk = kwargs.get('pk')
|
||||||
|
account = get_object_or_404(GatheredAccount, pk=pk)
|
||||||
|
serializer = self.get_serializer(account.detail)
|
||||||
|
return Response(data=serializer.data)
|
|
@ -1,59 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
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 GatheredAccount
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
|
||||||
from .base import AutomationExecutionViewSet
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'GatherAccountsAutomationViewSet', 'GatherAccountsExecutionViewSet',
|
|
||||||
'GatheredAccountViewSet'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
|
|
||||||
model = GatherAccountsAutomation
|
|
||||||
filterset_fields = ('name',)
|
|
||||||
search_fields = filterset_fields
|
|
||||||
serializer_class = serializers.GatherAccountAutomationSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class GatherAccountsExecutionViewSet(AutomationExecutionViewSet):
|
|
||||||
rbac_perms = (
|
|
||||||
("list", "accounts.view_gatheraccountsexecution"),
|
|
||||||
("retrieve", "accounts.view_gatheraccountsexecution"),
|
|
||||||
("create", "accounts.add_gatheraccountsexecution"),
|
|
||||||
)
|
|
||||||
|
|
||||||
tp = AutomationTypes.gather_accounts
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
queryset = queryset.filter(automation__type=self.tp)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class GatheredAccountViewSet(OrgBulkModelViewSet):
|
|
||||||
model = GatheredAccount
|
|
||||||
search_fields = ('username',)
|
|
||||||
filterset_class = GatheredAccountFilterSet
|
|
||||||
serializer_classes = {
|
|
||||||
'default': serializers.GatheredAccountSerializer,
|
|
||||||
}
|
|
||||||
rbac_perms = {
|
|
||||||
'sync_accounts': 'assets.add_gatheredaccount',
|
|
||||||
}
|
|
||||||
|
|
||||||
@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')
|
|
||||||
gathered_accounts = self.model.objects.filter(id__in=gathered_account_ids)
|
|
||||||
self.model.sync_accounts(gathered_accounts)
|
|
||||||
return Response(status=status.HTTP_201_CREATED)
|
|
|
@ -1,15 +1,16 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
from rest_framework import mixins
|
||||||
|
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.models import PushAccountAutomation, ChangeSecretRecord
|
from accounts.filters import PushAccountRecordFilterSet
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from accounts.models import PushAccountAutomation, PushSecretRecord
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
||||||
from .base import (
|
from .base import (
|
||||||
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
|
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
|
||||||
AutomationNodeAddRemoveApi, AutomationExecutionViewSet
|
AutomationNodeAddRemoveApi, AutomationExecutionViewSet
|
||||||
)
|
)
|
||||||
from .change_secret import ChangeSecretRecordViewSet
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'PushAccountAutomationViewSet', 'PushAccountAssetsListApi', 'PushAccountRemoveAssetApi',
|
'PushAccountAutomationViewSet', 'PushAccountAssetsListApi', 'PushAccountRemoveAssetApi',
|
||||||
|
@ -30,6 +31,7 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||||
("list", "accounts.view_pushaccountexecution"),
|
("list", "accounts.view_pushaccountexecution"),
|
||||||
("retrieve", "accounts.view_pushaccountexecution"),
|
("retrieve", "accounts.view_pushaccountexecution"),
|
||||||
("create", "accounts.add_pushaccountexecution"),
|
("create", "accounts.add_pushaccountexecution"),
|
||||||
|
("report", "accounts.view_pushaccountexecution"),
|
||||||
)
|
)
|
||||||
|
|
||||||
tp = AutomationTypes.push_account
|
tp = AutomationTypes.push_account
|
||||||
|
@ -40,13 +42,19 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class PushAccountRecordViewSet(ChangeSecretRecordViewSet):
|
class PushAccountRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||||
serializer_class = serializers.ChangeSecretRecordSerializer
|
filterset_class = PushAccountRecordFilterSet
|
||||||
|
search_fields = ('asset__address', 'account_username')
|
||||||
|
ordering_fields = ('date_finished',)
|
||||||
tp = AutomationTypes.push_account
|
tp = AutomationTypes.push_account
|
||||||
|
serializer_classes = {
|
||||||
|
'default': serializers.PushSecretRecordSerializer,
|
||||||
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return ChangeSecretRecord.objects.filter(
|
qs = PushSecretRecord.get_valid_records()
|
||||||
execution__automation__type=AutomationTypes.push_account
|
return qs.filter(
|
||||||
|
execution__automation__type=self.tp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,17 @@ import time
|
||||||
from collections import defaultdict, OrderedDict
|
from collections import defaultdict, OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db.models import F
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from xlsxwriter import Workbook
|
from xlsxwriter import Workbook
|
||||||
|
|
||||||
from accounts.const import AccountBackupType
|
from accounts.const import AccountBackupType
|
||||||
from accounts.models.automations.backup_account import AccountBackupAutomation
|
from accounts.models import BackupAccountAutomation, Account
|
||||||
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
|
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
|
||||||
from accounts.serializers import AccountSecretSerializer
|
from accounts.serializers import AccountSecretSerializer
|
||||||
from assets.const import AllTypes
|
from assets.const import AllTypes
|
||||||
|
from common.const import Status
|
||||||
from common.utils.file import encrypt_and_compress_zip_file, zip_files
|
from common.utils.file import encrypt_and_compress_zip_file, zip_files
|
||||||
from common.utils.timezone import local_now_filename, local_now_display
|
from common.utils.timezone import local_now_filename, local_now_display
|
||||||
from terminal.models.component.storage import ReplayStorage
|
from terminal.models.component.storage import ReplayStorage
|
||||||
|
@ -20,6 +22,7 @@ from users.models import User
|
||||||
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
||||||
split_help_text = _('The account key will be split into two parts and sent')
|
split_help_text = _('The account key will be split into two parts and sent')
|
||||||
|
|
||||||
|
|
||||||
class RecipientsNotFound(Exception):
|
class RecipientsNotFound(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -73,9 +76,9 @@ class BaseAccountHandler:
|
||||||
|
|
||||||
class AssetAccountHandler(BaseAccountHandler):
|
class AssetAccountHandler(BaseAccountHandler):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_filename(plan_name):
|
def get_filename(name):
|
||||||
filename = os.path.join(
|
filename = os.path.join(
|
||||||
PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.xlsx'
|
PATH, f'{name}-{local_now_filename()}-{time.time()}.xlsx'
|
||||||
)
|
)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
@ -117,32 +120,41 @@ class AssetAccountHandler(BaseAccountHandler):
|
||||||
cls.handler_secret(data, section)
|
cls.handler_secret(data, section)
|
||||||
data_map.update(cls.add_rows(data, header_fields, sheet_name))
|
data_map.update(cls.add_rows(data, header_fields, sheet_name))
|
||||||
number_of_backup_accounts = _('Number of backup accounts')
|
number_of_backup_accounts = _('Number of backup accounts')
|
||||||
print('\n\033[33m- {}: {}\033[0m'.format(number_of_backup_accounts, accounts.count()))
|
print('\033[33m- {}: {}\033[0m'.format(number_of_backup_accounts, accounts.count()))
|
||||||
return data_map
|
return data_map
|
||||||
|
|
||||||
|
|
||||||
class AccountBackupHandler:
|
class AccountBackupHandler:
|
||||||
def __init__(self, execution):
|
def __init__(self, manager, execution):
|
||||||
|
self.manager = manager
|
||||||
self.execution = execution
|
self.execution = execution
|
||||||
self.plan_name = self.execution.plan.name
|
self.name = self.execution.snapshot.get('name', '-')
|
||||||
self.is_frozen = False # 任务状态冻结标志
|
|
||||||
|
def get_accounts(self):
|
||||||
|
# TODO 可以优化一下查询 在账号上做 category 的缓存 避免数据量大时连表操作
|
||||||
|
types = self.execution.snapshot.get('types', [])
|
||||||
|
self.manager.summary['total_types'] = len(types)
|
||||||
|
qs = Account.objects.filter(
|
||||||
|
asset__platform__type__in=types
|
||||||
|
).annotate(type=F('asset__platform__type'))
|
||||||
|
return qs
|
||||||
|
|
||||||
def create_excel(self, section='complete'):
|
def create_excel(self, section='complete'):
|
||||||
hint = _('Generating asset or application related backup information files')
|
hint = _('Generating asset related backup information files')
|
||||||
print(
|
print(
|
||||||
'\n'
|
|
||||||
f'\033[32m>>> {hint}\033[0m'
|
f'\033[32m>>> {hint}\033[0m'
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
# Print task start date
|
|
||||||
time_start = time.time()
|
time_start = time.time()
|
||||||
files = []
|
files = []
|
||||||
accounts = self.execution.backup_accounts
|
accounts = self.get_accounts()
|
||||||
|
self.manager.summary['total_accounts'] = accounts.count()
|
||||||
data_map = AssetAccountHandler.create_data_map(accounts, section)
|
data_map = AssetAccountHandler.create_data_map(accounts, section)
|
||||||
if not data_map:
|
if not data_map:
|
||||||
return files
|
return files
|
||||||
|
|
||||||
filename = AssetAccountHandler.get_filename(self.plan_name)
|
filename = AssetAccountHandler.get_filename(self.name)
|
||||||
|
|
||||||
wb = Workbook(filename)
|
wb = Workbook(filename)
|
||||||
for sheet, data in data_map.items():
|
for sheet, data in data_map.items():
|
||||||
|
@ -153,7 +165,7 @@ class AccountBackupHandler:
|
||||||
wb.close()
|
wb.close()
|
||||||
files.append(filename)
|
files.append(filename)
|
||||||
timedelta = round((time.time() - time_start), 2)
|
timedelta = round((time.time() - time_start), 2)
|
||||||
time_cost = _('Time cost')
|
time_cost = _('Duration')
|
||||||
file_created = _('Backup file creation completed')
|
file_created = _('Backup file creation completed')
|
||||||
print('{}: {} {}s'.format(file_created, time_cost, timedelta))
|
print('{}: {} {}s'.format(file_created, time_cost, timedelta))
|
||||||
return files
|
return files
|
||||||
|
@ -163,21 +175,19 @@ class AccountBackupHandler:
|
||||||
return
|
return
|
||||||
recipients = User.objects.filter(id__in=list(recipients))
|
recipients = User.objects.filter(id__in=list(recipients))
|
||||||
print(
|
print(
|
||||||
'\n'
|
|
||||||
f'\033[32m>>> {_("Start sending backup emails")}\033[0m'
|
f'\033[32m>>> {_("Start sending backup emails")}\033[0m'
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
plan_name = self.plan_name
|
name = self.name
|
||||||
for user in recipients:
|
for user in recipients:
|
||||||
if not user.secret_key:
|
if not user.secret_key:
|
||||||
attachment_list = []
|
attachment_list = []
|
||||||
else:
|
else:
|
||||||
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
|
attachment = os.path.join(PATH, f'{name}-{local_now_filename()}-{time.time()}.zip')
|
||||||
encrypt_and_compress_zip_file(attachment, user.secret_key, files)
|
encrypt_and_compress_zip_file(attachment, user.secret_key, files)
|
||||||
attachment_list = [attachment, ]
|
attachment_list = [attachment]
|
||||||
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
|
AccountBackupExecutionTaskMsg(name, user).publish(attachment_list)
|
||||||
email_sent_to = _('Email sent to')
|
|
||||||
print('{} {}({})'.format(email_sent_to, user, user.email))
|
|
||||||
for file in files:
|
for file in files:
|
||||||
os.remove(file)
|
os.remove(file)
|
||||||
|
|
||||||
|
@ -186,63 +196,41 @@ class AccountBackupHandler:
|
||||||
return
|
return
|
||||||
recipients = ReplayStorage.objects.filter(id__in=list(recipients))
|
recipients = ReplayStorage.objects.filter(id__in=list(recipients))
|
||||||
print(
|
print(
|
||||||
'\n'
|
|
||||||
'\033[32m>>> 📃 ---> sftp \033[0m'
|
'\033[32m>>> 📃 ---> sftp \033[0m'
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
plan_name = self.plan_name
|
name = self.name
|
||||||
encrypt_file = _('Encrypting files using encryption password')
|
encrypt_file = _('Encrypting files using encryption password')
|
||||||
for rec in recipients:
|
for rec in recipients:
|
||||||
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
|
attachment = os.path.join(PATH, f'{name}-{local_now_filename()}-{time.time()}.zip')
|
||||||
if password:
|
if password:
|
||||||
print(f'\033[32m>>> {encrypt_file}\033[0m')
|
print(f'\033[32m>>> {encrypt_file}\033[0m')
|
||||||
encrypt_and_compress_zip_file(attachment, password, files)
|
encrypt_and_compress_zip_file(attachment, password, files)
|
||||||
else:
|
else:
|
||||||
zip_files(attachment, files)
|
zip_files(attachment, files)
|
||||||
attachment_list = attachment
|
attachment_list = attachment
|
||||||
AccountBackupByObjStorageExecutionTaskMsg(plan_name, rec).publish(attachment_list)
|
AccountBackupByObjStorageExecutionTaskMsg(name, rec).publish(attachment_list)
|
||||||
file_sent_to = _('The backup file will be sent to')
|
file_sent_to = _('The backup file will be sent to')
|
||||||
print('{}: {}({})'.format(file_sent_to, rec.name, rec.id))
|
print('{}: {}({})'.format(file_sent_to, rec.name, rec.id))
|
||||||
for file in files:
|
for file in files:
|
||||||
os.remove(file)
|
os.remove(file)
|
||||||
|
|
||||||
def step_perform_task_update(self, is_success, reason):
|
|
||||||
self.execution.reason = reason[:1024]
|
|
||||||
self.execution.is_success = is_success
|
|
||||||
self.execution.save()
|
|
||||||
finish = _('Finish')
|
|
||||||
print(f'\n{finish}\n')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def step_finished(is_success):
|
|
||||||
if is_success:
|
|
||||||
print(_('Success'))
|
|
||||||
else:
|
|
||||||
print(_('Failed'))
|
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
is_success = False
|
|
||||||
error = '-'
|
|
||||||
try:
|
try:
|
||||||
backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email.value)
|
backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email)
|
||||||
if backup_type == AccountBackupType.email.value:
|
if backup_type == AccountBackupType.email:
|
||||||
self.backup_by_email()
|
self.backup_by_email()
|
||||||
elif backup_type == AccountBackupType.object_storage.value:
|
elif backup_type == AccountBackupType.object_storage:
|
||||||
self.backup_by_obj_storage()
|
self.backup_by_obj_storage()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.is_frozen = True
|
|
||||||
print(e)
|
|
||||||
error = str(e)
|
error = str(e)
|
||||||
else:
|
print(f'\033[31m>>> {error}\033[0m')
|
||||||
is_success = True
|
self.execution.status = Status.error
|
||||||
finally:
|
self.execution.summary['error'] = error
|
||||||
reason = error
|
|
||||||
self.step_perform_task_update(is_success, reason)
|
|
||||||
self.step_finished(is_success)
|
|
||||||
|
|
||||||
def backup_by_obj_storage(self):
|
def backup_by_obj_storage(self):
|
||||||
object_id = self.execution.snapshot.get('id')
|
object_id = self.execution.snapshot.get('id')
|
||||||
zip_encrypt_password = AccountBackupAutomation.objects.get(id=object_id).zip_encrypt_password
|
zip_encrypt_password = BackupAccountAutomation.objects.get(id=object_id).zip_encrypt_password
|
||||||
obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', [])
|
obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', [])
|
||||||
obj_recipients_part_two = self.execution.snapshot.get('obj_recipients_part_two', [])
|
obj_recipients_part_two = self.execution.snapshot.get('obj_recipients_part_two', [])
|
||||||
no_assigned_sftp_server = _('The backup task has no assigned sftp server')
|
no_assigned_sftp_server = _('The backup task has no assigned sftp server')
|
||||||
|
@ -266,7 +254,6 @@ class AccountBackupHandler:
|
||||||
self.send_backup_obj_storage(files, recipients, zip_encrypt_password)
|
self.send_backup_obj_storage(files, recipients, zip_encrypt_password)
|
||||||
|
|
||||||
def backup_by_email(self):
|
def backup_by_email(self):
|
||||||
|
|
||||||
warn_text = _('The backup task has no assigned recipient')
|
warn_text = _('The backup task has no assigned recipient')
|
||||||
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
|
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
|
||||||
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
|
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
|
||||||
|
@ -276,7 +263,7 @@ class AccountBackupHandler:
|
||||||
f'\033[31m>>> {warn_text}\033[0m'
|
f'\033[31m>>> {warn_text}\033[0m'
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
raise RecipientsNotFound('Not Found Recipients')
|
return
|
||||||
if recipients_part_one and recipients_part_two:
|
if recipients_part_one and recipients_part_two:
|
||||||
print(f'\033[32m>>> {split_help_text}\033[0m')
|
print(f'\033[32m>>> {split_help_text}\033[0m')
|
||||||
files = self.create_excel(section='front')
|
files = self.create_excel(section='front')
|
||||||
|
@ -290,18 +277,5 @@ class AccountBackupHandler:
|
||||||
self.send_backup_mail(files, recipients)
|
self.send_backup_mail(files, recipients)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
plan_start = _('Plan start')
|
print('{}: {}'.format(_('Plan start'), local_now_display()))
|
||||||
plan_end = _('Plan end')
|
self._run()
|
||||||
time_cost = _('Time cost')
|
|
||||||
error = _('An exception occurred during task execution')
|
|
||||||
print('{}: {}'.format(plan_start, local_now_display()))
|
|
||||||
time_start = time.time()
|
|
||||||
try:
|
|
||||||
self._run()
|
|
||||||
except Exception as e:
|
|
||||||
print(error)
|
|
||||||
print(e)
|
|
||||||
finally:
|
|
||||||
print('\n{}: {}'.format(plan_end, local_now_display()))
|
|
||||||
timedelta = round((time.time() - time_start), 2)
|
|
||||||
print('{}: {}s'.format(time_cost, timedelta))
|
|
||||||
|
|
|
@ -1,48 +1,30 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import time
|
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from assets.automations.base.manager import BaseManager
|
||||||
from common.utils.timezone import local_now_display
|
from common.utils.timezone import local_now_display
|
||||||
from .handlers import AccountBackupHandler
|
from .handlers import AccountBackupHandler
|
||||||
|
|
||||||
|
|
||||||
class AccountBackupManager:
|
class AccountBackupManager(BaseManager):
|
||||||
def __init__(self, execution):
|
|
||||||
self.execution = execution
|
|
||||||
self.date_start = timezone.now()
|
|
||||||
self.time_start = time.time()
|
|
||||||
self.date_end = None
|
|
||||||
self.time_end = None
|
|
||||||
self.timedelta = 0
|
|
||||||
|
|
||||||
def do_run(self):
|
def do_run(self):
|
||||||
execution = self.execution
|
execution = self.execution
|
||||||
account_backup_execution_being_executed = _('The account backup plan is being executed')
|
account_backup_execution_being_executed = _('The account backup plan is being executed')
|
||||||
print(f'\n\033[33m# {account_backup_execution_being_executed}\033[0m')
|
print(f'\033[33m# {account_backup_execution_being_executed}\033[0m')
|
||||||
handler = AccountBackupHandler(execution)
|
handler = AccountBackupHandler(self, execution)
|
||||||
handler.run()
|
handler.run()
|
||||||
|
|
||||||
def pre_run(self):
|
def send_report_if_need(self):
|
||||||
self.execution.date_start = self.date_start
|
pass
|
||||||
self.execution.save()
|
|
||||||
|
|
||||||
def post_run(self):
|
|
||||||
self.time_end = time.time()
|
|
||||||
self.date_end = timezone.now()
|
|
||||||
|
|
||||||
|
def print_summary(self):
|
||||||
print('\n\n' + '-' * 80)
|
print('\n\n' + '-' * 80)
|
||||||
plan_execution_end = _('Plan execution end')
|
plan_execution_end = _('Plan execution end')
|
||||||
print('{} {}\n'.format(plan_execution_end, local_now_display()))
|
print('{} {}\n'.format(plan_execution_end, local_now_display()))
|
||||||
self.timedelta = self.time_end - self.time_start
|
time_cost = _('Duration')
|
||||||
time_cost = _('Time cost')
|
print('{}: {}s'.format(time_cost, self.duration))
|
||||||
print('{}: {}s'.format(time_cost, self.timedelta))
|
|
||||||
self.execution.timedelta = self.timedelta
|
|
||||||
self.execution.save()
|
|
||||||
|
|
||||||
def run(self):
|
def get_report_template(self):
|
||||||
self.pre_run()
|
return "accounts/backup_account_report.html"
|
||||||
self.do_run()
|
|
||||||
self.post_run()
|
|
||||||
|
|
|
@ -1,12 +1,173 @@
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.automations.methods import platform_automation_methods
|
from accounts.automations.methods import platform_automation_methods
|
||||||
|
from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice
|
||||||
|
from accounts.models import BaseAccountQuerySet
|
||||||
from assets.automations.base.manager import BasePlaybookManager
|
from assets.automations.base.manager import BasePlaybookManager
|
||||||
|
from assets.const import HostTypes
|
||||||
|
from common.db.utils import safe_db_connection
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AccountBasePlaybookManager(BasePlaybookManager):
|
class AccountBasePlaybookManager(BasePlaybookManager):
|
||||||
|
template_path = ''
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def platform_automation_methods(self):
|
def platform_automation_methods(self):
|
||||||
return platform_automation_methods
|
return platform_automation_methods
|
||||||
|
|
||||||
|
|
||||||
|
class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.secret_type = self.execution.snapshot.get('secret_type')
|
||||||
|
self.secret_strategy = self.execution.snapshot.get(
|
||||||
|
'secret_strategy', SecretStrategy.custom
|
||||||
|
)
|
||||||
|
self.ssh_key_change_strategy = self.execution.snapshot.get(
|
||||||
|
'ssh_key_change_strategy', SSHKeyStrategy.set_jms
|
||||||
|
)
|
||||||
|
self.account_ids = self.execution.snapshot['accounts']
|
||||||
|
self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试
|
||||||
|
self.name_recorder_mapper = {} # 做个映射,方便后面处理
|
||||||
|
|
||||||
|
def gen_account_inventory(self, account, asset, h, path_dir):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_ssh_params(self, secret, secret_type):
|
||||||
|
kwargs = {}
|
||||||
|
if secret_type != SecretType.SSH_KEY:
|
||||||
|
return kwargs
|
||||||
|
kwargs['strategy'] = self.ssh_key_change_strategy
|
||||||
|
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||||
|
|
||||||
|
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
|
||||||
|
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None:
|
||||||
|
if not privilege_account:
|
||||||
|
print('Not privilege account')
|
||||||
|
return
|
||||||
|
|
||||||
|
asset = privilege_account.asset
|
||||||
|
accounts = asset.accounts.all()
|
||||||
|
accounts = accounts.filter(id__in=self.account_ids, secret_reset=True)
|
||||||
|
|
||||||
|
if self.secret_type:
|
||||||
|
accounts = accounts.filter(secret_type=self.secret_type)
|
||||||
|
|
||||||
|
if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED:
|
||||||
|
accounts = accounts.filter(privileged=False).exclude(
|
||||||
|
username__in=['root', 'administrator', privilege_account.username]
|
||||||
|
)
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
def handle_ssh_secret(self, secret_type, new_secret, path_dir):
|
||||||
|
private_key_path = None
|
||||||
|
if secret_type == SecretType.SSH_KEY:
|
||||||
|
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||||
|
new_secret = self.generate_public_key(new_secret)
|
||||||
|
return new_secret, private_key_path
|
||||||
|
|
||||||
|
def gen_inventory(self, h, account, new_secret, private_key_path, asset):
|
||||||
|
secret_type = account.secret_type
|
||||||
|
h['ssh_params'].update(self.get_ssh_params(new_secret, secret_type))
|
||||||
|
h['account'] = {
|
||||||
|
'name': account.name,
|
||||||
|
'username': account.username,
|
||||||
|
'secret_type': secret_type,
|
||||||
|
'secret': account.escape_jinja2_syntax(new_secret),
|
||||||
|
'private_key_path': private_key_path,
|
||||||
|
'become': account.get_ansible_become_auth(),
|
||||||
|
}
|
||||||
|
if asset.platform.type == 'oracle':
|
||||||
|
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||||
|
return h
|
||||||
|
|
||||||
|
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
|
||||||
|
host = super().host_callback(
|
||||||
|
host, asset=asset, account=account, automation=automation,
|
||||||
|
path_dir=path_dir, **kwargs
|
||||||
|
)
|
||||||
|
if host.get('error'):
|
||||||
|
return host
|
||||||
|
|
||||||
|
host['check_conn_after_change'] = self.execution.snapshot.get('check_conn_after_change', True)
|
||||||
|
host['ssh_params'] = {}
|
||||||
|
|
||||||
|
accounts = self.get_accounts(account)
|
||||||
|
error_msg = _("No pending accounts found")
|
||||||
|
if not accounts:
|
||||||
|
print(f'{asset}: {error_msg}')
|
||||||
|
return []
|
||||||
|
|
||||||
|
if asset.type == HostTypes.WINDOWS:
|
||||||
|
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
|
||||||
|
|
||||||
|
inventory_hosts = []
|
||||||
|
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
||||||
|
print(f'Windows {asset} does not support ssh key push')
|
||||||
|
return inventory_hosts
|
||||||
|
|
||||||
|
for account in accounts:
|
||||||
|
h = deepcopy(host)
|
||||||
|
h['name'] += '(' + account.username + ')' # To distinguish different accounts
|
||||||
|
h = self.gen_account_inventory(account, asset, h, path_dir)
|
||||||
|
inventory_hosts.append(h)
|
||||||
|
|
||||||
|
return inventory_hosts
|
||||||
|
|
||||||
|
def on_host_success(self, host, result):
|
||||||
|
recorder = self.name_recorder_mapper.get(host)
|
||||||
|
if not recorder:
|
||||||
|
return
|
||||||
|
recorder.status = ChangeSecretRecordStatusChoice.success.value
|
||||||
|
recorder.date_finished = timezone.now()
|
||||||
|
|
||||||
|
account = recorder.account
|
||||||
|
if not account:
|
||||||
|
print("Account not found, deleted ?")
|
||||||
|
return
|
||||||
|
|
||||||
|
account.secret = getattr(recorder, 'new_secret', account.secret)
|
||||||
|
account.date_updated = timezone.now()
|
||||||
|
|
||||||
|
with safe_db_connection():
|
||||||
|
recorder.save(update_fields=['status', 'date_finished'])
|
||||||
|
account.save(update_fields=['secret', 'date_updated'])
|
||||||
|
|
||||||
|
self.summary['ok_accounts'] += 1
|
||||||
|
self.result['ok_accounts'].append(
|
||||||
|
{
|
||||||
|
"asset": str(account.asset),
|
||||||
|
"username": account.username,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
super().on_host_success(host, result)
|
||||||
|
|
||||||
|
def on_host_error(self, host, error, result):
|
||||||
|
recorder = self.name_recorder_mapper.get(host)
|
||||||
|
if not recorder:
|
||||||
|
return
|
||||||
|
recorder.status = ChangeSecretRecordStatusChoice.failed.value
|
||||||
|
recorder.date_finished = timezone.now()
|
||||||
|
recorder.error = error
|
||||||
|
try:
|
||||||
|
recorder.save()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\033[31m Save {host} recorder error: {e} \033[0m\n")
|
||||||
|
self.summary['fail_accounts'] += 1
|
||||||
|
self.result['fail_accounts'].append(
|
||||||
|
{
|
||||||
|
"asset": str(recorder.asset),
|
||||||
|
"username": recorder.account.username,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
super().on_host_error(host, error, result)
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
register: ping_info
|
register: ping_info
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
|
@ -39,9 +40,12 @@
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret }}"
|
password: "{{ account.secret }}"
|
||||||
commands: "{{ params.commands }}"
|
commands: "{{ params.commands }}"
|
||||||
first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}"
|
answers: "{{ params.answers }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
|
delay_time: "{{ params.delay_time | default(2) }}"
|
||||||
|
prompt: "{{ params.prompt | default('.*') }}"
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: ping_info is succeeded
|
when: ping_info is succeeded and check_conn_after_change
|
||||||
register: change_info
|
register: change_info
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
|
@ -58,4 +62,6 @@
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
when: check_conn_after_change
|
|
@ -10,10 +10,30 @@ protocol: ssh
|
||||||
priority: 50
|
priority: 50
|
||||||
params:
|
params:
|
||||||
- name: commands
|
- name: commands
|
||||||
type: list
|
type: text
|
||||||
label: "{{ 'Params commands label' | trans }}"
|
label: "{{ 'Params commands label' | trans }}"
|
||||||
default: [ '' ]
|
default: ''
|
||||||
help_text: "{{ 'Params commands help text' | trans }}"
|
help_text: "{{ 'Params commands help text' | trans }}"
|
||||||
|
- name: recv_timeout
|
||||||
|
type: int
|
||||||
|
label: "{{ 'Params recv_timeout label' | trans }}"
|
||||||
|
default: 30
|
||||||
|
help_text: "{{ 'Params recv_timeout help text' | trans }}"
|
||||||
|
- name: delay_time
|
||||||
|
type: int
|
||||||
|
label: "{{ 'Params delay_time label' | trans }}"
|
||||||
|
default: 2
|
||||||
|
help_text: "{{ 'Params delay_time help text' | trans }}"
|
||||||
|
- name: prompt
|
||||||
|
type: str
|
||||||
|
label: "{{ 'Params prompt label' | trans }}"
|
||||||
|
default: '.*'
|
||||||
|
help_text: "{{ 'Params prompt help text' | trans }}"
|
||||||
|
- name: answers
|
||||||
|
type: text
|
||||||
|
label: "{{ 'Params answer label' | trans }}"
|
||||||
|
default: '.*'
|
||||||
|
help_text: "{{ 'Params answer help text' | trans }}"
|
||||||
|
|
||||||
i18n:
|
i18n:
|
||||||
SSH account change secret:
|
SSH account change secret:
|
||||||
|
@ -22,11 +42,91 @@ i18n:
|
||||||
en: 'Custom password change by SSH command line'
|
en: 'Custom password change by SSH command line'
|
||||||
|
|
||||||
Params commands help text:
|
Params commands help text:
|
||||||
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
zh: |
|
||||||
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.{login_password}<br />3 .ターミナルの設定<br / >4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード} <br />5. 終了'
|
请将命令中的指定位置改成特殊符号 <br />
|
||||||
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use {username}, {password}, {login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br / >4. username {username} privilege 0 password {password} <br />5. end'
|
1. 改密账号 -> {username} <br />
|
||||||
|
2. 改密密码 -> {password} <br />
|
||||||
|
3. 登录用户密码 -> {login_password} <br />
|
||||||
|
<strong>多条命令使用换行分割,</strong>执行任务时系统会根据特殊符号替换真实数据。<br />
|
||||||
|
比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />
|
||||||
|
enable <br />
|
||||||
|
{login_password} <br />
|
||||||
|
configure terminal <br />
|
||||||
|
username {username} privilege 0 password {password} <br />
|
||||||
|
end <br />
|
||||||
|
ja: |
|
||||||
|
コマンド内の指定された位置を特殊記号に変更してください。<br />
|
||||||
|
新しいパスワード(アカウント変更) -> {username} <br />
|
||||||
|
新しいパスワード(パスワード変更) -> {password} <br />
|
||||||
|
ログインユーザーパスワード -> {login_password} <br />
|
||||||
|
<strong>複数のコマンドは改行で区切り、</strong>タスクを実行するときにシステムは特殊記号を使用して実際のデータを置き換えます。<br />
|
||||||
|
例えば、Cisco機器のパスワードを変更する場合、一般的には5つのコマンドを設定する必要があります:<br />
|
||||||
|
enable <br />
|
||||||
|
{login_password} <br />
|
||||||
|
configure terminal <br />
|
||||||
|
username {username} privilege 0 password {password} <br />
|
||||||
|
end <br />
|
||||||
|
en: |
|
||||||
|
Please change the specified positions in the command to special symbols. <br />
|
||||||
|
Change password account -> {username} <br />
|
||||||
|
Change password -> {password} <br />
|
||||||
|
Login user password -> {login_password} <br />
|
||||||
|
<strong>Multiple commands are separated by new lines,</strong> and when executing tasks, <br />
|
||||||
|
the system will replace the special symbols with real data. <br />
|
||||||
|
For example, to change the password for a Cisco device, you generally need to configure five commands: <br />
|
||||||
|
enable <br />
|
||||||
|
{login_password} <br />
|
||||||
|
configure terminal <br />
|
||||||
|
username {username} privilege 0 password {password} <br />
|
||||||
|
end <br />
|
||||||
|
|
||||||
Params commands label:
|
Params commands label:
|
||||||
zh: '自定义命令'
|
zh: '自定义命令'
|
||||||
ja: 'カスタムコマンド'
|
ja: 'カスタムコマンド'
|
||||||
en: 'Custom command'
|
en: 'Custom command'
|
||||||
|
|
||||||
|
Params recv_timeout label:
|
||||||
|
zh: '超时时间'
|
||||||
|
ja: 'タイムアウト'
|
||||||
|
en: 'Timeout'
|
||||||
|
|
||||||
|
Params recv_timeout help text:
|
||||||
|
zh: '等待命令结果返回的超时时间(秒)'
|
||||||
|
ja: 'コマンドの結果を待つタイムアウト時間(秒)'
|
||||||
|
en: 'The timeout for waiting for the command result to return (Seconds)'
|
||||||
|
|
||||||
|
Params delay_time label:
|
||||||
|
zh: '延迟发送时间'
|
||||||
|
ja: '遅延送信時間'
|
||||||
|
en: 'Delayed send time'
|
||||||
|
|
||||||
|
Params delay_time help text:
|
||||||
|
zh: '每条命令延迟发送的时间间隔(秒)'
|
||||||
|
ja: '各コマンド送信の遅延間隔(秒)'
|
||||||
|
en: 'Time interval for each command delay in sending (Seconds)'
|
||||||
|
|
||||||
|
Params prompt label:
|
||||||
|
zh: '提示符'
|
||||||
|
ja: 'ヒント'
|
||||||
|
en: 'Prompt'
|
||||||
|
|
||||||
|
Params prompt help text:
|
||||||
|
zh: '终端连接后显示的提示符信息(正则表达式)'
|
||||||
|
ja: 'ターミナル接続後に表示されるプロンプト情報(正規表現)'
|
||||||
|
en: 'Prompt information displayed after terminal connection (Regular expression)'
|
||||||
|
|
||||||
|
Params answer label:
|
||||||
|
zh: '命令结果'
|
||||||
|
ja: 'コマンド結果'
|
||||||
|
en: 'Command result'
|
||||||
|
|
||||||
|
Params answer help text:
|
||||||
|
zh: |
|
||||||
|
根据结果匹配度决定是否执行下一条命令,输入框的内容和上方 “自定义命令” 内容按行一一对应(正则表达式)
|
||||||
|
ja: |
|
||||||
|
結果の一致度に基づいて次のコマンドを実行するかどうかを決定します。
|
||||||
|
入力欄の内容は、上の「カスタムコマンド」の内容と行ごとに対応しています(せいきひょうげん)
|
||||||
|
en: |
|
||||||
|
Decide whether to execute the next command based on the result match.
|
||||||
|
The input content corresponds line by line with the content
|
||||||
|
of the `Custom command` above. (Regular expression)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mongodb
|
- hosts: mongodb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test MongoDB connection
|
- name: Test MongoDB connection
|
||||||
|
@ -53,3 +53,4 @@
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
|
when: check_conn_after_change
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
|
@ -54,3 +54,4 @@
|
||||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||||
filter: version
|
filter: version
|
||||||
|
when: check_conn_after_change
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: oracle
|
- hosts: oracle
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test Oracle connection
|
- name: Test Oracle connection
|
||||||
|
@ -40,3 +40,4 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
mode: "{{ account.mode }}"
|
mode: "{{ account.mode }}"
|
||||||
|
when: check_conn_after_change
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: postgre
|
- hosts: postgre
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||||
|
@ -55,3 +55,4 @@
|
||||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||||
|
when: check_conn_after_change
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: sqlserver
|
- hosts: sqlserver
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test SQLServer connection
|
- name: Test SQLServer connection
|
||||||
|
@ -64,3 +64,4 @@
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
script: |
|
script: |
|
||||||
SELECT @@version
|
SELECT @@version
|
||||||
|
when: check_conn_after_change
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
database: passwd
|
database: passwd
|
||||||
key: "{{ account.username }}"
|
key: "{{ account.username }}"
|
||||||
register: user_info
|
register: user_info
|
||||||
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
- name: "Add {{ account.username }} user"
|
- name: "Add {{ account.username }} user"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
|
@ -18,10 +19,10 @@
|
||||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||||
append: yes
|
append: "{{ true if params.groups | length > 0 else false }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
state: present
|
state: present
|
||||||
when: user_info.failed
|
when: user_info.msg is defined
|
||||||
|
|
||||||
- name: "Set {{ account.username }} sudo setting"
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
|
@ -31,7 +32,7 @@
|
||||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
validate: visudo -cf %s
|
validate: visudo -cf %s
|
||||||
when:
|
when:
|
||||||
- user_info.failed or params.modify_sudo
|
- user_info.msg is defined or params.modify_sudo
|
||||||
- params.sudo
|
- params.sudo
|
||||||
|
|
||||||
- name: "Change {{ account.username }} password"
|
- name: "Change {{ account.username }} password"
|
||||||
|
@ -100,7 +101,7 @@
|
||||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password" and check_conn_after_change
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
|
@ -111,5 +112,5 @@
|
||||||
login_private_key_path: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key" and check_conn_after_change
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
database: passwd
|
database: passwd
|
||||||
key: "{{ account.username }}"
|
key: "{{ account.username }}"
|
||||||
register: user_info
|
register: user_info
|
||||||
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
- name: "Add {{ account.username }} user"
|
- name: "Add {{ account.username }} user"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
|
@ -18,10 +19,10 @@
|
||||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||||
append: yes
|
append: "{{ true if params.groups | length > 0 else false }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
state: present
|
state: present
|
||||||
when: user_info.failed
|
when: user_info.msg is defined
|
||||||
|
|
||||||
- name: "Set {{ account.username }} sudo setting"
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
|
@ -31,7 +32,7 @@
|
||||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
validate: visudo -cf %s
|
validate: visudo -cf %s
|
||||||
when:
|
when:
|
||||||
- user_info.failed or params.modify_sudo
|
- user_info.msg is defined or params.modify_sudo
|
||||||
- params.sudo
|
- params.sudo
|
||||||
|
|
||||||
- name: "Change {{ account.username }} password"
|
- name: "Change {{ account.username }} password"
|
||||||
|
@ -100,7 +101,7 @@
|
||||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password" and check_conn_after_change
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
|
@ -111,5 +112,5 @@
|
||||||
login_private_key_path: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key" and check_conn_after_change
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
|
@ -4,10 +4,6 @@
|
||||||
- name: Test privileged account
|
- name: Test privileged account
|
||||||
ansible.windows.win_ping:
|
ansible.windows.win_ping:
|
||||||
|
|
||||||
# - name: Print variables
|
|
||||||
# debug:
|
|
||||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
|
||||||
|
|
||||||
- name: Change password
|
- name: Change password
|
||||||
ansible.windows.win_user:
|
ansible.windows.win_user:
|
||||||
fullname: "{{ account.username}}"
|
fullname: "{{ account.username}}"
|
||||||
|
@ -28,4 +24,4 @@
|
||||||
vars:
|
vars:
|
||||||
ansible_user: "{{ account.username }}"
|
ansible_user: "{{ account.username }}"
|
||||||
ansible_password: "{{ account.secret }}"
|
ansible_password: "{{ account.secret }}"
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password" and check_conn_after_change
|
||||||
|
|
|
@ -4,10 +4,6 @@
|
||||||
- name: Test privileged account
|
- name: Test privileged account
|
||||||
ansible.windows.win_ping:
|
ansible.windows.win_ping:
|
||||||
|
|
||||||
# - name: Print variables
|
|
||||||
# debug:
|
|
||||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
|
||||||
|
|
||||||
- name: Change password
|
- name: Change password
|
||||||
ansible.windows.win_user:
|
ansible.windows.win_user:
|
||||||
fullname: "{{ account.username}}"
|
fullname: "{{ account.username}}"
|
||||||
|
@ -31,5 +27,5 @@
|
||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_secret_type: "{{ account.secret_type }}"
|
login_secret_type: "{{ account.secret_type }}"
|
||||||
gateway_args: "{{ jms_gateway | default({}) }}"
|
gateway_args: "{{ jms_gateway | default({}) }}"
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password" and check_conn_after_change
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
|
@ -1,218 +1,71 @@
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from xlsxwriter import Workbook
|
from xlsxwriter import Workbook
|
||||||
|
|
||||||
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice
|
from accounts.const import (
|
||||||
from accounts.models import ChangeSecretRecord, BaseAccountQuerySet
|
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
|
||||||
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg
|
)
|
||||||
|
from accounts.models import ChangeSecretRecord
|
||||||
|
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
|
||||||
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
||||||
from assets.const import HostTypes
|
from common.decorators import bulk_create_decorator
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.utils.file import encrypt_and_compress_zip_file
|
from common.utils.file import encrypt_and_compress_zip_file
|
||||||
from common.utils.timezone import local_now_filename
|
from common.utils.timezone import local_now_filename
|
||||||
from users.models import User
|
from ..base.manager import BaseChangeSecretPushManager
|
||||||
from ..base.manager import AccountBasePlaybookManager
|
|
||||||
from ...utils import SecretGenerator
|
from ...utils import SecretGenerator
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChangeSecretManager(AccountBasePlaybookManager):
|
class ChangeSecretManager(BaseChangeSecretPushManager):
|
||||||
ansible_account_prefer = ''
|
ansible_account_prefer = ''
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.record_map = self.execution.snapshot.get('record_map', {})
|
|
||||||
self.secret_type = self.execution.snapshot.get('secret_type')
|
|
||||||
self.secret_strategy = self.execution.snapshot.get(
|
|
||||||
'secret_strategy', SecretStrategy.custom
|
|
||||||
)
|
|
||||||
self.ssh_key_change_strategy = self.execution.snapshot.get(
|
|
||||||
'ssh_key_change_strategy', SSHKeyStrategy.add
|
|
||||||
)
|
|
||||||
self.account_ids = self.execution.snapshot['accounts']
|
|
||||||
self.name_recorder_mapper = {} # 做个映射,方便后面处理
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def method_type(cls):
|
def method_type(cls):
|
||||||
return AutomationTypes.change_secret
|
return AutomationTypes.change_secret
|
||||||
|
|
||||||
def get_ssh_params(self, account, secret, secret_type):
|
def get_secret(self, account):
|
||||||
kwargs = {}
|
|
||||||
if secret_type != SecretType.SSH_KEY:
|
|
||||||
return kwargs
|
|
||||||
kwargs['strategy'] = self.ssh_key_change_strategy
|
|
||||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
|
||||||
|
|
||||||
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
|
|
||||||
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def secret_generator(self, secret_type):
|
|
||||||
return SecretGenerator(
|
|
||||||
self.secret_strategy, secret_type,
|
|
||||||
self.execution.snapshot.get('password_rules')
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_secret(self, secret_type):
|
|
||||||
if self.secret_strategy == SecretStrategy.custom:
|
if self.secret_strategy == SecretStrategy.custom:
|
||||||
return self.execution.snapshot['secret']
|
new_secret = self.execution.snapshot['secret']
|
||||||
else:
|
else:
|
||||||
return self.secret_generator(secret_type).get_secret()
|
generator = SecretGenerator(
|
||||||
|
self.secret_strategy, self.secret_type,
|
||||||
def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None:
|
self.execution.snapshot.get('password_rules')
|
||||||
if not privilege_account:
|
|
||||||
print('Not privilege account')
|
|
||||||
return
|
|
||||||
|
|
||||||
asset = privilege_account.asset
|
|
||||||
accounts = asset.accounts.all()
|
|
||||||
accounts = accounts.filter(id__in=self.account_ids)
|
|
||||||
if self.secret_type:
|
|
||||||
accounts = accounts.filter(secret_type=self.secret_type)
|
|
||||||
|
|
||||||
if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED:
|
|
||||||
accounts = accounts.filter(privileged=False).exclude(
|
|
||||||
username__in=['root', 'administrator', privilege_account.username]
|
|
||||||
)
|
)
|
||||||
return accounts
|
new_secret = generator.get_secret()
|
||||||
|
return new_secret
|
||||||
|
|
||||||
def host_callback(
|
def gen_account_inventory(self, account, asset, h, path_dir):
|
||||||
self, host, asset=None, account=None,
|
record = self.get_or_create_record(asset, account, h['name'])
|
||||||
automation=None, path_dir=None, **kwargs
|
new_secret, private_key_path = self.handle_ssh_secret(account.secret_type, record.new_secret, path_dir)
|
||||||
):
|
h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
|
||||||
host = super().host_callback(
|
return h
|
||||||
host, asset=asset, account=account, automation=automation,
|
|
||||||
path_dir=path_dir, **kwargs
|
def get_or_create_record(self, asset, account, name):
|
||||||
|
asset_account_id = f'{asset.id}-{account.id}'
|
||||||
|
|
||||||
|
if asset_account_id in self.record_map:
|
||||||
|
record_id = self.record_map[asset_account_id]
|
||||||
|
recorder = ChangeSecretRecord.objects.filter(id=record_id).first()
|
||||||
|
else:
|
||||||
|
new_secret = self.get_secret(account)
|
||||||
|
recorder = self.create_record(asset, account, new_secret)
|
||||||
|
|
||||||
|
self.name_recorder_mapper[name] = recorder
|
||||||
|
return recorder
|
||||||
|
|
||||||
|
@bulk_create_decorator(ChangeSecretRecord)
|
||||||
|
def create_record(self, asset, account, new_secret):
|
||||||
|
recorder = ChangeSecretRecord(
|
||||||
|
asset=asset, account=account, execution=self.execution,
|
||||||
|
old_secret=account.secret, new_secret=new_secret,
|
||||||
|
comment=f'{account.username}@{asset.address}'
|
||||||
)
|
)
|
||||||
if host.get('error'):
|
return recorder
|
||||||
return host
|
|
||||||
|
|
||||||
accounts = self.get_accounts(account)
|
|
||||||
error_msg = _("No pending accounts found")
|
|
||||||
if not accounts:
|
|
||||||
print(f'{asset}: {error_msg}')
|
|
||||||
return []
|
|
||||||
|
|
||||||
records = []
|
|
||||||
inventory_hosts = []
|
|
||||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
|
||||||
print(f'Windows {asset} does not support ssh key push')
|
|
||||||
return inventory_hosts
|
|
||||||
|
|
||||||
if asset.type == HostTypes.WINDOWS:
|
|
||||||
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
|
|
||||||
|
|
||||||
host['ssh_params'] = {}
|
|
||||||
for account in accounts:
|
|
||||||
h = deepcopy(host)
|
|
||||||
secret_type = account.secret_type
|
|
||||||
h['name'] += '(' + account.username + ')'
|
|
||||||
if self.secret_type is None:
|
|
||||||
new_secret = account.secret
|
|
||||||
else:
|
|
||||||
new_secret = self.get_secret(secret_type)
|
|
||||||
|
|
||||||
if new_secret is None:
|
|
||||||
print(f'new_secret is None, account: {account}')
|
|
||||||
continue
|
|
||||||
|
|
||||||
asset_account_id = f'{asset.id}-{account.id}'
|
|
||||||
if asset_account_id not in self.record_map:
|
|
||||||
recorder = ChangeSecretRecord(
|
|
||||||
asset=asset, account=account, execution=self.execution,
|
|
||||||
old_secret=account.secret, new_secret=new_secret,
|
|
||||||
comment=f'{account.username}@{asset.address}'
|
|
||||||
)
|
|
||||||
records.append(recorder)
|
|
||||||
else:
|
|
||||||
record_id = self.record_map[asset_account_id]
|
|
||||||
try:
|
|
||||||
recorder = ChangeSecretRecord.objects.get(id=record_id)
|
|
||||||
except ChangeSecretRecord.DoesNotExist:
|
|
||||||
print(f"Record {record_id} not found")
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.name_recorder_mapper[h['name']] = recorder
|
|
||||||
|
|
||||||
private_key_path = None
|
|
||||||
if secret_type == SecretType.SSH_KEY:
|
|
||||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
|
||||||
new_secret = self.generate_public_key(new_secret)
|
|
||||||
|
|
||||||
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
|
|
||||||
h['account'] = {
|
|
||||||
'name': account.name,
|
|
||||||
'username': account.username,
|
|
||||||
'secret_type': secret_type,
|
|
||||||
'secret': account.escape_jinja2_syntax(new_secret),
|
|
||||||
'private_key_path': private_key_path,
|
|
||||||
'become': account.get_ansible_become_auth(),
|
|
||||||
}
|
|
||||||
if asset.platform.type == 'oracle':
|
|
||||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
|
||||||
inventory_hosts.append(h)
|
|
||||||
ChangeSecretRecord.objects.bulk_create(records)
|
|
||||||
return inventory_hosts
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def require_update_version(account, recorder):
|
|
||||||
return account.secret != recorder.new_secret
|
|
||||||
|
|
||||||
def on_host_success(self, host, result):
|
|
||||||
recorder = self.name_recorder_mapper.get(host)
|
|
||||||
if not recorder:
|
|
||||||
return
|
|
||||||
recorder.status = ChangeSecretRecordStatusChoice.success.value
|
|
||||||
recorder.date_finished = timezone.now()
|
|
||||||
|
|
||||||
account = recorder.account
|
|
||||||
if not account:
|
|
||||||
print("Account not found, deleted ?")
|
|
||||||
return
|
|
||||||
|
|
||||||
version_update_required = self.require_update_version(account, recorder)
|
|
||||||
account.secret = recorder.new_secret
|
|
||||||
account.date_updated = timezone.now()
|
|
||||||
|
|
||||||
max_retries = 3
|
|
||||||
retry_count = 0
|
|
||||||
|
|
||||||
while retry_count < max_retries:
|
|
||||||
try:
|
|
||||||
recorder.save()
|
|
||||||
account_update_fields = ['secret', 'date_updated']
|
|
||||||
if version_update_required:
|
|
||||||
account_update_fields.append('version')
|
|
||||||
account.save(update_fields=account_update_fields)
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
retry_count += 1
|
|
||||||
if retry_count == max_retries:
|
|
||||||
self.on_host_error(host, str(e), result)
|
|
||||||
else:
|
|
||||||
print(f'retry {retry_count} times for {host} recorder save error: {e}')
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
def on_host_error(self, host, error, result):
|
|
||||||
recorder = self.name_recorder_mapper.get(host)
|
|
||||||
if not recorder:
|
|
||||||
return
|
|
||||||
recorder.status = ChangeSecretRecordStatusChoice.failed.value
|
|
||||||
recorder.date_finished = timezone.now()
|
|
||||||
recorder.error = error
|
|
||||||
try:
|
|
||||||
recorder.save()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\033[31m Save {host} recorder error: {e} \033[0m\n")
|
|
||||||
|
|
||||||
def on_runner_failed(self, runner, e):
|
|
||||||
logger.error("Account error: ", e)
|
|
||||||
|
|
||||||
def check_secret(self):
|
def check_secret(self):
|
||||||
if self.secret_strategy == SecretStrategy.custom \
|
if self.secret_strategy == SecretStrategy.custom \
|
||||||
|
@ -230,47 +83,39 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
total += 1
|
total += 1
|
||||||
|
|
||||||
summary = _('Success: %s, Failed: %s, Total: %s') % (succeed, failed, total)
|
summary = _('Success: %s, Failed: %s, Total: %s') % (succeed, failed, total)
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def print_summary(self):
|
||||||
if self.secret_type and not self.check_secret():
|
|
||||||
self.execution.status = 'success'
|
|
||||||
self.execution.date_finished = timezone.now()
|
|
||||||
self.execution.save()
|
|
||||||
return
|
|
||||||
super().run(*args, **kwargs)
|
|
||||||
recorders = list(self.name_recorder_mapper.values())
|
recorders = list(self.name_recorder_mapper.values())
|
||||||
summary = self.get_summary(recorders)
|
summary = self.get_summary(recorders)
|
||||||
print(summary, end='')
|
print('\n\n' + '-' * 80)
|
||||||
|
plan_execution_end = _('Plan execution end')
|
||||||
|
print('{} {}\n'.format(plan_execution_end, local_now_filename()))
|
||||||
|
time_cost = _('Duration')
|
||||||
|
print('{}: {}s'.format(time_cost, self.duration))
|
||||||
|
print(summary)
|
||||||
|
|
||||||
|
def send_report_if_need(self, *args, **kwargs):
|
||||||
|
if self.secret_type and not self.check_secret():
|
||||||
|
return
|
||||||
|
|
||||||
|
recorders = list(self.name_recorder_mapper.values())
|
||||||
if self.record_map:
|
if self.record_map:
|
||||||
return
|
return
|
||||||
|
|
||||||
failed_recorders = [
|
|
||||||
r for r in recorders
|
|
||||||
if r.status == ChangeSecretRecordStatusChoice.failed.value
|
|
||||||
]
|
|
||||||
|
|
||||||
recipients = self.execution.recipients
|
recipients = self.execution.recipients
|
||||||
recipients = User.objects.filter(id__in=list(recipients.keys()))
|
|
||||||
if not recipients:
|
if not recipients:
|
||||||
return
|
return
|
||||||
|
|
||||||
if failed_recorders:
|
context = self.get_report_context()
|
||||||
name = self.execution.snapshot.get('name')
|
for user in recipients:
|
||||||
execution_id = str(self.execution.id)
|
ChangeSecretReportMsg(user, context).publish()
|
||||||
_ids = [r.id for r in failed_recorders]
|
|
||||||
asset_account_errors = ChangeSecretRecord.objects.filter(
|
|
||||||
id__in=_ids).values_list('asset__name', 'account__username', 'error')
|
|
||||||
|
|
||||||
for user in recipients:
|
|
||||||
ChangeSecretFailedMsg(name, execution_id, user, asset_account_errors).publish()
|
|
||||||
|
|
||||||
if not recorders:
|
if not recorders:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
summary = self.get_summary(recorders)
|
||||||
self.send_recorder_mail(recipients, recorders, summary)
|
self.send_recorder_mail(recipients, recorders, summary)
|
||||||
|
|
||||||
def send_recorder_mail(self, recipients, recorders, summary):
|
def send_recorder_mail(self, recipients, recorders, summary):
|
||||||
|
@ -307,3 +152,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||||
ws.write_string(row_index, col_index, col_data)
|
ws.write_string(row_index, col_index, col_data)
|
||||||
wb.close()
|
wb.close()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_report_template(self):
|
||||||
|
return "accounts/change_secret_report.html"
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def is_weak_password(password):
|
||||||
|
if len(password) < 8:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 判断是否只有一种字符类型
|
||||||
|
if password.isdigit() or password.isalpha():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 判断是否只包含数字或字母
|
||||||
|
if password.islower() or password.isupper():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 判断是否包含常见弱密码
|
||||||
|
common_passwords = ["123456", "password", "12345678", "qwerty", "abc123"]
|
||||||
|
if password.lower() in common_passwords:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 正则表达式判断字符多样性(数字、字母、特殊字符)
|
||||||
|
if (
|
||||||
|
not re.search(r"[A-Za-z]", password)
|
||||||
|
or not re.search(r"[0-9]", password)
|
||||||
|
or not re.search(r"[\W_]", password)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def parse_it(fname):
|
||||||
|
count = 0
|
||||||
|
lines = []
|
||||||
|
with open(fname, 'rb') as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
line = line.decode().strip()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(line) > 32:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_weak_password(line):
|
||||||
|
continue
|
||||||
|
|
||||||
|
lines.append(line)
|
||||||
|
count += 0
|
||||||
|
print(line)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def insert_to_db(lines):
|
||||||
|
conn = sqlite3.connect('./leak_passwords.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
create_table_sql = '''
|
||||||
|
CREATE TABLE IF NOT EXISTS passwords (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
password CHAR(32)
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
create_index_sql = 'CREATE INDEX IF NOT EXISTS idx_password ON passwords(password)'
|
||||||
|
cursor.execute(create_table_sql)
|
||||||
|
cursor.execute(create_index_sql)
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
cursor.execute('INSERT INTO passwords (password) VALUES (?)', [line])
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
filename = sys.argv[1]
|
||||||
|
lines = parse_it(filename)
|
||||||
|
insert_to_db(lines)
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a2805a0264fc07ae597704841ab060edef8bf74654f525bc778cb9195d8cad0e
|
||||||
|
size 2547712
|
|
@ -0,0 +1,284 @@
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from accounts.models import Account, AccountRisk, RiskChoice
|
||||||
|
from assets.automations.base.manager import BaseManager
|
||||||
|
from common.const import ConfirmOrIgnore
|
||||||
|
from common.decorators import bulk_create_decorator, bulk_update_decorator
|
||||||
|
|
||||||
|
|
||||||
|
@bulk_create_decorator(AccountRisk)
|
||||||
|
def create_risk(data):
|
||||||
|
return AccountRisk(**data)
|
||||||
|
|
||||||
|
|
||||||
|
@bulk_update_decorator(AccountRisk, update_fields=["details", "status"])
|
||||||
|
def update_risk(risk):
|
||||||
|
return risk
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCheckHandler:
|
||||||
|
risk = ''
|
||||||
|
|
||||||
|
def __init__(self, assets):
|
||||||
|
self.assets = assets
|
||||||
|
|
||||||
|
def check(self, account):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CheckSecretHandler(BaseCheckHandler):
|
||||||
|
risk = RiskChoice.weak_password
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_weak_password(password):
|
||||||
|
# 判断密码长度
|
||||||
|
if len(password) < 8:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 判断是否只有一种字符类型
|
||||||
|
if password.isdigit() or password.isalpha():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 判断是否只包含数字或字母
|
||||||
|
if password.islower() or password.isupper():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 判断是否包含常见弱密码
|
||||||
|
common_passwords = ["123456", "password", "12345678", "qwerty", "abc123"]
|
||||||
|
if password.lower() in common_passwords:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 正则表达式判断字符多样性(数字、字母、特殊字符)
|
||||||
|
if (
|
||||||
|
not re.search(r"[A-Za-z]", password)
|
||||||
|
or not re.search(r"[0-9]", password)
|
||||||
|
or not re.search(r"[\W_]", password)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self, account):
|
||||||
|
if not account.secret:
|
||||||
|
return False
|
||||||
|
return self.is_weak_password(account.secret)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckRepeatHandler(BaseCheckHandler):
|
||||||
|
risk = RiskChoice.repeated_password
|
||||||
|
|
||||||
|
def __init__(self, assets):
|
||||||
|
super().__init__(assets)
|
||||||
|
self.path, self.conn, self.cursor = self.init_repeat_check_db()
|
||||||
|
self.add_password_for_check_repeat()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def init_repeat_check_db():
|
||||||
|
path = os.path.join('/tmp', 'accounts_' + str(uuid.uuid4()) + '.db')
|
||||||
|
sql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
digest CHAR(32)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
index = "CREATE INDEX IF NOT EXISTS idx_digest ON accounts(digest)"
|
||||||
|
conn = sqlite3.connect(path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(sql)
|
||||||
|
cursor.execute(index)
|
||||||
|
return path, conn, cursor
|
||||||
|
|
||||||
|
def check(self, account):
|
||||||
|
if not account.secret:
|
||||||
|
return False
|
||||||
|
|
||||||
|
digest = self.digest(account.secret)
|
||||||
|
sql = 'SELECT COUNT(*) FROM accounts WHERE digest = ?'
|
||||||
|
self.cursor.execute(sql, [digest])
|
||||||
|
result = self.cursor.fetchone()
|
||||||
|
if not result:
|
||||||
|
return False
|
||||||
|
return result[0] > 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def digest(secret):
|
||||||
|
return hashlib.md5(secret.encode()).hexdigest()
|
||||||
|
|
||||||
|
def add_password_for_check_repeat(self):
|
||||||
|
accounts = Account.objects.all().only('id', '_secret', 'secret_type')
|
||||||
|
sql = "INSERT INTO accounts (digest) VALUES (?)"
|
||||||
|
|
||||||
|
for account in accounts:
|
||||||
|
secret = account.secret
|
||||||
|
if not secret:
|
||||||
|
continue
|
||||||
|
digest = self.digest(secret)
|
||||||
|
self.cursor.execute(sql, [digest])
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
self.cursor.close()
|
||||||
|
self.conn.close()
|
||||||
|
os.remove(self.path)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckLeakHandler(BaseCheckHandler):
|
||||||
|
risk = RiskChoice.leaked_password
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
super().__init__(*args)
|
||||||
|
self.conn, self.cursor = self.init_leak_password_db()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def init_leak_password_db():
|
||||||
|
db_path = os.path.join(
|
||||||
|
settings.APPS_DIR, 'accounts', 'automations',
|
||||||
|
'check_account', 'leak_passwords.db'
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings.LEAK_PASSWORD_DB_PATH and os.path.isfile(settings.LEAK_PASSWORD_DB_PATH):
|
||||||
|
db_path = settings.LEAK_PASSWORD_DB_PATH
|
||||||
|
|
||||||
|
db_conn = sqlite3.connect(db_path)
|
||||||
|
db_cursor = db_conn.cursor()
|
||||||
|
return db_conn, db_cursor
|
||||||
|
|
||||||
|
def check(self, account):
|
||||||
|
if not account.secret:
|
||||||
|
return False
|
||||||
|
|
||||||
|
sql = 'SELECT 1 FROM passwords WHERE password = ? LIMIT 1'
|
||||||
|
self.cursor.execute(sql, (account.secret,))
|
||||||
|
leak = self.cursor.fetchone() is not None
|
||||||
|
return leak
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
self.cursor.close()
|
||||||
|
self.conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
class CheckAccountManager(BaseManager):
|
||||||
|
batch_size = 100
|
||||||
|
tmpl = 'Checked the status of account %s: %s'
|
||||||
|
|
||||||
|
def __init__(self, execution):
|
||||||
|
super().__init__(execution)
|
||||||
|
self.assets = []
|
||||||
|
self.batch_risks = []
|
||||||
|
self.handlers = []
|
||||||
|
|
||||||
|
def add_risk(self, risk, account):
|
||||||
|
self.summary[risk] += 1
|
||||||
|
self.result[risk].append({
|
||||||
|
'asset': str(account.asset), 'username': account.username,
|
||||||
|
})
|
||||||
|
risk_obj = {'account': account, 'risk': risk}
|
||||||
|
self.batch_risks.append(risk_obj)
|
||||||
|
|
||||||
|
def commit_risks(self, assets):
|
||||||
|
account_risks = AccountRisk.objects.filter(asset__in=assets)
|
||||||
|
ori_risk_map = {}
|
||||||
|
|
||||||
|
for risk in account_risks:
|
||||||
|
key = f'{risk.account_id}_{risk.risk}'
|
||||||
|
ori_risk_map[key] = risk
|
||||||
|
|
||||||
|
now = timezone.now().isoformat()
|
||||||
|
for d in self.batch_risks:
|
||||||
|
account = d["account"]
|
||||||
|
key = f'{account.id}_{d["risk"]}'
|
||||||
|
origin_risk = ori_risk_map.get(key)
|
||||||
|
|
||||||
|
if origin_risk and origin_risk.status != ConfirmOrIgnore.pending:
|
||||||
|
details = origin_risk.details or []
|
||||||
|
details.append({"datetime": now, 'type': 'refind'})
|
||||||
|
|
||||||
|
if len(details) > 10:
|
||||||
|
details = [*details[:5], *details[-5:]]
|
||||||
|
|
||||||
|
origin_risk.details = details
|
||||||
|
origin_risk.status = ConfirmOrIgnore.pending
|
||||||
|
update_risk(origin_risk)
|
||||||
|
else:
|
||||||
|
create_risk({
|
||||||
|
"account": account,
|
||||||
|
"asset": account.asset,
|
||||||
|
"username": account.username,
|
||||||
|
"risk": d["risk"],
|
||||||
|
"details": [{"datetime": now, 'type': 'init'}],
|
||||||
|
})
|
||||||
|
|
||||||
|
def pre_run(self):
|
||||||
|
super().pre_run()
|
||||||
|
self.assets = self.execution.get_all_assets()
|
||||||
|
self.execution.date_start = timezone.now()
|
||||||
|
self.execution.save(update_fields=["date_start"])
|
||||||
|
|
||||||
|
def batch_check(self, handler):
|
||||||
|
print("Engine: {}".format(handler.__class__.__name__))
|
||||||
|
for i in range(0, len(self.assets), self.batch_size):
|
||||||
|
_assets = self.assets[i: i + self.batch_size]
|
||||||
|
accounts = Account.objects.filter(asset__in=_assets)
|
||||||
|
|
||||||
|
print("Start to check accounts: {}".format(len(accounts)))
|
||||||
|
|
||||||
|
for account in accounts:
|
||||||
|
error = handler.check(account)
|
||||||
|
msg = handler.risk if error else 'ok'
|
||||||
|
|
||||||
|
print("Check: {} => {}".format(account, msg))
|
||||||
|
if not error:
|
||||||
|
continue
|
||||||
|
self.add_risk(handler.risk, account)
|
||||||
|
self.commit_risks(_assets)
|
||||||
|
|
||||||
|
def do_run(self, *args, **kwargs):
|
||||||
|
for engine in self.execution.snapshot.get("engines", []):
|
||||||
|
if engine == "check_account_secret":
|
||||||
|
handler = CheckSecretHandler(self.assets)
|
||||||
|
elif engine == "check_account_repeat":
|
||||||
|
handler = CheckRepeatHandler(self.assets)
|
||||||
|
elif engine == "check_account_leak":
|
||||||
|
handler = CheckLeakHandler(self.assets)
|
||||||
|
else:
|
||||||
|
print("Unknown engine: {}".format(engine))
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.handlers.append(handler)
|
||||||
|
self.batch_check(handler)
|
||||||
|
|
||||||
|
def post_run(self):
|
||||||
|
super().post_run()
|
||||||
|
for handler in self.handlers:
|
||||||
|
handler.clean()
|
||||||
|
|
||||||
|
def get_report_subject(self):
|
||||||
|
return "Check account report of %s" % self.execution.id
|
||||||
|
|
||||||
|
def get_report_template(self):
|
||||||
|
return "accounts/check_account_report.html"
|
||||||
|
|
||||||
|
def print_summary(self):
|
||||||
|
tmpl = (
|
||||||
|
"\n---\nSummary: \nok: %s, weak password: %s, leaked password: %s, "
|
||||||
|
"repeated password: %s, no secret: %s, using time: %ss"
|
||||||
|
% (
|
||||||
|
self.summary["ok"],
|
||||||
|
self.summary[RiskChoice.weak_password],
|
||||||
|
self.summary[RiskChoice.leaked_password],
|
||||||
|
self.summary[RiskChoice.repeated_password],
|
||||||
|
|
||||||
|
self.summary["no_secret"],
|
||||||
|
int(self.duration),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(tmpl)
|
|
@ -1,6 +1,7 @@
|
||||||
from .backup_account.manager import AccountBackupManager
|
from .backup_account.manager import AccountBackupManager
|
||||||
from .change_secret.manager import ChangeSecretManager
|
from .change_secret.manager import ChangeSecretManager
|
||||||
from .gather_accounts.manager import GatherAccountsManager
|
from .check_account.manager import CheckAccountManager
|
||||||
|
from .gather_account.manager import GatherAccountsManager
|
||||||
from .push_account.manager import PushAccountManager
|
from .push_account.manager import PushAccountManager
|
||||||
from .remove_account.manager import RemoveAccountManager
|
from .remove_account.manager import RemoveAccountManager
|
||||||
from .verify_account.manager import VerifyAccountManager
|
from .verify_account.manager import VerifyAccountManager
|
||||||
|
@ -16,8 +17,8 @@ class ExecutionManager:
|
||||||
AutomationTypes.remove_account: RemoveAccountManager,
|
AutomationTypes.remove_account: RemoveAccountManager,
|
||||||
AutomationTypes.gather_accounts: GatherAccountsManager,
|
AutomationTypes.gather_accounts: GatherAccountsManager,
|
||||||
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
|
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
|
||||||
# TODO 后期迁移到自动化策略中
|
AutomationTypes.check_account: CheckAccountManager,
|
||||||
'backup_account': AccountBackupManager,
|
AutomationTypes.backup_account: AccountBackupManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, execution):
|
def __init__(self, execution):
|
||||||
|
@ -26,3 +27,6 @@ class ExecutionManager:
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
return self._runner.run(*args, **kwargs)
|
return self._runner.run(*args, **kwargs)
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
return getattr(self._runner, item)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mongodb
|
- hosts: mongodb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- name: Get info
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
filter: users
|
filter: users
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: oralce
|
- hosts: oralce
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- name: Get info
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: postgresql
|
- hosts: postgresql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
|
@ -0,0 +1,43 @@
|
||||||
|
- hosts: sqlserver
|
||||||
|
gather_facts: no
|
||||||
|
vars:
|
||||||
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Test SQLServer connection
|
||||||
|
community.general.mssql_script:
|
||||||
|
login_user: "{{ jms_account.username }}"
|
||||||
|
login_password: "{{ jms_account.secret }}"
|
||||||
|
login_host: "{{ jms_asset.address }}"
|
||||||
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
script: |
|
||||||
|
SELECT
|
||||||
|
l.name,
|
||||||
|
l.modify_date,
|
||||||
|
l.is_disabled,
|
||||||
|
l.create_date,
|
||||||
|
l.default_database_name,
|
||||||
|
LOGINPROPERTY(name, 'DaysUntilExpiration') AS days_until_expiration,
|
||||||
|
MAX(s.login_time) AS last_login_time
|
||||||
|
FROM
|
||||||
|
sys.sql_logins l
|
||||||
|
LEFT JOIN
|
||||||
|
sys.dm_exec_sessions s
|
||||||
|
ON
|
||||||
|
l.name = s.login_name
|
||||||
|
WHERE
|
||||||
|
s.is_user_process = 1 OR s.login_name IS NULL
|
||||||
|
GROUP BY
|
||||||
|
l.name, l.create_date, l.modify_date, l.is_disabled, l.default_database_name
|
||||||
|
ORDER BY
|
||||||
|
last_login_time DESC;
|
||||||
|
output: dict
|
||||||
|
register: db_info
|
||||||
|
|
||||||
|
- name: Define info by set_fact
|
||||||
|
set_fact:
|
||||||
|
info: "{{ db_info.query_results_dict }}"
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: info
|
|
@ -0,0 +1,10 @@
|
||||||
|
id: gather_accounts_sqlserver
|
||||||
|
name: "{{ 'SQLServer account gather' | trans }}"
|
||||||
|
category: database
|
||||||
|
type:
|
||||||
|
- sqlserver
|
||||||
|
method: gather_accounts
|
||||||
|
i18n:
|
||||||
|
SQLServer account gather:
|
||||||
|
zh: SQLServer 账号收集
|
||||||
|
ja: SQLServer アカウントの収集
|
|
@ -0,0 +1,248 @@
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
__all__ = ['GatherAccountsFilter']
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date(date_str, default=None):
|
||||||
|
if not date_str:
|
||||||
|
return default
|
||||||
|
if date_str in ['Never', 'null']:
|
||||||
|
return default
|
||||||
|
formats = [
|
||||||
|
'%Y/%m/%d %H:%M:%S',
|
||||||
|
'%Y-%m-%dT%H:%M:%S',
|
||||||
|
'%d-%m-%Y %H:%M:%S',
|
||||||
|
'%Y/%m/%d',
|
||||||
|
'%d-%m-%Y',
|
||||||
|
]
|
||||||
|
for fmt in formats:
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(date_str, fmt)
|
||||||
|
return timezone.make_aware(dt, timezone.get_current_timezone())
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
# TODO 后期会挪到 playbook 中
|
||||||
|
class GatherAccountsFilter:
|
||||||
|
def __init__(self, tp):
|
||||||
|
self.tp = tp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mysql_filter(info):
|
||||||
|
result = {}
|
||||||
|
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
|
||||||
|
def postgresql_filter(info):
|
||||||
|
result = {}
|
||||||
|
for username, user_info in info.items():
|
||||||
|
user = {
|
||||||
|
'username': username,
|
||||||
|
'date_password_change': None,
|
||||||
|
'date_password_expired': parse_date(user_info.get('valid_until')),
|
||||||
|
'date_last_login': None,
|
||||||
|
'groups': '',
|
||||||
|
}
|
||||||
|
detail = {
|
||||||
|
'can_login': user_info.get('canlogin'),
|
||||||
|
'superuser': user_info.get('superuser'),
|
||||||
|
}
|
||||||
|
user['detail'] = detail
|
||||||
|
result[username] = user
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sqlserver_filter(info):
|
||||||
|
if not info:
|
||||||
|
return {}
|
||||||
|
result = {}
|
||||||
|
for user_info in info[0][0]:
|
||||||
|
days_until_expiration = user_info.get('days_until_expiration')
|
||||||
|
date_password_expired = timezone.now() + timezone.timedelta(
|
||||||
|
days=int(days_until_expiration)) if days_until_expiration else None
|
||||||
|
user = {
|
||||||
|
'username': user_info.get('name', ''),
|
||||||
|
'date_password_change': parse_date(user_info.get('modify_date')),
|
||||||
|
'date_password_expired': date_password_expired,
|
||||||
|
'date_last_login': parse_date(user_info.get('last_login_time')),
|
||||||
|
'groups': '',
|
||||||
|
}
|
||||||
|
detail = {
|
||||||
|
'create_date': user_info.get('create_date', ''),
|
||||||
|
'is_disabled': user_info.get('is_disabled', ''),
|
||||||
|
'default_database_name': user_info.get('default_database_name', ''),
|
||||||
|
}
|
||||||
|
user['detail'] = detail
|
||||||
|
result[user['username']] = user
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def oracle_filter(info):
|
||||||
|
result = {}
|
||||||
|
for default_tablespace, users in info.items():
|
||||||
|
for username, user_info in users.items():
|
||||||
|
user = {
|
||||||
|
'username': username,
|
||||||
|
'date_password_change': parse_date(user_info.get('password_change_date')),
|
||||||
|
'date_password_expired': parse_date(user_info.get('expiry_date')),
|
||||||
|
'date_last_login': parse_date(user_info.get('last_login')),
|
||||||
|
'groups': '',
|
||||||
|
}
|
||||||
|
detail = {
|
||||||
|
'uid': user_info.get('user_id', ''),
|
||||||
|
'create_date': user_info.get('created', ''),
|
||||||
|
'account_status': user_info.get('account_status', ''),
|
||||||
|
'default_tablespace': default_tablespace,
|
||||||
|
'roles': user_info.get('roles', []),
|
||||||
|
'privileges': user_info.get('privileges', []),
|
||||||
|
}
|
||||||
|
user['detail'] = detail
|
||||||
|
result[user['username']] = user
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def posix_filter(info):
|
||||||
|
user_groups = info.pop('user_groups', [])
|
||||||
|
username_groups = {}
|
||||||
|
for line in user_groups:
|
||||||
|
if ':' not in line:
|
||||||
|
continue
|
||||||
|
username, groups = line.split(':', 1)
|
||||||
|
username_groups[username.strip()] = groups.strip()
|
||||||
|
|
||||||
|
user_sudo = info.pop('user_sudo', [])
|
||||||
|
username_sudo = {}
|
||||||
|
for line in user_sudo:
|
||||||
|
if ':' not in line:
|
||||||
|
continue
|
||||||
|
username, sudo = line.split(':', 1)
|
||||||
|
if not sudo.strip():
|
||||||
|
continue
|
||||||
|
username_sudo[username.strip()] = sudo.strip()
|
||||||
|
|
||||||
|
last_login = info.pop('last_login', '')
|
||||||
|
user_last_login = {}
|
||||||
|
for line in last_login:
|
||||||
|
if not line.strip() or ' ' not in line:
|
||||||
|
continue
|
||||||
|
username, login = line.split(' ', 1)
|
||||||
|
user_last_login[username] = login.split()
|
||||||
|
|
||||||
|
user_authorized = info.pop('user_authorized', [])
|
||||||
|
username_authorized = {}
|
||||||
|
for line in user_authorized:
|
||||||
|
if ':' not in line:
|
||||||
|
continue
|
||||||
|
username, authorized = line.split(':', 1)
|
||||||
|
username_authorized[username.strip()] = authorized.strip()
|
||||||
|
|
||||||
|
passwd_date = info.pop('passwd_date', [])
|
||||||
|
username_password_date = {}
|
||||||
|
for line in passwd_date:
|
||||||
|
if ':' not in line:
|
||||||
|
continue
|
||||||
|
username, password_date = line.split(':', 1)
|
||||||
|
username_password_date[username.strip()] = password_date.strip().split()
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
users = info.pop('users', '')
|
||||||
|
|
||||||
|
for username in users:
|
||||||
|
if not username:
|
||||||
|
continue
|
||||||
|
user = dict()
|
||||||
|
|
||||||
|
login = user_last_login.get(username) or ''
|
||||||
|
if login and len(login) == 3:
|
||||||
|
user['address_last_login'] = login[0][:32]
|
||||||
|
try:
|
||||||
|
login_date = timezone.datetime.fromisoformat(login[1])
|
||||||
|
user['date_last_login'] = login_date
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
start_date = timezone.make_aware(timezone.datetime(1970, 1, 1))
|
||||||
|
_password_date = username_password_date.get(username) or ''
|
||||||
|
if _password_date and len(_password_date) == 2:
|
||||||
|
if _password_date[0] and _password_date[0] != '0':
|
||||||
|
user['date_password_change'] = start_date + timezone.timedelta(days=int(_password_date[0]))
|
||||||
|
if _password_date[1] and _password_date[1] != '0':
|
||||||
|
user['date_password_expired'] = start_date + timezone.timedelta(days=int(_password_date[1]))
|
||||||
|
detail = {
|
||||||
|
'groups': username_groups.get(username) or '',
|
||||||
|
'sudoers': username_sudo.get(username) or '',
|
||||||
|
'authorized_keys': username_authorized.get(username) or ''
|
||||||
|
}
|
||||||
|
user['detail'] = detail
|
||||||
|
result[username] = user
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def windows_filter(info):
|
||||||
|
result = {}
|
||||||
|
for user_details in info['user_details']:
|
||||||
|
user_info = {}
|
||||||
|
lines = user_details['stdout_lines']
|
||||||
|
for line in lines:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
parts = line.split(' ', 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
key, value = parts
|
||||||
|
user_info[key.strip()] = value.strip()
|
||||||
|
detail = {'groups': user_info.get('Global Group memberships', ''), }
|
||||||
|
user = {
|
||||||
|
'username': user_info.get('User name', ''),
|
||||||
|
'date_password_change': parse_date(user_info.get('Password last set', '')),
|
||||||
|
'date_password_expired': parse_date(user_info.get('Password expires', '')),
|
||||||
|
'date_last_login': parse_date(user_info.get('Last logon', '')),
|
||||||
|
'groups': detail,
|
||||||
|
}
|
||||||
|
result[user['username']] = user
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mongodb_filter(info):
|
||||||
|
result = {}
|
||||||
|
for db, users in info.items():
|
||||||
|
for username, user_info in users.items():
|
||||||
|
user = {
|
||||||
|
'username': username,
|
||||||
|
'date_password_change': None,
|
||||||
|
'date_password_expired': None,
|
||||||
|
'date_last_login': None,
|
||||||
|
'groups': '',
|
||||||
|
}
|
||||||
|
result['detail'] = {'db': db, 'roles': user_info.get('roles', [])}
|
||||||
|
result[username] = user
|
||||||
|
return result
|
||||||
|
|
||||||
|
def run(self, method_id_meta_mapper, info):
|
||||||
|
run_method_name = None
|
||||||
|
for k, v in method_id_meta_mapper.items():
|
||||||
|
if self.tp not in v['type']:
|
||||||
|
continue
|
||||||
|
run_method_name = k.replace(f'{v["method"]}_', '')
|
||||||
|
|
||||||
|
if not run_method_name:
|
||||||
|
return info
|
||||||
|
|
||||||
|
if hasattr(self, f'{run_method_name}_filter'):
|
||||||
|
return getattr(self, f'{run_method_name}_filter')(info)
|
||||||
|
return info
|
|
@ -0,0 +1,61 @@
|
||||||
|
- hosts: demo
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- name: Get users
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: >
|
||||||
|
getent passwd | awk -F: '$7 !~ /(false|nologin|true|sync)$/' | grep -v '^$' | awk -F":" '{ print $1 }'
|
||||||
|
register: users
|
||||||
|
|
||||||
|
- name: Gather posix account last login
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||||
|
last -i --time-format iso -n 1 ${user} | awk '{ print $1,$3,$4, $NF }' | head -1 | grep -v ^$
|
||||||
|
done
|
||||||
|
register: last_login
|
||||||
|
|
||||||
|
- name: Get user password change date and expiry
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||||
|
k=$(getent shadow $user | awk -F: '{ print $3, $5 }')
|
||||||
|
echo "$user:$k"
|
||||||
|
done
|
||||||
|
register: passwd_date
|
||||||
|
|
||||||
|
- name: Get user groups
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||||
|
echo "$(groups $user)" | sed 's@ : @:@g'
|
||||||
|
done
|
||||||
|
register: user_groups
|
||||||
|
|
||||||
|
- name: Get sudoers
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||||
|
echo "$user: $(grep "^$user " /etc/sudoers | tr '\n' ';' || echo '')"
|
||||||
|
done
|
||||||
|
register: user_sudo
|
||||||
|
|
||||||
|
- name: Get authorized keys
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||||
|
home=$(getent passwd $user | cut -d: -f6)
|
||||||
|
echo -n "$user:"
|
||||||
|
if [[ -f ${home}/.ssh/authorized_keys ]]; then
|
||||||
|
cat ${home}/.ssh/authorized_keys | tr '\n' ';'
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
register: user_authorized
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
info:
|
||||||
|
users: "{{ users.stdout_lines }}"
|
||||||
|
last_login: "{{ last_login.stdout_lines }}"
|
||||||
|
user_groups: "{{ user_groups.stdout_lines }}"
|
||||||
|
user_sudo: "{{ user_sudo.stdout_lines }}"
|
||||||
|
user_authorized: "{{ user_authorized.stdout_lines }}"
|
||||||
|
passwd_date: "{{ passwd_date.stdout_lines }}"
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: info
|
|
@ -0,0 +1,32 @@
|
||||||
|
- hosts: demo
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- name: Run net user command to get all users
|
||||||
|
win_shell: net user
|
||||||
|
register: user_list_output
|
||||||
|
|
||||||
|
- name: Parse all users from net user command
|
||||||
|
set_fact:
|
||||||
|
all_users: >-
|
||||||
|
{%- set users = [] -%}
|
||||||
|
{%- for line in user_list_output.stdout_lines -%}
|
||||||
|
{%- if loop.index > 4 and line.strip() != "" and not line.startswith("The command completed") -%}
|
||||||
|
{%- for user in line.split() -%}
|
||||||
|
{%- set _ = users.append(user) -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
{{ users }}
|
||||||
|
|
||||||
|
- name: Run net user command for each user to get details
|
||||||
|
win_shell: net user {{ item }}
|
||||||
|
loop: "{{ all_users }}"
|
||||||
|
register: user_details
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
info:
|
||||||
|
user_details: "{{ user_details.results }}"
|
||||||
|
|
||||||
|
- debug:
|
||||||
|
var: info
|
|
@ -0,0 +1,385 @@
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from accounts.const import AutomationTypes
|
||||||
|
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
|
||||||
|
from common.utils.strings import get_text_diff
|
||||||
|
from orgs.utils import tmp_to_org
|
||||||
|
from .filter import GatherAccountsFilter
|
||||||
|
from ..base.manager import AccountBasePlaybookManager
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
risk_items = [
|
||||||
|
"authorized_keys",
|
||||||
|
"sudoers",
|
||||||
|
"groups",
|
||||||
|
]
|
||||||
|
common_risk_items = [
|
||||||
|
"address_last_login",
|
||||||
|
"date_last_login",
|
||||||
|
"date_password_change",
|
||||||
|
"date_password_expired",
|
||||||
|
"detail"
|
||||||
|
]
|
||||||
|
diff_items = risk_items + common_risk_items
|
||||||
|
|
||||||
|
|
||||||
|
def format_datetime(value):
|
||||||
|
if isinstance(value, timezone.datetime):
|
||||||
|
return value.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def get_items_diff(ori_account, d):
|
||||||
|
if hasattr(ori_account, "_diff"):
|
||||||
|
return ori_account._diff
|
||||||
|
|
||||||
|
diff = {}
|
||||||
|
for item in diff_items:
|
||||||
|
get_item_diff(item, ori_account, d, diff)
|
||||||
|
ori_account._diff = diff
|
||||||
|
return diff
|
||||||
|
|
||||||
|
|
||||||
|
def get_item_diff(item, ori_account, d, diff):
|
||||||
|
detail = getattr(ori_account, 'detail', {})
|
||||||
|
new_detail = d.get('detail', {})
|
||||||
|
ori = getattr(ori_account, item, None) or detail.get(item)
|
||||||
|
new = d.get(item, "") or new_detail.get(item)
|
||||||
|
if not ori and not new:
|
||||||
|
return
|
||||||
|
|
||||||
|
ori = format_datetime(ori)
|
||||||
|
new = format_datetime(new)
|
||||||
|
|
||||||
|
if new != ori:
|
||||||
|
diff[item] = get_text_diff(str(ori), str(new))
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyseAccountRisk:
|
||||||
|
long_time = timezone.timedelta(days=90)
|
||||||
|
datetime_check_items = [
|
||||||
|
{"field": "date_last_login", "risk": "long_time_no_login", "delta": long_time},
|
||||||
|
{
|
||||||
|
"field": "date_password_change",
|
||||||
|
"risk": RiskChoice.long_time_password,
|
||||||
|
"delta": long_time,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "date_password_expired",
|
||||||
|
"risk": "password_expired",
|
||||||
|
"delta": timezone.timedelta(seconds=1),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, check_risk=True):
|
||||||
|
self.check_risk = check_risk
|
||||||
|
self.now = timezone.now()
|
||||||
|
self.pending_add_risks = []
|
||||||
|
|
||||||
|
def _analyse_item_changed(self, ori_ga, d):
|
||||||
|
diff = get_items_diff(ori_ga, d)
|
||||||
|
if not diff:
|
||||||
|
return
|
||||||
|
|
||||||
|
risks = []
|
||||||
|
for k, v in diff.items():
|
||||||
|
if k not in risk_items:
|
||||||
|
continue
|
||||||
|
risks.append(
|
||||||
|
dict(
|
||||||
|
asset_id=str(ori_ga.asset_id),
|
||||||
|
username=ori_ga.username,
|
||||||
|
gathered_account=ori_ga,
|
||||||
|
risk=k + "_changed",
|
||||||
|
detail={"diff": v},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.save_or_update_risks(risks)
|
||||||
|
|
||||||
|
def _analyse_datetime_changed(self, ori_account, d, asset, username):
|
||||||
|
basic = {"asset_id": str(asset.id), "username": username}
|
||||||
|
|
||||||
|
risks = []
|
||||||
|
for item in self.datetime_check_items:
|
||||||
|
field = item["field"]
|
||||||
|
risk = item["risk"]
|
||||||
|
delta = item["delta"]
|
||||||
|
|
||||||
|
date = d.get(field)
|
||||||
|
if not date:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pre_date = ori_account and getattr(ori_account, field)
|
||||||
|
if pre_date == date:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if date and date < timezone.now() - delta:
|
||||||
|
risks.append(
|
||||||
|
dict(**basic, risk=risk, detail={"date": date.isoformat()})
|
||||||
|
)
|
||||||
|
|
||||||
|
self.save_or_update_risks(risks)
|
||||||
|
|
||||||
|
def save_or_update_risks(self, risks):
|
||||||
|
# 提前取出来,避免每次都查数据库
|
||||||
|
asset_ids = {r["asset_id"] for r in risks}
|
||||||
|
assets_risks = AccountRisk.objects.filter(asset_id__in=asset_ids)
|
||||||
|
assets_risks = {f"{r.asset_id}_{r.username}_{r.risk}": r for r in assets_risks}
|
||||||
|
|
||||||
|
for d in risks:
|
||||||
|
detail = d.pop("detail", {})
|
||||||
|
detail["datetime"] = self.now.isoformat()
|
||||||
|
key = f"{d['asset_id']}_{d['username']}_{d['risk']}"
|
||||||
|
found = assets_risks.get(key)
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
self._create_risk(dict(**d, details=[detail]))
|
||||||
|
continue
|
||||||
|
|
||||||
|
found.details.append(detail)
|
||||||
|
self._update_risk(found)
|
||||||
|
|
||||||
|
@bulk_create_decorator(AccountRisk)
|
||||||
|
def _create_risk(self, data):
|
||||||
|
return AccountRisk(**data)
|
||||||
|
|
||||||
|
@bulk_update_decorator(AccountRisk, update_fields=["details"])
|
||||||
|
def _update_risk(self, account):
|
||||||
|
return account
|
||||||
|
|
||||||
|
def analyse_risk(self, asset, ga, d, sys_found):
|
||||||
|
if not self.check_risk:
|
||||||
|
return
|
||||||
|
|
||||||
|
basic = {"asset": asset, "username": d["username"], 'gathered_account': ga.id}
|
||||||
|
if ga:
|
||||||
|
self._analyse_item_changed(ga, d)
|
||||||
|
elif not sys_found:
|
||||||
|
self._create_risk(
|
||||||
|
dict(
|
||||||
|
**basic,
|
||||||
|
risk=RiskChoice.new_found,
|
||||||
|
details=[{"datetime": self.now.isoformat()}],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._analyse_datetime_changed(ga, d, asset, d["username"])
|
||||||
|
|
||||||
|
|
||||||
|
class GatherAccountsManager(AccountBasePlaybookManager):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.host_asset_mapper = {}
|
||||||
|
self.asset_account_info = {}
|
||||||
|
self.asset_usernames_mapper = defaultdict(set)
|
||||||
|
self.ori_asset_usernames = defaultdict(set)
|
||||||
|
self.ori_gathered_usernames = defaultdict(set)
|
||||||
|
self.ori_gathered_accounts_mapper = dict()
|
||||||
|
self.is_sync_account = self.execution.snapshot.get("is_sync_account")
|
||||||
|
self.check_risk = self.execution.snapshot.get("check_risk", False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def method_type(cls):
|
||||||
|
return AutomationTypes.gather_accounts
|
||||||
|
|
||||||
|
def host_callback(self, host, asset=None, **kwargs):
|
||||||
|
super().host_callback(host, asset=asset, **kwargs)
|
||||||
|
self.host_asset_mapper[host["name"]] = asset
|
||||||
|
return host
|
||||||
|
|
||||||
|
def _filter_success_result(self, tp, result):
|
||||||
|
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_nested_info(data, *keys):
|
||||||
|
for key in keys:
|
||||||
|
data = data.get(key, {})
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _collect_asset_account_info(self, asset, info):
|
||||||
|
result = self._filter_success_result(asset.type, info)
|
||||||
|
accounts = []
|
||||||
|
for username, info in result.items():
|
||||||
|
self.asset_usernames_mapper[str(asset.id)].add(username)
|
||||||
|
|
||||||
|
d = {"asset": asset, "username": username, "remote_present": True, **info}
|
||||||
|
accounts.append(d)
|
||||||
|
self.asset_account_info[asset] = accounts
|
||||||
|
|
||||||
|
def on_host_success(self, host, result):
|
||||||
|
super().on_host_success(host, result)
|
||||||
|
info = self._get_nested_info(result, "debug", "res", "info")
|
||||||
|
asset = self.host_asset_mapper.get(host)
|
||||||
|
|
||||||
|
if asset and info:
|
||||||
|
self._collect_asset_account_info(asset, info)
|
||||||
|
else:
|
||||||
|
print(f"\033[31m Not found {host} info \033[0m\n")
|
||||||
|
|
||||||
|
def prefetch_origin_account_usernames(self):
|
||||||
|
"""
|
||||||
|
提起查出来,避免每次 sql 查询
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
assets = self.asset_usernames_mapper.keys()
|
||||||
|
accounts = Account.objects.filter(asset__in=assets).values_list(
|
||||||
|
"asset", "username"
|
||||||
|
)
|
||||||
|
|
||||||
|
for asset_id, username in accounts:
|
||||||
|
self.ori_asset_usernames[str(asset_id)].add(username)
|
||||||
|
|
||||||
|
ga_accounts = GatheredAccount.objects.filter(asset__in=assets)
|
||||||
|
for account in ga_accounts:
|
||||||
|
self.ori_gathered_usernames[str(account.asset_id)].add(account.username)
|
||||||
|
key = "{}_{}".format(account.asset_id, account.username)
|
||||||
|
self.ori_gathered_accounts_mapper[key] = account
|
||||||
|
|
||||||
|
def update_gather_accounts_status(self, asset):
|
||||||
|
"""
|
||||||
|
远端账号,收集中的账号,vault 中的账号。
|
||||||
|
要根据账号新增见啥,标识 收集账号的状态, 让管理员关注
|
||||||
|
|
||||||
|
远端账号 -> 收集账号 -> 特权账号
|
||||||
|
"""
|
||||||
|
remote_users = self.asset_usernames_mapper[str(asset.id)]
|
||||||
|
ori_users = self.ori_asset_usernames[str(asset.id)]
|
||||||
|
ori_ga_users = self.ori_gathered_usernames[str(asset.id)]
|
||||||
|
|
||||||
|
queryset = GatheredAccount.objects.filter(asset=asset).exclude(
|
||||||
|
status=ConfirmOrIgnore.ignored
|
||||||
|
)
|
||||||
|
|
||||||
|
# 远端账号 比 收集账号多的
|
||||||
|
# 新增创建,不用处理状态
|
||||||
|
new_found_users = remote_users - ori_ga_users
|
||||||
|
if new_found_users:
|
||||||
|
self.summary["new_accounts"] += len(new_found_users)
|
||||||
|
for username in new_found_users:
|
||||||
|
self.result["new_accounts"].append(
|
||||||
|
{
|
||||||
|
"asset": str(asset),
|
||||||
|
"username": username,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 远端上 比 收集账号少的
|
||||||
|
# 标识 remote_present=False, 标记为待处理
|
||||||
|
# 远端资产上不存在的,标识为待处理,需要管理员介入
|
||||||
|
lost_users = ori_ga_users - remote_users
|
||||||
|
if lost_users:
|
||||||
|
queryset.filter(username__in=lost_users).update(
|
||||||
|
status=ConfirmOrIgnore.pending, remote_present=False
|
||||||
|
)
|
||||||
|
self.summary["lost_accounts"] += len(lost_users)
|
||||||
|
for username in lost_users:
|
||||||
|
self.result["lost_accounts"].append(
|
||||||
|
{
|
||||||
|
"asset": str(asset),
|
||||||
|
"username": username,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 收集的账号 比 账号列表多的, 有可能是账号中删掉了, 但这时候状态已经是 confirm 了
|
||||||
|
# 标识状态为 待处理, 让管理员去确认
|
||||||
|
ga_added_users = ori_ga_users - ori_users
|
||||||
|
if ga_added_users:
|
||||||
|
queryset.filter(username__in=ga_added_users).update(status=ConfirmOrIgnore.pending)
|
||||||
|
|
||||||
|
# 收集的账号 比 账号列表少的
|
||||||
|
# 这个好像不不用对比,原始情况就这样
|
||||||
|
|
||||||
|
# 远端账号 比 账号列表少的
|
||||||
|
# 创建收集账号,标识 remote_present=False, 状态待处理
|
||||||
|
|
||||||
|
# 远端账号 比 账号列表多的
|
||||||
|
# 正常情况, 不用处理,因为远端账号会创建到收集账号,收集账号再去对比
|
||||||
|
|
||||||
|
# 不过这个好像也处理一下 status,因为已存在,这是状态应该是确认
|
||||||
|
(
|
||||||
|
queryset.filter(username__in=ori_users)
|
||||||
|
.exclude(status=ConfirmOrIgnore.confirmed)
|
||||||
|
.update(status=ConfirmOrIgnore.confirmed)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 远端存在的账号,标识为已存在
|
||||||
|
(
|
||||||
|
queryset.filter(username__in=remote_users, remote_present=False).update(
|
||||||
|
remote_present=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 资产上没有的,标识为为存在
|
||||||
|
(
|
||||||
|
queryset.exclude(username__in=ori_users)
|
||||||
|
.filter(present=True)
|
||||||
|
.update(present=False)
|
||||||
|
)
|
||||||
|
(
|
||||||
|
queryset.filter(username__in=ori_users)
|
||||||
|
.filter(present=False)
|
||||||
|
.update(present=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
@bulk_create_decorator(GatheredAccount)
|
||||||
|
def create_gathered_account(self, d):
|
||||||
|
ga = GatheredAccount()
|
||||||
|
for k, v in d.items():
|
||||||
|
setattr(ga, k, v)
|
||||||
|
|
||||||
|
return ga
|
||||||
|
|
||||||
|
@bulk_update_decorator(GatheredAccount, update_fields=common_risk_items)
|
||||||
|
def update_gathered_account(self, ori_account, d):
|
||||||
|
diff = get_items_diff(ori_account, d)
|
||||||
|
if not diff:
|
||||||
|
return
|
||||||
|
for k in diff:
|
||||||
|
if k not in common_risk_items:
|
||||||
|
continue
|
||||||
|
v = d.get(k)
|
||||||
|
setattr(ori_account, k, v)
|
||||||
|
return ori_account
|
||||||
|
|
||||||
|
def do_run(self, *args, **kwargs):
|
||||||
|
super().do_run(*args, **kwargs)
|
||||||
|
self.prefetch_origin_account_usernames()
|
||||||
|
risk_analyser = AnalyseAccountRisk(self.check_risk)
|
||||||
|
|
||||||
|
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):
|
||||||
|
for d in accounts_data:
|
||||||
|
username = d["username"]
|
||||||
|
ori_account = self.ori_gathered_accounts_mapper.get(
|
||||||
|
"{}_{}".format(asset.id, username)
|
||||||
|
)
|
||||||
|
if not ori_account:
|
||||||
|
ga = self.create_gathered_account(d)
|
||||||
|
else:
|
||||||
|
ga = ori_account
|
||||||
|
self.update_gathered_account(ori_account, d)
|
||||||
|
ori_found = username in ori_users
|
||||||
|
risk_analyser.analyse_risk(asset, ga, d, ori_found)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def get_report_template(self):
|
||||||
|
return "accounts/gather_account_report.html"
|
|
@ -1,75 +0,0 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
__all__ = ['GatherAccountsFilter']
|
|
||||||
|
|
||||||
|
|
||||||
# TODO 后期会挪到playbook中
|
|
||||||
class GatherAccountsFilter:
|
|
||||||
|
|
||||||
def __init__(self, tp):
|
|
||||||
self.tp = tp
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mysql_filter(info):
|
|
||||||
result = {}
|
|
||||||
for _, user_dict in info.items():
|
|
||||||
for username, _ in user_dict.items():
|
|
||||||
if len(username.split('.')) == 1:
|
|
||||||
result[username] = {}
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def postgresql_filter(info):
|
|
||||||
result = {}
|
|
||||||
for username in info:
|
|
||||||
result[username] = {}
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def posix_filter(info):
|
|
||||||
username_pattern = re.compile(r'^(\S+)')
|
|
||||||
ip_pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
|
|
||||||
login_time_pattern = re.compile(r'\w{3} \w{3}\s+\d{1,2} \d{2}:\d{2}:\d{2} \d{4}')
|
|
||||||
result = {}
|
|
||||||
for line in info:
|
|
||||||
usernames = username_pattern.findall(line)
|
|
||||||
username = ''.join(usernames)
|
|
||||||
if username:
|
|
||||||
result[username] = {}
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
ip_addrs = ip_pattern.findall(line)
|
|
||||||
ip_addr = ''.join(ip_addrs)
|
|
||||||
if ip_addr:
|
|
||||||
result[username].update({'address': ip_addr})
|
|
||||||
login_times = login_time_pattern.findall(line)
|
|
||||||
if login_times:
|
|
||||||
datetime_str = login_times[0].split(' ', 1)[1] + " +0800"
|
|
||||||
date = timezone.datetime.strptime(datetime_str, '%b %d %H:%M:%S %Y %z')
|
|
||||||
result[username].update({'date': date})
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def windows_filter(info):
|
|
||||||
info = info[4:-2]
|
|
||||||
result = {}
|
|
||||||
for i in info:
|
|
||||||
for username in i.split():
|
|
||||||
result[username] = {}
|
|
||||||
return result
|
|
||||||
|
|
||||||
def run(self, method_id_meta_mapper, info):
|
|
||||||
run_method_name = None
|
|
||||||
for k, v in method_id_meta_mapper.items():
|
|
||||||
if self.tp not in v['type']:
|
|
||||||
continue
|
|
||||||
run_method_name = k.replace(f'{v["method"]}_', '')
|
|
||||||
|
|
||||||
if not run_method_name:
|
|
||||||
return info
|
|
||||||
|
|
||||||
if hasattr(self, f'{run_method_name}_filter'):
|
|
||||||
return getattr(self, f'{run_method_name}_filter')(info)
|
|
||||||
return info
|
|
|
@ -1,21 +0,0 @@
|
||||||
- hosts: demo
|
|
||||||
gather_facts: no
|
|
||||||
tasks:
|
|
||||||
- name: Gather posix account
|
|
||||||
ansible.builtin.shell:
|
|
||||||
cmd: >
|
|
||||||
users=$(getent passwd | grep -v nologin | grep -v shutdown | awk -F":" '{ print $1 }');for i in $users;
|
|
||||||
do k=$(last -w -F $i -1 | head -1 | grep -v ^$ | awk '{ print $0 }')
|
|
||||||
if [ -n "$k" ]; then
|
|
||||||
echo $k
|
|
||||||
else
|
|
||||||
echo $i
|
|
||||||
fi;done
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- name: Define info by set_fact
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
info: "{{ result.stdout_lines }}"
|
|
||||||
|
|
||||||
- debug:
|
|
||||||
var: info
|
|
|
@ -1,14 +0,0 @@
|
||||||
- hosts: demo
|
|
||||||
gather_facts: no
|
|
||||||
tasks:
|
|
||||||
- name: Gather windows account
|
|
||||||
ansible.builtin.win_shell: net user
|
|
||||||
register: result
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- name: Define info by set_fact
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
info: "{{ result.stdout_lines }}"
|
|
||||||
|
|
||||||
- debug:
|
|
||||||
var: info
|
|
|
@ -1,139 +0,0 @@
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
|
||||||
from accounts.models import GatheredAccount
|
|
||||||
from assets.models import Asset
|
|
||||||
from common.utils import get_logger
|
|
||||||
from orgs.utils import tmp_to_org
|
|
||||||
from users.models import User
|
|
||||||
from .filter import GatherAccountsFilter
|
|
||||||
from ..base.manager import AccountBasePlaybookManager
|
|
||||||
from ...notifications import GatherAccountChangeMsg
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class GatherAccountsManager(AccountBasePlaybookManager):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.host_asset_mapper = {}
|
|
||||||
self.asset_account_info = {}
|
|
||||||
|
|
||||||
self.asset_username_mapper = defaultdict(set)
|
|
||||||
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def method_type(cls):
|
|
||||||
return AutomationTypes.gather_accounts
|
|
||||||
|
|
||||||
def host_callback(self, host, asset=None, **kwargs):
|
|
||||||
super().host_callback(host, asset=asset, **kwargs)
|
|
||||||
self.host_asset_mapper[host['name']] = asset
|
|
||||||
return host
|
|
||||||
|
|
||||||
def filter_success_result(self, tp, result):
|
|
||||||
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def generate_data(self, asset, result):
|
|
||||||
data = []
|
|
||||||
for username, info in result.items():
|
|
||||||
self.asset_username_mapper[str(asset.id)].add(username)
|
|
||||||
d = {'asset': asset, 'username': username, 'present': True}
|
|
||||||
if info.get('date'):
|
|
||||||
d['date_last_login'] = info['date']
|
|
||||||
if info.get('address'):
|
|
||||||
d['address_last_login'] = info['address'][:32]
|
|
||||||
data.append(d)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def collect_asset_account_info(self, asset, result):
|
|
||||||
data = self.generate_data(asset, result)
|
|
||||||
self.asset_account_info[asset] = data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_nested_info(data, *keys):
|
|
||||||
for key in keys:
|
|
||||||
data = data.get(key, {})
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
return data
|
|
||||||
|
|
||||||
def on_host_success(self, host, result):
|
|
||||||
info = self.get_nested_info(result, 'debug', 'res', 'info')
|
|
||||||
asset = self.host_asset_mapper.get(host)
|
|
||||||
if asset and info:
|
|
||||||
result = self.filter_success_result(asset.type, info)
|
|
||||||
self.collect_asset_account_info(asset, result)
|
|
||||||
else:
|
|
||||||
print(f'\033[31m Not found {host} info \033[0m\n')
|
|
||||||
|
|
||||||
def update_or_create_accounts(self):
|
|
||||||
for asset, data in self.asset_account_info.items():
|
|
||||||
with tmp_to_org(asset.org_id):
|
|
||||||
gathered_accounts = []
|
|
||||||
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
|
||||||
for d in data:
|
|
||||||
username = d['username']
|
|
||||||
gathered_account, __ = GatheredAccount.objects.update_or_create(
|
|
||||||
defaults=d, asset=asset, username=username,
|
|
||||||
)
|
|
||||||
gathered_accounts.append(gathered_account)
|
|
||||||
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()
|
|
||||||
self.update_or_create_accounts()
|
|
||||||
self.send_email_if_need(users, change_info)
|
|
||||||
|
|
||||||
def generate_send_users_and_change_info(self):
|
|
||||||
recipients = self.execution.recipients
|
|
||||||
if not self.asset_username_mapper or not recipients:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
users = User.objects.filter(id__in=recipients)
|
|
||||||
if not users.exists():
|
|
||||||
return users, None
|
|
||||||
|
|
||||||
asset_ids = self.asset_username_mapper.keys()
|
|
||||||
|
|
||||||
assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts')
|
|
||||||
gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, present=True)
|
|
||||||
|
|
||||||
asset_id_map = {str(asset.id): asset for asset in assets}
|
|
||||||
asset_id_username = list(assets.values_list('id', 'accounts__username'))
|
|
||||||
asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username')))
|
|
||||||
|
|
||||||
system_asset_username_mapper = defaultdict(set)
|
|
||||||
for asset_id, username in asset_id_username:
|
|
||||||
system_asset_username_mapper[str(asset_id)].add(username)
|
|
||||||
|
|
||||||
change_info = defaultdict(dict)
|
|
||||||
for asset_id, usernames in self.asset_username_mapper.items():
|
|
||||||
system_usernames = system_asset_username_mapper.get(asset_id)
|
|
||||||
if not system_usernames:
|
|
||||||
continue
|
|
||||||
|
|
||||||
add_usernames = usernames - system_usernames
|
|
||||||
remove_usernames = system_usernames - usernames
|
|
||||||
|
|
||||||
if not add_usernames and not remove_usernames:
|
|
||||||
continue
|
|
||||||
|
|
||||||
change_info[str(asset_id_map[asset_id])] = {
|
|
||||||
'add_usernames': add_usernames,
|
|
||||||
'remove_usernames': remove_usernames
|
|
||||||
}
|
|
||||||
|
|
||||||
return users, dict(change_info)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def send_email_if_need(users, change_info):
|
|
||||||
if not users or not change_info:
|
|
||||||
return
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
GatherAccountChangeMsg(user, change_info).publish_async()
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
- hosts: custom
|
||||||
|
gather_facts: no
|
||||||
|
vars:
|
||||||
|
ansible_connection: local
|
||||||
|
ansible_become: false
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Test privileged account (paramiko)
|
||||||
|
ssh_ping:
|
||||||
|
login_host: "{{ jms_asset.address }}"
|
||||||
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
login_user: "{{ jms_account.username }}"
|
||||||
|
login_password: "{{ jms_account.secret }}"
|
||||||
|
login_secret_type: "{{ jms_account.secret_type }}"
|
||||||
|
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||||
|
become: "{{ jms_custom_become | default(False) }}"
|
||||||
|
become_method: "{{ jms_custom_become_method | default('su') }}"
|
||||||
|
become_user: "{{ jms_custom_become_user | default('') }}"
|
||||||
|
become_password: "{{ jms_custom_become_password | default('') }}"
|
||||||
|
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||||
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
register: ping_info
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Change asset password (paramiko)
|
||||||
|
custom_command:
|
||||||
|
login_user: "{{ jms_account.username }}"
|
||||||
|
login_password: "{{ jms_account.secret }}"
|
||||||
|
login_host: "{{ jms_asset.address }}"
|
||||||
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
login_secret_type: "{{ jms_account.secret_type }}"
|
||||||
|
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||||
|
become: "{{ jms_custom_become | default(False) }}"
|
||||||
|
become_method: "{{ jms_custom_become_method | default('su') }}"
|
||||||
|
become_user: "{{ jms_custom_become_user | default('') }}"
|
||||||
|
become_password: "{{ jms_custom_become_password | default('') }}"
|
||||||
|
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
password: "{{ account.secret }}"
|
||||||
|
commands: "{{ params.commands }}"
|
||||||
|
first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}"
|
||||||
|
ignore_errors: true
|
||||||
|
when: ping_info is succeeded and check_conn_after_change
|
||||||
|
register: change_info
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Verify password (paramiko)
|
||||||
|
ssh_ping:
|
||||||
|
login_user: "{{ account.username }}"
|
||||||
|
login_password: "{{ account.secret }}"
|
||||||
|
login_host: "{{ jms_asset.address }}"
|
||||||
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
become: "{{ account.become.ansible_become | default(False) }}"
|
||||||
|
become_method: su
|
||||||
|
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||||
|
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||||
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
delegate_to: localhost
|
||||||
|
when: check_conn_after_change
|
|
@ -0,0 +1,32 @@
|
||||||
|
id: push_account_by_ssh
|
||||||
|
name: "{{ 'SSH account push' | trans }}"
|
||||||
|
category:
|
||||||
|
- device
|
||||||
|
- host
|
||||||
|
type:
|
||||||
|
- all
|
||||||
|
method: push_account
|
||||||
|
protocol: ssh
|
||||||
|
priority: 50
|
||||||
|
params:
|
||||||
|
- name: commands
|
||||||
|
type: list
|
||||||
|
label: "{{ 'Params commands label' | trans }}"
|
||||||
|
default: [ '' ]
|
||||||
|
help_text: "{{ 'Params commands help text' | trans }}"
|
||||||
|
|
||||||
|
i18n:
|
||||||
|
SSH account push:
|
||||||
|
zh: '使用 SSH 命令行自定义推送'
|
||||||
|
ja: 'SSHコマンドラインを使用してプッシュをカスタマイズする'
|
||||||
|
en: 'Custom push using SSH command line'
|
||||||
|
|
||||||
|
Params commands help text:
|
||||||
|
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
||||||
|
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.{login_password}<br />3 .ターミナルの設定<br / >4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード} <br />5. 終了'
|
||||||
|
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use {username}, {password}, {login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br / >4. username {username} privilege 0 password {password} <br />5. end'
|
||||||
|
|
||||||
|
Params commands label:
|
||||||
|
zh: '自定义命令'
|
||||||
|
ja: 'カスタムコマンド'
|
||||||
|
en: 'Custom command'
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mongodb
|
- hosts: mongodb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test MongoDB connection
|
- name: Test MongoDB connection
|
||||||
|
@ -53,3 +53,4 @@
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
|
when: check_conn_after_change
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
|
@ -54,3 +54,4 @@
|
||||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||||
filter: version
|
filter: version
|
||||||
|
when: check_conn_after_change
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: oracle
|
- hosts: oracle
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test Oracle connection
|
- name: Test Oracle connection
|
||||||
|
@ -40,3 +40,4 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
mode: "{{ account.mode }}"
|
mode: "{{ account.mode }}"
|
||||||
|
when: check_conn_after_change
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: postgre
|
- hosts: postgre
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||||
|
@ -59,5 +59,6 @@
|
||||||
when:
|
when:
|
||||||
- result is succeeded
|
- result is succeeded
|
||||||
- change_info is succeeded
|
- change_info is succeeded
|
||||||
|
- check_conn_after_change
|
||||||
register: result
|
register: result
|
||||||
failed_when: not result.is_available
|
failed_when: not result.is_available
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: sqlserver
|
- hosts: sqlserver
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test SQLServer connection
|
- name: Test SQLServer connection
|
||||||
|
@ -66,3 +66,4 @@
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
script: |
|
script: |
|
||||||
SELECT @@version
|
SELECT @@version
|
||||||
|
when: check_conn_after_change
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
database: passwd
|
database: passwd
|
||||||
key: "{{ account.username }}"
|
key: "{{ account.username }}"
|
||||||
register: user_info
|
register: user_info
|
||||||
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
- name: "Add {{ account.username }} user"
|
- name: "Add {{ account.username }} user"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
|
@ -18,10 +19,10 @@
|
||||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||||
append: yes
|
append: "{{ true if params.groups | length > 0 else false }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
state: present
|
state: present
|
||||||
when: user_info.failed
|
when: user_info.msg is defined
|
||||||
|
|
||||||
- name: "Set {{ account.username }} sudo setting"
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
|
@ -31,7 +32,7 @@
|
||||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
validate: visudo -cf %s
|
validate: visudo -cf %s
|
||||||
when:
|
when:
|
||||||
- user_info.failed or params.modify_sudo
|
- user_info.msg is defined or params.modify_sudo
|
||||||
- params.sudo
|
- params.sudo
|
||||||
|
|
||||||
- name: "Change {{ account.username }} password"
|
- name: "Change {{ account.username }} password"
|
||||||
|
@ -100,7 +101,7 @@
|
||||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password" and check_conn_after_change
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
|
@ -111,6 +112,6 @@
|
||||||
login_private_key_path: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key" and check_conn_after_change
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
database: passwd
|
database: passwd
|
||||||
key: "{{ account.username }}"
|
key: "{{ account.username }}"
|
||||||
register: user_info
|
register: user_info
|
||||||
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
- name: "Add {{ account.username }} user"
|
- name: "Add {{ account.username }} user"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
|
@ -18,10 +19,10 @@
|
||||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||||
append: yes
|
append: "{{ true if params.groups | length > 0 else false }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
state: present
|
state: present
|
||||||
when: user_info.failed
|
when: user_info.msg is defined
|
||||||
|
|
||||||
- name: "Set {{ account.username }} sudo setting"
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
|
@ -31,7 +32,7 @@
|
||||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
validate: visudo -cf %s
|
validate: visudo -cf %s
|
||||||
when:
|
when:
|
||||||
- user_info.failed or params.modify_sudo
|
- user_info.msg is defined or params.modify_sudo
|
||||||
- params.sudo
|
- params.sudo
|
||||||
|
|
||||||
- name: "Change {{ account.username }} password"
|
- name: "Change {{ account.username }} password"
|
||||||
|
@ -100,7 +101,7 @@
|
||||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password" and check_conn_after_change
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
|
@ -111,6 +112,6 @@
|
||||||
login_private_key_path: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key" and check_conn_after_change
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,6 @@
|
||||||
- name: Test privileged account
|
- name: Test privileged account
|
||||||
ansible.windows.win_ping:
|
ansible.windows.win_ping:
|
||||||
|
|
||||||
# - name: Print variables
|
|
||||||
# debug:
|
|
||||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
|
||||||
|
|
||||||
- name: Push user password
|
- name: Push user password
|
||||||
ansible.windows.win_user:
|
ansible.windows.win_user:
|
||||||
fullname: "{{ account.username}}"
|
fullname: "{{ account.username}}"
|
||||||
|
@ -28,4 +24,4 @@
|
||||||
vars:
|
vars:
|
||||||
ansible_user: "{{ account.username }}"
|
ansible_user: "{{ account.username }}"
|
||||||
ansible_password: "{{ account.secret }}"
|
ansible_password: "{{ account.secret }}"
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password" and check_conn_after_change
|
||||||
|
|
|
@ -4,10 +4,6 @@
|
||||||
- name: Test privileged account
|
- name: Test privileged account
|
||||||
ansible.windows.win_ping:
|
ansible.windows.win_ping:
|
||||||
|
|
||||||
# - name: Print variables
|
|
||||||
# debug:
|
|
||||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
|
||||||
|
|
||||||
- name: Push user password
|
- name: Push user password
|
||||||
ansible.windows.win_user:
|
ansible.windows.win_user:
|
||||||
fullname: "{{ account.username}}"
|
fullname: "{{ account.username}}"
|
||||||
|
@ -31,5 +27,5 @@
|
||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_secret_type: "{{ account.secret_type }}"
|
login_secret_type: "{{ account.secret_type }}"
|
||||||
gateway_args: "{{ jms_gateway | default({}) }}"
|
gateway_args: "{{ jms_gateway | default({}) }}"
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password" and check_conn_after_change
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
|
from common.decorators import bulk_create_decorator
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from ..base.manager import AccountBasePlaybookManager
|
from common.utils.timezone import local_now_filename
|
||||||
from ..change_secret.manager import ChangeSecretManager
|
from ..base.manager import BaseChangeSecretPushManager
|
||||||
|
from ...models import PushSecretRecord
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
class PushAccountManager(BaseChangeSecretPushManager):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def require_update_version(account, recorder):
|
def require_update_version(account, recorder):
|
||||||
|
@ -17,63 +21,43 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||||
def method_type(cls):
|
def method_type(cls):
|
||||||
return AutomationTypes.push_account
|
return AutomationTypes.push_account
|
||||||
|
|
||||||
# @classmethod
|
def get_secret(self, account):
|
||||||
# def trigger_by_asset_create(cls, asset):
|
return account.secret
|
||||||
# automations = PushAccountAutomation.objects.filter(
|
|
||||||
# triggers__contains=TriggerChoice.on_asset_create
|
|
||||||
# )
|
|
||||||
# account_automation_map = {auto.username: auto for auto in automations}
|
|
||||||
#
|
|
||||||
# util = AssetPermissionUtil()
|
|
||||||
# permissions = util.get_permissions_for_assets([asset], with_node=True)
|
|
||||||
# account_permission_map = defaultdict(list)
|
|
||||||
# for permission in permissions:
|
|
||||||
# for account in permission.accounts:
|
|
||||||
# account_permission_map[account].append(permission)
|
|
||||||
#
|
|
||||||
# username_automation_map = {}
|
|
||||||
# for username, automation in account_automation_map.items():
|
|
||||||
# if username != '@USER':
|
|
||||||
# username_automation_map[username] = automation
|
|
||||||
# continue
|
|
||||||
#
|
|
||||||
# asset_permissions = account_permission_map.get(username)
|
|
||||||
# if not asset_permissions:
|
|
||||||
# continue
|
|
||||||
# asset_permissions = util.get_permissions([p.id for p in asset_permissions])
|
|
||||||
# usernames = asset_permissions.values_list('users__username', flat=True).distinct()
|
|
||||||
# for _username in usernames:
|
|
||||||
# username_automation_map[_username] = automation
|
|
||||||
#
|
|
||||||
# asset_usernames_exists = asset.accounts.values_list('username', flat=True)
|
|
||||||
# accounts_to_create = []
|
|
||||||
# accounts_to_push = []
|
|
||||||
# for username, automation in username_automation_map.items():
|
|
||||||
# if username in asset_usernames_exists:
|
|
||||||
# continue
|
|
||||||
#
|
|
||||||
# if automation.secret_strategy != SecretStrategy.custom:
|
|
||||||
# secret_generator = SecretGenerator(
|
|
||||||
# automation.secret_strategy, automation.secret_type,
|
|
||||||
# automation.password_rules
|
|
||||||
# )
|
|
||||||
# secret = secret_generator.get_secret()
|
|
||||||
# else:
|
|
||||||
# secret = automation.secret
|
|
||||||
#
|
|
||||||
# account = Account(
|
|
||||||
# username=username, secret=secret,
|
|
||||||
# asset=asset, secret_type=automation.secret_type,
|
|
||||||
# comment='Create by account creation {}'.format(automation.name),
|
|
||||||
# )
|
|
||||||
# accounts_to_create.append(account)
|
|
||||||
# if automation.action == 'create_and_push':
|
|
||||||
# accounts_to_push.append(account)
|
|
||||||
# else:
|
|
||||||
# accounts_to_create.append(account)
|
|
||||||
#
|
|
||||||
# logger.debug(f'Create account {account} for asset {asset}')
|
|
||||||
|
|
||||||
# @classmethod
|
def gen_account_inventory(self, account, asset, h, path_dir):
|
||||||
# def trigger_by_permission_accounts_change(cls):
|
self.get_or_create_record(asset, account, h['name'])
|
||||||
# pass
|
secret = self.get_secret(account)
|
||||||
|
secret_type = account.secret_type
|
||||||
|
new_secret, private_key_path = self.handle_ssh_secret(secret_type, secret, path_dir)
|
||||||
|
h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
|
||||||
|
return h
|
||||||
|
|
||||||
|
def get_or_create_record(self, asset, account, name):
|
||||||
|
asset_account_id = f'{asset.id}-{account.id}'
|
||||||
|
|
||||||
|
if asset_account_id in self.record_map:
|
||||||
|
record_id = self.record_map[asset_account_id]
|
||||||
|
recorder = PushSecretRecord.objects.filter(id=record_id).first()
|
||||||
|
else:
|
||||||
|
recorder = self.create_record(asset, account)
|
||||||
|
|
||||||
|
self.name_recorder_mapper[name] = recorder
|
||||||
|
return recorder
|
||||||
|
|
||||||
|
@bulk_create_decorator(PushSecretRecord)
|
||||||
|
def create_record(self, asset, account):
|
||||||
|
recorder = PushSecretRecord(
|
||||||
|
asset=asset, account=account, execution=self.execution,
|
||||||
|
comment=f'{account.username}@{asset.address}'
|
||||||
|
)
|
||||||
|
return recorder
|
||||||
|
|
||||||
|
def print_summary(self):
|
||||||
|
print('\n\n' + '-' * 80)
|
||||||
|
plan_execution_end = _('Plan execution end')
|
||||||
|
print('{} {}\n'.format(plan_execution_end, local_now_filename()))
|
||||||
|
time_cost = _('Duration')
|
||||||
|
print('{}: {}s'.format(time_cost, self.duration))
|
||||||
|
|
||||||
|
def get_report_template(self):
|
||||||
|
return "accounts/push_account_report.html"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mongodb
|
- hosts: mongodb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: "Remove account"
|
- name: "Remove account"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: oracle
|
- hosts: oracle
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: "Remove account"
|
- name: "Remove account"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: postgresql
|
- hosts: postgresql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: sqlserver
|
- hosts: sqlserver
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: "Remove account"
|
- name: "Remove account"
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import os
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.models import Account
|
from accounts.models import Account, GatheredAccount, AccountRisk
|
||||||
|
from common.const import ConfirmOrIgnore
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from ..base.manager import AccountBasePlaybookManager
|
from ..base.manager import AccountBasePlaybookManager
|
||||||
|
|
||||||
|
@ -12,59 +14,82 @@ logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RemoveAccountManager(AccountBasePlaybookManager):
|
class RemoveAccountManager(AccountBasePlaybookManager):
|
||||||
|
super_accounts = ["root", "administrator"]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.host_account_mapper = {}
|
self.host_account_mapper = dict()
|
||||||
|
self.host_accounts = defaultdict(list)
|
||||||
|
snapshot_account = self.execution.snapshot.get("accounts", [])
|
||||||
|
self.snapshot_asset_account_map = defaultdict(list)
|
||||||
|
for account in snapshot_account:
|
||||||
|
self.snapshot_asset_account_map[str(account["asset"])].append(account)
|
||||||
|
|
||||||
|
# 给 handler 使用
|
||||||
|
self.delete = self.execution.snapshot.get("delete", "both")
|
||||||
|
self.confirm_risk = self.execution.snapshot.get("risk", "")
|
||||||
|
|
||||||
def prepare_runtime_dir(self):
|
def prepare_runtime_dir(self):
|
||||||
path = super().prepare_runtime_dir()
|
path = super().prepare_runtime_dir()
|
||||||
ansible_config_path = os.path.join(path, 'ansible.cfg')
|
ansible_config_path = os.path.join(path, "ansible.cfg")
|
||||||
|
|
||||||
with open(ansible_config_path, 'w') as f:
|
with open(ansible_config_path, "w") as f:
|
||||||
f.write('[ssh_connection]\n')
|
f.write("[ssh_connection]\n")
|
||||||
f.write('ssh_args = -o ControlMaster=no -o ControlPersist=no\n')
|
f.write("ssh_args = -o ControlMaster=no -o ControlPersist=no\n")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def method_type(cls):
|
def method_type(cls):
|
||||||
return AutomationTypes.remove_account
|
return AutomationTypes.remove_account
|
||||||
|
|
||||||
def get_gather_accounts(self, privilege_account, gather_accounts: QuerySet):
|
def host_callback(
|
||||||
gather_account_ids = self.execution.snapshot['gather_accounts']
|
self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs
|
||||||
gather_accounts = gather_accounts.filter(id__in=gather_account_ids)
|
):
|
||||||
gather_accounts = gather_accounts.exclude(
|
if host.get("error"):
|
||||||
username__in=[privilege_account.username, 'root', 'Administrator']
|
|
||||||
)
|
|
||||||
return gather_accounts
|
|
||||||
|
|
||||||
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
|
|
||||||
if host.get('error'):
|
|
||||||
return host
|
return host
|
||||||
|
|
||||||
gather_accounts = asset.gatheredaccount_set.all()
|
|
||||||
gather_accounts = self.get_gather_accounts(account, gather_accounts)
|
|
||||||
|
|
||||||
inventory_hosts = []
|
inventory_hosts = []
|
||||||
|
accounts_to_remove = self.snapshot_asset_account_map.get(str(asset.id), [])
|
||||||
|
|
||||||
for gather_account in gather_accounts:
|
for account in accounts_to_remove:
|
||||||
|
username = account.get("username")
|
||||||
|
if not username or username.lower() in self.super_accounts:
|
||||||
|
print("Super account can not be remove: ", username)
|
||||||
|
continue
|
||||||
h = deepcopy(host)
|
h = deepcopy(host)
|
||||||
h['name'] += '(' + gather_account.username + ')'
|
h["name"] += "(" + username + ")"
|
||||||
self.host_account_mapper[h['name']] = (asset, gather_account)
|
self.host_account_mapper[h["name"]] = account
|
||||||
h['account'] = {'username': gather_account.username}
|
h["account"] = {"username": username}
|
||||||
inventory_hosts.append(h)
|
inventory_hosts.append(h)
|
||||||
return inventory_hosts
|
return inventory_hosts
|
||||||
|
|
||||||
def on_host_success(self, host, result):
|
def on_host_success(self, host, result):
|
||||||
tuple_asset_gather_account = self.host_account_mapper.get(host)
|
super().on_host_success(host, result)
|
||||||
if not tuple_asset_gather_account:
|
account = self.host_account_mapper.get(host)
|
||||||
|
|
||||||
|
if not account:
|
||||||
return
|
return
|
||||||
asset, gather_account = tuple_asset_gather_account
|
|
||||||
try:
|
try:
|
||||||
Account.objects.filter(
|
if self.delete == "both":
|
||||||
asset_id=asset.id,
|
Account.objects.filter(
|
||||||
username=gather_account.username
|
asset_id=account["asset"],
|
||||||
|
username=account["username"]
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
if self.confirm_risk:
|
||||||
|
AccountRisk.objects.filter(
|
||||||
|
asset_id=account["asset"],
|
||||||
|
username=account["username"],
|
||||||
|
risk__in=[self.confirm_risk],
|
||||||
|
).update(status=ConfirmOrIgnore.confirmed)
|
||||||
|
|
||||||
|
GatheredAccount.objects.filter(
|
||||||
|
asset_id=account["asset"],
|
||||||
|
username=account["username"]
|
||||||
).delete()
|
).delete()
|
||||||
gather_account.delete()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'\033[31m Delete account {gather_account.username} failed: {e} \033[0m\n')
|
logger.error(
|
||||||
|
f"Failed to delete account {account['username']} on asset {account['asset']}: {e}"
|
||||||
|
)
|
||||||
|
|
|
@ -21,3 +21,4 @@
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mongodb
|
- hosts: mongodb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: oracle
|
- hosts: oracle
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: postgresql
|
- hosts: postgresql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- hosts: sqlserver
|
- hosts: sqlserver
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /opt/py3/bin/python
|
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account
|
||||||
|
|
|
@ -24,7 +24,7 @@ class AliasAccount(TextChoices):
|
||||||
|
|
||||||
class Source(TextChoices):
|
class Source(TextChoices):
|
||||||
LOCAL = 'local', _('Local')
|
LOCAL = 'local', _('Local')
|
||||||
COLLECTED = 'collected', _('Collected')
|
DISCOVERY = 'collected', _('Discovery')
|
||||||
TEMPLATE = 'template', _('Template')
|
TEMPLATE = 'template', _('Template')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ __all__ = [
|
||||||
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
|
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
|
||||||
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
|
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
|
||||||
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
|
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
|
||||||
|
'GatherAccountDetailField'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,18 +28,23 @@ class AutomationTypes(models.TextChoices):
|
||||||
remove_account = 'remove_account', _('Remove account')
|
remove_account = 'remove_account', _('Remove account')
|
||||||
gather_accounts = 'gather_accounts', _('Gather accounts')
|
gather_accounts = 'gather_accounts', _('Gather accounts')
|
||||||
verify_gateway_account = 'verify_gateway_account', _('Verify gateway account')
|
verify_gateway_account = 'verify_gateway_account', _('Verify gateway account')
|
||||||
|
check_account = 'check_account', _('Check account')
|
||||||
|
backup_account = 'backup_account', _('Backup account')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_type_model(cls, tp):
|
def get_type_model(cls, tp):
|
||||||
from accounts.models import (
|
from accounts.models import (
|
||||||
PushAccountAutomation, ChangeSecretAutomation,
|
PushAccountAutomation, ChangeSecretAutomation,
|
||||||
VerifyAccountAutomation, GatherAccountsAutomation,
|
VerifyAccountAutomation, GatherAccountsAutomation,
|
||||||
|
CheckAccountAutomation, BackupAccountAutomation
|
||||||
)
|
)
|
||||||
type_model_dict = {
|
type_model_dict = {
|
||||||
cls.push_account: PushAccountAutomation,
|
cls.push_account: PushAccountAutomation,
|
||||||
cls.change_secret: ChangeSecretAutomation,
|
cls.change_secret: ChangeSecretAutomation,
|
||||||
cls.verify_account: VerifyAccountAutomation,
|
cls.verify_account: VerifyAccountAutomation,
|
||||||
cls.gather_accounts: GatherAccountsAutomation,
|
cls.gather_accounts: GatherAccountsAutomation,
|
||||||
|
cls.check_account: CheckAccountAutomation,
|
||||||
|
cls.backup_account: BackupAccountAutomation,
|
||||||
}
|
}
|
||||||
return type_model_dict.get(tp)
|
return type_model_dict.get(tp)
|
||||||
|
|
||||||
|
@ -49,9 +55,9 @@ class SecretStrategy(models.TextChoices):
|
||||||
|
|
||||||
|
|
||||||
class SSHKeyStrategy(models.TextChoices):
|
class SSHKeyStrategy(models.TextChoices):
|
||||||
|
# add = 'add', _('Append SSH KEY')
|
||||||
set_jms = 'set_jms', _('Replace (Replace only keys pushed by JumpServer) ')
|
set_jms = 'set_jms', _('Replace (Replace only keys pushed by JumpServer) ')
|
||||||
set = 'set', _('Empty and append SSH KEY')
|
set = 'set', _('Empty and append SSH KEY')
|
||||||
add = 'add', _('Append SSH KEY')
|
|
||||||
|
|
||||||
|
|
||||||
class TriggerChoice(models.TextChoices, TreeChoices):
|
class TriggerChoice(models.TextChoices, TreeChoices):
|
||||||
|
@ -109,3 +115,20 @@ class ChangeSecretRecordStatusChoice(models.TextChoices):
|
||||||
failed = 'failed', _('Failed')
|
failed = 'failed', _('Failed')
|
||||||
success = 'success', _('Success')
|
success = 'success', _('Success')
|
||||||
pending = 'pending', _('Pending')
|
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')
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Instructions
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
This API provides PAM asset account viewing service, supports RESTful style calls, and returns data in JSON format.
|
||||||
|
|
||||||
|
## 2. Environment Requirements
|
||||||
|
|
||||||
|
- `cURL`
|
||||||
|
|
||||||
|
## 3. Usage
|
||||||
|
|
||||||
|
**Request Method**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**Request Parameters**
|
||||||
|
|
||||||
|
| Parameter Name | Type | Required | Description |
|
||||||
|
|----------------|------|----------|-------------------|
|
||||||
|
| asset | str | Yes | Asset ID / Name |
|
||||||
|
| account | str | Yes | Account ID / Name |
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
|
Q: How to obtain the API Key?
|
||||||
|
|
||||||
|
A: You can create an application in PAM - Application Management to generate KEY_ID and KEY_SECRET.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
|
||||||
|
| Version | Changes | Date |
|
||||||
|
|---------|------------------------|------------|
|
||||||
|
| 1.0.0 | Initial version | 2025-02-11 |
|
|
@ -0,0 +1,42 @@
|
||||||
|
# 使用方法
|
||||||
|
|
||||||
|
## 1. 概要
|
||||||
|
|
||||||
|
本 API は PAM 資産アカウントサービスの表示を提供し、RESTful スタイルの呼び出しをサポートし、データは JSON 形式で返されます。
|
||||||
|
|
||||||
|
## 2. 環境要件
|
||||||
|
|
||||||
|
- `cURL`
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
**リクエスト方法**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**リクエストパラメータ**
|
||||||
|
|
||||||
|
| パラメータ名 | タイプ | 必須 | 説明 |
|
||||||
|
|-------------|------|----|----------------|
|
||||||
|
| asset | str | はい | 資産 ID / 資産名 |
|
||||||
|
| account | str | はい | アカウント ID / アカウント名 |
|
||||||
|
|
||||||
|
|
||||||
|
**レスポンス例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## よくある質問(FAQ)
|
||||||
|
|
||||||
|
Q: APIキーはどのように取得しますか?
|
||||||
|
|
||||||
|
A: PAM - アプリケーション管理でアプリケーションを作成し、KEY_IDとKEY_SECRETを生成できます。
|
||||||
|
|
||||||
|
## バージョン履歴(Changelog)
|
||||||
|
|
||||||
|
|
||||||
|
| バージョン | 変更内容 | 日付 |
|
||||||
|
| -------- | ----------------- |------------|
|
||||||
|
| 1.0.0 | 初期バージョン | 2025-02-11 |
|
|
@ -0,0 +1,40 @@
|
||||||
|
# 使用说明
|
||||||
|
|
||||||
|
## 1. 简介
|
||||||
|
|
||||||
|
本 API 提供了 PAM 查看资产账号服务,支持 RESTful 风格的调用,返回数据采用 JSON 格式。
|
||||||
|
|
||||||
|
## 2. 环境要求
|
||||||
|
|
||||||
|
- `cURL`
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
**请求方式**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**请求参数**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|----------|------|-----|---------------|
|
||||||
|
| asset | str | 是 | 资产 ID / 资产名称 |
|
||||||
|
| account | str | 是 | 账号 ID / 账号名称 |
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题(FAQ)
|
||||||
|
|
||||||
|
Q: API Key 如何获取?
|
||||||
|
|
||||||
|
A: 你可以在 PAM - 应用管理 创建应用生成 KEY_ID 和 KEY_SECRET。
|
||||||
|
|
||||||
|
## 版本历史(Changelog)
|
||||||
|
|
||||||
|
| 版本号 | 变更内容 | 日期 |
|
||||||
|
| ----- | ----------------- |------------|
|
||||||
|
| 1.0.0 | 初始版本 | 2025-02-11 |
|
|
@ -0,0 +1,39 @@
|
||||||
|
## 1. 簡介
|
||||||
|
|
||||||
|
本 API 提供了 PAM 查看資產賬號服務,支持 RESTful 風格的調用,返回數據採用 JSON 格式。
|
||||||
|
|
||||||
|
## 2. 環境要求
|
||||||
|
|
||||||
|
- `cURL`
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
**請求方式**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**請求參數**
|
||||||
|
|
||||||
|
| 參數名 | 類型 | 必填 | 說明 |
|
||||||
|
|----------|------|-----|---------------|
|
||||||
|
| asset | str | 是 | 資產 ID / 資產名稱 |
|
||||||
|
| account | str | 是 | 賬號 ID / 賬號名稱 |
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常見問題(FAQ)
|
||||||
|
|
||||||
|
Q: API Key 如何獲取?
|
||||||
|
|
||||||
|
A: 你可以在 PAM - 應用管理 創建應用生成 KEY_ID 和 KEY_SECRET。
|
||||||
|
|
||||||
|
## 版本歷史(Changelog)
|
||||||
|
|
||||||
|
|
||||||
|
| 版本號 | 變更內容 | 日期 |
|
||||||
|
| ----- | ----------------- |------------|
|
||||||
|
| 1.0.0 | 初始版本 | 2025-02-11 |
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 配置参数
|
||||||
|
API_URL=${API_URL:-"http://127.0.0.1:8080"}
|
||||||
|
KEY_ID=${API_KEY_ID:-"72b0b0aa-ad82-4182-a631-ae4865e8ae0e"}
|
||||||
|
KEY_SECRET=${API_KEY_SECRET:-"6fuSO7P1m4cj8SSlgaYdblOjNAmnxDVD7tr8"}
|
||||||
|
ORG_ID=${ORG_ID:-"00000000-0000-0000-0000-000000000002"}
|
||||||
|
|
||||||
|
# 查询参数
|
||||||
|
ASSET="ubuntu_docker"
|
||||||
|
ACCOUNT="root"
|
||||||
|
QUERY_STRING="asset=${ASSET}&account=${ACCOUNT}"
|
||||||
|
|
||||||
|
# 计算时间戳
|
||||||
|
DATE=$(date -u +"%a, %d %b %Y %H:%M:%S GMT")
|
||||||
|
|
||||||
|
# 计算 (request-target) 需要包含查询参数
|
||||||
|
REQUEST_TARGET="get /api/v1/accounts/integration-applications/account-secret/?${QUERY_STRING}"
|
||||||
|
|
||||||
|
# 生成签名字符串
|
||||||
|
SIGNING_STRING="(request-target): $REQUEST_TARGET
|
||||||
|
accept: application/json
|
||||||
|
date: $DATE
|
||||||
|
x-jms-org: $ORG_ID"
|
||||||
|
|
||||||
|
# 计算 HMAC-SHA256 签名
|
||||||
|
SIGNATURE=$(echo -n "$SIGNING_STRING" | openssl dgst -sha256 -hmac "$KEY_SECRET" -binary | base64)
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
curl -G "$API_URL/api/v1/accounts/integration-applications/account-secret/" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-H "Date: $DATE" \
|
||||||
|
-H "X-JMS-ORG: $ORG_ID" \
|
||||||
|
-H "X-Source: jms-pam" \
|
||||||
|
-H "Authorization: Signature keyId=\"$KEY_ID\",algorithm=\"hmac-sha256\",headers=\"(request-target) accept date x-jms-org\",signature=\"$SIGNATURE\"" \
|
||||||
|
--data-urlencode "asset=$ASSET" \
|
||||||
|
--data-urlencode "account=$ACCOUNT"
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Instructions
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
This API provides the PAM asset account service, supports RESTful style calls, and returns data in JSON format.
|
||||||
|
|
||||||
|
## 2. Environment Requirements
|
||||||
|
|
||||||
|
- `Go 1.16+`
|
||||||
|
- `crypto/hmac`
|
||||||
|
- `crypto/sha256`
|
||||||
|
- `encoding/base64`
|
||||||
|
- `net/http`
|
||||||
|
|
||||||
|
## 3. Usage
|
||||||
|
|
||||||
|
**Request Method**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**Request Parameters**
|
||||||
|
|
||||||
|
| Parameter Name | Type | Required | Description |
|
||||||
|
|----------------|------|----------|-------------------|
|
||||||
|
| asset | str | Yes | Asset ID / Asset Name |
|
||||||
|
| account | str | Yes | Account ID / Account Name |
|
||||||
|
|
||||||
|
**Response Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
|
Q: How to obtain the API Key?
|
||||||
|
|
||||||
|
A: You can create an application in PAM - Application Management to generate KEY_ID and KEY_SECRET.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
|
||||||
|
| Version | Changes | Date |
|
||||||
|
|---------|------------------------|------------|
|
||||||
|
| 1.0.0 | Initial version | 2025-02-11 |
|
|
@ -0,0 +1,45 @@
|
||||||
|
# 使用方法
|
||||||
|
|
||||||
|
## 1. 概要
|
||||||
|
|
||||||
|
このAPIは、PAMの資産アカウントサービスの表示を提供し、RESTfulスタイルの呼び出しをサポートし、データはJSON形式で返されます。
|
||||||
|
|
||||||
|
## 2. 環境要件
|
||||||
|
|
||||||
|
- `Go 1.16+`
|
||||||
|
- `crypto/hmac`
|
||||||
|
- `crypto/sha256`
|
||||||
|
- `encoding/base64`
|
||||||
|
- `net/http`
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
**リクエスト方法**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**リクエストパラメータ**
|
||||||
|
|
||||||
|
| パラメータ名 | タイプ | 必須 | 説明 |
|
||||||
|
|-------------|-------|----|--------------|
|
||||||
|
| asset | str | はい | 資産ID / 資産名 |
|
||||||
|
| account | str | はい | アカウントID / アカウント名 |
|
||||||
|
|
||||||
|
**レスポンス例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## よくある質問(FAQ)
|
||||||
|
|
||||||
|
Q: APIキーはどのように取得しますか?
|
||||||
|
|
||||||
|
A: PAM - アプリケーション管理でアプリケーションを作成し、KEY_IDとKEY_SECRETを生成できます。
|
||||||
|
|
||||||
|
## バージョン履歴(Changelog)
|
||||||
|
|
||||||
|
|
||||||
|
| バージョン | 変更内容 | 日付 |
|
||||||
|
| -------- | ----------------- |------------|
|
||||||
|
| 1.0.0 | 初期バージョン | 2025-02-11 |
|
|
@ -0,0 +1,45 @@
|
||||||
|
# 使用说明
|
||||||
|
|
||||||
|
## 1. 简介
|
||||||
|
|
||||||
|
本 API 提供了 PAM 查看资产账号服务,支持 RESTful 风格的调用,返回数据采用 JSON 格式。
|
||||||
|
|
||||||
|
## 2. 环境要求
|
||||||
|
|
||||||
|
- `Go 1.16+`
|
||||||
|
- `crypto/hmac`
|
||||||
|
- `crypto/sha256`
|
||||||
|
- `encoding/base64`
|
||||||
|
- `net/http`
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
**请求方式**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**请求参数**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|----------|------|-----|---------------|
|
||||||
|
| asset | str | 是 | 资产 ID / 资产名称 |
|
||||||
|
| account | str | 是 | 账号 ID / 账号名称 |
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题(FAQ)
|
||||||
|
|
||||||
|
Q: API Key 如何获取?
|
||||||
|
|
||||||
|
A: 你可以在 PAM - 应用管理 创建应用生成 KEY_ID 和 KEY_SECRET。
|
||||||
|
|
||||||
|
## 版本历史(Changelog)
|
||||||
|
|
||||||
|
|
||||||
|
| 版本号 | 变更内容 | 日期 |
|
||||||
|
| ----- | ----------------- |------------|
|
||||||
|
| 1.0.0 | 初始版本 | 2025-02-11 |
|
|
@ -0,0 +1,153 @@
|
||||||
|
## 1. 簡介
|
||||||
|
|
||||||
|
本 API 提供了 PAM 查看資產賬號服務,支持 RESTful 風格的調用,返回數據採用 JSON 格式。
|
||||||
|
|
||||||
|
## 2. 環境要求
|
||||||
|
|
||||||
|
- `Go 1.16+`
|
||||||
|
- `crypto/hmac`
|
||||||
|
- `crypto/sha256`
|
||||||
|
- `encoding/base64`
|
||||||
|
- `net/http`
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
**請求方式**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**請求參數**
|
||||||
|
|
||||||
|
| 參數名 | 類型 | 必填 | 說明 |
|
||||||
|
|----------|------|-----|---------------|
|
||||||
|
| asset | str | 是 | 資產 ID / 資產名稱 |
|
||||||
|
| account | str | 是 | 賬號 ID / 賬號名稱 |
|
||||||
|
|
||||||
|
**響應示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常見問題(FAQ)
|
||||||
|
|
||||||
|
Q: API Key 如何獲取?
|
||||||
|
|
||||||
|
A: 你可以在 PAM - 應用管理 創建應用生成 KEY_ID 和 KEY_SECRET。
|
||||||
|
|
||||||
|
## 版本歷史(Changelog)
|
||||||
|
|
||||||
|
|
||||||
|
| 版本號 | 變更內容 | 日期 |
|
||||||
|
| ----- | ----------------- |------------|
|
||||||
|
| 1.0.0 | 初始版本 | 2025-02-11 |
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 初始化
|
||||||
|
|
||||||
|
要使用 JumpServer PAM 客戶端,通過提供所需的 `endpoint`、`keyID` 和 `keySecret` 創建一個實例。
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"your_module_path/jms_pam"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := jms_pam.NewJumpServerPAM(
|
||||||
|
"http://127.0.0.1", // 替換為您的 JumpServer 端點
|
||||||
|
"your-key-id", // 替換為您的實際 Key ID
|
||||||
|
"your-key-secret", // 替換為您的實際 Key Secret
|
||||||
|
"", // 留空以使用默認的組織 ID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 創建密碼請求
|
||||||
|
|
||||||
|
您可以通過指定資產或帳戶信息來創建請求。
|
||||||
|
|
||||||
|
```go
|
||||||
|
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("創建請求時出錯:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 發送請求
|
||||||
|
|
||||||
|
使用客戶端的 `Send` 方法發送請求。
|
||||||
|
|
||||||
|
```go
|
||||||
|
secretObj, err := client.Send(request)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("發送請求時出錯:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 處理響應
|
||||||
|
|
||||||
|
檢查密碼是否成功檢索,並相應地處理響應。
|
||||||
|
|
||||||
|
```go
|
||||||
|
if secretObj.Valid {
|
||||||
|
fmt.Println("密碼:", secretObj.Secret)
|
||||||
|
} else {
|
||||||
|
fmt.Println("獲取密碼失敗:", string(secretObj.Desc))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整示例
|
||||||
|
|
||||||
|
以下是如何使用該客戶端的完整示例:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"your_module_path/jms_pam"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := jms_pam.NewJumpServerPAM(
|
||||||
|
"http://127.0.0.1",
|
||||||
|
"your-key-id",
|
||||||
|
"your-key-secret",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("創建請求時出錯:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
secretObj, err := client.Send(request)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("發送請求時出錯:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretObj.Valid {
|
||||||
|
fmt.Println("密碼:", secretObj.Secret)
|
||||||
|
} else {
|
||||||
|
fmt.Println("獲取密碼失敗:", string(secretObj.Desc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 錯誤處理
|
||||||
|
|
||||||
|
該庫會在創建 `SecretRequest` 時返回無效參數的錯誤。這包括對有效 UUID 的檢查以及確保提供了必需的參數。
|
||||||
|
|
||||||
|
## 貢獻
|
||||||
|
|
||||||
|
歡迎貢獻!如有任何增強或錯誤修復,請提出問題或提交拉取請求。
|
|
@ -0,0 +1,119 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIClient struct {
|
||||||
|
Client *http.Client
|
||||||
|
APIURL string
|
||||||
|
KeyID string
|
||||||
|
KeySecret string
|
||||||
|
OrgID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIClient() *APIClient {
|
||||||
|
return &APIClient{
|
||||||
|
Client: &http.Client{},
|
||||||
|
APIURL: getEnv("API_URL", "http://127.0.0.1:8080"),
|
||||||
|
KeyID: getEnv("API_KEY_ID", "72b0b0aa-ad82-4182-a631-ae4865e8ae0e"),
|
||||||
|
KeySecret: getEnv("API_KEY_SECRET", "6fuSO7P1m4cj8SSlgaYdblOjNAmnxDVD7tr8"),
|
||||||
|
OrgID: getEnv("ORG_ID", "00000000-0000-0000-0000-000000000002"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) GetAccountSecret(asset, account string) (map[string]interface{}, error) {
|
||||||
|
u, err := url.Parse(c.APIURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse API URL: %v", err)
|
||||||
|
}
|
||||||
|
u.Path = "/api/v1/accounts/integration-applications/account-secret/"
|
||||||
|
|
||||||
|
q := u.Query()
|
||||||
|
q.Add("asset", asset)
|
||||||
|
q.Add("account", account)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
date := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("X-JMS-ORG", c.OrgID)
|
||||||
|
req.Header.Set("Date", date)
|
||||||
|
req.Header.Set("X-Source", "jms-pam")
|
||||||
|
|
||||||
|
headersList := []string{"(request-target)", "accept", "date", "x-jms-org"}
|
||||||
|
var signatureParts []string
|
||||||
|
|
||||||
|
for _, h := range headersList {
|
||||||
|
var value string
|
||||||
|
if h == "(request-target)" {
|
||||||
|
value = strings.ToLower(req.Method) + " " + req.URL.RequestURI()
|
||||||
|
} else {
|
||||||
|
canonicalKey := http.CanonicalHeaderKey(h)
|
||||||
|
value = req.Header.Get(canonicalKey)
|
||||||
|
}
|
||||||
|
signatureParts = append(signatureParts, fmt.Sprintf("%s: %s", h, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureString := strings.Join(signatureParts, "\n")
|
||||||
|
mac := hmac.New(sha256.New, []byte(c.KeySecret))
|
||||||
|
mac.Write([]byte(signatureString))
|
||||||
|
signatureB64 := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
|
headersJoined := strings.Join(headersList, " ")
|
||||||
|
authHeader := fmt.Sprintf(
|
||||||
|
`Signature keyId="%s",algorithm="hmac-sha256",headers="%s",signature="%s"`,
|
||||||
|
c.KeyID,
|
||||||
|
headersJoined,
|
||||||
|
signatureB64,
|
||||||
|
)
|
||||||
|
req.Header.Set("Authorization", authHeader)
|
||||||
|
|
||||||
|
resp, err := c.Client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("API returned non-200 status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := NewAPIClient()
|
||||||
|
result, err := client.GetAccountSecret("ubuntu_docker", "root")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Result: %+v\n", result)
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gopkg.in/twindagger/httpsig.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultOrgId = "00000000-0000-0000-0000-000000000002"
|
||||||
|
|
||||||
|
type RequestParamsError struct {
|
||||||
|
Params []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RequestParamsError) Error() string {
|
||||||
|
return fmt.Sprintf("At least one of the following fields must be provided: %v.", e.Params)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecretRequest struct {
|
||||||
|
AccountID string
|
||||||
|
AssetID string
|
||||||
|
Asset string
|
||||||
|
Account string
|
||||||
|
Method string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSecretRequest(asset, assetID, account, accountID string) (*SecretRequest, error) {
|
||||||
|
req := &SecretRequest{
|
||||||
|
Asset: asset,
|
||||||
|
AssetID: assetID,
|
||||||
|
Account: account,
|
||||||
|
AccountID: accountID,
|
||||||
|
Method: http.MethodGet,
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, req.validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SecretRequest) validate() error {
|
||||||
|
if r.AccountID != "" {
|
||||||
|
if _, err := uuid.Parse(r.AccountID); err != nil {
|
||||||
|
return fmt.Errorf("invalid UUID: %s. Value must be a valid UUID", r.AccountID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.AssetID == "" && r.Asset == "" {
|
||||||
|
return &RequestParamsError{Params: []string{"asset", "asset_id"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Account == "" {
|
||||||
|
return &RequestParamsError{Params: []string{"account", "account_id"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.AssetID != "" {
|
||||||
|
if _, err := uuid.Parse(r.AssetID); err != nil {
|
||||||
|
return fmt.Errorf("invalid UUID: %s. Value must be a valid UUID", r.AssetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SecretRequest) GetURL() string {
|
||||||
|
return "/api/v1/accounts/service-integrations/account-secret/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SecretRequest) GetQuery() url.Values {
|
||||||
|
query := url.Values{}
|
||||||
|
if r.AccountID != "" {
|
||||||
|
query.Add("account_id", r.AccountID)
|
||||||
|
}
|
||||||
|
if r.AssetID != "" {
|
||||||
|
query.Add("asset_id", r.AssetID)
|
||||||
|
}
|
||||||
|
if r.Asset != "" {
|
||||||
|
query.Add("asset", r.Asset)
|
||||||
|
}
|
||||||
|
if r.Account != "" {
|
||||||
|
query.Add("account", r.Account)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
type Secret struct {
|
||||||
|
Secret string `json:"secret,omitempty"`
|
||||||
|
Desc json.RawMessage `json:"desc,omitempty"`
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromResponse(response *http.Response) Secret {
|
||||||
|
var secret Secret
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
var raw json.RawMessage
|
||||||
|
if err := json.NewDecoder(response.Body).Decode(&raw); err == nil {
|
||||||
|
secret.Desc = raw
|
||||||
|
} else {
|
||||||
|
secret.Desc = json.RawMessage(`{"error": "Unknown error occurred"}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_ = json.NewDecoder(response.Body).Decode(&secret)
|
||||||
|
secret.Valid = true
|
||||||
|
}
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
|
type JumpServerPAM struct {
|
||||||
|
Endpoint string
|
||||||
|
KeyID string
|
||||||
|
KeySecret string
|
||||||
|
OrgID string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJumpServerPAM(endpoint, keyID, keySecret, orgID string) *JumpServerPAM {
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = DefaultOrgId
|
||||||
|
}
|
||||||
|
return &JumpServerPAM{
|
||||||
|
Endpoint: endpoint,
|
||||||
|
KeyID: keyID,
|
||||||
|
KeySecret: keySecret,
|
||||||
|
OrgID: orgID,
|
||||||
|
httpClient: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JumpServerPAM) SignRequest(r *http.Request) error {
|
||||||
|
headers := []string{"(request-target)", "date"}
|
||||||
|
signer, err := httpsig.NewRequestSigner(c.KeyID, c.KeySecret, "hmac-sha256")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return signer.SignRequest(r, headers, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JumpServerPAM) Send(req *SecretRequest) (Secret, error) {
|
||||||
|
fullUrl := c.Endpoint + req.GetURL()
|
||||||
|
query := req.GetQuery()
|
||||||
|
fullURL := fmt.Sprintf("%s?%s", fullUrl, query.Encode())
|
||||||
|
|
||||||
|
request, err := http.NewRequest(req.Method, fullURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return Secret{}, err
|
||||||
|
}
|
||||||
|
request.Header.Set("Accept", "application/json")
|
||||||
|
request.Header.Set("X-Source", "jms-pam")
|
||||||
|
err = c.SignRequest(request)
|
||||||
|
if err != nil {
|
||||||
|
return Secret{Desc: json.RawMessage(`{"error": "` + err.Error() + `"}`)}, nil
|
||||||
|
}
|
||||||
|
response, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return Secret{Desc: json.RawMessage(`{"error": "` + err.Error() + `"}`)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return FromResponse(response), nil
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Base64;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
|
||||||
|
public class Demo {
|
||||||
|
private static final String API_URL = System.getenv().getOrDefault("API_URL", "http://127.0.0.1:8080");
|
||||||
|
private static final String KEY_ID = System.getenv().getOrDefault("API_KEY_ID", "72b0b0aa-ad82-4182-a631-ae4865e8ae0e");
|
||||||
|
private static final String KEY_SECRET = System.getenv().getOrDefault("API_KEY_SECRET", "6fuSO7P1m4cj8SSlgaYdblOjNAmnxDVD7tr8");
|
||||||
|
private static final String ORG_ID = System.getenv().getOrDefault("ORG_ID", "00000000-0000-0000-0000-000000000002");
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
APIClient client = new APIClient();
|
||||||
|
String result = client.getAccountSecret("ubuntu_docker", "root");
|
||||||
|
System.out.println(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class APIClient {
|
||||||
|
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
|
||||||
|
public String getAccountSecret(String asset, String account) throws Exception {
|
||||||
|
// 编码 URL 参数
|
||||||
|
String queryString = "asset=" + URLEncoder.encode(asset, StandardCharsets.UTF_8) +
|
||||||
|
"&account=" + URLEncoder.encode(account, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// 完整的 URL(带参数)
|
||||||
|
String url = API_URL + "/api/v1/accounts/integration-applications/account-secret/?" + queryString;
|
||||||
|
|
||||||
|
// 获取当前 UTC 时间
|
||||||
|
String date = ZonedDateTime.now().format(DateTimeFormatter.RFC_1123_DATE_TIME);
|
||||||
|
|
||||||
|
// 构造 (request-target),包括查询参数
|
||||||
|
String requestTarget = "get /api/v1/accounts/integration-applications/account-secret/?" + queryString;
|
||||||
|
|
||||||
|
// 生成签名字符串
|
||||||
|
String signingString = "(request-target): " + requestTarget + "\n" +
|
||||||
|
"accept: application/json\n" +
|
||||||
|
"date: " + date + "\n" +
|
||||||
|
"x-jms-org: " + ORG_ID;
|
||||||
|
String signature = sign(signingString, KEY_SECRET);
|
||||||
|
|
||||||
|
// 构造 HTTP 请求
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Date", date)
|
||||||
|
.header("X-JMS-ORG", ORG_ID)
|
||||||
|
.header("X-Source", "jms-pam")
|
||||||
|
.header("Authorization", "Signature keyId=\"" + KEY_ID + "\",algorithm=\"hmac-sha256\",headers=\"(request-target) accept date x-jms-org\",signature=\"" + signature + "\"")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() == 200) {
|
||||||
|
return response.body();
|
||||||
|
} else {
|
||||||
|
System.err.println("API 请求失败: " + response.statusCode());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMAC-SHA256 签名计算
|
||||||
|
private String sign(String data, String key) throws Exception {
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||||
|
mac.init(secretKeySpec);
|
||||||
|
byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return Base64.getEncoder().encodeToString(rawHmac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Instructions
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
This API provides PAM asset account viewing service, supports RESTful style calls, and returns data in JSON format.
|
||||||
|
|
||||||
|
## 2. Environment Requirements
|
||||||
|
|
||||||
|
- `Java 8+`
|
||||||
|
- `HttpClient`
|
||||||
|
|
||||||
|
## 3. Usage
|
||||||
|
|
||||||
|
**Request Method**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**Request Parameters**
|
||||||
|
|
||||||
|
| Parameter Name | Type | Required | Description |
|
||||||
|
|----------------|------|----------|-------------------|
|
||||||
|
| asset | str | Yes | Asset ID / Name |
|
||||||
|
| account | str | Yes | Account ID / Name |
|
||||||
|
|
||||||
|
**Response Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
|
Q: How to obtain the API Key?
|
||||||
|
|
||||||
|
A: You can create an application in PAM - Application Management to generate KEY_ID and KEY_SECRET.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
|
||||||
|
| Version | Changes | Date |
|
||||||
|
|---------|------------------------|------------|
|
||||||
|
| 1.0.0 | Initial version | 2025-02-11 |
|
|
@ -0,0 +1,42 @@
|
||||||
|
# 使用方法
|
||||||
|
|
||||||
|
## 1. 概要
|
||||||
|
|
||||||
|
本 API は PAM 資産アカウントサービスの表示を提供し、RESTful スタイルの呼び出しをサポートし、データは JSON 形式で返されます。
|
||||||
|
|
||||||
|
## 2. 環境要件
|
||||||
|
|
||||||
|
- `Java 8+`
|
||||||
|
- `HttpClient`
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
**リクエスト方法**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**リクエストパラメータ**
|
||||||
|
|
||||||
|
| パラメータ名 | タイプ | 必須 | 説明 |
|
||||||
|
|-------------|------|----|----------------|
|
||||||
|
| asset | str | はい | 資産 ID / 資産名 |
|
||||||
|
| account | str | はい | アカウント ID / アカウント名 |
|
||||||
|
|
||||||
|
**レスポンス例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## よくある質問(FAQ)
|
||||||
|
|
||||||
|
Q: APIキーはどのように取得しますか?
|
||||||
|
|
||||||
|
A: PAM - アプリケーション管理でアプリケーションを作成し、KEY_IDとKEY_SECRETを生成できます。
|
||||||
|
|
||||||
|
## バージョン履歴(Changelog)
|
||||||
|
|
||||||
|
|
||||||
|
| バージョン | 変更内容 | 日付 |
|
||||||
|
| -------- | ----------------- |------------|
|
||||||
|
| 1.0.0 | 初期バージョン | 2025-02-11 |
|
|
@ -0,0 +1,41 @@
|
||||||
|
# 使用说明
|
||||||
|
|
||||||
|
## 1. 简介
|
||||||
|
|
||||||
|
本 API 提供了 PAM 查看资产账号服务,支持 RESTful 风格的调用,返回数据采用 JSON 格式。
|
||||||
|
|
||||||
|
## 2. 环境要求
|
||||||
|
|
||||||
|
- `Java 8+`
|
||||||
|
- `HttpClient`
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
**请求方式**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**请求参数**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|----------|------|-----|---------------|
|
||||||
|
| asset | str | 是 | 资产 ID / 资产名称 |
|
||||||
|
| account | str | 是 | 账号 ID / 账号名称 |
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题(FAQ)
|
||||||
|
|
||||||
|
Q: API Key 如何获取?
|
||||||
|
|
||||||
|
A: 你可以在 PAM - 应用管理 创建应用生成 KEY_ID 和 KEY_SECRET。
|
||||||
|
|
||||||
|
## 版本历史(Changelog)
|
||||||
|
|
||||||
|
| 版本号 | 变更内容 | 日期 |
|
||||||
|
| ----- | ----------------- |------------|
|
||||||
|
| 1.0.0 | 初始版本 | 2025-02-11 |
|
|
@ -0,0 +1,40 @@
|
||||||
|
## 1. 簡介
|
||||||
|
|
||||||
|
本 API 提供了 PAM 查看資產賬號服務,支持 RESTful 風格的調用,返回數據採用 JSON 格式。
|
||||||
|
|
||||||
|
## 2. 環境要求
|
||||||
|
|
||||||
|
- `Java 8+`
|
||||||
|
- `HttpClient`
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
**請求方式**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**請求參數**
|
||||||
|
|
||||||
|
| 參數名 | 類型 | 必填 | 說明 |
|
||||||
|
|----------|------|-----|---------------|
|
||||||
|
| asset | str | 是 | 資產 ID / 資產名稱 |
|
||||||
|
| account | str | 是 | 賬號 ID / 賬號名稱 |
|
||||||
|
|
||||||
|
**響應示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常見問題(FAQ)
|
||||||
|
|
||||||
|
Q: API Key 如何獲取?
|
||||||
|
|
||||||
|
A: 你可以在 PAM - 應用管理 創建應用生成 KEY_ID 和 KEY_SECRET。
|
||||||
|
|
||||||
|
## 版本歷史(Changelog)
|
||||||
|
|
||||||
|
|
||||||
|
| 版本號 | 變更內容 | 日期 |
|
||||||
|
| ----- | ----------------- |------------|
|
||||||
|
| 1.0.0 | 初始版本 | 2025-02-11 |
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Instructions
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
This API provides PAM asset account viewing service, supports RESTful style calls, and returns data in JSON format.
|
||||||
|
|
||||||
|
## 2. Environment Requirements
|
||||||
|
|
||||||
|
- `Node.js 16+`
|
||||||
|
- `axios ^1.7.9`
|
||||||
|
- `moment ^2.30.1`
|
||||||
|
|
||||||
|
## 3. Usage
|
||||||
|
|
||||||
|
**Request Method**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**Request Parameters**
|
||||||
|
|
||||||
|
| Parameter Name | Type | Required | Description |
|
||||||
|
|----------------|------|----------|-------------------|
|
||||||
|
| asset | str | Yes | Asset ID / Name |
|
||||||
|
| account | str | Yes | Account ID / Name |
|
||||||
|
|
||||||
|
**Response Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
|
Q: How to obtain the API Key?
|
||||||
|
|
||||||
|
A: You can create an application in PAM - Application Management to generate KEY_ID and KEY_SECRET.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
|
||||||
|
| Version | Changes | Date |
|
||||||
|
|---------|------------------------|------------|
|
||||||
|
| 1.0.0 | Initial version | 2025-02-11 |
|
|
@ -0,0 +1,41 @@
|
||||||
|
# 使用方法
|
||||||
|
|
||||||
|
## 1. 概要
|
||||||
|
|
||||||
|
本 API は PAM 資産アカウントサービスの表示を提供し、RESTful スタイルの呼び出しをサポートし、データは JSON 形式で返されます。
|
||||||
|
|
||||||
|
## 2. 環境要件
|
||||||
|
|
||||||
|
- `Node.js 16+`
|
||||||
|
- `axios ^1.7.9`
|
||||||
|
- `moment ^2.30.1`
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
**リクエスト方法**: `GET api/v1/accounts/integration-applications/account-secret/`
|
||||||
|
|
||||||
|
**リクエストパラメータ**
|
||||||
|
|
||||||
|
| パラメータ名 | タイプ | 必須 | 説明 |
|
||||||
|
|-------------|------|----|----------------|
|
||||||
|
| asset | str | はい | 資産 ID / 資産名 |
|
||||||
|
| account | str | はい | アカウント ID / アカウント名 |
|
||||||
|
|
||||||
|
**レスポンス例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "72b0b0aa-ad82-4182-a631-ae4865e8ae0e",
|
||||||
|
"secret": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
よくある質問(FAQ)
|
||||||
|
|
||||||
|
Q: API キーはどのように取得しますか?
|
||||||
|
|
||||||
|
A: PAM - アプリケーション管理でアプリケーションを作成し、KEY_ID と KEY_SECRET を生成できます。
|
||||||
|
|
||||||
|
バージョン履歴(Changelog)
|
||||||
|
|
||||||
|
| バージョン | 変更内容 | 日付 |
|
||||||
|
| ----- | ----------------- |------------|
|
||||||
|
| 1.0.0 | 初始版本 | 2025-02-11 |
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue