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
fit2bot 2025-02-21 16:39:57 +08:00 committed by GitHub
parent d516349a68
commit 3f4141ca0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
399 changed files with 14655 additions and 15063 deletions

2
.gitattributes vendored
View File

@ -1,4 +1,4 @@
*.mmdb filter=lfs diff=lfs merge=lfs -text
*.mo 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

2
.pylintrc Normal file
View File

@ -0,0 +1,2 @@
[MESSAGES CONTROL]
disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,too-many-ancestors

View File

@ -1,4 +1,6 @@
from .account import *
from .application import *
from .pam_dashboard import *
from .task import *
from .template import *
from .virtual import *

View File

@ -1,20 +1,27 @@
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from rest_framework.decorators import action
from rest_framework.generics import ListAPIView, CreateAPIView
from rest_framework.response import Response
from rest_framework.status import HTTP_200_OK
from accounts import serializers
from accounts.const import ChangeSecretRecordStatusChoice
from accounts.filters import AccountFilterSet
from accounts.mixins import AccountRecordViewLogMixin
from accounts.models import Account
from accounts.models import Account, ChangeSecretRecord
from assets.models import Asset, Node
from authentication.permissions import UserConfirmation, ConfirmType
from common.api.mixin import ExtraFilterFieldsMixin
from common.drf.filters import AttrRulesFilterBackend
from common.permissions import IsValidUser
from common.utils import lazyproperty, get_logger
from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission
logger = get_logger(__file__)
__all__ = [
'AccountViewSet', 'AccountSecretsViewSet',
'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
@ -24,6 +31,7 @@ __all__ = [
class AccountViewSet(OrgBulkModelViewSet):
model = Account
search_fields = ('username', 'name', 'asset__name', 'asset__address', 'comment')
extra_filter_backends = [AttrRulesFilterBackend]
filterset_class = AccountFilterSet
serializer_classes = {
'default': serializers.AccountSerializer,
@ -33,6 +41,8 @@ class AccountViewSet(OrgBulkModelViewSet):
'partial_update': ['accounts.change_account'],
'su_from_accounts': 'accounts.view_account',
'clear_secret': 'accounts.change_account',
'move_to_assets': 'accounts.create_account',
'copy_to_assets': 'accounts.create_account',
}
export_as_zip = True
@ -86,6 +96,45 @@ class AccountViewSet(OrgBulkModelViewSet):
self.model.objects.filter(id__in=account_ids).update(secret=None)
return Response(status=HTTP_200_OK)
def _copy_or_move_to_assets(self, request, move=False):
account = self.get_object()
asset_ids = request.data.get('assets', [])
assets = Asset.objects.filter(id__in=asset_ids)
field_names = [
'name', 'username', 'secret_type', 'secret',
'privileged', 'is_active', 'source', 'source_id', 'comment'
]
account_data = {field: getattr(account, field) for field in field_names}
creation_results = {}
success_count = 0
for asset in assets:
account_data['asset'] = asset
creation_results[asset] = {'state': 'created'}
try:
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):
"""
@ -125,17 +174,31 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
'GET': 'accounts.view_accountsecret',
}
def get_object(self):
@lazyproperty
def account(self) -> Account:
return get_object_or_404(Account, pk=self.kwargs.get('pk'))
def get_object(self):
return self.account
@lazyproperty
def latest_history(self):
return self.account.history.first()
@property
def latest_change_secret_record(self) -> ChangeSecretRecord:
return self.account.changesecretrecords.filter(
status=ChangeSecretRecordStatusChoice.pending
).order_by('-date_created').first()
@staticmethod
def filter_spm_queryset(resource_ids, queryset):
return queryset.filter(history_id__in=resource_ids)
def get_queryset(self):
account = self.get_object()
account = self.account
histories = account.history.all()
latest_history = account.history.first()
latest_history = self.latest_history
if not latest_history:
return histories
if account.secret != latest_history.secret:
@ -144,3 +207,25 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
return histories
histories = histories.exclude(history_id=latest_history.history_id)
return histories
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = list(queryset)
latest_history = self.latest_history
if not latest_history:
return queryset
latest_change_secret_record = self.latest_change_secret_record
if not latest_change_secret_record:
return queryset
if latest_change_secret_record.date_created > latest_history.history_date:
temp_history = self.model(
secret=latest_change_secret_record.new_secret,
secret_type=self.account.secret_type,
version=latest_history.version,
history_date=latest_change_secret_record.date_created,
)
queryset = [temp_history] + queryset
return queryset

View File

@ -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})

View File

@ -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)

View File

@ -1,5 +1,7 @@
from .backup import *
from .base 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 *

View File

@ -1,41 +1,36 @@
# -*- coding: utf-8 -*-
#
from rest_framework import status, viewsets
from rest_framework.response import Response
from accounts import serializers
from accounts.const import AutomationTypes
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 .base import AutomationExecutionViewSet
__all__ = [
'AccountBackupPlanViewSet', 'AccountBackupPlanExecutionViewSet'
'BackupAccountViewSet', 'BackupAccountExecutionViewSet'
]
class AccountBackupPlanViewSet(OrgBulkModelViewSet):
model = AccountBackupAutomation
class BackupAccountViewSet(OrgBulkModelViewSet):
model = BackupAccountAutomation
filterset_fields = ('name',)
search_fields = filterset_fields
serializer_class = serializers.AccountBackupSerializer
serializer_class = serializers.BackupAccountSerializer
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
serializer_class = serializers.AccountBackupPlanExecutionSerializer
search_fields = ('trigger', 'plan__name')
filterset_fields = ('trigger', 'plan_id', 'plan__name')
http_method_names = ['get', 'post', 'options']
class BackupAccountExecutionViewSet(AutomationExecutionViewSet):
rbac_perms = (
("list", "accounts.view_backupaccountexecution"),
("retrieve", "accounts.view_backupaccountexecution"),
("create", "accounts.add_backupaccountexecution"),
("report", "accounts.view_backupaccountexecution"),
)
tp = AutomationTypes.backup_account
def get_queryset(self):
queryset = AccountBackupExecution.objects.all()
queryset = super().get_queryset()
queryset = queryset.filter(automation__type=self.tp)
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)

View File

@ -1,6 +1,8 @@
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from rest_framework import status, mixins, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from accounts.models import AutomationExecution
@ -98,7 +100,6 @@ class AutomationExecutionViewSet(
search_fields = ('trigger', 'automation__name')
filterset_fields = ('trigger', 'automation_id', 'automation__name')
serializer_class = serializers.AutomationExecutionSerializer
tp: str
def get_queryset(self):
@ -113,3 +114,10 @@ class AutomationExecutionViewSet(
pid=str(automation.pk), trigger=Trigger.manual, tp=self.tp
)
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)

View File

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
#
from django.db.models import Max, Q, Subquery, OuterRef
from rest_framework import status, mixins
from rest_framework.decorators import action
from rest_framework.response import Response
from accounts import serializers
from accounts.const import AutomationTypes
from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice
from accounts.filters import ChangeSecretRecordFilterSet
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
from accounts.tasks import execute_automation_record_task
@ -34,7 +35,8 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
filterset_class = ChangeSecretRecordFilterSet
search_fields = ('asset__address',)
search_fields = ('asset__address', 'account_username')
ordering_fields = ('date_finished',)
tp = AutomationTypes.change_secret
serializer_classes = {
'default': serializers.ChangeSecretRecordSerializer,
@ -43,6 +45,8 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
rbac_perms = {
'execute': 'accounts.add_changesecretexecution',
'secret': 'accounts.view_changesecretrecord',
'dashboard': 'accounts.view_changesecretrecord',
'ignore_fail': 'accounts.view_changesecretrecord',
}
def get_permissions(self):
@ -53,8 +57,37 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
]
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):
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')
def execute(self, request, *args, **kwargs):
@ -75,12 +108,24 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
serializer = self.get_serializer(instance)
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):
rbac_perms = (
("list", "accounts.view_changesecretexecution"),
("retrieve", "accounts.view_changesecretexecution"),
("create", "accounts.add_changesecretexecution"),
("report", "accounts.view_changesecretexecution"),
)
tp = AutomationTypes.change_secret

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
#
from rest_framework import mixins
from accounts import serializers
from accounts.const import AutomationTypes
from accounts.models import PushAccountAutomation, ChangeSecretRecord
from orgs.mixins.api import OrgBulkModelViewSet
from accounts.filters import PushAccountRecordFilterSet
from accounts.models import PushAccountAutomation, PushSecretRecord
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
from .base import (
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
AutomationNodeAddRemoveApi, AutomationExecutionViewSet
)
from .change_secret import ChangeSecretRecordViewSet
__all__ = [
'PushAccountAutomationViewSet', 'PushAccountAssetsListApi', 'PushAccountRemoveAssetApi',
@ -30,6 +31,7 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
("list", "accounts.view_pushaccountexecution"),
("retrieve", "accounts.view_pushaccountexecution"),
("create", "accounts.add_pushaccountexecution"),
("report", "accounts.view_pushaccountexecution"),
)
tp = AutomationTypes.push_account
@ -40,13 +42,19 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
return queryset
class PushAccountRecordViewSet(ChangeSecretRecordViewSet):
serializer_class = serializers.ChangeSecretRecordSerializer
class PushAccountRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
filterset_class = PushAccountRecordFilterSet
search_fields = ('asset__address', 'account_username')
ordering_fields = ('date_finished',)
tp = AutomationTypes.push_account
serializer_classes = {
'default': serializers.PushSecretRecordSerializer,
}
def get_queryset(self):
return ChangeSecretRecord.objects.filter(
execution__automation__type=AutomationTypes.push_account
qs = PushSecretRecord.get_valid_records()
return qs.filter(
execution__automation__type=self.tp
)

View File

@ -3,15 +3,17 @@ import time
from collections import defaultdict, OrderedDict
from django.conf import settings
from django.db.models import F
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from xlsxwriter import Workbook
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.serializers import AccountSecretSerializer
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.timezone import local_now_filename, local_now_display
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')
split_help_text = _('The account key will be split into two parts and sent')
class RecipientsNotFound(Exception):
pass
@ -73,9 +76,9 @@ class BaseAccountHandler:
class AssetAccountHandler(BaseAccountHandler):
@staticmethod
def get_filename(plan_name):
def get_filename(name):
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
@ -117,32 +120,41 @@ class AssetAccountHandler(BaseAccountHandler):
cls.handler_secret(data, section)
data_map.update(cls.add_rows(data, header_fields, sheet_name))
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
class AccountBackupHandler:
def __init__(self, execution):
def __init__(self, manager, execution):
self.manager = manager
self.execution = execution
self.plan_name = self.execution.plan.name
self.is_frozen = False # 任务状态冻结标志
self.name = self.execution.snapshot.get('name', '-')
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'):
hint = _('Generating asset or application related backup information files')
hint = _('Generating asset related backup information files')
print(
'\n'
f'\033[32m>>> {hint}\033[0m'
''
)
# Print task start date
time_start = time.time()
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)
if not data_map:
return files
filename = AssetAccountHandler.get_filename(self.plan_name)
filename = AssetAccountHandler.get_filename(self.name)
wb = Workbook(filename)
for sheet, data in data_map.items():
@ -153,7 +165,7 @@ class AccountBackupHandler:
wb.close()
files.append(filename)
timedelta = round((time.time() - time_start), 2)
time_cost = _('Time cost')
time_cost = _('Duration')
file_created = _('Backup file creation completed')
print('{}: {} {}s'.format(file_created, time_cost, timedelta))
return files
@ -163,21 +175,19 @@ class AccountBackupHandler:
return
recipients = User.objects.filter(id__in=list(recipients))
print(
'\n'
f'\033[32m>>> {_("Start sending backup emails")}\033[0m'
''
)
plan_name = self.plan_name
name = self.name
for user in recipients:
if not user.secret_key:
attachment_list = []
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)
attachment_list = [attachment, ]
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
email_sent_to = _('Email sent to')
print('{} {}({})'.format(email_sent_to, user, user.email))
attachment_list = [attachment]
AccountBackupExecutionTaskMsg(name, user).publish(attachment_list)
for file in files:
os.remove(file)
@ -186,63 +196,41 @@ class AccountBackupHandler:
return
recipients = ReplayStorage.objects.filter(id__in=list(recipients))
print(
'\n'
'\033[32m>>> 📃 ---> sftp \033[0m'
''
)
plan_name = self.plan_name
name = self.name
encrypt_file = _('Encrypting files using encryption password')
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:
print(f'\033[32m>>> {encrypt_file}\033[0m')
encrypt_and_compress_zip_file(attachment, password, files)
else:
zip_files(attachment, files)
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')
print('{}: {}({})'.format(file_sent_to, rec.name, rec.id))
for file in files:
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):
is_success = False
error = '-'
try:
backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email.value)
if backup_type == AccountBackupType.email.value:
backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email)
if backup_type == AccountBackupType.email:
self.backup_by_email()
elif backup_type == AccountBackupType.object_storage.value:
elif backup_type == AccountBackupType.object_storage:
self.backup_by_obj_storage()
except Exception as e:
self.is_frozen = True
print(e)
error = str(e)
else:
is_success = True
finally:
reason = error
self.step_perform_task_update(is_success, reason)
self.step_finished(is_success)
print(f'\033[31m>>> {error}\033[0m')
self.execution.status = Status.error
self.execution.summary['error'] = error
def backup_by_obj_storage(self):
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_two = self.execution.snapshot.get('obj_recipients_part_two', [])
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)
def backup_by_email(self):
warn_text = _('The backup task has no assigned recipient')
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
@ -276,7 +263,7 @@ class AccountBackupHandler:
f'\033[31m>>> {warn_text}\033[0m'
''
)
raise RecipientsNotFound('Not Found Recipients')
return
if recipients_part_one and recipients_part_two:
print(f'\033[32m>>> {split_help_text}\033[0m')
files = self.create_excel(section='front')
@ -290,18 +277,5 @@ class AccountBackupHandler:
self.send_backup_mail(files, recipients)
def run(self):
plan_start = _('Plan start')
plan_end = _('Plan end')
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))
print('{}: {}'.format(_('Plan start'), local_now_display()))
self._run()

View File

@ -1,48 +1,30 @@
# -*- coding: utf-8 -*-
#
import time
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from assets.automations.base.manager import BaseManager
from common.utils.timezone import local_now_display
from .handlers import AccountBackupHandler
class AccountBackupManager:
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
class AccountBackupManager(BaseManager):
def do_run(self):
execution = self.execution
account_backup_execution_being_executed = _('The account backup plan is being executed')
print(f'\n\033[33m# {account_backup_execution_being_executed}\033[0m')
handler = AccountBackupHandler(execution)
print(f'\033[33m# {account_backup_execution_being_executed}\033[0m')
handler = AccountBackupHandler(self, execution)
handler.run()
def pre_run(self):
self.execution.date_start = self.date_start
self.execution.save()
def post_run(self):
self.time_end = time.time()
self.date_end = timezone.now()
def send_report_if_need(self):
pass
def print_summary(self):
print('\n\n' + '-' * 80)
plan_execution_end = _('Plan execution end')
print('{} {}\n'.format(plan_execution_end, local_now_display()))
self.timedelta = self.time_end - self.time_start
time_cost = _('Time cost')
print('{}: {}s'.format(time_cost, self.timedelta))
self.execution.timedelta = self.timedelta
self.execution.save()
time_cost = _('Duration')
print('{}: {}s'.format(time_cost, self.duration))
def run(self):
self.pre_run()
self.do_run()
self.post_run()
def get_report_template(self):
return "accounts/backup_account_report.html"

View File

@ -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.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice
from accounts.models import BaseAccountQuerySet
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
logger = get_logger(__name__)
class AccountBasePlaybookManager(BasePlaybookManager):
template_path = ''
@property
def platform_automation_methods(self):
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)

View File

@ -20,6 +20,7 @@
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) }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
register: ping_info
delegate_to: localhost
@ -39,9 +40,12 @@
name: "{{ account.username }}"
password: "{{ account.secret }}"
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
when: ping_info is succeeded
when: ping_info is succeeded and check_conn_after_change
register: change_info
delegate_to: localhost
@ -58,4 +62,6 @@
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) }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
delegate_to: localhost
when: check_conn_after_change

View File

@ -10,10 +10,30 @@ protocol: ssh
priority: 50
params:
- name: commands
type: list
type: text
label: "{{ 'Params commands label' | trans }}"
default: [ '' ]
default: ''
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:
SSH account change secret:
@ -22,11 +42,91 @@ i18n:
en: 'Custom password change by SSH command line'
Params commands help text:
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 &#123;username&#125;、&#123;password&#125;、&#123;login_password&#125;格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. &#123;login_password&#125;<br />3. configure terminal<br />4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end'
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />&#123;ユーザー名&#125;、&#123;パスワード&#125;、&#123;login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.&#123;login_password&#125;<br />3 .ターミナルの設定<br / >4. ユーザー名 &#123;ユーザー名&#125; 権限 0 パスワード &#123;パスワード&#125; <br />5. 終了'
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use &#123;username&#125;, &#123;password&#125;, &#123;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. &#123;login_password&#125;<br />3. configure terminal<br / >4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end'
zh: |
请将命令中的指定位置改成特殊符号 <br />
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:
zh: '自定义命令'
ja: 'カスタムコマンド'
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)

View File

@ -1,7 +1,7 @@
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Test MongoDB connection
@ -53,3 +53,4 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
when: check_conn_after_change

View File

@ -1,7 +1,7 @@
- hosts: mysql
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
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 }}"
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_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
filter: version
when: check_conn_after_change

View File

@ -1,7 +1,7 @@
- hosts: oracle
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Test Oracle connection
@ -40,3 +40,4 @@
login_port: "{{ jms_asset.port }}"
login_database: "{{ jms_asset.spec_info.db_name }}"
mode: "{{ account.mode }}"
when: check_conn_after_change

View File

@ -1,7 +1,7 @@
- hosts: postgre
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_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_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
when: check_conn_after_change

View File

@ -1,7 +1,7 @@
- hosts: sqlserver
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Test SQLServer connection
@ -64,3 +64,4 @@
name: '{{ jms_asset.spec_info.db_name }}'
script: |
SELECT @@version
when: check_conn_after_change

View File

@ -9,7 +9,8 @@
database: passwd
key: "{{ account.username }}"
register: user_info
ignore_errors: yes # 忽略错误如果用户不存在时不会导致playbook失败
failed_when: false
changed_when: false
- name: "Add {{ account.username }} user"
ansible.builtin.user:
@ -18,10 +19,10 @@
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
append: "{{ true if params.groups | length > 0 else false }}"
expires: -1
state: present
when: user_info.failed
when: user_info.msg is defined
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
@ -31,7 +32,7 @@
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- user_info.msg is defined or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
@ -100,7 +101,7 @@
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) }}"
when: account.secret_type == "password"
when: account.secret_type == "password" and check_conn_after_change
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@ -111,5 +112,5 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
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

View File

@ -9,7 +9,8 @@
database: passwd
key: "{{ account.username }}"
register: user_info
ignore_errors: yes # 忽略错误如果用户不存在时不会导致playbook失败
failed_when: false
changed_when: false
- name: "Add {{ account.username }} user"
ansible.builtin.user:
@ -18,10 +19,10 @@
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
append: "{{ true if params.groups | length > 0 else false }}"
expires: -1
state: present
when: user_info.failed
when: user_info.msg is defined
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
@ -31,7 +32,7 @@
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- user_info.msg is defined or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
@ -100,7 +101,7 @@
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) }}"
when: account.secret_type == "password"
when: account.secret_type == "password" and check_conn_after_change
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@ -111,5 +112,5 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
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

View File

@ -4,10 +4,6 @@
- name: Test privileged account
ansible.windows.win_ping:
# - name: Print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
- name: Change password
ansible.windows.win_user:
fullname: "{{ account.username}}"
@ -28,4 +24,4 @@
vars:
ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}"
when: account.secret_type == "password"
when: account.secret_type == "password" and check_conn_after_change

View File

@ -4,10 +4,6 @@
- name: Test privileged account
ansible.windows.win_ping:
# - name: Print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
- name: Change password
ansible.windows.win_user:
fullname: "{{ account.username}}"
@ -31,5 +27,5 @@
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
gateway_args: "{{ jms_gateway | default({}) }}"
when: account.secret_type == "password"
when: account.secret_type == "password" and check_conn_after_change
delegate_to: localhost

View File

@ -1,218 +1,71 @@
import os
import time
from copy import deepcopy
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from xlsxwriter import Workbook
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice
from accounts.models import ChangeSecretRecord, BaseAccountQuerySet
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg
from accounts.const import (
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
)
from accounts.models import ChangeSecretRecord
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
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.file import encrypt_and_compress_zip_file
from common.utils.timezone import local_now_filename
from users.models import User
from ..base.manager import AccountBasePlaybookManager
from ..base.manager import BaseChangeSecretPushManager
from ...utils import SecretGenerator
logger = get_logger(__name__)
class ChangeSecretManager(AccountBasePlaybookManager):
class ChangeSecretManager(BaseChangeSecretPushManager):
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
def method_type(cls):
return AutomationTypes.change_secret
def get_ssh_params(self, account, 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 secret_generator(self, secret_type):
return SecretGenerator(
self.secret_strategy, secret_type,
self.execution.snapshot.get('password_rules')
)
def get_secret(self, secret_type):
def get_secret(self, account):
if self.secret_strategy == SecretStrategy.custom:
return self.execution.snapshot['secret']
new_secret = self.execution.snapshot['secret']
else:
return self.secret_generator(secret_type).get_secret()
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)
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]
generator = SecretGenerator(
self.secret_strategy, self.secret_type,
self.execution.snapshot.get('password_rules')
)
return accounts
new_secret = generator.get_secret()
return new_secret
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
def gen_account_inventory(self, account, asset, h, path_dir):
record = self.get_or_create_record(asset, account, h['name'])
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)
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 = 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 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)
return recorder
def check_secret(self):
if self.secret_strategy == SecretStrategy.custom \
@ -230,47 +83,39 @@ class ChangeSecretManager(AccountBasePlaybookManager):
else:
failed += 1
total += 1
summary = _('Success: %s, Failed: %s, Total: %s') % (succeed, failed, total)
return summary
def run(self, *args, **kwargs):
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)
def print_summary(self):
recorders = list(self.name_recorder_mapper.values())
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:
return
failed_recorders = [
r for r in recorders
if r.status == ChangeSecretRecordStatusChoice.failed.value
]
recipients = self.execution.recipients
recipients = User.objects.filter(id__in=list(recipients.keys()))
if not recipients:
return
if failed_recorders:
name = self.execution.snapshot.get('name')
execution_id = str(self.execution.id)
_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()
context = self.get_report_context()
for user in recipients:
ChangeSecretReportMsg(user, context).publish()
if not recorders:
return
summary = self.get_summary(recorders)
self.send_recorder_mail(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)
wb.close()
return True
def get_report_template(self):
return "accounts/change_secret_report.html"

View File

@ -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)

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a2805a0264fc07ae597704841ab060edef8bf74654f525bc778cb9195d8cad0e
size 2547712

View File

@ -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)

View File

@ -1,6 +1,7 @@
from .backup_account.manager import AccountBackupManager
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 .remove_account.manager import RemoveAccountManager
from .verify_account.manager import VerifyAccountManager
@ -16,8 +17,8 @@ class ExecutionManager:
AutomationTypes.remove_account: RemoveAccountManager,
AutomationTypes.gather_accounts: GatherAccountsManager,
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
# TODO 后期迁移到自动化策略中
'backup_account': AccountBackupManager,
AutomationTypes.check_account: CheckAccountManager,
AutomationTypes.backup_account: AccountBackupManager,
}
def __init__(self, execution):
@ -26,3 +27,6 @@ class ExecutionManager:
def run(self, *args, **kwargs):
return self._runner.run(*args, **kwargs)
def __getattr__(self, item):
return getattr(self._runner, item)

View File

@ -1,7 +1,7 @@
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Get info
@ -15,7 +15,7 @@
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
filter: users
register: db_info

View File

@ -1,7 +1,7 @@
- hosts: mysql
gather_facts: no
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 }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"

View File

@ -1,7 +1,7 @@
- hosts: oralce
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Get info

View File

@ -1,7 +1,7 @@
- hosts: postgresql
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"

View File

@ -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

View File

@ -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 アカウントの収集

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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 />请使用 &#123;username&#125;、&#123;password&#125;、&#123;login_password&#125;格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. &#123;login_password&#125;<br />3. configure terminal<br />4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end'
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />&#123;ユーザー名&#125;、&#123;パスワード&#125;、&#123;login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.&#123;login_password&#125;<br />3 .ターミナルの設定<br / >4. ユーザー名 &#123;ユーザー名&#125; 権限 0 パスワード &#123;パスワード&#125; <br />5. 終了'
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use &#123;username&#125;, &#123;password&#125;, &#123;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. &#123;login_password&#125;<br />3. configure terminal<br / >4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end'
Params commands label:
zh: '自定义命令'
ja: 'カスタムコマンド'
en: 'Custom command'

View File

@ -1,7 +1,7 @@
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Test MongoDB connection
@ -53,3 +53,4 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
when: check_conn_after_change

View File

@ -1,7 +1,7 @@
- hosts: mysql
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
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 }}"
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_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
filter: version
when: check_conn_after_change

View File

@ -1,7 +1,7 @@
- hosts: oracle
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Test Oracle connection
@ -40,3 +40,4 @@
login_port: "{{ jms_asset.port }}"
login_database: "{{ jms_asset.spec_info.db_name }}"
mode: "{{ account.mode }}"
when: check_conn_after_change

View File

@ -1,7 +1,7 @@
- hosts: postgre
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
@ -59,5 +59,6 @@
when:
- result is succeeded
- change_info is succeeded
- check_conn_after_change
register: result
failed_when: not result.is_available

View File

@ -1,7 +1,7 @@
- hosts: sqlserver
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Test SQLServer connection
@ -66,3 +66,4 @@
name: '{{ jms_asset.spec_info.db_name }}'
script: |
SELECT @@version
when: check_conn_after_change

View File

@ -9,7 +9,8 @@
database: passwd
key: "{{ account.username }}"
register: user_info
ignore_errors: yes # 忽略错误如果用户不存在时不会导致playbook失败
failed_when: false
changed_when: false
- name: "Add {{ account.username }} user"
ansible.builtin.user:
@ -18,10 +19,10 @@
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
append: "{{ true if params.groups | length > 0 else false }}"
expires: -1
state: present
when: user_info.failed
when: user_info.msg is defined
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
@ -31,7 +32,7 @@
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- user_info.msg is defined or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
@ -100,7 +101,7 @@
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) }}"
when: account.secret_type == "password"
when: account.secret_type == "password" and check_conn_after_change
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@ -111,6 +112,6 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
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

View File

@ -9,7 +9,8 @@
database: passwd
key: "{{ account.username }}"
register: user_info
ignore_errors: yes # 忽略错误如果用户不存在时不会导致playbook失败
failed_when: false
changed_when: false
- name: "Add {{ account.username }} user"
ansible.builtin.user:
@ -18,10 +19,10 @@
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
append: "{{ true if params.groups | length > 0 else false }}"
expires: -1
state: present
when: user_info.failed
when: user_info.msg is defined
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
@ -31,7 +32,7 @@
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- user_info.msg is defined or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
@ -100,7 +101,7 @@
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) }}"
when: account.secret_type == "password"
when: account.secret_type == "password" and check_conn_after_change
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@ -111,6 +112,6 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
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

View File

@ -4,10 +4,6 @@
- name: Test privileged account
ansible.windows.win_ping:
# - name: Print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
- name: Push user password
ansible.windows.win_user:
fullname: "{{ account.username}}"
@ -28,4 +24,4 @@
vars:
ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}"
when: account.secret_type == "password"
when: account.secret_type == "password" and check_conn_after_change

View File

@ -4,10 +4,6 @@
- name: Test privileged account
ansible.windows.win_ping:
# - name: Print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
- name: Push user password
ansible.windows.win_user:
fullname: "{{ account.username}}"
@ -31,5 +27,5 @@
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
gateway_args: "{{ jms_gateway | default({}) }}"
when: account.secret_type == "password"
when: account.secret_type == "password" and check_conn_after_change
delegate_to: localhost

View File

@ -1,12 +1,16 @@
from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes
from common.decorators import bulk_create_decorator
from common.utils import get_logger
from ..base.manager import AccountBasePlaybookManager
from ..change_secret.manager import ChangeSecretManager
from common.utils.timezone import local_now_filename
from ..base.manager import BaseChangeSecretPushManager
from ...models import PushSecretRecord
logger = get_logger(__name__)
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
class PushAccountManager(BaseChangeSecretPushManager):
@staticmethod
def require_update_version(account, recorder):
@ -17,63 +21,43 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
def method_type(cls):
return AutomationTypes.push_account
# @classmethod
# def trigger_by_asset_create(cls, asset):
# 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}')
def get_secret(self, account):
return account.secret
# @classmethod
# def trigger_by_permission_accounts_change(cls):
# pass
def gen_account_inventory(self, account, asset, h, path_dir):
self.get_or_create_record(asset, account, h['name'])
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"

View File

@ -1,7 +1,7 @@
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: "Remove account"

View File

@ -1,7 +1,7 @@
- hosts: mysql
gather_facts: no
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 }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"

View File

@ -1,7 +1,7 @@
- hosts: oracle
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: "Remove account"

View File

@ -1,7 +1,7 @@
- hosts: postgresql
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"

View File

@ -1,7 +1,7 @@
- hosts: sqlserver
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: "Remove account"

View File

@ -1,10 +1,12 @@
import os
from collections import defaultdict
from copy import deepcopy
from django.db.models import QuerySet
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 ..base.manager import AccountBasePlaybookManager
@ -12,59 +14,82 @@ logger = get_logger(__name__)
class RemoveAccountManager(AccountBasePlaybookManager):
super_accounts = ["root", "administrator"]
def __init__(self, *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):
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:
f.write('[ssh_connection]\n')
f.write('ssh_args = -o ControlMaster=no -o ControlPersist=no\n')
with open(ansible_config_path, "w") as f:
f.write("[ssh_connection]\n")
f.write("ssh_args = -o ControlMaster=no -o ControlPersist=no\n")
return path
@classmethod
def method_type(cls):
return AutomationTypes.remove_account
def get_gather_accounts(self, privilege_account, gather_accounts: QuerySet):
gather_account_ids = self.execution.snapshot['gather_accounts']
gather_accounts = gather_accounts.filter(id__in=gather_account_ids)
gather_accounts = gather_accounts.exclude(
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'):
def host_callback(
self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs
):
if host.get("error"):
return host
gather_accounts = asset.gatheredaccount_set.all()
gather_accounts = self.get_gather_accounts(account, gather_accounts)
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['name'] += '(' + gather_account.username + ')'
self.host_account_mapper[h['name']] = (asset, gather_account)
h['account'] = {'username': gather_account.username}
h["name"] += "(" + username + ")"
self.host_account_mapper[h["name"]] = account
h["account"] = {"username": username}
inventory_hosts.append(h)
return inventory_hosts
def on_host_success(self, host, result):
tuple_asset_gather_account = self.host_account_mapper.get(host)
if not tuple_asset_gather_account:
super().on_host_success(host, result)
account = self.host_account_mapper.get(host)
if not account:
return
asset, gather_account = tuple_asset_gather_account
try:
Account.objects.filter(
asset_id=asset.id,
username=gather_account.username
if self.delete == "both":
Account.objects.filter(
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()
gather_account.delete()
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}"
)

View File

@ -21,3 +21,4 @@
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) }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"

View File

@ -1,7 +1,7 @@
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Verify account

View File

@ -1,7 +1,7 @@
- hosts: mysql
gather_facts: no
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 }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"

View File

@ -1,7 +1,7 @@
- hosts: oracle
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Verify account

View File

@ -1,7 +1,7 @@
- hosts: postgresql
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"

View File

@ -1,7 +1,7 @@
- hosts: sqlserver
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Verify account

View File

@ -24,7 +24,7 @@ class AliasAccount(TextChoices):
class Source(TextChoices):
LOCAL = 'local', _('Local')
COLLECTED = 'collected', _('Collected')
DISCOVERY = 'collected', _('Discovery')
TEMPLATE = 'template', _('Template')

View File

@ -17,6 +17,7 @@ __all__ = [
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
'GatherAccountDetailField'
]
@ -27,18 +28,23 @@ class AutomationTypes(models.TextChoices):
remove_account = 'remove_account', _('Remove account')
gather_accounts = 'gather_accounts', _('Gather accounts')
verify_gateway_account = 'verify_gateway_account', _('Verify gateway account')
check_account = 'check_account', _('Check account')
backup_account = 'backup_account', _('Backup account')
@classmethod
def get_type_model(cls, tp):
from accounts.models import (
PushAccountAutomation, ChangeSecretAutomation,
VerifyAccountAutomation, GatherAccountsAutomation,
CheckAccountAutomation, BackupAccountAutomation
)
type_model_dict = {
cls.push_account: PushAccountAutomation,
cls.change_secret: ChangeSecretAutomation,
cls.verify_account: VerifyAccountAutomation,
cls.gather_accounts: GatherAccountsAutomation,
cls.check_account: CheckAccountAutomation,
cls.backup_account: BackupAccountAutomation,
}
return type_model_dict.get(tp)
@ -49,9 +55,9 @@ class SecretStrategy(models.TextChoices):
class SSHKeyStrategy(models.TextChoices):
# add = 'add', _('Append SSH KEY')
set_jms = 'set_jms', _('Replace (Replace only keys pushed by JumpServer) ')
set = 'set', _('Empty and append SSH KEY')
add = 'add', _('Append SSH KEY')
class TriggerChoice(models.TextChoices, TreeChoices):
@ -109,3 +115,20 @@ class ChangeSecretRecordStatusChoice(models.TextChoices):
failed = 'failed', _('Failed')
success = 'success', _('Success')
pending = 'pending', _('Pending')
class GatherAccountDetailField(models.TextChoices):
can_login = 'can_login', _('Can login')
superuser = 'superuser', _('Superuser')
create_date = 'create_date', _('Create date')
is_disabled = 'is_disabled', _('Is disabled')
default_database_name = 'default_database_name', _('Default database name')
uid = 'uid', _('UID')
account_status = 'account_status', _('Account status')
default_tablespace = 'default_tablespace', _('Default tablespace')
roles = 'roles', _('Roles')
privileges = 'privileges', _('Privileges')
groups = 'groups', _('Groups')
sudoers = 'sudoers', 'sudoers'
authorized_keys = 'authorized_keys', _('Authorized keys')
db = 'db', _('DB')

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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"

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 的檢查以及確保提供了必需的參數。
## 貢獻
歡迎貢獻!如有任何增強或錯誤修復,請提出問題或提交拉取請求。

View File

@ -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)
}

View File

@ -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
}

View File

@ -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);
}
}
}

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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