Merge pull request #10196 from jumpserver/dev

v3.2.0 rc1
pull/10289/head
Jiangjie.Bai 2023-04-13 19:21:05 +08:00 committed by GitHub
commit 68351b1c39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
143 changed files with 3430 additions and 1812 deletions

View File

@ -8,4 +8,4 @@ celerybeat.pid
### Vagrant ###
.vagrant/
apps/xpack/.git
.history/

View File

@ -3,7 +3,10 @@ name: 需求建议
about: 提出针对本项目的想法和建议
title: "[Feature] "
labels: 类型:需求
assignees: ibuler
assignees:
- ibuler
- baijiangjie
---

View File

@ -3,7 +3,9 @@ name: Bug 提交
about: 提交产品缺陷帮助我们更好的改进
title: "[Bug] "
labels: 类型:bug
assignees: wojiushixiaobai
assignees:
- wojiushixiaobai
- baijiangjie
---

View File

@ -3,7 +3,9 @@ name: 问题咨询
about: 提出针对本项目安装部署、使用及其他方面的相关问题
title: "[Question] "
labels: 类型:提问
assignees: wojiushixiaobai
assignees:
- wojiushixiaobai
- baijiangjie
---

1
.gitignore vendored
View File

@ -43,3 +43,4 @@ releashe
/apps/script.py
data/*
test.py
.history/

View File

@ -55,7 +55,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
&& echo "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \

View File

@ -53,7 +53,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
&& echo "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \

View File

@ -54,6 +54,7 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
- [产品文档](https://docs.jumpserver.org)
- [在线学习](https://edu.fit2cloud.com/page/2635362)
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
## 案例研究

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
id: push_account_mongodb
name: Push account for MongoDB
category: database
type:
- mongodb
method: push_account

View File

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

View File

@ -0,0 +1,7 @@
id: push_account_mysql
name: Push account for MySQL
category: database
type:
- mysql
- mariadb
method: push_account

View File

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

View File

@ -0,0 +1,6 @@
id: push_account_oracle
name: Push account for Oracle
category: database
type:
- oracle
method: push_account

View File

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

View File

@ -0,0 +1,6 @@
id: push_account_postgresql
name: Push account for PostgreSQL
category: database
type:
- postgresql
method: push_account

View File

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

View File

@ -0,0 +1,6 @@
id: push_account_sqlserver
name: Push account for SQLServer
category: database
type:
- sqlserver
method: push_account

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

View 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: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'

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

View File

@ -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: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'

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

View File

@ -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: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'

View File

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

View File

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

View File

@ -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'),
),
]

View 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'),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
from .asset import *
from .host import *
from .database import *
from .web import *
from .cloud import *
from .custom import *
from .database import *
from .device import *
from .host import *
from .permission import *
from .web import *

View File

@ -2,15 +2,17 @@
#
import django_filters
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.status import HTTP_200_OK
from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task
from assets import serializers
from assets.exceptions import NotSupportedTemporarilyError
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend
from assets.models import Asset, Gateway
from assets.models import Asset, Gateway, Platform
from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual
from common.api import SuggestionMixin
from common.drf.filters import BaseFilterSet
@ -18,6 +20,7 @@ from common.utils import get_logger, is_uuid
from orgs.mixins import generics
from orgs.mixins.api import OrgBulkModelViewSet
from ..mixin import NodeFilterMixin
from ...notifications import BulkUpdatePlatformSkipAssetUserMsg
logger = get_logger(__file__)
__all__ = [
@ -99,16 +102,16 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
("platform", serializers.PlatformSerializer),
("suggestion", serializers.MiniAssetSerializer),
("gateways", serializers.GatewaySerializer),
("spec_info", serializers.SpecSerializer),
)
rbac_perms = (
("match", "assets.match_asset"),
("platform", "assets.view_platform"),
("gateways", "assets.view_gateway"),
("spec_info", "assets.view_asset"),
("info", "assets.view_asset"),
("gathered_info", "assets.view_asset"),
)
extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend]
skip_assets = []
def get_serializer_class(self):
cls = super().get_serializer_class()
@ -124,11 +127,6 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
serializer = super().get_serializer(instance=asset.platform)
return Response(serializer.data)
@action(methods=["GET"], detail=True, url_path="spec-info")
def spec_info(self, *args, **kwargs):
asset = super().get_object()
return Response(asset.spec_info)
@action(methods=["GET"], detail=True, url_path="gateways")
def gateways(self, *args, **kwargs):
asset = self.get_object()
@ -144,6 +142,31 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
return Response({'error': error}, status=400)
return super().create(request, *args, **kwargs)
def filter_bulk_update_data(self):
bulk_data = []
for data in self.request.data:
pk = data.get('id')
platform = data.get('platform')
if not platform:
bulk_data.append(data)
continue
asset = get_object_or_404(Asset, pk=pk)
platform = get_object_or_404(Platform, **platform)
if platform.type == asset.type:
bulk_data.append(data)
continue
self.skip_assets.append(asset)
return bulk_data
def bulk_update(self, request, *args, **kwargs):
bulk_data = self.filter_bulk_update_data()
request._full_data = bulk_data
response = super().bulk_update(request, *args, **kwargs)
if response.status_code == HTTP_200_OK and self.skip_assets:
user = request.user
BulkUpdatePlatformSkipAssetUserMsg(user, self.skip_assets).publish()
return response
class AssetsTaskMixin:
def perform_assets_task(self, serializer):
@ -154,8 +177,8 @@ class AssetsTaskMixin:
task = update_assets_hardware_info_manual(assets)
else:
asset = assets[0]
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 = test_assets_connectivity_manual(assets)
return task

View File

@ -0,0 +1,16 @@
from assets.models import Custom, Asset
from assets.serializers import CustomSerializer
from .asset import AssetViewSet
__all__ = ['CustomViewSet']
class CustomViewSet(AssetViewSet):
model = Custom
perm_model = Asset
def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes()
serializer_classes['default'] = CustomSerializer
return serializer_classes

View File

@ -1,8 +1,5 @@
from rest_framework.decorators import action
from rest_framework.response import Response
from assets.models import Host, Asset
from assets.serializers import HostSerializer, HostInfoSerializer
from assets.serializers import HostSerializer
from .asset import AssetViewSet
__all__ = ['HostViewSet']
@ -15,16 +12,4 @@ class HostViewSet(AssetViewSet):
def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes()
serializer_classes['default'] = HostSerializer
serializer_classes['info'] = HostInfoSerializer
return serializer_classes
@action(methods=["GET"], detail=True, url_path="info")
def info(self, *args, **kwargs):
asset = super().get_object()
serializer = self.get_serializer(asset.info)
data = serializer.data
data['asset'] = {
'id': asset.id, 'name': asset.name,
'address': asset.address
}
return Response(data)

View File

@ -8,6 +8,15 @@ from common.utils import lazyproperty, timeit
class SerializeToTreeNodeMixin:
request: Request
@lazyproperty
def is_sync(self):
sync_paths = ['/api/v1/perms/users/self/nodes/all-with-assets/tree/']
for p in sync_paths:
if p == self.request.path:
return True
return False
@timeit
def serialize_nodes(self, nodes: List[Node], with_asset_amount=False):
@ -17,6 +26,16 @@ class SerializeToTreeNodeMixin:
else:
def _name(node: Node):
return node.value
def _open(node):
if not self.is_sync:
# 异步加载资产树时,默认展开节点
return True
if not node.parent_key:
return True
else:
return False
data = [
{
'id': node.key,
@ -24,7 +43,7 @@ class SerializeToTreeNodeMixin:
'title': _name(node),
'pId': node.parent_key,
'isParent': True,
'open': True,
'open': _open(node),
'meta': {
'data': {
"id": node.id,
@ -52,7 +71,7 @@ class SerializeToTreeNodeMixin:
{
'id': str(asset.id),
'name': asset.name,
'title': asset.address,
'title': f'{asset.address}\n{asset.comment}',
'pId': get_pid(asset),
'isParent': False,
'open': False,
@ -64,6 +83,8 @@ class SerializeToTreeNodeMixin:
'platform_type': asset.platform.type,
'org_name': asset.org_name,
'sftp': asset.platform_id in sftp_enabled_platform,
'name': asset.name,
'address': asset.address
},
}
}

View File

@ -1,10 +1,16 @@
from rest_framework import generics
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from assets.const import AllTypes
from assets.models import Platform
from assets.models import Platform, Node, Asset
from assets.serializers import PlatformSerializer
from common.api import JMSModelViewSet
from common.permissions import IsValidUser
from common.serializers import GroupedChoiceSerializer
__all__ = ['AssetPlatformViewSet']
__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi']
class AssetPlatformViewSet(JMSModelViewSet):
@ -18,12 +24,13 @@ class AssetPlatformViewSet(JMSModelViewSet):
rbac_perms = {
'categories': 'assets.view_platform',
'type_constraints': 'assets.view_platform',
'ops_methods': 'assets.view_platform'
'ops_methods': 'assets.view_platform',
'filter_nodes_assets': 'assets.view_platform'
}
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.filter(type__in=AllTypes.get_types())
queryset = queryset.filter(type__in=AllTypes.get_types_values())
return queryset
def get_object(self):
@ -38,3 +45,44 @@ class AssetPlatformViewSet(JMSModelViewSet):
request, message={"detail": "Internal platform"}
)
return super().check_object_permissions(request, obj)
@action(methods=['post'], detail=False, url_path='filter-nodes-assets')
def filter_nodes_assets(self, request, *args, **kwargs):
node_ids = request.data.get('node_ids', [])
asset_ids = request.data.get('asset_ids', [])
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 = Asset.objects.filter(id__in=asset_ids).values_list('id', flat=True)
platform_ids = Asset.objects.filter(
id__in=set(list(direct_asset_ids) + list(node_asset_ids))
).values_list('platform_id', flat=True)
platforms = Platform.objects.filter(id__in=platform_ids)
serializer = self.get_serializer(platforms, many=True)
return Response(serializer.data)
class PlatformAutomationMethodsApi(generics.ListAPIView):
permission_classes = (IsValidUser,)
@staticmethod
def automation_methods():
return AllTypes.get_automation_methods()
def generate_serializer_fields(self):
data = self.automation_methods()
fields = {
i['id']: i['params_serializer']()
if i['params_serializer'] else None
for i in data
}
return fields
def get_serializer_class(self):
fields = self.generate_serializer_fields()
serializer_name = 'AutomationMethodsSerializer'
return type(serializer_name, (serializers.Serializer,), fields)
def list(self, request, *args, **kwargs):
data = self.generate_serializer_fields()
serializer = self.get_serializer(data)
return Response(serializer.data)

View File

@ -41,6 +41,26 @@ class BasePlaybookManager:
self.method_hosts_mapper = defaultdict(list)
self.playbooks = []
self.gateway_servers = dict()
params = self.execution.snapshot.get('params')
self.params = params or {}
def get_params(self, automation, method_type):
method_attr = '{}_method'.format(method_type)
method_params = '{}_params'.format(method_type)
method_id = getattr(automation, method_attr)
automation_params = getattr(automation, method_params)
serializer = self.method_id_meta_mapper[method_id]['params_serializer']
if serializer is None:
return {}
data = self.params.get(method_id, {})
params = serializer(data).data
return {
field_name: automation_params.get(field_name, '')
if not params[field_name] else params[field_name]
for field_name in params
}
@property
def platform_automation_methods(self):
@ -101,8 +121,9 @@ class BasePlaybookManager:
return host
def host_callback(self, host, automation=None, **kwargs):
enabled_attr = '{}_enabled'.format(self.__class__.method_type())
method_attr = '{}_method'.format(self.__class__.method_type())
method_type = self.__class__.method_type()
enabled_attr = '{}_enabled'.format(method_type)
method_attr = '{}_method'.format(method_type)
method_enabled = automation and \
getattr(automation, enabled_attr) and \
@ -114,6 +135,7 @@ class BasePlaybookManager:
return host
host = self.convert_cert_to_file(host, kwargs.get('path_dir'))
host['params'] = self.get_params(automation, method_type)
return host
@staticmethod
@ -239,10 +261,12 @@ class BasePlaybookManager:
jms_asset, jms_gateway = host['jms_asset'], host.get('gateway')
if not jms_gateway:
continue
server = SSHTunnelForwarder(
(jms_gateway['address'], jms_gateway['port']),
ssh_username=jms_gateway['username'],
ssh_password=jms_gateway['secret'],
ssh_pkey=jms_gateway['private_key_path'],
remote_bind_address=(jms_asset['address'], jms_asset['port'])
)
try:
@ -252,8 +276,8 @@ class BasePlaybookManager:
print('\033[31m %s \033[0m\n' % err_msg)
not_valid.append(k)
else:
jms_asset['address'] = '127.0.0.1'
jms_asset['port'] = server.local_bind_port
host['ansible_host'] = jms_asset['address'] = '127.0.0.1'
host['ansible_port'] = jms_asset['port'] = server.local_bind_port
servers.append(server)
# 网域不可连接的,就不继续执行此资源的后续任务了

View File

@ -29,7 +29,7 @@ class GatherFactsManager(BasePlaybookManager):
asset = self.host_asset_mapper.get(host)
if asset and info:
info = self.format_asset_info(asset.type, info)
asset.info = info
asset.save(update_fields=['info'])
asset.gathered_info = info
asset.save(update_fields=['gathered_info'])
else:
logger.error("Not found info: {}".format(host))

View File

@ -1,8 +1,9 @@
import os
import yaml
import json
import os
from functools import partial
import yaml
def check_platform_method(manifest, manifest_path):
required_keys = ['category', 'method', 'name', 'id', 'type']
@ -21,6 +22,15 @@ def check_platform_methods(methods):
raise ValueError("Duplicate id: {}".format(_id))
def generate_serializer(data):
from common.serializers import create_serializer_class
params = data.pop('params', None)
if not params:
return None
serializer_name = data['id'].title().replace('_', '') + 'Serializer'
return create_serializer_class(serializer_name, params)
def get_platform_automation_methods(path):
methods = []
for root, dirs, files in os.walk(path, topdown=False):
@ -33,6 +43,7 @@ def get_platform_automation_methods(path):
manifest = yaml.safe_load(f)
check_platform_method(manifest, path)
manifest['dir'] = os.path.dirname(path)
manifest['params_serializer'] = generate_serializer(manifest)
methods.append(manifest)
check_platform_methods(methods)
@ -46,12 +57,12 @@ def filter_key(manifest, attr, value):
return value in manifest_value or 'all' in manifest_value
def filter_platform_methods(category, tp, method=None, methods=None):
def filter_platform_methods(category, tp_name, method=None, methods=None):
methods = platform_automation_methods if methods is None else methods
if category:
methods = filter(partial(filter_key, attr='category', value=category), methods)
if tp:
methods = filter(partial(filter_key, attr='type', value=tp), methods)
if tp_name:
methods = filter(partial(filter_key, attr='type', value=tp_name), methods)
if method:
methods = filter(lambda x: x['method'] == method, methods)
return methods

View File

@ -4,10 +4,21 @@ from jumpserver.utils import has_valid_xpack_license
from .protocol import Protocol
class Type:
def __init__(self, label, value):
self.label = label
self.value = value
def __str__(self):
return self.value
class BaseType(TextChoices):
"""
约束应该考虑代是对平台对限制避免多余对选项: mysql 开启 ssh, 或者开启了也没有作用, 比如 k8s 开启了 domain目前还不支持
约束应该考虑代是对平台对限制避免多余对选项: mysql 开启 ssh,
或者开启了也没有作用, 比如 k8s 开启了 domain目前还不支持
"""
@classmethod
def get_constrains(cls):
constrains = {}
@ -20,7 +31,7 @@ class BaseType(TextChoices):
protocols_default = protocols.pop('*', {})
automation_default = automation.pop('*', {})
for k, v in cls.choices:
for k, v in cls.get_choices():
tp_base = {**base_default, **base.get(k, {})}
tp_auto = {**automation_default, **automation.get(k, {})}
tp_protocols = {**protocols_default, **protocols.get(k, {})}
@ -35,8 +46,12 @@ class BaseType(TextChoices):
choices = protocol.get('choices', [])
if choices == '__self__':
choices = [tp]
protocols = [{'name': name, **settings.get(name, {})} for name in choices]
protocols[0]['primary'] = True
protocols = [
{'name': name, **settings.get(name, {})}
for name in choices
]
if protocols:
protocols[0]['default'] = True
return protocols
@classmethod
@ -56,23 +71,21 @@ class BaseType(TextChoices):
raise NotImplementedError
@classmethod
def get_community_types(cls):
raise NotImplementedError
def _get_choices_to_types(cls):
choices = cls.get_choices()
return [Type(label, value) for value, label in choices]
@classmethod
def get_types(cls):
tps = [tp for tp in cls]
tps = cls._get_choices_to_types()
if not has_valid_xpack_license():
tps = cls.get_community_types()
return tps
@classmethod
def get_community_types(cls):
return cls._get_choices_to_types()
@classmethod
def get_choices(cls):
tps = cls.get_types()
cls_choices = cls.choices
return [
choice for choice in cls_choices
if choice[0] in tps
]
return cls.choices

View File

@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _
from common.db.models import ChoicesMixin
__all__ = ['Category']
@ -13,13 +12,10 @@ class Category(ChoicesMixin, models.TextChoices):
DATABASE = 'database', _("Database")
CLOUD = 'cloud', _("Cloud service")
WEB = 'web', _("Web")
CUSTOM = 'custom', _("Custom type")
@classmethod
def filter_choices(cls, category):
_category = getattr(cls, category.upper(), None)
choices = [(_category.value, _category.label)] if _category else cls.choices
return choices

View File

@ -15,6 +15,9 @@ class CloudTypes(BaseType):
'charset_enabled': False,
'domain_enabled': False,
'su_enabled': False,
},
cls.K8S: {
'domain_enabled': True,
}
}

View File

@ -0,0 +1,60 @@
from .base import BaseType
class CustomTypes(BaseType):
@classmethod
def get_choices(cls):
try:
platforms = list(cls.get_custom_platforms())
except Exception:
return []
types = [p.type for p in platforms]
return [(t, t) for t in types]
@classmethod
def _get_base_constrains(cls) -> dict:
return {
'*': {
'charset_enabled': False,
'domain_enabled': False,
'su_enabled': False,
},
}
@classmethod
def _get_automation_constrains(cls) -> dict:
constrains = {
'*': {
'ansible_enabled': False,
'ansible_config': {},
'gather_facts_enabled': False,
'verify_account_enabled': False,
'change_secret_enabled': False,
'push_account_enabled': False,
'gather_accounts_enabled': False,
}
}
return constrains
@classmethod
def _get_protocol_constrains(cls) -> dict:
constrains = {}
for platform in cls.get_custom_platforms():
choices = list(platform.protocols.values_list('name', flat=True))
if platform.type in constrains:
choices = constrains[platform.type]['choices'] + choices
constrains[platform.type] = {'choices': choices}
return constrains
@classmethod
def internal_platforms(cls):
return {
# cls.PUBLIC: [],
# cls.PRIVATE: [{'name': 'Vmware-vSphere'}],
# cls.K8S: [{'name': 'Kubernetes'}],
}
@classmethod
def get_custom_platforms(cls):
from assets.models import Platform
return Platform.objects.filter(category='custom')

View File

@ -15,7 +15,8 @@ class DeviceTypes(BaseType):
'*': {
'charset_enabled': False,
'domain_enabled': True,
'su_enabled': False,
'su_enabled': True,
'su_methods': ['enable', 'super', 'super_level']
}
}

View File

@ -19,10 +19,7 @@ class HostTypes(BaseType):
'charset': 'utf-8', # default
'domain_enabled': True,
'su_enabled': True,
'su_methods': [
{'name': 'sudo su', 'id': 'sudo su'},
{'name': 'su -', 'id': 'su -'}
],
'su_methods': ['sudo', 'su'],
},
cls.WINDOWS: {
'su_enabled': False,
@ -39,7 +36,7 @@ class HostTypes(BaseType):
'choices': ['ssh', 'telnet', 'vnc', 'rdp']
},
cls.WINDOWS: {
'choices': ['rdp', 'ssh', 'vnc']
'choices': ['rdp', 'ssh', 'vnc', 'winrm']
}
}
@ -61,7 +58,7 @@ class HostTypes(BaseType):
cls.WINDOWS: {
'ansible_config': {
'ansible_shell_type': 'cmd',
'ansible_connection': 'ssh',
'ansible_connection': 'smart',
},
},
cls.OTHER_HOST: {

View File

@ -10,6 +10,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet'
vnc = 'vnc', 'VNC'
winrm = 'winrm', 'WinRM'
mysql = 'mysql', 'MySQL'
mariadb = 'mariadb', 'MariaDB'
@ -51,6 +52,13 @@ class Protocol(ChoicesMixin, models.TextChoices):
'port': 23,
'secret_types': ['password'],
},
cls.winrm: {
'port': 5985,
'secret_types': ['password'],
'setting': {
'use_ssl': False,
}
},
}
@classmethod
@ -116,7 +124,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
'setting': {
'username_selector': 'name=username',
'password_selector': 'name=password',
'submit_selector': 'id=longin_button',
'submit_selector': 'id=login_button',
}
},
}
@ -128,3 +136,11 @@ class Protocol(ChoicesMixin, models.TextChoices):
**cls.database_protocols(),
**cls.cloud_protocols()
}
@classmethod
def protocol_secret_types(cls):
settings = cls.settings()
return {
protocol: settings[protocol]['secret_types'] or ['password']
for protocol in cls.settings()
}

View File

@ -6,6 +6,7 @@ from django.utils.translation import gettext as _
from common.db.models import ChoicesMixin
from .category import Category
from .cloud import CloudTypes
from .custom import CustomTypes
from .database import DatabaseTypes
from .device import DeviceTypes
from .host import HostTypes
@ -16,7 +17,7 @@ class AllTypes(ChoicesMixin):
choices: list
includes = [
HostTypes, DeviceTypes, DatabaseTypes,
CloudTypes, WebTypes,
CloudTypes, WebTypes, CustomTypes
]
_category_constrains = {}
@ -24,22 +25,29 @@ class AllTypes(ChoicesMixin):
def choices(cls):
choices = []
for tp in cls.includes:
choices.extend(tp.choices)
choices.extend(tp.get_choices())
return choices
@classmethod
def get_choices(cls):
return cls.choices()
@classmethod
def filter_choices(cls, category):
choices = dict(cls.category_types()).get(category, cls).choices
choices = dict(cls.category_types()).get(category, cls).get_choices()
return choices() if callable(choices) else choices
@classmethod
def get_constraints(cls, category, tp):
def get_constraints(cls, category, tp_name):
if not isinstance(tp_name, str):
tp_name = tp_name.value
types_cls = dict(cls.category_types()).get(category)
if not types_cls:
return {}
type_constraints = types_cls.get_constrains()
constraints = type_constraints.get(tp, {})
cls.set_automation_methods(category, tp, constraints)
constraints = type_constraints.get(tp_name, {})
cls.set_automation_methods(category, tp_name, constraints)
return constraints
@classmethod
@ -56,7 +64,7 @@ class AllTypes(ChoicesMixin):
return asset_methods + account_methods
@classmethod
def set_automation_methods(cls, category, tp, constraints):
def set_automation_methods(cls, category, tp_name, constraints):
from assets.automations import filter_platform_methods
automation = constraints.get('automation', {})
automation_methods = {}
@ -66,7 +74,7 @@ class AllTypes(ChoicesMixin):
continue
item_name = item.replace('_enabled', '')
methods = filter_platform_methods(
category, tp, item_name, methods=platform_automation_methods
category, tp_name, item_name, methods=platform_automation_methods
)
methods = [{'name': m['name'], 'id': m['id']} for m in methods]
automation_methods[item_name + '_methods'] = methods
@ -113,7 +121,7 @@ class AllTypes(ChoicesMixin):
@classmethod
def grouped_choices(cls):
grouped_types = [(str(ca), tp.choices) for ca, tp in cls.category_types()]
grouped_types = [(str(ca), tp.get_choices()) for ca, tp in cls.category_types()]
return grouped_types
@classmethod
@ -136,16 +144,22 @@ class AllTypes(ChoicesMixin):
(Category.HOST, HostTypes),
(Category.DEVICE, DeviceTypes),
(Category.DATABASE, DatabaseTypes),
(Category.CLOUD, CloudTypes),
(Category.WEB, WebTypes),
(Category.CLOUD, CloudTypes)
(Category.CUSTOM, CustomTypes),
)
@classmethod
def get_types(cls):
tps = []
choices = []
for i in dict(cls.category_types()).values():
tps.extend(i.get_types())
return tps
choices.extend(i.get_types())
return choices
@classmethod
def get_types_values(cls):
choices = cls.get_types()
return [c.value for c in choices]
@staticmethod
def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None):

View File

@ -49,7 +49,10 @@ def migrate_asset_accounts(apps, schema_editor):
account_values.update(auth_book_auth)
auth_infos = []
username = account_values['username']
username = account_values.get('username')
if not username:
continue
for attr in auth_attrs:
secret = account_values.pop(attr, None)
if not secret:

View File

@ -0,0 +1,49 @@
# Generated by Django 3.2.17 on 2023-03-21 08:33
from django.db import migrations, models
from assets.const import AllTypes
def migrate_platform_charset(apps, schema_editor):
platform_model = apps.get_model('assets', 'Platform')
platform_model.objects.filter(charset='utf8').update(charset='utf-8')
def migrate_platform_protocol_primary(apps, schema_editor):
platform_model = apps.get_model('assets', 'Platform')
platforms = platform_model.objects.all()
for platform in platforms:
p = platform.protocols.first()
if not p:
continue
p.primary = True
p.save()
def migrate_internal_platforms(apps, schema_editor):
platform_cls = apps.get_model('assets', 'Platform')
AllTypes.create_or_update_internal_platforms(platform_cls)
class Migration(migrations.Migration):
dependencies = [
('assets', '0110_auto_20230315_1741'),
]
operations = [
migrations.AddField(
model_name='platformprotocol',
name='primary',
field=models.BooleanField(default=False, verbose_name='Primary'),
),
migrations.AddField(
model_name='platformprotocol',
name='public',
field=models.BooleanField(default=True, verbose_name='Public'),
),
migrations.RunPython(migrate_platform_charset),
migrations.RunPython(migrate_platform_protocol_primary),
migrations.RunPython(migrate_internal_platforms),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 3.2.17 on 2023-04-04 08:31
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0111_auto_20230321_1633'),
]
operations = [
migrations.CreateModel(
name='Custom',
fields=[
('asset_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='assets.asset')),
],
options={
'verbose_name': 'Custom asset',
},
bases=('assets.asset',),
),
migrations.AddField(
model_name='platform',
name='custom_fields',
field=models.JSONField(default=list, null=True, verbose_name='Custom fields'),
),
migrations.AddField(
model_name='asset',
name='custom_info',
field=models.JSONField(default=dict, verbose_name='Custom info'),
),
migrations.AddField(
model_name='asset',
name='gathered_info',
field=models.JSONField(blank=True, default=dict, verbose_name='Gathered info'),
),
migrations.RemoveField(
model_name='asset',
name='info',
),
]

View File

@ -0,0 +1,66 @@
# Generated by Django 3.2.16 on 2023-04-11 10:14
from django.db import migrations, models
from assets.const import AllTypes
def migrate_automation_push_account_params(apps, schema_editor):
platform_automation_model = apps.get_model('assets', 'PlatformAutomation')
platform_automation_methods = AllTypes.get_automation_methods()
methods_id_data_map = {
i['id']: None if i['params_serializer'] is None else i['params_serializer']({}).data
for i in platform_automation_methods
if i['method'] == 'push_account'
}
automation_objs = []
for automation in platform_automation_model.objects.all():
push_account_method = automation.push_account_method
if not push_account_method:
continue
value = methods_id_data_map.get(push_account_method)
if value is None:
continue
automation.push_account_params = value
automation_objs.append(automation)
platform_automation_model.objects.bulk_update(automation_objs, ['push_account_params'])
class Migration(migrations.Migration):
dependencies = [
('assets', '0112_auto_20230404_1631'),
]
operations = [
migrations.AddField(
model_name='platformautomation',
name='change_secret_params',
field=models.JSONField(default=dict, verbose_name='Change secret params'),
),
migrations.AddField(
model_name='platformautomation',
name='gather_accounts_params',
field=models.JSONField(default=dict, verbose_name='Gather facts params'),
),
migrations.AddField(
model_name='platformautomation',
name='gather_facts_params',
field=models.JSONField(default=dict, verbose_name='Gather facts params'),
),
migrations.AddField(
model_name='platformautomation',
name='ping_params',
field=models.JSONField(default=dict, verbose_name='Ping params'),
),
migrations.AddField(
model_name='platformautomation',
name='push_account_params',
field=models.JSONField(default=dict, verbose_name='Push account params'),
),
migrations.AddField(
model_name='platformautomation',
name='verify_account_params',
field=models.JSONField(default=dict, verbose_name='Verify account params'),
),
migrations.RunPython(migrate_automation_push_account_params),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 3.2.17 on 2023-04-11 11:17
from django.db import migrations
def migrate_device_platform_su_method(apps, schema_editor):
platform_model = apps.get_model('assets', 'Platform')
device_map = {
'Huawei': 'super',
'Cisco': 'enable',
'H3C': 'super_level',
}
platforms = platform_model.objects.filter(name__in=device_map.keys())
print()
for platform in platforms:
print("Migrate platform su method: {}".format(platform.name))
if platform.name not in device_map:
continue
platform.su_method = device_map[platform.name]
platform.su_enabled = True
platform.save(update_fields=['su_method', 'su_enabled'])
class Migration(migrations.Migration):
dependencies = [
('assets', '0112_auto_20230404_1631'),
]
operations = [
migrations.RunPython(migrate_device_platform_su_method)
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.16 on 2023-04-13 10:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0113_auto_20230411_1814'),
]
operations = [
migrations.AddField(
model_name='baseautomation',
name='params',
field=models.JSONField(default=dict, verbose_name='Params'),
),
]

View File

@ -1,6 +1,7 @@
from .cloud import *
from .common import *
from .host import *
from .custom import *
from .database import *
from .device import *
from .host import *
from .web import *
from .cloud import *

View File

@ -1,12 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
import json
import logging
from collections import defaultdict
from django.db import models
from django.forms import model_to_dict
from django.utils.translation import ugettext_lazy as _
from assets import const
@ -94,6 +94,20 @@ class Protocol(models.Model):
def __str__(self):
return '{}/{}'.format(self.name, self.port)
@lazyproperty
def asset_platform_protocol(self):
protocols = self.asset.platform.protocols.values('name', 'public', 'setting')
protocols = list(filter(lambda p: p['name'] == self.name, protocols))
return protocols[0] if len(protocols) > 0 else {}
@property
def setting(self):
return self.asset_platform_protocol.get('setting', {})
@property
def public(self):
return self.asset_platform_protocol.get('public', True)
class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
Category = const.Category
@ -108,7 +122,8 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
info = models.JSONField(verbose_name=_('Info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict)
objects = AssetManager.from_queryset(AssetQuerySet)()
@ -148,20 +163,27 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
return self.get_spec_values(instance, spec_fields)
@lazyproperty
def auto_info(self):
def info(self):
info = {}
info.update(self.gathered_info or {})
info.update(self.custom_info or {})
info.update(self.spec_info or {})
return info
@lazyproperty
def auto_config(self):
platform = self.platform
automation = self.platform.automation
return {
auto_config = {
'su_enabled': platform.su_enabled,
'ping_enabled': automation.ping_enabled,
'domain_enabled': platform.domain_enabled,
'ansible_enabled': automation.ansible_enabled,
'push_account_enabled': automation.push_account_enabled,
'gather_facts_enabled': automation.gather_facts_enabled,
'change_secret_enabled': automation.change_secret_enabled,
'verify_account_enabled': automation.verify_account_enabled,
'gather_accounts_enabled': automation.gather_accounts_enabled,
'ansible_enabled': False
}
if not automation:
return auto_config
auto_config.update(model_to_dict(automation))
return auto_config
def get_target_ip(self):
return self.address
@ -191,25 +213,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
names.append(n.name + ':' + n.value)
return names
@lazyproperty
def primary_protocol(self):
from assets.const.types import AllTypes
primary_protocol_name = AllTypes.get_primary_protocol_name(self.category, self.type)
protocol = self.protocols.filter(name=primary_protocol_name).first()
return protocol
@lazyproperty
def protocol(self):
if not self.primary_protocol:
return 'none'
return self.primary_protocol.name
@lazyproperty
def port(self):
if not self.primary_protocol:
return 0
return self.primary_protocol.port
@lazyproperty
def type(self):
return self.platform.type
@ -275,6 +278,22 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
tree_node = TreeNode(**data)
return tree_node
@staticmethod
def get_secret_type_assets(asset_ids, secret_type):
assets = Asset.objects.filter(id__in=asset_ids)
asset_protocol = assets.prefetch_related('protocols').values_list('id', 'protocols__name')
protocol_secret_types_map = const.Protocol.protocol_secret_types()
asset_secret_types_mapp = defaultdict(set)
for asset_id, protocol in asset_protocol:
secret_types = set(protocol_secret_types_map.get(protocol, []))
asset_secret_types_mapp[asset_id].update(secret_types)
return [
asset for asset in assets
if secret_type in asset_secret_types_mapp.get(asset.id, [])
]
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Asset")

View File

@ -0,0 +1,8 @@
from django.utils.translation import gettext_lazy as _
from .common import Asset
class Custom(Asset):
class Meta:
verbose_name = _("Custom asset")

View File

@ -19,6 +19,7 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
type = models.CharField(max_length=16, verbose_name=_('Type'))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
params = models.JSONField(default=dict, verbose_name=_("Params"))
def __str__(self):
return self.name + '@' + str(self.created_by)

View File

@ -2,11 +2,10 @@
#
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgManager
from assets.models.platform import Platform
from assets.const import GATEWAY_NAME
from assets.models.platform import Platform
from common.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgManager
from .asset.host import Host
logger = get_logger(__file__)
@ -57,6 +56,14 @@ class Gateway(Host):
account = self.select_account
return account.password if account else None
@lazyproperty
def port(self):
protocol = self.protocols.filter(name='ssh').first()
if protocol:
return protocol.port
else:
return '22'
@lazyproperty
def private_key(self):
account = self.select_account

View File

@ -10,56 +10,68 @@ __all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
class PlatformProtocol(models.Model):
SETTING_ATTRS = {
'console': False,
'security': 'any,tls,rdp',
'sftp_enabled': True,
'sftp_home': '/tmp'
}
default = models.BooleanField(default=False, verbose_name=_('Default'))
required = models.BooleanField(default=False, verbose_name=_('Required'))
name = models.CharField(max_length=32, verbose_name=_('Name'))
port = models.IntegerField(verbose_name=_('Port'))
primary = models.BooleanField(default=False, verbose_name=_('Primary'))
required = models.BooleanField(default=False, verbose_name=_('Required'))
default = models.BooleanField(default=False, verbose_name=_('Default'))
public = models.BooleanField(default=True, verbose_name=_('Public'))
setting = models.JSONField(verbose_name=_('Setting'), default=dict)
platform = models.ForeignKey('Platform', on_delete=models.CASCADE, related_name='protocols')
def __str__(self):
return '{}/{}'.format(self.name, self.port)
@property
def primary(self):
primary_protocol_name = AllTypes.get_primary_protocol_name(
self.platform.category, self.platform.type
)
return self.name == primary_protocol_name
@property
def secret_types(self):
return Protocol.settings().get(self.name, {}).get('secret_types')
return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
def set_public(self):
private_protocol_set = ('winrm',)
self.public = self.name not in private_protocol_set
def save(self, **kwargs):
self.set_public()
return super().save(**kwargs)
class PlatformAutomation(models.Model):
ansible_enabled = models.BooleanField(default=False, verbose_name=_("Enabled"))
ansible_config = models.JSONField(default=dict, verbose_name=_("Ansible config"))
ping_enabled = models.BooleanField(default=False, verbose_name=_("Ping enabled"))
ping_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Ping method"))
ping_params = models.JSONField(default=dict, verbose_name=_("Ping params"))
gather_facts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
gather_facts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method"))
gather_facts_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")
)
gather_facts_params = models.JSONField(default=dict, verbose_name=_("Gather facts params"))
change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change secret enabled"))
change_secret_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Change secret method")
)
change_secret_params = models.JSONField(default=dict, verbose_name=_("Change secret params"))
push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled"))
push_account_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Push account method")
)
push_account_params = models.JSONField(default=dict, verbose_name=_("Push account params"))
verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled"))
verify_account_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Verify account method"))
max_length=32, blank=True, null=True, verbose_name=_("Verify account method")
)
verify_account_params = models.JSONField(default=dict, verbose_name=_("Verify account params"))
gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
gather_accounts_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")
)
gather_accounts_params = models.JSONField(default=dict, verbose_name=_("Gather facts params"))
class Platform(JMSBaseModel):
@ -80,14 +92,18 @@ class Platform(JMSBaseModel):
internal = models.BooleanField(default=False, verbose_name=_("Internal"))
# 资产有关的
charset = models.CharField(
default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset")
default=CharsetChoices.utf8, choices=CharsetChoices.choices,
max_length=8, verbose_name=_("Charset")
)
domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
# 账号有关的
su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled"))
su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method"))
automation = models.OneToOneField(PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
blank=True, null=True, verbose_name=_("Automation"))
automation = models.OneToOneField(
PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
blank=True, null=True, verbose_name=_("Automation")
)
custom_fields = models.JSONField(null=True, default=list, verbose_name=_("Custom fields"))
@property
def type_constraints(self):
@ -100,11 +116,6 @@ class Platform(JMSBaseModel):
)
return linux.id
@property
def primary_protocol(self):
primary_protocol_name = AllTypes.get_primary_protocol_name(self.category, self.type)
return self.protocols.filter(name=primary_protocol_name).first()
def __str__(self):
return self.name

View File

@ -0,0 +1,25 @@
from django.utils.translation import ugettext as _
from notifications.notifications import UserMessage
class BulkUpdatePlatformSkipAssetUserMsg(UserMessage):
def __init__(self, user, assets):
super().__init__(user)
self.assets = assets
def get_html_msg(self) -> dict:
subject = _("Batch update platform in assets, skipping assets that do not meet platform type")
message = f'<ol>{"".join([f"<li>{asset}</li>" for asset in self.assets])}</ol>'
return {
'subject': subject,
'message': message
}
@classmethod
def gen_test_msg(cls):
from users.models import User
from assets.models import Asset
user = User.objects.first()
assets = Asset.objects.all()[:10]
return cls(user, assets)

View File

@ -1,6 +1,8 @@
# No pass
from .cloud import *
from .common import *
from .host import *
from .custom import *
from .database import *
from .device import *
from .cloud import *
from .host import *
from .web import *

View File

@ -1,15 +1,18 @@
# -*- coding: utf-8 -*-
#
import re
from django.db.models import F
from django.db.transaction import atomic
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from accounts.models import Account
from accounts.serializers import AccountSerializerCreateValidateMixin
from accounts.serializers import AuthValidateMixin
from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer
from accounts.serializers import AccountSerializer
from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer, \
MethodSerializer
from common.serializers.dynamic import create_serializer_class
from common.serializers.fields import LabeledChoiceField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ...const import Category, AllTypes
@ -19,9 +22,11 @@ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer',
'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer',
'AccountSecretSerializer', 'SpecSerializer'
'AccountSecretSerializer', 'AssetProtocolsPermsSerializer'
]
uuid_pattern = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
class AssetProtocolsSerializer(serializers.ModelSerializer):
port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=1)
@ -38,6 +43,11 @@ class AssetProtocolsSerializer(serializers.ModelSerializer):
fields = ['name', 'port']
class AssetProtocolsPermsSerializer(AssetProtocolsSerializer):
class Meta(AssetProtocolsSerializer.Meta):
fields = AssetProtocolsSerializer.Meta.fields + ['public', 'setting']
class AssetLabelSerializer(serializers.ModelSerializer):
class Meta:
model = Label
@ -59,45 +69,39 @@ class AssetPlatformSerializer(serializers.ModelSerializer):
}
class AssetAccountSerializer(
AuthValidateMixin,
AccountSerializerCreateValidateMixin,
CommonModelSerializer
):
class AssetAccountSerializer(AccountSerializer):
add_org_fields = False
push_now = serializers.BooleanField(
default=False, label=_("Push now"), write_only=True
)
template = serializers.BooleanField(
default=False, label=_("Template"), write_only=True
)
name = serializers.CharField(max_length=128, required=True, label=_("Name"))
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, required=False, write_only=True)
clone_id: str
class Meta:
model = Account
fields_mini = [
'id', 'name', 'username', 'privileged',
'is_active', 'version', 'secret_type',
def to_internal_value(self, data):
clone_id = data.pop('id', None)
ret = super().to_internal_value(data)
self.clone_id = clone_id
return ret
def set_secret(self, attrs):
_id = self.clone_id
if not _id:
return attrs
account = Account.objects.get(id=_id)
attrs['secret'] = account.secret
return attrs
def validate(self, attrs):
attrs = super().validate(attrs)
return self.set_secret(attrs)
class Meta(AccountSerializer.Meta):
fields = [
f for f in AccountSerializer.Meta.fields
if f not in ['spec_info']
]
fields_write_only = [
'secret', 'passphrase', 'push_now', 'template'
]
fields = fields_mini + fields_write_only
extra_kwargs = {
'secret': {'write_only': True},
**AccountSerializer.Meta.extra_kwargs,
}
def validate_push_now(self, value):
request = self.context['request']
if not request.user.has_perms('accounts.push_account'):
return False
return value
def validate_name(self, value):
if not value:
value = self.initial_data.get('username')
return value
class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
class Meta:
@ -110,44 +114,32 @@ class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
}
class SpecSerializer(serializers.Serializer):
# 数据库
db_name = serializers.CharField(label=_("Database"), max_length=128, required=False)
use_ssl = serializers.BooleanField(label=_("Use SSL"), required=False)
allow_invalid_cert = serializers.BooleanField(label=_("Allow invalid cert"), required=False)
# Web
autofill = serializers.CharField(label=_("Auto fill"), required=False)
username_selector = serializers.CharField(label=_("Username selector"), required=False)
password_selector = serializers.CharField(label=_("Password selector"), required=False)
submit_selector = serializers.CharField(label=_("Submit selector"), required=False)
script = serializers.JSONField(label=_("Script"), required=False)
class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
labels = AssetLabelSerializer(many=True, required=False, label=_('Label'))
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account'))
nodes_display = serializers.ListField(read_only=True, label=_("Node path"))
nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
custom_info = MethodSerializer(label=_('Custom info'))
class Meta:
model = Asset
fields_mini = ['id', 'name', 'address']
fields_small = fields_mini + ['is_active', 'comment']
fields_small = fields_mini + ['custom_info', 'is_active', 'comment']
fields_fk = ['domain', 'platform']
fields_m2m = [
'nodes', 'labels', 'protocols',
'nodes_display', 'accounts'
'nodes_display', 'accounts',
]
read_only_fields = [
'category', 'type', 'connectivity', 'auto_info',
'category', 'type', 'connectivity', 'auto_config',
'date_verified', 'created_by', 'date_created',
]
fields = fields_small + fields_fk + fields_m2m + read_only_fields
fields_unexport = ['auto_info']
fields_unexport = ['auto_config']
extra_kwargs = {
'auto_info': {'label': _('Auto info')},
'auto_config': {'label': _('Auto info')},
'name': {'label': _("Name")},
'address': {'label': _('Address')},
'nodes_display': {'label': _('Node path')},
@ -197,6 +189,36 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
.annotate(type=F("platform__type"))
return queryset
def get_custom_info_serializer(self):
request = self.context.get('request')
default_field = serializers.DictField(required=False, label=_('Custom info'))
if not request:
return default_field
if self.instance and isinstance(self.instance, list):
return default_field
if not self.instance and uuid_pattern.findall(request.path):
pk = uuid_pattern.findall(request.path)[0]
self.instance = Asset.objects.filter(id=pk).first()
platform = None
if self.instance:
platform = self.instance.platform
elif request.query_params.get('platform'):
platform_id = request.query_params.get('platform')
platform_id = int(platform_id) if platform_id.isdigit() else 0
platform = Platform.objects.filter(id=platform_id).first()
if not platform:
return default_field
custom_fields = platform.custom_fields
if not custom_fields:
return default_field
name = platform.name.title() + 'CustomSerializer'
return create_serializer_class(name, custom_fields)()
@staticmethod
def perform_nodes_display_create(instance, nodes_display):
if not nodes_display:
@ -276,8 +298,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
if not accounts_data:
return
for data in accounts_data:
data['asset'] = asset
AssetAccountSerializer().create(data)
data['asset'] = asset.id
s = AssetAccountSerializer(data=accounts_data, many=True)
s.is_valid(raise_exception=True)
s.save()
@atomic
def create(self, validated_data):
@ -300,16 +325,46 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
class DetailMixin(serializers.Serializer):
accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts'))
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
auto_info = serializers.DictField(read_only=True, label=_('Auto info'))
spec_info = MethodSerializer(label=_('Spec info'), read_only=True)
gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
def get_instance(self):
request = self.context.get('request')
if not self.instance and uuid_pattern.findall(request.path):
pk = uuid_pattern.findall(request.path)[0]
self.instance = Asset.objects.filter(id=pk).first()
return self.instance
def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info)
names.extend([
'accounts', 'info', 'spec_info', 'auto_info'
'accounts', 'gathered_info', 'spec_info',
'auto_config',
])
return names
def get_category(self):
request = self.context.get('request')
if request.query_params.get('category'):
category = request.query_params.get('category')
else:
instance = self.get_instance()
category = instance.category
return category
def get_gathered_info_serializer(self):
category = self.get_category()
from .info.gathered import category_gathered_serializer_map
serializer_cls = category_gathered_serializer_map.get(category, serializers.DictField)
return serializer_cls()
def get_spec_info_serializer(self):
category = self.get_category()
from .info.spec import category_spec_serializer_map
serializer_cls = category_spec_serializer_map.get(category, serializers.DictField)
return serializer_cls()
class AssetDetailSerializer(DetailMixin, AssetSerializer):
pass

View File

@ -0,0 +1,9 @@
from assets.models import Custom
from .common import AssetSerializer
__all__ = ['CustomSerializer']
class CustomSerializer(AssetSerializer):
class Meta(AssetSerializer.Meta):
model = Custom

View File

@ -1,9 +1,9 @@
from rest_framework.serializers import ValidationError
from django.utils.translation import ugettext_lazy as _
from rest_framework.serializers import ValidationError
from assets.models import Database
from assets.serializers.gateway import GatewayWithAccountSecretSerializer
from .common import AssetSerializer
from ..gateway import GatewayWithAccountSecretSerializer
__all__ = ['DatabaseSerializer', 'DatabaseWithGatewaySerializer']

View File

@ -1,34 +1,18 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from assets.models import Host
from .common import AssetSerializer
from .info.gathered import HostGatheredInfoSerializer
__all__ = ['HostInfoSerializer', 'HostSerializer']
class HostInfoSerializer(serializers.Serializer):
vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor'))
model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model'))
sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number'))
cpu_model = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('CPU model'))
cpu_count = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU count'))
cpu_cores = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU cores'))
cpu_vcpus = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU vcpus'))
memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory'))
disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total'))
distribution = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS'))
distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version'))
arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch'))
__all__ = ['HostSerializer']
class HostSerializer(AssetSerializer):
info = HostInfoSerializer(required=False, label=_('Info'))
gathered_info = HostGatheredInfoSerializer(required=False, read_only=True, label=_("Gathered info"))
class Meta(AssetSerializer.Meta):
model = Host
fields = AssetSerializer.Meta.fields + ['info']
fields = AssetSerializer.Meta.fields + ['gathered_info']
extra_kwargs = {
**AssetSerializer.Meta.extra_kwargs,
'address': {

View File

@ -0,0 +1,23 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
class HostGatheredInfoSerializer(serializers.Serializer):
vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor'))
model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model'))
sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number'))
cpu_model = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('CPU model'))
cpu_count = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU count'))
cpu_cores = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU cores'))
cpu_vcpus = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU vcpus'))
memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory'))
disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total'))
distribution = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS'))
distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version'))
arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch'))
category_gathered_serializer_map = {
'host': HostGatheredInfoSerializer,
}

View File

@ -0,0 +1,24 @@
from rest_framework import serializers
from assets.models import Database, Web
class DatabaseSpecSerializer(serializers.ModelSerializer):
class Meta:
model = Database
fields = ['db_name', 'use_ssl', 'allow_invalid_cert']
class WebSpecSerializer(serializers.ModelSerializer):
class Meta:
model = Web
fields = [
'autofill', 'username_selector', 'password_selector',
'submit_selector', 'script'
]
category_spec_serializer_map = {
'database': DatabaseSpecSerializer,
'web': WebSpecSerializer,
}

View File

@ -24,6 +24,6 @@ class WebSerializer(AssetSerializer):
'default': 'name=password'
},
'submit_selector': {
'default': 'id=longin_button',
'default': 'id=login_button',
},
}

View File

@ -3,8 +3,8 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from .asset import HostSerializer
from .asset.common import AccountSecretSerializer
from .asset.host import HostSerializer
from ..models import Gateway, Asset
__all__ = ['GatewaySerializer', 'GatewayWithAccountSecretSerializer']

View File

@ -4,6 +4,7 @@ from rest_framework import serializers
from assets.const.web import FillType
from common.serializers import WritableNestedModelSerializer
from common.serializers.fields import LabeledChoiceField
from common.utils import lazyproperty
from ..const import Category, AllTypes
from ..models import Platform, PlatformProtocol, PlatformAutomation
@ -37,10 +38,12 @@ class ProtocolSettingSerializer(serializers.Serializer):
default="", allow_blank=True, label=_("Submit selector")
)
script = serializers.JSONField(default=list, label=_("Script"))
# Redis
auth_username = serializers.BooleanField(default=False, label=_("Auth with username"))
# WinRM
use_ssl = serializers.BooleanField(default=False, label=_("Use SSL"))
class PlatformAutomationSerializer(serializers.ModelSerializer):
class Meta:
@ -48,12 +51,12 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
fields = [
"id",
"ansible_enabled", "ansible_config",
"ping_enabled", "ping_method",
"push_account_enabled", "push_account_method",
"gather_facts_enabled", "gather_facts_method",
"change_secret_enabled", "change_secret_method",
"verify_account_enabled", "verify_account_method",
"gather_accounts_enabled", "gather_accounts_method",
"ping_enabled", "ping_method", "ping_params",
"push_account_enabled", "push_account_method", "push_account_params",
"gather_facts_enabled", "gather_facts_method", "gather_facts_params",
"change_secret_enabled", "change_secret_method", "change_secret_params",
"verify_account_enabled", "verify_account_method", "verify_account_params",
"gather_accounts_enabled", "gather_accounts_method", "gather_accounts_params",
]
extra_kwargs = {
# 启用资产探测
@ -72,33 +75,53 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
}
class PlatformProtocolsSerializer(serializers.ModelSerializer):
class PlatformProtocolSerializer(serializers.ModelSerializer):
setting = ProtocolSettingSerializer(required=False, allow_null=True)
primary = serializers.BooleanField(read_only=True, label=_("Primary"))
class Meta:
model = PlatformProtocol
fields = [
"id", "name", "port", "primary",
"default", "required", "secret_types",
"setting",
"required", "default",
"secret_types", "setting",
]
class PlatformCustomField(serializers.Serializer):
TYPE_CHOICES = [
("str", "str"),
("text", "text"),
("int", "int"),
("bool", "bool"),
("choice", "choice"),
("list", "list"),
]
name = serializers.CharField(label=_("Name"), max_length=128)
label = serializers.CharField(label=_("Label"), max_length=128)
type = serializers.ChoiceField(choices=TYPE_CHOICES, label=_("Type"), default='str')
default = serializers.CharField(default="", allow_blank=True, label=_("Default"), max_length=1024)
help_text = serializers.CharField(default="", allow_blank=True, label=_("Help text"), max_length=1024)
choices = serializers.ListField(default=list, label=_("Choices"), required=False)
class PlatformSerializer(WritableNestedModelSerializer):
charset = LabeledChoiceField(
choices=Platform.CharsetChoices.choices, label=_("Charset")
)
SU_METHOD_CHOICES = [
("sudo", "sudo su -"),
("su", "su - "),
("enable", "enable"),
("super", "super 15"),
("super_level", "super level 15")
]
charset = LabeledChoiceField(choices=Platform.CharsetChoices.choices, label=_("Charset"), default='utf-8')
type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type"))
category = LabeledChoiceField(choices=Category.choices, label=_("Category"))
protocols = PlatformProtocolsSerializer(
label=_("Protocols"), many=True, required=False
)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False)
protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
su_method = LabeledChoiceField(
choices=[("sudo", "sudo su -"), ("su", "su - ")],
label=_("Su method"), required=False, default="sudo", allow_null=True
choices=SU_METHOD_CHOICES, label=_("Su method"),
required=False, default="sudo", allow_null=True
)
custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)
class Meta:
model = Platform
@ -106,19 +129,54 @@ class PlatformSerializer(WritableNestedModelSerializer):
fields_small = fields_mini + [
"category", "type", "charset",
]
fields_other = [
'date_created', 'date_updated', 'created_by', 'updated_by',
read_only_fields = [
'internal', 'date_created', 'date_updated',
'created_by', 'updated_by'
]
fields = fields_small + [
"protocols", "domain_enabled", "su_enabled",
"su_method", "automation", "comment",
] + fields_other
"su_method", "automation", "comment", "custom_fields",
] + read_only_fields
extra_kwargs = {
"su_enabled": {"label": _('Su enabled')},
"domain_enabled": {"label": _('Domain enabled')},
"domain_default": {"label": _('Default Domain')},
}
@property
def platform_category_type(self):
if self.instance:
return self.instance.category, self.instance.type
if self.initial_data:
return self.initial_data.get('category'), self.initial_data.get('type')
raise serializers.ValidationError({'type': _("type is required")})
def add_type_choices(self, name, label):
tp = self.fields['type']
tp.choices[name] = label
tp.choice_mapper[name] = label
tp.choice_strings_to_values[name] = label
@lazyproperty
def constraints(self):
category, tp = self.platform_category_type
constraints = AllTypes.get_constraints(category, tp)
return constraints
def validate(self, attrs):
domain_enabled = attrs.get('domain_enabled', False) and self.constraints.get('domain_enabled', False)
su_enabled = attrs.get('su_enabled', False) and self.constraints.get('su_enabled', False)
automation = attrs.get('automation', {})
automation['ansible_enabled'] = automation.get('ansible_enabled', False) \
and self.constraints.get('ansible_enabled', False)
attrs.update({
'domain_enabled': domain_enabled,
'su_enabled': su_enabled,
'automation': automation,
})
self.initial_data['automation'] = automation
return attrs
@classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related(
@ -126,6 +184,16 @@ class PlatformSerializer(WritableNestedModelSerializer):
)
return queryset
def validate_protocols(self, protocols):
if not protocols:
raise serializers.ValidationError(_("Protocols is required"))
primary = [p for p in protocols if p.get('primary')]
if not primary:
protocols[0]['primary'] = True
# 这里不设置不行write_nested 不使用 validated 中的
self.initial_data['protocols'] = protocols
return protocols
class PlatformOpsMethodSerializer(serializers.Serializer):
id = serializers.CharField(read_only=True)

View File

@ -66,11 +66,11 @@ def on_asset_create(sender, instance=None, created=False, **kwargs):
ensure_asset_has_node(assets=(instance,))
# 获取资产硬件信息
auto_info = instance.auto_info
if auto_info.get('ping_enabled'):
auto_config = instance.auto_config
if auto_config.get('ping_enabled'):
logger.debug('Asset {} ping enabled, test connectivity'.format(instance.name))
test_assets_connectivity_handler(assets=(instance,))
if auto_info.get('gather_facts_enabled'):
if auto_config.get('gather_facts_enabled'):
logger.debug('Asset {} gather facts enabled, gather facts'.format(instance.name))
gather_assets_facts_handler(assets=(instance,))

View File

@ -14,6 +14,7 @@ router.register(r'devices', api.DeviceViewSet, 'device')
router.register(r'databases', api.DatabaseViewSet, 'database')
router.register(r'webs', api.WebViewSet, 'web')
router.register(r'clouds', api.CloudViewSet, 'cloud')
router.register(r'customs', api.CustomViewSet, 'custom')
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
router.register(r'labels', api.LabelViewSet, 'label')
router.register(r'nodes', api.NodeViewSet, 'node')
@ -45,6 +46,7 @@ urlpatterns = [
path('nodes/<uuid:pk>/tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'),
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
path('platform-automation-methods/', api.PlatformAutomationMethodsApi.as_view(), name='platform-automation-methods'),
]
urlpatterns += router.urls

View File

@ -162,7 +162,8 @@ class RDPFileClientProtocolURLMixin:
def get_smart_endpoint(self, protocol, asset=None):
target_ip = asset.get_target_ip() if asset else ''
endpoint = EndpointRule.match_endpoint(
target_instance=asset, target_ip=target_ip, protocol=protocol, request=self.request
target_instance=asset, target_ip=target_ip,
protocol=protocol, request=self.request
)
return endpoint

View File

@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import PermissionDenied
from assets.const import Protocol
from assets.const.host import GATEWAY_NAME
from common.db.fields import EncryptTextField
from common.exceptions import JMSException
from common.utils import lazyproperty, pretty_string, bulk_get
@ -231,12 +232,14 @@ class ConnectionToken(JMSOrgBaseModel):
def domain(self):
if not self.asset.platform.domain_enabled:
return
domain = self.asset.domain if self.asset else None
if self.asset.platform.name == GATEWAY_NAME:
return
domain = self.asset.domain if self.asset.domain else None
return domain
@lazyproperty
def gateway(self):
if not self.asset:
if not self.asset or not self.domain:
return
return self.asset.gateway

View File

@ -5,7 +5,8 @@ from accounts.const import SecretType
from accounts.models import Account
from acls.models import CommandGroup, CommandFilterACL
from assets.models import Asset, Platform, Gateway, Domain
from assets.serializers import PlatformSerializer, AssetProtocolsSerializer
from assets.serializers.asset import AssetProtocolsSerializer
from assets.serializers.platform import PlatformSerializer
from common.serializers.fields import LabeledChoiceField
from common.serializers.fields import ObjectRelatedField
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
@ -26,18 +27,17 @@ class _ConnectionTokenUserSerializer(serializers.ModelSerializer):
class _ConnectionTokenAssetSerializer(serializers.ModelSerializer):
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
info = serializers.DictField()
class Meta:
model = Asset
fields = [
'id', 'name', 'address', 'protocols',
'category', 'type', 'org_id', 'spec_info',
'secret_info',
'id', 'name', 'address', 'protocols', 'category',
'type', 'org_id', 'info', 'secret_info', 'spec_info'
]
class _SimpleAccountSerializer(serializers.ModelSerializer):
""" Account """
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
class Meta:
@ -46,20 +46,18 @@ class _SimpleAccountSerializer(serializers.ModelSerializer):
class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
""" Account """
su_from = _SimpleAccountSerializer(required=False, label=_('Su from'))
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
class Meta:
model = Account
fields = [
'id', 'name', 'username', 'secret_type', 'secret', 'su_from', 'privileged'
'id', 'name', 'username', 'secret_type',
'secret', 'su_from', 'privileged'
]
class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
""" Gateway """
account = _SimpleAccountSerializer(
required=False, source='select_account', read_only=True
)
@ -85,7 +83,8 @@ class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
class Meta:
model = CommandFilterACL
fields = [
'id', 'name', 'command_groups', 'action', 'reviewers', 'priority', 'is_active'
'id', 'name', 'command_groups', 'action',
'reviewers', 'priority', 'is_active'
]
@ -136,8 +135,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
'id', 'value', 'user', 'asset', 'account',
'platform', 'command_filter_acls', 'protocol',
'domain', 'gateway', 'actions', 'expire_at',
'from_ticket',
'expire_now', 'connect_method',
'from_ticket', 'expire_now', 'connect_method',
]
extra_kwargs = {
'value': {'read_only': True},

View File

@ -111,7 +111,7 @@ class BaseFileParser(BaseParser):
return {'pk': obj_id, 'name': obj_name}
def parse_value(self, field, value):
if value is '-':
if value == '-' and field and field.allow_null:
return None
elif hasattr(field, 'to_file_internal_value'):
value = field.to_file_internal_value(value)

View File

@ -1,2 +1,3 @@
from .common import *
from .dynamic import *
from .mixin import *

View File

@ -0,0 +1,55 @@
from rest_framework import serializers
example_info = [
{"name": "name", "label": "姓名", "required": False, "default": "广州老广", "type": "str"},
{"name": "age", "label": "年龄", "required": False, "default": 18, "type": "int"},
]
type_field_map = {
"str": serializers.CharField,
"int": serializers.IntegerField,
"bool": serializers.BooleanField,
"text": serializers.CharField,
"choice": serializers.ChoiceField,
"list": serializers.ListField,
}
def set_default_if_need(data, i):
field_name = data.pop('name', 'Attr{}'.format(i + 1))
data['name'] = field_name
if not data.get('label'):
data['label'] = field_name
return data
def set_default_by_type(tp, data, field_info):
if tp == 'str':
data['max_length'] = 4096
elif tp == 'choice':
choices = field_info.pop('choices', [])
if isinstance(choices, str):
choices = choices.split(',')
choices = [
(c, c.title()) if not isinstance(c, (tuple, list)) else c
for c in choices
]
data['choices'] = choices
return data
def create_serializer_class(serializer_name, fields_info):
serializer_fields = {}
fields_name = ['name', 'label', 'default', 'type', 'help_text']
for i, field_info in enumerate(fields_info):
data = {k: field_info.get(k) for k in fields_name}
field_type = data.pop('type', 'str')
data = set_default_by_type(field_type, data, field_info)
data = set_default_if_need(data, i)
field_name = data.pop('name')
field_class = type_field_map.get(field_type, serializers.CharField)
serializer_fields[field_name] = field_class(**data)
return type(serializer_name, (serializers.Serializer,), serializer_fields)

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
import phonenumbers
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@ -17,6 +18,7 @@ __all__ = [
"BitChoicesField",
"TreeChoicesField",
"LabeledMultipleChoiceField",
"PhoneField",
]
@ -201,3 +203,11 @@ class BitChoicesField(TreeChoicesField):
value = self.to_internal_value(data)
self.run_validators(value)
return value
class PhoneField(serializers.CharField):
def to_representation(self, value):
if value:
phone = phonenumbers.parse(value, 'CN')
value = {'code': '+%s' % phone.country_code, 'phone': phone.national_number}
return value

View File

@ -2,12 +2,15 @@
#
import re
import phonenumbers
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
from rest_framework.validators import (
UniqueTogetherValidator, ValidationError
)
from rest_framework import serializers
from phonenumbers.phonenumberutil import NumberParseException
from common.utils.strings import no_special_chars
@ -42,9 +45,14 @@ class NoSpecialChars:
class PhoneValidator:
pattern = re.compile(r"^1[3456789]\d{9}$")
message = _('The mobile phone number format is incorrect')
def __call__(self, value):
if not self.pattern.match(value):
try:
phone = phonenumbers.parse(value, 'CN')
valid = phonenumbers.is_valid_number(phone)
except NumberParseException:
valid = False
if not valid:
raise serializers.ValidationError(self.message)

View File

@ -1,15 +1,17 @@
# -*- coding: utf-8 -*-
#
from django.utils import translation
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_noop
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http.response import JsonResponse
from rest_framework import permissions
from rest_framework.request import Request
from common.exceptions import UserConfirmRequired
from common.utils import i18n_fmt
from audits.handler import create_or_update_operate_log
from audits.const import ActionChoices
from audits.const import ActionChoices, ActivityChoices
from audits.models import ActivityLog
__all__ = [
"PermissionsMixin",
@ -49,38 +51,63 @@ class RecordViewLogMixin:
ACTION = ActionChoices.view
@staticmethod
def get_resource_display(request):
def _filter_params(params):
new_params = {}
need_pop_params = ('format', 'order')
for key, value in params.items():
if key in need_pop_params:
continue
if isinstance(value, list):
value = list(filter(None, value))
if value:
new_params[key] = value
return new_params
def get_resource_display(self, request):
query_params = dict(request.query_params)
if query_params.get("format"):
query_params.pop("format")
spm_filter = query_params.pop("spm") if query_params.get("spm") else None
if not query_params and not spm_filter:
display_message = _("Export all")
params = self._filter_params(query_params)
spm_filter = params.pop("spm", None)
if not params and not spm_filter:
display_message = gettext_noop("Export all")
elif spm_filter:
display_message = _("Export only selected items")
display_message = gettext_noop("Export only selected items")
else:
query = ",".join(
["%s=%s" % (key, value) for key, value in query_params.items()]
["%s=%s" % (key, value) for key, value in params.items()]
)
display_message = _("Export filtered: %s") % query
display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query)
return display_message
def record_logs(self, ids, **kwargs):
resource_type = self.model._meta.verbose_name
create_or_update_operate_log(
self.ACTION, resource_type, force=True, **kwargs
)
detail = i18n_fmt(
gettext_noop('User %s view/export secret'), self.request.user
)
activities = [
ActivityLog(
resource_id=getattr(resource_id, 'pk', resource_id),
type=ActivityChoices.operate_log, detail=detail
)
for resource_id in ids
]
ActivityLog.objects.bulk_create(activities)
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
with translation.override('en'):
resource_display = self.get_resource_display(request)
resource_type = self.model._meta.verbose_name
create_or_update_operate_log(
self.ACTION, resource_type, force=True,
resource_display=resource_display
)
ids = [q.id for q in self.get_queryset()]
self.record_logs(ids, resource_display=resource_display)
return response
def retrieve(self, request, *args, **kwargs):
response = super().retrieve(request, *args, **kwargs)
with translation.override('en'):
resource_type = self.model._meta.verbose_name
create_or_update_operate_log(
self.ACTION, resource_type, force=True, resource=self.get_object()
)
resource = self.get_object()
self.record_logs([resource.id], resource=resource)
return response

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6fa80b59b9b5f95a9cfcad8ec47eacd519bb962d139ab90463795a7b306a0a72
size 137935
oid sha256:975e9e264596ef5f7233fc1d2fb45281a5fe13f5a722fc2b9d5c40562ada069d
size 138303

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More