mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-09-18 00:08:31 +00:00
@@ -1,20 +1,21 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.generics import ListAPIView
|
||||
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.filters import AccountFilterSet
|
||||
from accounts.models import Account
|
||||
from assets.models import Asset, Node
|
||||
from common.permissions import UserConfirmation, ConfirmType
|
||||
from common.permissions import UserConfirmation, ConfirmType, IsValidUser
|
||||
from common.views.mixins import RecordViewLogMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
__all__ = [
|
||||
'AccountViewSet', 'AccountSecretsViewSet',
|
||||
'AccountHistoriesSecretAPI'
|
||||
'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
|
||||
]
|
||||
|
||||
|
||||
@@ -28,7 +29,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
rbac_perms = {
|
||||
'partial_update': ['accounts.change_account'],
|
||||
'su_from_accounts': 'accounts.view_account',
|
||||
'username_suggestions': 'accounts.view_account',
|
||||
'clear_secret': 'accounts.change_account',
|
||||
}
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from-accounts')
|
||||
@@ -48,7 +49,10 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
serializer = serializers.AccountSerializer(accounts, many=True)
|
||||
return Response(data=serializer.data)
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='username-suggestions')
|
||||
@action(
|
||||
methods=['get'], detail=False, url_path='username-suggestions',
|
||||
permission_classes=[IsValidUser]
|
||||
)
|
||||
def username_suggestions(self, request, *args, **kwargs):
|
||||
asset_ids = request.query_params.get('assets')
|
||||
node_keys = request.query_params.get('keys')
|
||||
@@ -71,6 +75,12 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
usernames = common + others
|
||||
return Response(data=usernames)
|
||||
|
||||
@action(methods=['patch'], detail=False, url_path='clear-secret')
|
||||
def clear_secret(self, request, *args, **kwargs):
|
||||
account_ids = request.data.get('account_ids', [])
|
||||
self.model.objects.filter(id__in=account_ids).update(secret=None)
|
||||
return Response(status=HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
@@ -87,6 +97,20 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
}
|
||||
|
||||
|
||||
class AssetAccountBulkCreateApi(CreateAPIView):
|
||||
serializer_class = serializers.AssetAccountBulkSerializer
|
||||
rbac_perms = {
|
||||
'POST': 'accounts.add_account',
|
||||
}
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.create(serializer.validated_data)
|
||||
serializer = serializers.AssetAccountBulkSerializerResultSerializer(data, many=True)
|
||||
return Response(data=serializer.data, status=HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView):
|
||||
model = Account.history.model
|
||||
serializer_class = serializers.AccountHistorySerializer
|
||||
|
@@ -31,8 +31,8 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
||||
else:
|
||||
account = accounts[0]
|
||||
asset = account.asset
|
||||
if not asset.auto_info['ansible_enabled'] or \
|
||||
not asset.auto_info['ping_enabled']:
|
||||
if not asset.auto_config['ansible_enabled'] or \
|
||||
not asset.auto_config['ping_enabled']:
|
||||
raise NotSupportedTemporarilyError()
|
||||
task = verify_accounts_connectivity_task.delay(account_ids)
|
||||
|
||||
|
@@ -1,13 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
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.const import Source
|
||||
from accounts.filters import GatheredAccountFilterSet
|
||||
from accounts.models import GatherAccountsAutomation
|
||||
from accounts.models import GatheredAccount
|
||||
@@ -50,22 +48,12 @@ class GatheredAccountViewSet(OrgBulkModelViewSet):
|
||||
'default': serializers.GatheredAccountSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'sync_account': 'assets.add_gatheredaccount',
|
||||
'sync_accounts': 'assets.add_gatheredaccount',
|
||||
}
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='sync')
|
||||
def sync_account(self, request, *args, **kwargs):
|
||||
gathered_account = super().get_object()
|
||||
asset = gathered_account.asset
|
||||
username = gathered_account.username
|
||||
accounts = asset.accounts.filter(username=username)
|
||||
|
||||
if accounts.exists():
|
||||
accounts.update(source=Source.COLLECTED)
|
||||
else:
|
||||
asset.accounts.model.objects.create(
|
||||
asset=asset, username=username,
|
||||
name=f'{username}-{_("Collected")}',
|
||||
source=Source.COLLECTED
|
||||
)
|
||||
@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)
|
||||
|
@@ -18,18 +18,18 @@
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ kwargs.dest }}"
|
||||
regexp: "{{ kwargs.regexp }}"
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- kwargs.strategy == "set_jms"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Change SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ kwargs.exclusive }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
|
@@ -18,18 +18,18 @@
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ kwargs.dest }}"
|
||||
regexp: "{{ kwargs.regexp }}"
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- kwargs.strategy == "set_jms"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Change SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ kwargs.exclusive }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
|
@@ -42,7 +42,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
def method_type(cls):
|
||||
return AutomationTypes.change_secret
|
||||
|
||||
def get_kwargs(self, account, secret, secret_type):
|
||||
def get_ssh_params(self, account, secret, secret_type):
|
||||
kwargs = {}
|
||||
if secret_type != SecretType.SSH_KEY:
|
||||
return kwargs
|
||||
@@ -76,6 +76,11 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
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']
|
||||
)
|
||||
return accounts
|
||||
|
||||
def host_callback(
|
||||
@@ -106,6 +111,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
print(f'Windows {asset} does not support ssh key push')
|
||||
return inventory_hosts
|
||||
|
||||
host['ssh_params'] = {}
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
secret_type = account.secret_type
|
||||
@@ -124,7 +130,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
|
||||
h['kwargs'] = self.get_kwargs(account, new_secret, secret_type)
|
||||
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
|
@@ -12,6 +12,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.host_asset_mapper = {}
|
||||
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
@@ -25,26 +26,38 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
def filter_success_result(self, tp, result):
|
||||
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def update_or_create_gathered_accounts(asset, result):
|
||||
def generate_data(asset, result):
|
||||
data = []
|
||||
for username, info in result.items():
|
||||
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 update_or_create_accounts(self, asset, result):
|
||||
data = self.generate_data(asset, result)
|
||||
with tmp_to_org(asset.org_id):
|
||||
gathered_accounts = []
|
||||
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
||||
for username, data in result.items():
|
||||
d = {'asset': asset, 'username': username, 'present': True}
|
||||
if data.get('date'):
|
||||
d['date_last_login'] = data['date']
|
||||
if data.get('address'):
|
||||
d['address_last_login'] = data['address'][:32]
|
||||
GatheredAccount.objects.update_or_create(
|
||||
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:
|
||||
return
|
||||
GatheredAccount.sync_accounts(gathered_accounts)
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
info = result.get('debug', {}).get('res', {}).get('info', {})
|
||||
asset = self.host_asset_mapper.get(host)
|
||||
if asset and info:
|
||||
result = self.filter_success_result(asset.type, info)
|
||||
self.update_or_create_gathered_accounts(asset, result)
|
||||
self.update_or_create_accounts(asset, result)
|
||||
else:
|
||||
logger.error("Not found info".format(host))
|
||||
|
@@ -1,30 +1,6 @@
|
||||
import os
|
||||
import copy
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from assets.automations.methods import get_platform_automation_methods
|
||||
|
||||
|
||||
def copy_change_secret_to_push_account(methods):
|
||||
push_account = AutomationTypes.push_account
|
||||
change_secret = AutomationTypes.change_secret
|
||||
copy_methods = copy.deepcopy(methods)
|
||||
for method in copy_methods:
|
||||
if not method['id'].startswith(change_secret):
|
||||
continue
|
||||
copy_method = copy.deepcopy(method)
|
||||
copy_method['method'] = push_account.value
|
||||
copy_method['id'] = copy_method['id'].replace(
|
||||
change_secret, push_account
|
||||
)
|
||||
copy_method['name'] = copy_method['name'].replace(
|
||||
'Change secret', 'Push account'
|
||||
)
|
||||
methods.append(copy_method)
|
||||
return methods
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
automation_methods = get_platform_automation_methods(BASE_DIR)
|
||||
|
||||
platform_automation_methods = copy_change_secret_to_push_account(automation_methods)
|
||||
platform_automation_methods = get_platform_automation_methods(BASE_DIR)
|
||||
|
@@ -0,0 +1,58 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test MongoDB connection
|
||||
mongodb_ping:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
register: db_info
|
||||
|
||||
- name: Display MongoDB version
|
||||
debug:
|
||||
var: db_info.server_version
|
||||
when: db_info is succeeded
|
||||
|
||||
- name: Change MongoDB password
|
||||
mongodb_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
when: db_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
mongodb_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
@@ -0,0 +1,6 @@
|
||||
id: push_account_mongodb
|
||||
name: Push account for MongoDB
|
||||
category: database
|
||||
type:
|
||||
- mongodb
|
||||
method: push_account
|
@@ -0,0 +1,43 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
community.mysql.mysql_info:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
- name: MySQL version
|
||||
debug:
|
||||
var: db_info.version.full
|
||||
|
||||
- name: Change MySQL password
|
||||
community.mysql.mysql_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
|
||||
when: db_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.mysql.mysql_info:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
filter: version
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
@@ -0,0 +1,7 @@
|
||||
id: push_account_mysql
|
||||
name: Push account for MySQL
|
||||
category: database
|
||||
type:
|
||||
- mysql
|
||||
- mariadb
|
||||
method: push_account
|
@@ -0,0 +1,44 @@
|
||||
- hosts: oracle
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test Oracle connection
|
||||
oracle_ping:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ jms_account.mode }}"
|
||||
register: db_info
|
||||
|
||||
- name: Display Oracle version
|
||||
debug:
|
||||
var: db_info.server_version
|
||||
when: db_info is succeeded
|
||||
|
||||
- name: Change Oracle password
|
||||
oracle_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ jms_account.mode }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
when: db_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
oracle_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
@@ -0,0 +1,6 @@
|
||||
id: push_account_oracle
|
||||
name: Push account for Oracle
|
||||
category: database
|
||||
type:
|
||||
- oracle
|
||||
method: push_account
|
@@ -0,0 +1,46 @@
|
||||
- hosts: postgre
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
community.postgresql.postgresql_ping:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
- name: Display PostgreSQL version
|
||||
debug:
|
||||
var: result.server_version.full
|
||||
when: result is succeeded
|
||||
|
||||
- name: Change PostgreSQL password
|
||||
community.postgresql.postgresql_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
role_attr_flags: LOGIN
|
||||
when: result is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.postgresql.postgresql_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
when:
|
||||
- result is succeeded
|
||||
- change_info is succeeded
|
||||
register: result
|
||||
failed_when: not result.is_available
|
@@ -0,0 +1,6 @@
|
||||
id: push_account_postgresql
|
||||
name: Push account for PostgreSQL
|
||||
category: database
|
||||
type:
|
||||
- postgresql
|
||||
method: push_account
|
@@ -0,0 +1,69 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
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 @@version
|
||||
register: db_info
|
||||
|
||||
- name: SQLServer version
|
||||
set_fact:
|
||||
info:
|
||||
version: "{{ db_info.query_results[0][0][0][0].splitlines()[0] }}"
|
||||
- debug:
|
||||
var: info
|
||||
|
||||
- name: Check whether SQLServer User exist
|
||||
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 1 from sys.sql_logins WHERE name='{{ account.username }}';"
|
||||
when: db_info is succeeded
|
||||
register: user_exist
|
||||
|
||||
- name: Change SQLServer password
|
||||
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: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
||||
when: user_exist.query_results[0] | length != 0
|
||||
register: change_info
|
||||
|
||||
- name: Add SQLServer user
|
||||
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: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
||||
when: user_exist.query_results[0] | length == 0
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: |
|
||||
SELECT @@version
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
@@ -0,0 +1,6 @@
|
||||
id: push_account_sqlserver
|
||||
name: Push account for SQLServer
|
||||
category: database
|
||||
type:
|
||||
- sqlserver
|
||||
method: push_account
|
93
apps/accounts/automations/push_account/host/aix/main.yml
Normal file
93
apps/accounts/automations/push_account/host/aix/main.yml
Normal file
@@ -0,0 +1,93 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Push user
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ '/home/' + account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
expires: -1
|
||||
state: present
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
|
||||
- name: Check home dir exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ '/home/' + account.username }}"
|
||||
register: home_existed
|
||||
|
||||
- name: Set home dir permission
|
||||
ansible.builtin.file:
|
||||
path: "{{ '/home/' + account.username }}"
|
||||
owner: "{{ account.username }}"
|
||||
group: "{{ account.username }}"
|
||||
mode: "0700"
|
||||
when:
|
||||
- home_existed.stat.exists == true
|
||||
|
||||
- name: Add user groups
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when: params.groups
|
||||
|
||||
- name: Push user password
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Push SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Set sudo setting
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
regexp: "^{{ account.username }} ALL="
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- params.sudo
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "ssh_key"
|
24
apps/accounts/automations/push_account/host/aix/manifest.yml
Normal file
24
apps/accounts/automations/push_account/host/aix/manifest.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
id: push_account_aix
|
||||
name: Push account for aix
|
||||
category: host
|
||||
type:
|
||||
- AIX
|
||||
method: push_account
|
||||
params:
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
default: '/bin/whoami'
|
||||
help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
|
||||
- name: shell
|
||||
type: str
|
||||
label: 'Shell'
|
||||
default: '/bin/bash'
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: ''
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
|
93
apps/accounts/automations/push_account/host/posix/main.yml
Normal file
93
apps/accounts/automations/push_account/host/posix/main.yml
Normal file
@@ -0,0 +1,93 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Push user
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ '/home/' + account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
expires: -1
|
||||
state: present
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
|
||||
- name: Check home dir exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ '/home/' + account.username }}"
|
||||
register: home_existed
|
||||
|
||||
- name: Set home dir permission
|
||||
ansible.builtin.file:
|
||||
path: "{{ '/home/' + account.username }}"
|
||||
owner: "{{ account.username }}"
|
||||
group: "{{ account.username }}"
|
||||
mode: "0700"
|
||||
when:
|
||||
- home_existed.stat.exists == true
|
||||
|
||||
- name: Add user groups
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when: params.groups
|
||||
|
||||
- name: Push user password
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Push SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Set sudo setting
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
state: present
|
||||
regexp: "^{{ account.username }} ALL="
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- params.sudo
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "ssh_key"
|
@@ -0,0 +1,25 @@
|
||||
id: push_account_posix
|
||||
name: Push account for posix
|
||||
category: host
|
||||
type:
|
||||
- unix
|
||||
- linux
|
||||
method: push_account
|
||||
params:
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
default: '/bin/whoami'
|
||||
help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
|
||||
- name: shell
|
||||
type: str
|
||||
label: 'Shell'
|
||||
default: '/bin/bash'
|
||||
help_text: ''
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: ''
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
30
apps/accounts/automations/push_account/host/windows/main.yml
Normal file
30
apps/accounts/automations/push_account/host/windows/main.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- 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}}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
password_never_expires: yes
|
||||
groups: "{{ params.groups }}"
|
||||
groups_action: add
|
||||
update_password: always
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.windows.win_ping:
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
when: account.secret_type == "password"
|
@@ -0,0 +1,13 @@
|
||||
id: push_account_local_windows
|
||||
name: Push account local account for Windows
|
||||
version: 1
|
||||
method: push_account
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
@@ -31,6 +31,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||
print(msg)
|
||||
return inventory_hosts
|
||||
|
||||
host['ssh_params'] = {}
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
secret_type = account.secret_type
|
||||
@@ -49,7 +50,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
|
||||
h['kwargs'] = self.get_kwargs(account, new_secret, secret_type)
|
||||
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
|
@@ -18,3 +18,10 @@ class AliasAccount(TextChoices):
|
||||
class Source(TextChoices):
|
||||
LOCAL = 'local', _('Local')
|
||||
COLLECTED = 'collected', _('Collected')
|
||||
TEMPLATE = 'template', _('Template')
|
||||
|
||||
|
||||
class AccountInvalidPolicy(TextChoices):
|
||||
SKIP = 'skip', _('Skip')
|
||||
UPDATE = 'update', _('Update')
|
||||
ERROR = 'error', _('Failed')
|
||||
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.16 on 2023-03-23 08:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0009_account_usernames_to_ids'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='gatheraccountsautomation',
|
||||
name='is_sync_account',
|
||||
field=models.BooleanField(blank=True, default=False, verbose_name='Is sync account'),
|
||||
),
|
||||
]
|
18
apps/accounts/migrations/0011_account_source_id.py
Normal file
18
apps/accounts/migrations/0011_account_source_id.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.17 on 2023-03-23 07:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0010_gatheraccountsautomation_is_sync_account'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='source_id',
|
||||
field=models.CharField(max_length=128, null=True, blank=True, verbose_name='Source ID'),
|
||||
),
|
||||
]
|
@@ -53,6 +53,7 @@ class Account(AbsConnectivity, BaseAccount):
|
||||
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
|
||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
||||
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Account')
|
||||
|
@@ -1,7 +1,9 @@
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.const import AutomationTypes, Source
|
||||
from accounts.models import Account
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
from .base import AccountBaseAutomation
|
||||
|
||||
@@ -19,6 +21,25 @@ class GatheredAccount(JMSOrgBaseModel):
|
||||
def address(self):
|
||||
return self.asset.address
|
||||
|
||||
@staticmethod
|
||||
def sync_accounts(gathered_accounts):
|
||||
account_objs = []
|
||||
for gathered_account in gathered_accounts:
|
||||
asset_id = gathered_account.asset_id
|
||||
username = gathered_account.username
|
||||
accounts = Account.objects.filter(
|
||||
Q(asset_id=asset_id, username=username) |
|
||||
Q(asset_id=asset_id, name=username)
|
||||
)
|
||||
if accounts.exists():
|
||||
continue
|
||||
account = Account(
|
||||
asset_id=asset_id, username=username,
|
||||
name=username, source=Source.COLLECTED
|
||||
)
|
||||
account_objs.append(account)
|
||||
Account.objects.bulk_create(account_objs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Gather account automation')
|
||||
unique_together = [
|
||||
@@ -31,6 +52,17 @@ class GatheredAccount(JMSOrgBaseModel):
|
||||
|
||||
|
||||
class GatherAccountsAutomation(AccountBaseAutomation):
|
||||
is_sync_account = models.BooleanField(
|
||||
default=False, blank=True, verbose_name=_("Is sync account")
|
||||
)
|
||||
|
||||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'is_sync_account': self.is_sync_account,
|
||||
})
|
||||
return attr_json
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.type = AutomationTypes.gather_accounts
|
||||
super().save(*args, **kwargs)
|
||||
|
@@ -51,7 +51,8 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'username': self.username
|
||||
'username': self.username,
|
||||
'params': self.params,
|
||||
})
|
||||
return attr_json
|
||||
|
||||
|
@@ -1,75 +1,172 @@
|
||||
import uuid
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from accounts.const import SecretType, Source
|
||||
from accounts.const import SecretType, Source, AccountInvalidPolicy
|
||||
from accounts.models import Account, AccountTemplate
|
||||
from accounts.tasks import push_accounts_to_assets_task
|
||||
from assets.const import Category, AllTypes
|
||||
from assets.models import Asset
|
||||
from common.serializers import SecretReadableMixin, BulkModelSerializer
|
||||
from common.serializers import SecretReadableMixin
|
||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
|
||||
from common.utils import get_logger
|
||||
from .base import BaseAccountSerializer
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class AccountSerializerCreateValidateMixin:
|
||||
from_id: str
|
||||
template: bool
|
||||
push_now: bool
|
||||
replace_attrs: callable
|
||||
|
||||
def to_internal_value(self, data):
|
||||
from_id = data.pop('id', None)
|
||||
ret = super().to_internal_value(data)
|
||||
self.from_id = from_id
|
||||
return ret
|
||||
class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
template = serializers.PrimaryKeyRelatedField(
|
||||
queryset=AccountTemplate.objects,
|
||||
required=False, label=_("Template"), write_only=True
|
||||
)
|
||||
push_now = serializers.BooleanField(
|
||||
default=False, label=_("Push now"), write_only=True
|
||||
)
|
||||
params = serializers.JSONField(
|
||||
decoder=None, encoder=None, required=False, style={'base_template': 'textarea.html'}
|
||||
)
|
||||
on_invalid = LabeledChoiceField(
|
||||
choices=AccountInvalidPolicy.choices, default=AccountInvalidPolicy.ERROR,
|
||||
write_only=True, label=_('Exist policy')
|
||||
)
|
||||
|
||||
def set_secret(self, attrs):
|
||||
_id = self.from_id
|
||||
template = attrs.pop('template', None)
|
||||
class Meta:
|
||||
fields = ['template', 'push_now', 'params', 'on_invalid']
|
||||
|
||||
if _id and template:
|
||||
account_template = AccountTemplate.objects.get(id=_id)
|
||||
attrs['secret'] = account_template.secret
|
||||
elif _id and not template:
|
||||
account = Account.objects.get(id=_id)
|
||||
attrs['secret'] = account.secret
|
||||
return attrs
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_initial_value()
|
||||
|
||||
def set_initial_value(self):
|
||||
if not getattr(self, 'initial_data', None):
|
||||
return
|
||||
if isinstance(self.initial_data, dict):
|
||||
initial_data = [self.initial_data]
|
||||
else:
|
||||
initial_data = self.initial_data
|
||||
|
||||
for data in initial_data:
|
||||
if not data.get('asset') and not self.instance:
|
||||
raise serializers.ValidationError({'asset': 'Asset is required'})
|
||||
asset = data.get('asset') or self.instance.asset
|
||||
self.from_template_if_need(data)
|
||||
self.set_uniq_name_if_need(data, asset)
|
||||
|
||||
def set_uniq_name_if_need(self, initial_data, asset):
|
||||
name = initial_data.get('name')
|
||||
if name is None:
|
||||
return
|
||||
if not name:
|
||||
name = initial_data.get('username')
|
||||
if self.instance and self.instance.name == name:
|
||||
return
|
||||
if Account.objects.filter(name=name, asset=asset).exists():
|
||||
name = name + '_' + uuid.uuid4().hex[:4]
|
||||
initial_data['name'] = name
|
||||
|
||||
@staticmethod
|
||||
def from_template_if_need(initial_data):
|
||||
template_id = initial_data.get('template')
|
||||
if not template_id:
|
||||
return
|
||||
if isinstance(template_id, (str, uuid.UUID)):
|
||||
template = AccountTemplate.objects.filter(id=template_id).first()
|
||||
else:
|
||||
template = template_id
|
||||
if not template:
|
||||
raise serializers.ValidationError({'template': 'Template not found'})
|
||||
|
||||
# Set initial data from template
|
||||
ignore_fields = ['id', 'date_created', 'date_updated', 'org_id']
|
||||
field_names = [
|
||||
field.name for field in template._meta.fields
|
||||
if field.name not in ignore_fields
|
||||
]
|
||||
attrs = {}
|
||||
for name in field_names:
|
||||
value = getattr(template, name, None)
|
||||
if value is None:
|
||||
continue
|
||||
attrs[name] = value
|
||||
initial_data.update(attrs)
|
||||
|
||||
@staticmethod
|
||||
def push_account_if_need(instance, push_now, params, stat):
|
||||
if not push_now or stat != 'created':
|
||||
return
|
||||
push_accounts_to_assets_task.delay([str(instance.id)], params)
|
||||
|
||||
def get_validators(self):
|
||||
_validators = super().get_validators()
|
||||
if getattr(self, 'initial_data', None) is None:
|
||||
return _validators
|
||||
on_invalid = self.initial_data.get('on_invalid')
|
||||
if on_invalid == AccountInvalidPolicy.ERROR:
|
||||
return _validators
|
||||
_validators = [v for v in _validators if not isinstance(v, UniqueTogetherValidator)]
|
||||
return _validators
|
||||
|
||||
@staticmethod
|
||||
def do_create(vd):
|
||||
on_invalid = vd.pop('on_invalid', None)
|
||||
|
||||
q = Q()
|
||||
if vd.get('name'):
|
||||
q |= Q(name=vd['name'])
|
||||
if vd.get('username'):
|
||||
q |= Q(username=vd['username'], secret_type=vd.get('secret_type'))
|
||||
|
||||
instance = Account.objects.filter(asset=vd['asset']).filter(q).first()
|
||||
# 不存在这个资产,不用关系策略
|
||||
if not instance:
|
||||
instance = Account.objects.create(**vd)
|
||||
return instance, 'created'
|
||||
|
||||
if on_invalid == AccountInvalidPolicy.SKIP:
|
||||
return instance, 'skipped'
|
||||
elif on_invalid == AccountInvalidPolicy.UPDATE:
|
||||
for k, v in vd.items():
|
||||
setattr(instance, k, v)
|
||||
instance.save()
|
||||
return instance, 'updated'
|
||||
else:
|
||||
raise serializers.ValidationError('Account already exists')
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
return self.set_secret(attrs)
|
||||
if self.instance:
|
||||
return attrs
|
||||
|
||||
@staticmethod
|
||||
def push_account(instance, push_now):
|
||||
if not push_now:
|
||||
return
|
||||
push_accounts_to_assets_task.delay([str(instance.id)])
|
||||
template = attrs.pop('template', None)
|
||||
if template:
|
||||
attrs['source'] = Source.TEMPLATE
|
||||
attrs['source_id'] = str(template.id)
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
push_now = validated_data.pop('push_now', None)
|
||||
instance = super().create(validated_data)
|
||||
self.push_account(instance, push_now)
|
||||
params = validated_data.pop('params', None)
|
||||
instance, stat = self.do_create(validated_data)
|
||||
self.push_account_if_need(instance, push_now, params, stat)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# account cannot be modified
|
||||
validated_data.pop('username', None)
|
||||
validated_data.pop('on_invalid', None)
|
||||
push_now = validated_data.pop('push_now', None)
|
||||
params = validated_data.pop('params', None)
|
||||
validated_data['source_id'] = None
|
||||
instance = super().update(instance, validated_data)
|
||||
self.push_account(instance, push_now)
|
||||
self.push_account_if_need(instance, push_now, params, 'updated')
|
||||
return instance
|
||||
|
||||
|
||||
class AccountSerializerCreateMixin(AccountSerializerCreateValidateMixin, BulkModelSerializer):
|
||||
template = serializers.BooleanField(
|
||||
default=False, label=_("Template"), write_only=True
|
||||
)
|
||||
push_now = serializers.BooleanField(
|
||||
default=False, label=_("Push now"), write_only=True
|
||||
)
|
||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||
|
||||
|
||||
class AccountAssetSerializer(serializers.ModelSerializer):
|
||||
platform = ObjectRelatedField(read_only=True)
|
||||
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
|
||||
@@ -77,11 +174,11 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_info']
|
||||
fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_config']
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, dict):
|
||||
i = data.get('id')
|
||||
i = data.get('id') or data.get('pk')
|
||||
else:
|
||||
i = data
|
||||
|
||||
@@ -91,9 +188,10 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
||||
raise serializers.ValidationError(_('Asset not found'))
|
||||
|
||||
|
||||
class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
|
||||
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
||||
asset = AccountAssetSerializer(label=_('Asset'))
|
||||
source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True)
|
||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||
su_from = ObjectRelatedField(
|
||||
required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
|
||||
label=_('Su from'), attrs=('id', 'name', 'username')
|
||||
@@ -102,27 +200,179 @@ class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
|
||||
class Meta(BaseAccountSerializer.Meta):
|
||||
model = Account
|
||||
fields = BaseAccountSerializer.Meta.fields + [
|
||||
'su_from', 'asset', 'template', 'version',
|
||||
'push_now', 'source', 'connectivity',
|
||||
'su_from', 'asset', 'version',
|
||||
'source', 'source_id', 'connectivity',
|
||||
] + AccountCreateUpdateSerializerMixin.Meta.fields
|
||||
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + [
|
||||
'source', 'source_id', 'connectivity'
|
||||
]
|
||||
extra_kwargs = {
|
||||
**BaseAccountSerializer.Meta.extra_kwargs,
|
||||
'name': {'required': False, 'allow_null': True},
|
||||
'name': {'required': False},
|
||||
}
|
||||
|
||||
def validate_name(self, value):
|
||||
if not value:
|
||||
value = self.initial_data.get('username')
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset \
|
||||
.prefetch_related('asset', 'asset__platform', 'asset__platform__automation')
|
||||
queryset = queryset.prefetch_related(
|
||||
'asset', 'asset__platform',
|
||||
'asset__platform__automation'
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
|
||||
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
||||
state = serializers.CharField(read_only=True, label=_('State'))
|
||||
error = serializers.CharField(read_only=True, label=_('Error'))
|
||||
changed = serializers.BooleanField(read_only=True, label=_('Changed'))
|
||||
|
||||
|
||||
class AssetAccountBulkSerializer(AccountCreateUpdateSerializerMixin, serializers.ModelSerializer):
|
||||
assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets'))
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = [
|
||||
'name', 'username', 'secret', 'secret_type',
|
||||
'privileged', 'is_active', 'comment', 'template',
|
||||
'on_invalid', 'push_now', 'assets',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': False},
|
||||
'secret_type': {'required': False},
|
||||
}
|
||||
|
||||
def set_initial_value(self):
|
||||
if not getattr(self, 'initial_data', None):
|
||||
return
|
||||
initial_data = self.initial_data
|
||||
self.from_template_if_need(initial_data)
|
||||
|
||||
@staticmethod
|
||||
def get_filter_lookup(vd):
|
||||
return {
|
||||
'username': vd['username'],
|
||||
'secret_type': vd['secret_type'],
|
||||
'asset': vd['asset'],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_uniq_name(vd):
|
||||
return vd['name'] + '-' + uuid.uuid4().hex[:4]
|
||||
|
||||
@staticmethod
|
||||
def _handle_update_create(vd, lookup):
|
||||
ori = Account.objects.filter(**lookup).first()
|
||||
if ori and ori.secret == vd['secret']:
|
||||
return ori, False, 'skipped'
|
||||
|
||||
instance, value = Account.objects.update_or_create(defaults=vd, **lookup)
|
||||
state = 'created' if value else 'updated'
|
||||
return instance, True, state
|
||||
|
||||
@staticmethod
|
||||
def _handle_skip_create(vd, lookup):
|
||||
instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
|
||||
state = 'created' if value else 'skipped'
|
||||
return instance, value, state
|
||||
|
||||
@staticmethod
|
||||
def _handle_err_create(vd, lookup):
|
||||
instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
|
||||
if not value:
|
||||
raise serializers.ValidationError(_('Account already exists'))
|
||||
return instance, True, 'created'
|
||||
|
||||
def perform_create(self, vd, handler):
|
||||
lookup = self.get_filter_lookup(vd)
|
||||
try:
|
||||
instance, changed, state = handler(vd, lookup)
|
||||
except IntegrityError:
|
||||
vd['name'] = self.get_uniq_name(vd)
|
||||
instance, changed, state = handler(vd, lookup)
|
||||
return instance, changed, state
|
||||
|
||||
def get_create_handler(self, on_invalid):
|
||||
if on_invalid == 'update':
|
||||
handler = self._handle_update_create
|
||||
elif on_invalid == 'skip':
|
||||
handler = self._handle_skip_create
|
||||
else:
|
||||
handler = self._handle_err_create
|
||||
return handler
|
||||
|
||||
def perform_bulk_create(self, vd):
|
||||
assets = vd.pop('assets')
|
||||
on_invalid = vd.pop('on_invalid', 'skip')
|
||||
secret_type = vd.get('secret_type', 'password')
|
||||
|
||||
if not vd.get('name'):
|
||||
vd['name'] = vd.get('username')
|
||||
|
||||
create_handler = self.get_create_handler(on_invalid)
|
||||
asset_ids = [asset.id for asset in assets]
|
||||
secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type)
|
||||
|
||||
_results = {}
|
||||
for asset in assets:
|
||||
if asset not in secret_type_supports:
|
||||
_results[asset] = {
|
||||
'error': _('Asset does not support this secret type: %s') % secret_type,
|
||||
'state': 'error',
|
||||
}
|
||||
continue
|
||||
|
||||
vd = vd.copy()
|
||||
vd['asset'] = asset
|
||||
try:
|
||||
instance, changed, state = self.perform_create(vd, create_handler)
|
||||
_results[asset] = {
|
||||
'changed': changed, 'instance': instance.id, 'state': state
|
||||
}
|
||||
except serializers.ValidationError as e:
|
||||
_results[asset] = {'error': e.detail[0], 'state': 'error'}
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
_results[asset] = {'error': str(e), 'state': 'error'}
|
||||
|
||||
results = [{'asset': asset, **result} for asset, result in _results.items()]
|
||||
state_score = {'created': 3, 'updated': 2, 'skipped': 1, 'error': 0}
|
||||
results = sorted(results, key=lambda x: state_score.get(x['state'], 4))
|
||||
|
||||
if on_invalid != 'error':
|
||||
return results
|
||||
|
||||
errors = []
|
||||
errors.extend([result for result in results if result['state'] == 'error'])
|
||||
for result in results:
|
||||
if result['state'] != 'skipped':
|
||||
continue
|
||||
errors.append({
|
||||
'error': _('Account has exist'),
|
||||
'state': 'error',
|
||||
'asset': str(result['asset'])
|
||||
})
|
||||
if errors:
|
||||
raise serializers.ValidationError(errors)
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def push_accounts_if_need(results, push_now):
|
||||
if not push_now:
|
||||
return
|
||||
accounts = [str(v['instance']) for v in results if v.get('instance')]
|
||||
push_accounts_to_assets_task.delay(accounts)
|
||||
|
||||
def create(self, validated_data):
|
||||
push_now = validated_data.pop('push_now', False)
|
||||
results = self.perform_bulk_create(validated_data)
|
||||
self.push_accounts_if_need(results, push_now)
|
||||
for res in results:
|
||||
res['asset'] = str(res['asset'])
|
||||
return results
|
||||
|
||||
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
extra_kwargs = {
|
||||
|
@@ -13,10 +13,10 @@ __all__ = ['AuthValidateMixin', 'BaseAccountSerializer']
|
||||
|
||||
class AuthValidateMixin(serializers.Serializer):
|
||||
secret_type = LabeledChoiceField(
|
||||
choices=SecretType.choices, required=True, label=_('Secret type')
|
||||
choices=SecretType.choices, label=_('Secret type'), default='password'
|
||||
)
|
||||
secret = EncryptedField(
|
||||
label=_('Secret/Password'), required=False, max_length=40960, allow_blank=True,
|
||||
label=_('Secret'), required=False, max_length=40960, allow_blank=True,
|
||||
allow_null=True, write_only=True,
|
||||
)
|
||||
passphrase = serializers.CharField(
|
||||
@@ -77,6 +77,5 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||
'date_verified', 'created_by', 'date_created',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': True},
|
||||
'spec_info': {'label': _('Spec info')},
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from accounts.models import AccountTemplate
|
||||
from accounts.models import AccountTemplate, Account
|
||||
from assets.models import Asset
|
||||
from common.serializers import SecretReadableMixin
|
||||
from .base import BaseAccountSerializer
|
||||
|
||||
@@ -7,17 +8,47 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
class Meta(BaseAccountSerializer.Meta):
|
||||
model = AccountTemplate
|
||||
|
||||
# @classmethod
|
||||
# def validate_required(cls, attrs):
|
||||
# # TODO 选择模版后检查一些必填项
|
||||
# required_field_dict = {}
|
||||
# error = _('This field is required.')
|
||||
# for k, v in cls().fields.items():
|
||||
# if v.required and k not in attrs:
|
||||
# required_field_dict[k] = error
|
||||
# if not required_field_dict:
|
||||
# return
|
||||
# raise serializers.ValidationError(required_field_dict)
|
||||
@staticmethod
|
||||
def bulk_update_accounts(instance, diff):
|
||||
accounts = Account.objects.filter(source_id=instance.id)
|
||||
if not accounts:
|
||||
return
|
||||
|
||||
secret_type = diff.pop('secret_type', None)
|
||||
diff.pop('secret', None)
|
||||
update_accounts = []
|
||||
for account in accounts:
|
||||
for field, value in diff.items():
|
||||
setattr(account, field, value)
|
||||
update_accounts.append(account)
|
||||
if update_accounts:
|
||||
Account.objects.bulk_update(update_accounts, diff.keys())
|
||||
|
||||
if secret_type is None:
|
||||
return
|
||||
|
||||
update_accounts = []
|
||||
asset_ids = accounts.values_list('asset_id', flat=True)
|
||||
secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type)
|
||||
asset_ids_supports = [asset.id for asset in secret_type_supports]
|
||||
for account in accounts:
|
||||
asset_id = account.asset_id
|
||||
if asset_id not in asset_ids_supports:
|
||||
continue
|
||||
account.secret_type = secret_type
|
||||
account.secret = instance.secret
|
||||
update_accounts.append(account)
|
||||
if update_accounts:
|
||||
Account.objects.bulk_update(update_accounts, ['secret', 'secret_type'])
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
diff = {
|
||||
k: v for k, v in validated_data.items()
|
||||
if getattr(instance, k) != v
|
||||
}
|
||||
instance = super().update(instance, validated_data)
|
||||
self.bulk_update_accounts(instance, diff)
|
||||
return instance
|
||||
|
||||
|
||||
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer):
|
||||
|
@@ -17,7 +17,8 @@ class GatherAccountAutomationSerializer(BaseAutomationSerializer):
|
||||
class Meta:
|
||||
model = GatherAccountsAutomation
|
||||
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||
fields = BaseAutomationSerializer.Meta.fields + read_only_fields
|
||||
fields = BaseAutomationSerializer.Meta.fields \
|
||||
+ ['is_sync_account'] + read_only_fields
|
||||
|
||||
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
|
||||
|
||||
|
@@ -7,9 +7,10 @@ from .change_secret import (
|
||||
|
||||
|
||||
class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer):
|
||||
|
||||
class Meta(ChangeSecretAutomationSerializer.Meta):
|
||||
model = PushAccountAutomation
|
||||
fields = [
|
||||
fields = ['params'] + [
|
||||
n for n in ChangeSecretAutomationSerializer.Meta.fields
|
||||
if n not in ['recipients']
|
||||
]
|
||||
|
@@ -8,8 +8,8 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Account)
|
||||
def on_account_pre_save(sender, instance, created=False, **kwargs):
|
||||
if created:
|
||||
def on_account_pre_save(sender, instance, **kwargs):
|
||||
if instance.version == 0:
|
||||
instance.version = 1
|
||||
else:
|
||||
instance.version = instance.history.count()
|
||||
|
@@ -15,7 +15,7 @@ __all__ = [
|
||||
queue="ansible", verbose_name=_('Push accounts to assets'),
|
||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None)
|
||||
)
|
||||
def push_accounts_to_assets_task(account_ids):
|
||||
def push_accounts_to_assets_task(account_ids, params=None):
|
||||
from accounts.models import PushAccountAutomation
|
||||
from accounts.models import Account
|
||||
|
||||
@@ -26,6 +26,7 @@ def push_accounts_to_assets_task(account_ids):
|
||||
task_snapshot = {
|
||||
'accounts': [str(account.id) for account in accounts],
|
||||
'assets': [str(account.asset_id) for account in accounts],
|
||||
'params': params or {},
|
||||
}
|
||||
|
||||
tp = AutomationTypes.push_account
|
||||
|
@@ -25,6 +25,7 @@ router.register(r'push-account-executions', api.PushAccountExecutionViewSet, 'pu
|
||||
router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record')
|
||||
|
||||
urlpatterns = [
|
||||
path('accounts/bulk/', api.AssetAccountBulkCreateApi.as_view(), name='account-bulk-create'),
|
||||
path('accounts/tasks/', api.AccountsTaskCreateAPI.as_view(), name='account-task-create'),
|
||||
path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(),
|
||||
name='account-secret-history'),
|
||||
|
Reference in New Issue
Block a user