mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
294 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79ff3b9423 | ||
|
|
11db46e61e | ||
|
|
07bb44dfc3 | ||
|
|
1204a869a9 | ||
|
|
0c8b36a6f4 | ||
|
|
d6cfda693f | ||
|
|
7aa96462a4 | ||
|
|
1986653214 | ||
|
|
4dc46cc754 | ||
|
|
dcb9270d02 | ||
|
|
f51d375f48 | ||
|
|
cea24f0ccf | ||
|
|
0292f2161f | ||
|
|
778918be48 | ||
|
|
36fdf754e9 | ||
|
|
ce9515a19a | ||
|
|
83108f84a3 | ||
|
|
e0b38fbcb6 | ||
|
|
ce24c1c3fd | ||
|
|
db9ee71ab3 | ||
|
|
db2331521d | ||
|
|
4aa4c6854b | ||
|
|
26a18a1f5c | ||
|
|
6870df6d75 | ||
|
|
03d1a187df | ||
|
|
ca0dca26c7 | ||
|
|
25a1989157 | ||
|
|
fef26c38fe | ||
|
|
a2fcc47436 | ||
|
|
00450121bc | ||
|
|
bdd885069f | ||
|
|
25d0c021e1 | ||
|
|
095c23ea4f | ||
|
|
3c3c112b07 | ||
|
|
d95a44fe44 | ||
|
|
e713bdab0b | ||
|
|
78f1b2b002 | ||
|
|
e0762573ae | ||
|
|
16e8c7faba | ||
|
|
9b019e45ae | ||
|
|
71d70501d6 | ||
|
|
5cd44ebfce | ||
|
|
03c27ab5b8 | ||
|
|
d3a283232f | ||
|
|
f088bbce12 | ||
|
|
b313598227 | ||
|
|
3a118b6753 | ||
|
|
578c2af57c | ||
|
|
b5ef239c6c | ||
|
|
e88e4438ba | ||
|
|
73b75df524 | ||
|
|
772684d24c | ||
|
|
741705b85b | ||
|
|
f5176bcc6f | ||
|
|
c917d8f346 | ||
|
|
5c0905b3b5 | ||
|
|
bda23b3d2a | ||
|
|
8b6526211c | ||
|
|
86e8f3a80b | ||
|
|
70661242c1 | ||
|
|
7dcae1e05a | ||
|
|
0a28c5650c | ||
|
|
f55c84ce3b | ||
|
|
ac11790192 | ||
|
|
f80ff279d0 | ||
|
|
d7ac08f6d9 | ||
|
|
b5714f7e14 | ||
|
|
d6b450f32a | ||
|
|
1daf1acaf3 | ||
|
|
ea0e852412 | ||
|
|
ce976f215f | ||
|
|
ffc057f844 | ||
|
|
588723a76c | ||
|
|
1ca912373f | ||
|
|
452ee1224c | ||
|
|
7eb497f9d3 | ||
|
|
58fd578ddd | ||
|
|
e1278360af | ||
|
|
c0de27ff7a | ||
|
|
116d0ba5c6 | ||
|
|
9f042cfa04 | ||
|
|
ce63ea7528 | ||
|
|
8b3fd2c117 | ||
|
|
23ccd6df8c | ||
|
|
614e019f14 | ||
|
|
38aa828eb8 | ||
|
|
7cd2736e82 | ||
|
|
443f6d25e8 | ||
|
|
e8652af054 | ||
|
|
fd6a8dd807 | ||
|
|
499eedd83e | ||
|
|
ca7d164034 | ||
|
|
3ef8e9603a | ||
|
|
09f71d80eb | ||
|
|
73db1bf50c | ||
|
|
6017f804a6 | ||
|
|
affa562384 | ||
|
|
0d101bc5ad | ||
|
|
70f0f55ddb | ||
|
|
333746e7c4 | ||
|
|
30b19d31eb | ||
|
|
a844ce23e4 | ||
|
|
d6c0139fef | ||
|
|
11157563ba | ||
|
|
95e7bde5d7 | ||
|
|
814350ab80 | ||
|
|
3ac35eec68 | ||
|
|
3d27986c96 | ||
|
|
c981e9cd9f | ||
|
|
e00c804a5a | ||
|
|
ef2b7b464e | ||
|
|
ae5d4257ad | ||
|
|
b42014d58e | ||
|
|
e71e8cd595 | ||
|
|
dd50044b89 | ||
|
|
68707085fa | ||
|
|
60399fae29 | ||
|
|
f206d963a0 | ||
|
|
42b4e7697d | ||
|
|
0c1f4d99f8 | ||
|
|
2aed3fcaea | ||
|
|
28196573bb | ||
|
|
27c505853b | ||
|
|
896d42c53e | ||
|
|
f79084c2df | ||
|
|
15a5dda9e0 | ||
|
|
2069fee795 | ||
|
|
56a26481a4 | ||
|
|
cbe3d66b39 | ||
|
|
7c67d882aa | ||
|
|
9bde2ff6e1 | ||
|
|
1f00c00183 | ||
|
|
c369b5478c | ||
|
|
10363dcc5b | ||
|
|
42bdb2cf14 | ||
|
|
d64e77db30 | ||
|
|
4065baf785 | ||
|
|
0f3ddc3bf1 | ||
|
|
138adeff76 | ||
|
|
0cf17310e1 | ||
|
|
43dbb4c226 | ||
|
|
cefd9f4ab2 | ||
|
|
7128593502 | ||
|
|
5d4fa22058 | ||
|
|
3c54c82ce9 | ||
|
|
91dce82b38 | ||
|
|
d102db7a7b | ||
|
|
1de7af4984 | ||
|
|
9892ff7dd6 | ||
|
|
4cb499953c | ||
|
|
0397bdeb46 | ||
|
|
1d6d92c160 | ||
|
|
cdbe5d31e9 | ||
|
|
b023ca0c69 | ||
|
|
803d590096 | ||
|
|
e11367088a | ||
|
|
1c74dd00ba | ||
|
|
ed832af631 | ||
|
|
948c499d9e | ||
|
|
a51549cf1c | ||
|
|
39baf88055 | ||
|
|
90131db55a | ||
|
|
ea3ff1ebcb | ||
|
|
f3ca45aa74 | ||
|
|
74cc174d7a | ||
|
|
0eba6d2175 | ||
|
|
58592a13e3 | ||
|
|
b8fb23a0a0 | ||
|
|
f5c43488fd | ||
|
|
19c76ba01c | ||
|
|
68c4cd5928 | ||
|
|
e5bfa29c7b | ||
|
|
cbb772def7 | ||
|
|
e6fe7c489e | ||
|
|
0b30f5cf88 | ||
|
|
018f1a0e8d | ||
|
|
24ed57b98a | ||
|
|
04a790c4ee | ||
|
|
2d9a3ef7d4 | ||
|
|
0d2adeccf2 | ||
|
|
886f977311 | ||
|
|
9367e79bcf | ||
|
|
af733ecbad | ||
|
|
09f9775eab | ||
|
|
1c2a362beb | ||
|
|
bb1e674367 | ||
|
|
a75677ab08 | ||
|
|
b1daa4d357 | ||
|
|
c32271ec6f | ||
|
|
beb4f14be9 | ||
|
|
e719904874 | ||
|
|
664bc2a4d9 | ||
|
|
b91db8c146 | ||
|
|
500aeeb77f | ||
|
|
3abc8bddfa | ||
|
|
5cbbf9e737 | ||
|
|
7204a86f87 | ||
|
|
829194420a | ||
|
|
61dc95d9ae | ||
|
|
a9f60a9117 | ||
|
|
82f96d6ed2 | ||
|
|
f6c56d4979 | ||
|
|
54d0a1b871 | ||
|
|
5b4a267ccd | ||
|
|
a6d78834e7 | ||
|
|
07da98e438 | ||
|
|
7c973616cd | ||
|
|
b9997b07db | ||
|
|
bcda879f3b | ||
|
|
d0f79c2df2 | ||
|
|
1249935bab | ||
|
|
5fa1ae9ee5 | ||
|
|
d0755c4719 | ||
|
|
72b215ed03 | ||
|
|
d7ca1a09d4 | ||
|
|
04e341a1bb | ||
|
|
a41909ec8d | ||
|
|
f9d6de9c39 | ||
|
|
816b284a51 | ||
|
|
d4c5dcf069 | ||
|
|
73037c21e8 | ||
|
|
c7f9259a2e | ||
|
|
8632bd2480 | ||
|
|
23723f4eda | ||
|
|
38601a84c2 | ||
|
|
e50189e284 | ||
|
|
da9bd11db5 | ||
|
|
9acb7d6183 | ||
|
|
dbd9a9fdac | ||
|
|
25301aa396 | ||
|
|
8cc1ca2770 | ||
|
|
bad01aefa2 | ||
|
|
56a989bfb9 | ||
|
|
578f66d5e2 | ||
|
|
8d6083bfb2 | ||
|
|
1138cd3334 | ||
|
|
db0b43ee84 | ||
|
|
40a460870a | ||
|
|
51910ea2c1 | ||
|
|
266a360a97 | ||
|
|
24194b4e4d | ||
|
|
992e34d652 | ||
|
|
894249a3d1 | ||
|
|
21c6fe19a1 | ||
|
|
e4e4f82143 | ||
|
|
2a5c635dc5 | ||
|
|
7dbaa28539 | ||
|
|
5bae4cde58 | ||
|
|
35c0d7be35 | ||
|
|
1f2a4b0fb5 | ||
|
|
7c3a3d599b | ||
|
|
d70770775a | ||
|
|
bc217e1bad | ||
|
|
d4469aeaf7 | ||
|
|
904406c5c1 | ||
|
|
09db2ad3e1 | ||
|
|
859268f7f3 | ||
|
|
72bb5a4037 | ||
|
|
6f3871d5fe | ||
|
|
2f0c346365 | ||
|
|
e9c090f656 | ||
|
|
7b0b07cf52 | ||
|
|
bebb90f688 | ||
|
|
ac14a70c51 | ||
|
|
642f92c0a3 | ||
|
|
04f4ecb3d1 | ||
|
|
60703c920c | ||
|
|
9634f397df | ||
|
|
f9a7a95191 | ||
|
|
bced33fd93 | ||
|
|
1044ff004b | ||
|
|
e11c7a264e | ||
|
|
3c497aa81e | ||
|
|
c8a1f4b092 | ||
|
|
9dd2dc8907 | ||
|
|
56285d906f | ||
|
|
44b536a23b | ||
|
|
a97003a03a | ||
|
|
4315cbe6d0 | ||
|
|
b2d9670721 | ||
|
|
78f66c46e8 | ||
|
|
f3af9c3108 | ||
|
|
822a124dbc | ||
|
|
20799ece93 | ||
|
|
4e2c7d7aab | ||
|
|
75e4895314 | ||
|
|
ea7b409a7f | ||
|
|
01d10a25e9 | ||
|
|
61ce39b4ba | ||
|
|
7506c7ea43 | ||
|
|
f6f162ec3a | ||
|
|
2e840e3b05 | ||
|
|
ff4560c2a7 | ||
|
|
deeb8da226 |
1
.github/ISSUE_TEMPLATE/----.md
vendored
1
.github/ISSUE_TEMPLATE/----.md
vendored
@@ -6,7 +6,6 @@ labels: 类型:需求
|
||||
assignees:
|
||||
- ibuler
|
||||
- baijiangjie
|
||||
- wojiushixiaobai
|
||||
---
|
||||
|
||||
**请描述您的需求或者改进建议.**
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug---.md
vendored
4
.github/ISSUE_TEMPLATE/bug---.md
vendored
@@ -2,11 +2,9 @@
|
||||
name: Bug 提交
|
||||
about: 提交产品缺陷帮助我们更好的改进
|
||||
title: "[Bug] "
|
||||
labels: 类型:bug
|
||||
labels: 类型:Bug
|
||||
assignees:
|
||||
- wojiushixiaobai
|
||||
- baijiangjie
|
||||
|
||||
---
|
||||
|
||||
**JumpServer 版本( v2.28 之前的版本不再支持 )**
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -4,9 +4,7 @@ about: 提出针对本项目安装部署、使用及其他方面的相关问题
|
||||
title: "[Question] "
|
||||
labels: 类型:提问
|
||||
assignees:
|
||||
- wojiushixiaobai
|
||||
- baijiangjie
|
||||
|
||||
---
|
||||
|
||||
**请描述您的问题.**
|
||||
|
||||
@@ -36,9 +36,11 @@ ARG TOOLS=" \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
iputils-ping \
|
||||
locales \
|
||||
nmap \
|
||||
openssh-client \
|
||||
patch \
|
||||
sshpass \
|
||||
telnet \
|
||||
vim \
|
||||
|
||||
86
Dockerfile-ce
Normal file
86
Dockerfile-ce
Normal file
@@ -0,0 +1,86 @@
|
||||
FROM python:3.11-slim-bullseye as stage-build
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
freetds-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libkrb5-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
freerdp2-dev \
|
||||
libaio-dev"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
iputils-ping \
|
||||
locales \
|
||||
nmap \
|
||||
openssh-client \
|
||||
patch \
|
||||
sshpass \
|
||||
telnet \
|
||||
vim \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& 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\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 \
|
||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
||||
&& sed -i "s@# alias @alias @g" ~/.bashrc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
set -ex \
|
||||
&& echo > /opt/jumpserver/config.yml \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& poetry install --only=main
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
@@ -12,8 +12,6 @@
|
||||
|
||||
|
||||
<p align="center">
|
||||
JumpServer <a href="https://github.com/jumpserver/jumpserver/releases/tag/v3.0.0">v3.0</a> 正式发布。
|
||||
<br>
|
||||
9 年时间,倾情投入,用心做好一款开源堡垒机。
|
||||
</p>
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ from rest_framework.status import HTTP_200_OK
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.filters import AccountFilterSet
|
||||
from accounts.mixins import AccountRecordViewLogMixin
|
||||
from accounts.models import Account
|
||||
from assets.models import Asset, Node
|
||||
from common.api import ExtraFilterFieldsMixin
|
||||
from common.permissions import UserConfirmation, ConfirmType, IsValidUser
|
||||
from common.views.mixins import RecordViewLogMixin
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from common.api.mixin import ExtraFilterFieldsMixin
|
||||
from common.permissions import IsValidUser
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
@@ -57,19 +58,19 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
permission_classes=[IsValidUser]
|
||||
)
|
||||
def username_suggestions(self, request, *args, **kwargs):
|
||||
asset_ids = request.data.get('assets')
|
||||
node_ids = request.data.get('nodes')
|
||||
username = request.data.get('username')
|
||||
asset_ids = request.data.get('assets', [])
|
||||
node_ids = request.data.get('nodes', [])
|
||||
username = request.data.get('username', '')
|
||||
|
||||
assets = Asset.objects.all()
|
||||
if asset_ids:
|
||||
assets = assets.filter(id__in=asset_ids)
|
||||
accounts = Account.objects.all()
|
||||
if node_ids:
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||
assets = assets.filter(id__in=set(list(asset_ids) + list(node_asset_ids)))
|
||||
asset_ids.extend(node_asset_ids)
|
||||
|
||||
if asset_ids:
|
||||
accounts = accounts.filter(asset_id__in=list(set(asset_ids)))
|
||||
|
||||
accounts = Account.objects.filter(asset__in=assets)
|
||||
if username:
|
||||
accounts = accounts.filter(username__icontains=username)
|
||||
usernames = list(accounts.values_list('username', flat=True).distinct()[:10])
|
||||
@@ -86,7 +87,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
return Response(status=HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||
"""
|
||||
@@ -115,7 +116,7 @@ class AssetAccountBulkCreateApi(CreateAPIView):
|
||||
return Response(data=serializer.data, status=HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, ListAPIView):
|
||||
class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixin, ListAPIView):
|
||||
model = Account.history.model
|
||||
serializer_class = serializers.AccountHistorySerializer
|
||||
http_method_names = ['get', 'options']
|
||||
@@ -143,4 +144,3 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, List
|
||||
return histories
|
||||
histories = histories.exclude(history_id=latest_history.history_id)
|
||||
return histories
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
||||
code = 'accounts.push_account'
|
||||
else:
|
||||
code = 'accounts.verify_account'
|
||||
return request.user.has_perm(code)
|
||||
has = request.user.has_perm(code)
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
data = serializer.validated_data
|
||||
@@ -44,6 +46,6 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return Response({"error": str(e)}, status=401)
|
||||
|
||||
return handler
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from django_filters import rest_framework as drf_filters
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.mixins import AccountRecordViewLogMixin
|
||||
from accounts.models import AccountTemplate
|
||||
from accounts.tasks import template_sync_related_accounts
|
||||
from assets.const import Protocol
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.permissions import UserConfirmation, ConfirmType
|
||||
from common.views.mixins import RecordViewLogMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
@@ -44,6 +46,7 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
|
||||
}
|
||||
rbac_perms = {
|
||||
'su_from_account_templates': 'accounts.view_accounttemplate',
|
||||
'sync_related_accounts': 'accounts.change_account',
|
||||
}
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from-account-templates')
|
||||
@@ -54,8 +57,15 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
|
||||
serializer = self.get_serializer(templates, many=True)
|
||||
return Response(data=serializer.data)
|
||||
|
||||
@action(methods=['patch'], detail=True, url_path='sync-related-accounts')
|
||||
def sync_related_accounts(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
user_id = str(request.user.id)
|
||||
task = template_sync_related_accounts.delay(str(instance.id), user_id)
|
||||
return Response({'task': task.id}, status=status.HTTP_200_OK)
|
||||
|
||||
class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet):
|
||||
|
||||
class AccountTemplateSecretsViewSet(AccountRecordViewLogMixin, AccountTemplateViewSet):
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountTemplateSecretSerializer,
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ from rest_framework import mixins
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution
|
||||
from common.utils import get_object_or_none
|
||||
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
|
||||
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
||||
from .base import (
|
||||
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
|
||||
@@ -30,8 +29,8 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
|
||||
|
||||
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
serializer_class = serializers.ChangeSecretRecordSerializer
|
||||
filter_fields = ['asset', 'execution_id']
|
||||
search_fields = ['asset__hostname']
|
||||
filter_fields = ('asset', 'execution_id')
|
||||
search_fields = ('asset__address',)
|
||||
|
||||
def get_queryset(self):
|
||||
return ChangeSecretRecord.objects.filter(
|
||||
@@ -41,10 +40,7 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
eid = self.request.query_params.get('execution_id')
|
||||
execution = get_object_or_none(AutomationExecution, pk=eid)
|
||||
if execution:
|
||||
queryset = queryset.filter(execution=execution)
|
||||
return queryset
|
||||
return queryset.filter(execution_id=eid)
|
||||
|
||||
|
||||
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
|
||||
|
||||
@@ -47,4 +47,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
become: false
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
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 }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl | default('') }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
register: db_info
|
||||
@@ -31,8 +31,8 @@
|
||||
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 }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
@@ -49,7 +49,7 @@
|
||||
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 }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@@ -24,6 +28,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
@@ -37,4 +45,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
filter: version
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
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"
|
||||
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length != 0
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
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"
|
||||
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; CREATE USER {{ account.username }} FOR LOGIN {{ account.username }}; select @@version"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length == 0
|
||||
|
||||
|
||||
@@ -80,7 +80,11 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -91,6 +95,5 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -80,7 +80,11 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -91,6 +95,5 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -136,7 +136,8 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
'username': account.username,
|
||||
'secret_type': secret_type,
|
||||
'secret': new_secret,
|
||||
'private_key_path': private_key_path
|
||||
'private_key_path': private_key_path,
|
||||
'become': account.get_ansible_become_auth(),
|
||||
}
|
||||
if asset.platform.type == 'oracle':
|
||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
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 }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
filter: users
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
filter: users
|
||||
register: db_info
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
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 }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
register: db_info
|
||||
@@ -31,8 +31,8 @@
|
||||
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 }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
@@ -49,7 +49,7 @@
|
||||
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 }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@@ -24,6 +28,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
@@ -37,4 +45,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
filter: version
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
role_attr_flags: LOGIN
|
||||
ignore_errors: true
|
||||
when: result is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.postgresql.postgresql_ping:
|
||||
@@ -42,3 +43,5 @@
|
||||
when:
|
||||
- result is succeeded
|
||||
- change_info is succeeded
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
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"
|
||||
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length != 0
|
||||
register: change_info
|
||||
@@ -52,7 +52,7 @@
|
||||
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"
|
||||
script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length == 0
|
||||
register: change_info
|
||||
|
||||
@@ -80,7 +80,11 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -91,7 +95,6 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
|
||||
@@ -80,7 +80,11 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -91,7 +95,6 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
become: false
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
|
||||
@@ -56,7 +56,8 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||
'username': account.username,
|
||||
'secret_type': secret_type,
|
||||
'secret': new_secret,
|
||||
'private_key_path': private_key_path
|
||||
'private_key_path': private_key_path,
|
||||
'become': account.get_ansible_become_auth(),
|
||||
}
|
||||
if asset.platform.type == 'oracle':
|
||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||
@@ -80,7 +81,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||
pass
|
||||
|
||||
def on_runner_failed(self, runner, e):
|
||||
logger.error("Pust account error: ", e)
|
||||
logger.error("Pust account error: {}".format(e))
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
if self.secret_type and not self.check_secret():
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- name: Verify account (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
become: "{{ custom_become | default(False) }}"
|
||||
become_method: "{{ custom_become_method | default('su') }}"
|
||||
become_user: "{{ custom_become_user | default('') }}"
|
||||
become_password: "{{ custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: "{{ account.become.ansible_become_method | default('su') }}"
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
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 }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
@@ -10,4 +10,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
filter: version
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
community.postgresql.postgresql_ping:
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Verify account connectivity
|
||||
become: no
|
||||
- name: Verify account connectivity(Do not switch)
|
||||
ansible.builtin.ping:
|
||||
vars:
|
||||
ansible_become: no
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
when: not account.become.ansible_become
|
||||
|
||||
- name: Verify account connectivity(Switch)
|
||||
ansible.builtin.ping:
|
||||
vars:
|
||||
ansible_become: yes
|
||||
ansible_user: "{{ account.become.ansible_user }}"
|
||||
ansible_password: "{{ account.become.ansible_password }}"
|
||||
ansible_ssh_private_key_file: "{{ account.become.ansible_ssh_private_key_file }}"
|
||||
ansible_become_method: "{{ account.become.ansible_become_method }}"
|
||||
ansible_become_user: "{{ account.become.ansible_become_user }}"
|
||||
ansible_become_password: "{{ account.become.ansible_become_password }}"
|
||||
when: account.become.ansible_become
|
||||
|
||||
@@ -42,7 +42,6 @@ class VerifyAccountManager(AccountBasePlaybookManager):
|
||||
if host.get('error'):
|
||||
return host
|
||||
|
||||
# host['ssh_args'] = '-o ControlMaster=no -o ControlPersist=no'
|
||||
accounts = asset.accounts.all()
|
||||
accounts = self.get_accounts(account, accounts)
|
||||
inventory_hosts = []
|
||||
@@ -64,7 +63,8 @@ class VerifyAccountManager(AccountBasePlaybookManager):
|
||||
'username': account.username,
|
||||
'secret_type': account.secret_type,
|
||||
'secret': secret,
|
||||
'private_key_path': private_key_path
|
||||
'private_key_path': private_key_path,
|
||||
'become': account.get_ansible_become_auth(),
|
||||
}
|
||||
if account.platform.type == 'oracle':
|
||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||
|
||||
@@ -4,11 +4,13 @@ from django.utils.translation import gettext_lazy as _
|
||||
from assets.const import Connectivity
|
||||
from common.db.fields import TreeChoices
|
||||
|
||||
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
|
||||
DEFAULT_PASSWORD_LENGTH = 30
|
||||
DEFAULT_PASSWORD_RULES = {
|
||||
'length': DEFAULT_PASSWORD_LENGTH,
|
||||
'symbol_set': string_punctuation
|
||||
'uppercase': True,
|
||||
'lowercase': True,
|
||||
'digit': True,
|
||||
'symbol': True,
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
@@ -41,8 +43,8 @@ class AutomationTypes(models.TextChoices):
|
||||
|
||||
|
||||
class SecretStrategy(models.TextChoices):
|
||||
custom = 'specific', _('Specific password')
|
||||
random = 'random', _('Random')
|
||||
custom = 'specific', _('Specific secret')
|
||||
random = 'random', _('Random generate')
|
||||
|
||||
|
||||
class SSHKeyStrategy(models.TextChoices):
|
||||
|
||||
@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
|
||||
'verbose_name': 'Automation execution',
|
||||
'verbose_name_plural': 'Automation executions',
|
||||
'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
|
||||
('add_changesecretexection', 'Can add change secret execution'),
|
||||
('add_changesecretexecution', 'Can add change secret execution'),
|
||||
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
||||
('add_gatheraccountsexecution', 'Can add gather accounts execution')],
|
||||
'proxy': True,
|
||||
@@ -113,7 +113,7 @@ class Migration(migrations.Migration):
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')),
|
||||
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')),
|
||||
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
|
||||
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
|
||||
('status', models.CharField(default='pending', max_length=16)),
|
||||
@@ -184,7 +184,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterModelOptions(
|
||||
name='automationexecution',
|
||||
options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
|
||||
('add_changesecretexection', 'Can add change secret execution'),
|
||||
('add_changesecretexecution', 'Can add change secret execution'),
|
||||
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
||||
('add_gatheraccountsexecution', 'Can add gather accounts execution'),
|
||||
('view_pushaccountexecution', 'Can view push account execution'),
|
||||
|
||||
@@ -13,11 +13,11 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='changesecretautomation',
|
||||
name='secret_strategy',
|
||||
field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pushaccountautomation',
|
||||
name='secret_strategy',
|
||||
field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
),
|
||||
]
|
||||
|
||||
34
apps/accounts/migrations/0015_auto_20230825_1120.py
Normal file
34
apps/accounts/migrations/0015_auto_20230825_1120.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.1.10 on 2023-08-25 03:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0122_auto_20230803_1553'),
|
||||
('accounts', '0014_virtualaccount'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='accounttemplate',
|
||||
name='auto_push',
|
||||
field=models.BooleanField(default=False, verbose_name='Auto push'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accounttemplate',
|
||||
name='platforms',
|
||||
field=models.ManyToManyField(related_name='account_templates', to='assets.platform', verbose_name='Platforms', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accounttemplate',
|
||||
name='push_params',
|
||||
field=models.JSONField(default=dict, verbose_name='Push params'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accounttemplate',
|
||||
name='secret_strategy',
|
||||
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.10 on 2023-09-18 08:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0015_auto_20230825_1120'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='accounttemplate',
|
||||
name='password_rules',
|
||||
field=models.JSONField(default=dict, verbose_name='Password rules'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.1.10 on 2023-10-24 05:59
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('accounts', '0016_accounttemplate_password_rules'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='automationexecution',
|
||||
options={
|
||||
'permissions': [
|
||||
('view_changesecretexecution', 'Can view change secret execution'),
|
||||
('add_changesecretexecution', 'Can add change secret execution'),
|
||||
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
||||
('add_gatheraccountsexecution', 'Can add gather accounts execution'),
|
||||
('view_pushaccountexecution', 'Can view push account execution'),
|
||||
('add_pushaccountexecution', 'Can add push account execution')
|
||||
],
|
||||
'verbose_name': 'Automation execution', 'verbose_name_plural': 'Automation executions'},
|
||||
),
|
||||
]
|
||||
75
apps/accounts/mixins.py
Normal file
75
apps/accounts/mixins.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.utils import translation
|
||||
from django.utils.translation import gettext_noop
|
||||
|
||||
from audits.const import ActionChoices
|
||||
from common.views.mixins import RecordViewLogMixin
|
||||
from common.utils import i18n_fmt
|
||||
|
||||
|
||||
class AccountRecordViewLogMixin(RecordViewLogMixin):
|
||||
get_object: callable
|
||||
get_queryset: callable
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
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 = gettext_noop("Export only selected items")
|
||||
else:
|
||||
query = ",".join(
|
||||
["%s=%s" % (key, value) for key, value in params.items()]
|
||||
)
|
||||
display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query)
|
||||
return display_message
|
||||
|
||||
@property
|
||||
def detail_msg(self):
|
||||
return i18n_fmt(
|
||||
gettext_noop('User %s view/export secret'), self.request.user
|
||||
)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
list_func = getattr(super(), 'list')
|
||||
if not callable(list_func):
|
||||
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
response = list_func(request, *args, **kwargs)
|
||||
with translation.override('en'):
|
||||
resource_display = self.get_resource_display(request)
|
||||
ids = [q.id for q in self.get_queryset()]
|
||||
self.record_logs(
|
||||
ids, ActionChoices.view, self.detail_msg, resource_display=resource_display
|
||||
)
|
||||
return response
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
retrieve_func = getattr(super(), 'retrieve')
|
||||
if not callable(retrieve_func):
|
||||
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
response = retrieve_func(request, *args, **kwargs)
|
||||
with translation.override('en'):
|
||||
resource = self.get_object()
|
||||
self.record_logs(
|
||||
[resource.id], ActionChoices.view, self.detail_msg, resource=resource
|
||||
)
|
||||
return response
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from .account import *
|
||||
from .automations import *
|
||||
from .base import *
|
||||
from .template import *
|
||||
from .virtual import *
|
||||
from .account import * # noqa
|
||||
from .base import * # noqa
|
||||
from .automations import * # noqa
|
||||
from .template import * # noqa
|
||||
from .virtual import * # noqa
|
||||
|
||||
@@ -95,6 +95,33 @@ class Account(AbsConnectivity, BaseAccount):
|
||||
""" 排除自己和以自己为 su-from 的账号 """
|
||||
return self.asset.accounts.exclude(id=self.id).exclude(su_from=self)
|
||||
|
||||
@staticmethod
|
||||
def make_account_ansible_vars(su_from):
|
||||
var = {
|
||||
'ansible_user': su_from.username,
|
||||
}
|
||||
if not su_from.secret:
|
||||
return var
|
||||
var['ansible_password'] = su_from.secret
|
||||
var['ansible_ssh_private_key_file'] = su_from.private_key_path
|
||||
return var
|
||||
|
||||
def get_ansible_become_auth(self):
|
||||
su_from = self.su_from
|
||||
platform = self.platform
|
||||
auth = {'ansible_become': False}
|
||||
if not (platform.su_enabled and su_from):
|
||||
return auth
|
||||
|
||||
auth.update(self.make_account_ansible_vars(su_from))
|
||||
become_method = 'sudo' if platform.su_method != 'su' else 'su'
|
||||
password = su_from.secret if become_method == 'sudo' else self.secret
|
||||
auth['ansible_become'] = True
|
||||
auth['ansible_become_method'] = become_method
|
||||
auth['ansible_become_user'] = self.username
|
||||
auth['ansible_become_password'] = password
|
||||
return auth
|
||||
|
||||
|
||||
def replace_history_model_with_mixin():
|
||||
"""
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const import SSHKeyStrategy
|
||||
from accounts.models import Account, SecretWithRandomMixin
|
||||
from accounts.tasks import execute_account_automation_task
|
||||
from assets.models.automations import (
|
||||
BaseAutomation as AssetBaseAutomation,
|
||||
AutomationExecution as AssetAutomationExecution
|
||||
)
|
||||
|
||||
__all__ = ['AccountBaseAutomation', 'AutomationExecution']
|
||||
__all__ = ['AccountBaseAutomation', 'AutomationExecution', 'ChangeSecretMixin']
|
||||
|
||||
|
||||
class AccountBaseAutomation(AssetBaseAutomation):
|
||||
@@ -30,7 +33,7 @@ class AutomationExecution(AssetAutomationExecution):
|
||||
verbose_name_plural = _("Automation executions")
|
||||
permissions = [
|
||||
('view_changesecretexecution', _('Can view change secret execution')),
|
||||
('add_changesecretexection', _('Can add change secret execution')),
|
||||
('add_changesecretexecution', _('Can add change secret execution')),
|
||||
|
||||
('view_gatheraccountsexecution', _('Can view gather accounts execution')),
|
||||
('add_gatheraccountsexecution', _('Can add gather accounts execution')),
|
||||
@@ -43,3 +46,40 @@ class AutomationExecution(AssetAutomationExecution):
|
||||
from accounts.automations.endpoint import ExecutionManager
|
||||
manager = ExecutionManager(execution=self)
|
||||
return manager.run()
|
||||
|
||||
|
||||
class ChangeSecretMixin(SecretWithRandomMixin):
|
||||
ssh_key_change_strategy = models.CharField(
|
||||
choices=SSHKeyStrategy.choices, max_length=16,
|
||||
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
|
||||
)
|
||||
get_all_assets: callable # get all assets
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def create_nonlocal_accounts(self, usernames, asset):
|
||||
pass
|
||||
|
||||
def get_account_ids(self):
|
||||
usernames = self.accounts
|
||||
accounts = Account.objects.none()
|
||||
for asset in self.get_all_assets():
|
||||
self.create_nonlocal_accounts(usernames, asset)
|
||||
accounts = accounts | asset.accounts.all()
|
||||
account_ids = accounts.filter(
|
||||
username__in=usernames, secret_type=self.secret_type
|
||||
).values_list('id', flat=True)
|
||||
return [str(_id) for _id in account_ids]
|
||||
|
||||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'secret': self.secret,
|
||||
'secret_type': self.secret_type,
|
||||
'accounts': self.get_account_ids(),
|
||||
'password_rules': self.password_rules,
|
||||
'secret_strategy': self.secret_strategy,
|
||||
'ssh_key_change_strategy': self.ssh_key_change_strategy,
|
||||
})
|
||||
return attr_json
|
||||
|
||||
@@ -2,62 +2,13 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const import (
|
||||
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
||||
AutomationTypes
|
||||
)
|
||||
from accounts.models import Account
|
||||
from common.db import fields
|
||||
from common.db.models import JMSBaseModel
|
||||
from .base import AccountBaseAutomation
|
||||
from .base import AccountBaseAutomation, ChangeSecretMixin
|
||||
|
||||
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'ChangeSecretMixin']
|
||||
|
||||
|
||||
class ChangeSecretMixin(models.Model):
|
||||
secret_type = models.CharField(
|
||||
choices=SecretType.choices, max_length=16,
|
||||
default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
||||
)
|
||||
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
||||
secret_strategy = models.CharField(
|
||||
choices=SecretStrategy.choices, max_length=16,
|
||||
default=SecretStrategy.custom, verbose_name=_('Secret strategy')
|
||||
)
|
||||
password_rules = models.JSONField(default=dict, verbose_name=_('Password rules'))
|
||||
ssh_key_change_strategy = models.CharField(
|
||||
choices=SSHKeyStrategy.choices, max_length=16,
|
||||
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
|
||||
)
|
||||
|
||||
get_all_assets: callable # get all assets
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def create_nonlocal_accounts(self, usernames, asset):
|
||||
pass
|
||||
|
||||
def get_account_ids(self):
|
||||
usernames = self.accounts
|
||||
accounts = Account.objects.none()
|
||||
for asset in self.get_all_assets():
|
||||
self.create_nonlocal_accounts(usernames, asset)
|
||||
accounts = accounts | asset.accounts.all()
|
||||
account_ids = accounts.filter(
|
||||
username__in=usernames, secret_type=self.secret_type
|
||||
).values_list('id', flat=True)
|
||||
return [str(_id) for _id in account_ids]
|
||||
|
||||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'secret': self.secret,
|
||||
'secret_type': self.secret_type,
|
||||
'accounts': self.get_account_ids(),
|
||||
'password_rules': self.password_rules,
|
||||
'secret_strategy': self.secret_strategy,
|
||||
'ssh_key_change_strategy': self.ssh_key_change_strategy,
|
||||
})
|
||||
return attr_json
|
||||
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', ]
|
||||
|
||||
|
||||
class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import Account
|
||||
from jumpserver.utils import has_valid_xpack_license
|
||||
from .base import AccountBaseAutomation
|
||||
from .change_secret import ChangeSecretMixin
|
||||
|
||||
@@ -17,9 +17,9 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||
|
||||
def create_nonlocal_accounts(self, usernames, asset):
|
||||
secret_type = self.secret_type
|
||||
account_usernames = asset.accounts.filter(secret_type=self.secret_type).values_list(
|
||||
'username', flat=True
|
||||
)
|
||||
account_usernames = asset.accounts \
|
||||
.filter(secret_type=self.secret_type) \
|
||||
.values_list('username', flat=True)
|
||||
create_usernames = set(usernames) - set(account_usernames)
|
||||
create_account_objs = [
|
||||
Account(
|
||||
@@ -30,9 +30,6 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||
]
|
||||
Account.objects.bulk_create(create_account_objs)
|
||||
|
||||
def set_period_schedule(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def dynamic_username(self):
|
||||
return self.username == '@USER'
|
||||
@@ -44,7 +41,7 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.type = AutomationTypes.push_account
|
||||
if not has_valid_xpack_license():
|
||||
if not settings.XPACK_LICENSE_IS_VALID:
|
||||
self.is_periodic = False
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const import SecretType
|
||||
from accounts.const import SecretType, SecretStrategy
|
||||
from accounts.models.mixins import VaultModelMixin, VaultManagerMixin, VaultQuerySetMixin
|
||||
from accounts.utils import SecretGenerator
|
||||
from common.db import fields
|
||||
from common.utils import (
|
||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
||||
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
|
||||
)
|
||||
from accounts.models.mixins import VaultModelMixin, VaultManagerMixin, VaultQuerySetMixin
|
||||
from orgs.mixins.models import JMSOrgBaseModel, OrgManager
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -29,6 +31,35 @@ class BaseAccountManager(VaultManagerMixin, OrgManager):
|
||||
return self.get_queryset().active()
|
||||
|
||||
|
||||
class SecretWithRandomMixin(models.Model):
|
||||
secret_type = models.CharField(
|
||||
choices=SecretType.choices, max_length=16,
|
||||
default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
||||
)
|
||||
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
||||
secret_strategy = models.CharField(
|
||||
choices=SecretStrategy.choices, max_length=16,
|
||||
default=SecretStrategy.custom, verbose_name=_('Secret strategy')
|
||||
)
|
||||
password_rules = models.JSONField(default=dict, verbose_name=_('Password rules'))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@lazyproperty
|
||||
def secret_generator(self):
|
||||
return SecretGenerator(
|
||||
self.secret_strategy, self.secret_type,
|
||||
self.password_rules,
|
||||
)
|
||||
|
||||
def get_secret(self):
|
||||
if self.secret_strategy == 'random':
|
||||
return self.secret_generator.get_secret()
|
||||
else:
|
||||
return self.secret
|
||||
|
||||
|
||||
class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||
|
||||
@@ -37,8 +37,9 @@ class VaultManagerMixin(models.Manager):
|
||||
post_save.send(obj.__class__, instance=obj, created=True)
|
||||
return objs
|
||||
|
||||
def bulk_update(self, objs, batch_size=None, ignore_conflicts=False):
|
||||
objs = super().bulk_update(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts)
|
||||
def bulk_update(self, objs, fields, batch_size=None):
|
||||
fields = ["_secret" if field == "secret" else field for field in fields]
|
||||
super().bulk_update(objs, fields, batch_size=batch_size)
|
||||
for obj in objs:
|
||||
post_save.send(obj.__class__, instance=obj, created=False)
|
||||
return objs
|
||||
|
||||
@@ -4,16 +4,22 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .account import Account
|
||||
from .base import BaseAccount
|
||||
from .base import BaseAccount, SecretWithRandomMixin
|
||||
|
||||
__all__ = ['AccountTemplate', ]
|
||||
|
||||
|
||||
class AccountTemplate(BaseAccount):
|
||||
class AccountTemplate(BaseAccount, SecretWithRandomMixin):
|
||||
su_from = models.ForeignKey(
|
||||
'self', related_name='su_to', null=True,
|
||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||
)
|
||||
auto_push = models.BooleanField(default=False, verbose_name=_('Auto push'))
|
||||
platforms = models.ManyToManyField(
|
||||
'assets.Platform', related_name='account_templates',
|
||||
verbose_name=_('Platforms'), blank=True,
|
||||
)
|
||||
push_params = models.JSONField(default=dict, verbose_name=_('Push params'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Account template')
|
||||
@@ -25,15 +31,15 @@ class AccountTemplate(BaseAccount):
|
||||
('change_accounttemplatesecret', _('Can change asset account template secret')),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}({self.username})'
|
||||
|
||||
@classmethod
|
||||
def get_su_from_account_templates(cls, pk=None):
|
||||
if pk is None:
|
||||
return cls.objects.all()
|
||||
return cls.objects.exclude(Q(id=pk) | Q(su_from_id=pk))
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}({self.username})'
|
||||
|
||||
def get_su_from_account(self, asset):
|
||||
su_from = self.su_from
|
||||
if su_from and asset.platform.su_enabled:
|
||||
@@ -43,8 +49,7 @@ class AccountTemplate(BaseAccount):
|
||||
).first()
|
||||
return account
|
||||
|
||||
@staticmethod
|
||||
def bulk_update_accounts(accounts, data):
|
||||
def bulk_update_accounts(self, accounts):
|
||||
history_model = Account.history.model
|
||||
account_ids = accounts.values_list('id', flat=True)
|
||||
history_accounts = history_model.objects.filter(id__in=account_ids)
|
||||
@@ -57,8 +62,7 @@ class AccountTemplate(BaseAccount):
|
||||
for account in accounts:
|
||||
account_id = str(account.id)
|
||||
account.version = account_id_count_map.get(account_id) + 1
|
||||
for k, v in data.items():
|
||||
setattr(account, k, v)
|
||||
account.secret = self.get_secret()
|
||||
Account.objects.bulk_update(accounts, ['version', 'secret'])
|
||||
|
||||
@staticmethod
|
||||
@@ -80,7 +84,5 @@ class AccountTemplate(BaseAccount):
|
||||
|
||||
def bulk_sync_account_secret(self, accounts, user_id):
|
||||
""" 批量同步账号密码 """
|
||||
if not accounts:
|
||||
return
|
||||
self.bulk_update_accounts(accounts, {'secret': self.secret})
|
||||
self.bulk_update_accounts(accounts)
|
||||
self.bulk_create_history_accounts(accounts, user_id)
|
||||
|
||||
@@ -2,6 +2,7 @@ import uuid
|
||||
from copy import deepcopy
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
@@ -73,6 +74,23 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
name = name + '_' + uuid.uuid4().hex[:4]
|
||||
initial_data['name'] = name
|
||||
|
||||
@staticmethod
|
||||
def get_template_attr_for_account(template):
|
||||
# Set initial data from template
|
||||
field_names = [
|
||||
'name', 'username', 'secret',
|
||||
'secret_type', 'privileged', 'is_active'
|
||||
]
|
||||
|
||||
attrs = {}
|
||||
for name in field_names:
|
||||
value = getattr(template, name, None)
|
||||
if value is None:
|
||||
continue
|
||||
attrs[name] = value
|
||||
attrs['secret'] = template.get_secret()
|
||||
return attrs
|
||||
|
||||
def from_template_if_need(self, initial_data):
|
||||
if isinstance(initial_data, str):
|
||||
return
|
||||
@@ -89,20 +107,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
raise serializers.ValidationError({'template': 'Template not found'})
|
||||
|
||||
self._template = template
|
||||
# Set initial data from template
|
||||
ignore_fields = ['id', 'date_created', 'date_updated', 'su_from', 'org_id']
|
||||
field_names = [
|
||||
field.name for field in template._meta.fields
|
||||
if field.name not in ignore_fields
|
||||
]
|
||||
field_names = [name if name != '_secret' else 'secret' for name in field_names]
|
||||
|
||||
attrs = {}
|
||||
for name in field_names:
|
||||
value = getattr(template, name, None)
|
||||
if value is None:
|
||||
continue
|
||||
attrs[name] = value
|
||||
attrs = self.get_template_attr_for_account(template)
|
||||
initial_data.update(attrs)
|
||||
initial_data.update({
|
||||
'source': Source.TEMPLATE,
|
||||
@@ -114,10 +119,13 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
initial_data['su_from'] = template.get_su_from_account(asset)
|
||||
|
||||
@staticmethod
|
||||
def push_account_if_need(instance, push_now, params, stat):
|
||||
def push_account_if_need(self, instance, push_now, params, stat):
|
||||
if not push_now or stat not in ['created', 'updated']:
|
||||
return
|
||||
transaction.on_commit(lambda: self.start_push(instance, params))
|
||||
|
||||
@staticmethod
|
||||
def start_push(instance, params):
|
||||
push_accounts_to_assets_task.delay([str(instance.id)], params)
|
||||
|
||||
def get_validators(self):
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from accounts.models import AccountTemplate, Account
|
||||
from accounts.const import SecretStrategy, SecretType
|
||||
from accounts.models import AccountTemplate
|
||||
from accounts.utils import SecretGenerator
|
||||
from common.serializers import SecretReadableMixin
|
||||
from common.serializers.fields import ObjectRelatedField
|
||||
from .base import BaseAccountSerializer
|
||||
|
||||
|
||||
class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
is_sync_account = serializers.BooleanField(default=False, write_only=True)
|
||||
_is_sync_account = False
|
||||
class PasswordRulesSerializer(serializers.Serializer):
|
||||
length = serializers.IntegerField(min_value=8, max_value=30, default=16, label=_('Password length'))
|
||||
lowercase = serializers.BooleanField(default=True, label=_('Lowercase'))
|
||||
uppercase = serializers.BooleanField(default=True, label=_('Uppercase'))
|
||||
digit = serializers.BooleanField(default=True, label=_('Digit'))
|
||||
symbol = serializers.BooleanField(default=True, label=_('Special symbol'))
|
||||
|
||||
|
||||
class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
password_rules = PasswordRulesSerializer(required=False, label=_('Password rules'))
|
||||
su_from = ObjectRelatedField(
|
||||
required=False, queryset=AccountTemplate.objects, allow_null=True,
|
||||
allow_empty=True, label=_('Su from'), attrs=('id', 'name', 'username')
|
||||
@@ -18,36 +26,38 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
|
||||
class Meta(BaseAccountSerializer.Meta):
|
||||
model = AccountTemplate
|
||||
fields = BaseAccountSerializer.Meta.fields + ['is_sync_account', 'su_from']
|
||||
|
||||
def sync_accounts_secret(self, instance, diff):
|
||||
if not self._is_sync_account or 'secret' not in diff:
|
||||
return
|
||||
query_data = {
|
||||
'source_id': instance.id,
|
||||
'username': instance.username,
|
||||
'secret_type': instance.secret_type
|
||||
fields = BaseAccountSerializer.Meta.fields + [
|
||||
'secret_strategy', 'password_rules',
|
||||
'auto_push', 'push_params', 'platforms',
|
||||
'su_from'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'secret_strategy': {'help_text': _('Secret generation strategy for account creation')},
|
||||
'auto_push': {'help_text': _('Whether to automatically push the account to the asset')},
|
||||
'platforms': {
|
||||
'help_text': _(
|
||||
'Associated platform, you can configure push parameters. '
|
||||
'If not associated, default parameters will be used'
|
||||
),
|
||||
'required': False
|
||||
},
|
||||
}
|
||||
accounts = Account.objects.filter(**query_data)
|
||||
instance.bulk_sync_account_secret(accounts, self.context['request'].user.id)
|
||||
|
||||
@staticmethod
|
||||
def generate_secret(attrs):
|
||||
secret_type = attrs.get('secret_type', SecretType.PASSWORD)
|
||||
secret_strategy = attrs.get('secret_strategy', SecretStrategy.custom)
|
||||
password_rules = attrs.get('password_rules')
|
||||
if secret_strategy != SecretStrategy.random:
|
||||
return
|
||||
generator = SecretGenerator(secret_strategy, secret_type, password_rules)
|
||||
attrs['secret'] = generator.get_secret()
|
||||
|
||||
def validate(self, attrs):
|
||||
self._is_sync_account = attrs.pop('is_sync_account', None)
|
||||
attrs = super().validate(attrs)
|
||||
self.generate_secret(attrs)
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
diff = {
|
||||
k: v for k, v in validated_data.items()
|
||||
if getattr(instance, k, None) != v
|
||||
}
|
||||
instance = super().update(instance, validated_data)
|
||||
if {'username', 'secret_type'} & set(diff.keys()):
|
||||
Account.objects.filter(source_id=instance.id).update(source_id=None)
|
||||
else:
|
||||
self.sync_accounts_secret(instance, diff)
|
||||
return instance
|
||||
|
||||
|
||||
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer):
|
||||
class Meta(AccountTemplateSerializer.Meta):
|
||||
|
||||
@@ -19,8 +19,12 @@ class VirtualAccountSerializer(serializers.ModelSerializer):
|
||||
'comment': {'label': _('Comment')},
|
||||
'name': {'label': _('Name')},
|
||||
'username': {'label': _('Username')},
|
||||
'secret_from_login': {'help_text': _('Current only support login from AD/LDAP. Secret priority: '
|
||||
'Same account in asset secret > Login secret > Manual input')
|
||||
},
|
||||
'secret_from_login': {
|
||||
'help_text': _(
|
||||
'Current only support login from AD/LDAP. Secret priority: '
|
||||
'Same account in asset secret > Login secret > Manual input. <br/ >'
|
||||
'For security, please set config CACHE_LOGIN_PASSWORD_ENABLED to true'
|
||||
)
|
||||
},
|
||||
'alias': {'required': False},
|
||||
}
|
||||
|
||||
@@ -4,14 +4,13 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from accounts.const import (
|
||||
AutomationTypes, DEFAULT_PASSWORD_RULES,
|
||||
SecretType, SecretStrategy, SSHKeyStrategy
|
||||
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
||||
)
|
||||
from accounts.models import (
|
||||
Account, ChangeSecretAutomation,
|
||||
ChangeSecretRecord, AutomationExecution
|
||||
)
|
||||
from accounts.serializers import AuthValidateMixin
|
||||
from accounts.serializers import AuthValidateMixin, PasswordRulesSerializer
|
||||
from assets.models import Asset
|
||||
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
||||
from common.utils import get_logger
|
||||
@@ -42,7 +41,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
ssh_key_change_strategy = LabeledChoiceField(
|
||||
choices=SSHKeyStrategy.choices, required=False, label=_('SSH Key strategy')
|
||||
)
|
||||
password_rules = serializers.DictField(default=DEFAULT_PASSWORD_RULES)
|
||||
password_rules = PasswordRulesSerializer(required=False, label=_('Password rules'))
|
||||
secret_type = LabeledChoiceField(choices=get_secret_types(), required=True, label=_('Secret type'))
|
||||
|
||||
class Meta:
|
||||
@@ -72,7 +71,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
return password_rules
|
||||
|
||||
length = password_rules.get('length')
|
||||
symbol_set = password_rules.get('symbol_set', '')
|
||||
|
||||
try:
|
||||
length = int(length)
|
||||
@@ -85,10 +83,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
msg = _('* Password length range 6-30 bits')
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
if not isinstance(symbol_set, str):
|
||||
symbol_set = str(symbol_set)
|
||||
|
||||
password_rules = {'length': length, 'symbol_set': ''.join(symbol_set)}
|
||||
return password_rules
|
||||
|
||||
def validate(self, attrs):
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
from django.db.models.signals import pre_save, post_save, post_delete
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db.models.signals import post_delete
|
||||
from django.db.models.signals import pre_save, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_noop
|
||||
|
||||
from accounts.backends import vault_client
|
||||
from common.utils import get_logger
|
||||
from audits.const import ActivityChoices
|
||||
from audits.signal_handlers import create_activities
|
||||
from common.decorators import merge_delay_run
|
||||
from common.utils import get_logger, i18n_fmt
|
||||
from .models import Account, AccountTemplate
|
||||
from .tasks.push_account import push_accounts_to_assets_task
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -16,6 +24,53 @@ def on_account_pre_save(sender, instance, **kwargs):
|
||||
instance.version = instance.history.count()
|
||||
|
||||
|
||||
@merge_delay_run(ttl=5)
|
||||
def push_accounts_if_need(accounts=()):
|
||||
from .models import AccountTemplate
|
||||
|
||||
template_accounts = defaultdict(list)
|
||||
for ac in accounts:
|
||||
# 再强调一次吧
|
||||
if ac.source != 'template':
|
||||
continue
|
||||
template_accounts[ac.source_id].append(ac)
|
||||
|
||||
for source_id, accounts in template_accounts.items():
|
||||
template = AccountTemplate.objects.filter(id=source_id).first()
|
||||
if not template or not template.auto_push:
|
||||
continue
|
||||
logger.debug("Push accounts to source: %s", source_id)
|
||||
account_ids = [str(ac.id) for ac in accounts]
|
||||
task = push_accounts_to_assets_task.delay(account_ids, params=template.push_params)
|
||||
detail = i18n_fmt(
|
||||
gettext_noop('Push related accounts to assets: %s, by system'),
|
||||
len(account_ids)
|
||||
)
|
||||
create_activities([str(template.id)], detail, task.id, ActivityChoices.task, template.org_id)
|
||||
logger.debug("Push accounts to source: %s, task: %s", source_id, task)
|
||||
|
||||
|
||||
def create_accounts_activities(account, action='create'):
|
||||
if action == 'create':
|
||||
detail = i18n_fmt(gettext_noop('Add account: %s'), str(account))
|
||||
else:
|
||||
detail = i18n_fmt(gettext_noop('Delete account: %s'), str(account))
|
||||
create_activities([account.asset_id], detail, None, ActivityChoices.operate_log, account.org_id)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Account)
|
||||
def on_account_create_by_template(sender, instance, created=False, **kwargs):
|
||||
if not created or instance.source != 'template':
|
||||
return
|
||||
push_accounts_if_need(accounts=(instance,))
|
||||
create_accounts_activities(instance, action='create')
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Account)
|
||||
def on_account_delete(sender, instance, **kwargs):
|
||||
create_accounts_activities(instance, action='delete')
|
||||
|
||||
|
||||
class VaultSignalHandler(object):
|
||||
""" 处理 Vault 相关的信号 """
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .backup_account import *
|
||||
from .automation import *
|
||||
from .push_account import *
|
||||
from .verify_account import *
|
||||
from .backup_account import *
|
||||
from .gather_accounts import *
|
||||
from .push_account import *
|
||||
from .template import *
|
||||
from .verify_account import *
|
||||
|
||||
60
apps/accounts/tasks/template.py
Normal file
60
apps/accounts/tasks/template.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from datetime import datetime
|
||||
|
||||
from celery import shared_task
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from orgs.utils import tmp_to_root_org, tmp_to_org
|
||||
|
||||
|
||||
@shared_task(
|
||||
verbose_name=_('Template sync info to related accounts'),
|
||||
activity_callback=lambda self, template_id, *args, **kwargs: (template_id, None)
|
||||
)
|
||||
def template_sync_related_accounts(template_id, user_id=None):
|
||||
from accounts.models import Account, AccountTemplate
|
||||
with tmp_to_root_org():
|
||||
template = get_object_or_404(AccountTemplate, id=template_id)
|
||||
org_id = template.org_id
|
||||
|
||||
with tmp_to_org(org_id):
|
||||
accounts = Account.objects.filter(source_id=template_id)
|
||||
if not accounts:
|
||||
print('\033[35m>>> 没有需要同步的账号, 结束任务')
|
||||
print('\033[0m')
|
||||
return
|
||||
|
||||
failed, succeeded = 0, 0
|
||||
succeeded_account_ids = []
|
||||
name = template.name
|
||||
username = template.username
|
||||
secret_type = template.secret_type
|
||||
print(f'\033[32m>>> 开始同步模版名称、用户名、密钥类型到相关联的账号 ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})')
|
||||
with tmp_to_org(org_id):
|
||||
for account in accounts:
|
||||
account.name = name
|
||||
account.username = username
|
||||
account.secret_type = secret_type
|
||||
try:
|
||||
account.save(update_fields=['name', 'username', 'secret_type'])
|
||||
succeeded += 1
|
||||
succeeded_account_ids.append(account.id)
|
||||
except Exception as e:
|
||||
account.source_id = None
|
||||
account.save(update_fields=['source_id'])
|
||||
print(f'\033[31m- 同步失败: [{account}] 原因: [{e}]')
|
||||
failed += 1
|
||||
accounts = Account.objects.filter(id__in=succeeded_account_ids)
|
||||
if accounts:
|
||||
print(f'\033[33m>>> 批量更新账号密文 ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})')
|
||||
template.bulk_sync_account_secret(accounts, user_id)
|
||||
|
||||
total = succeeded + failed
|
||||
print(
|
||||
f'\033[33m>>> 同步完成:, '
|
||||
f'共计: {total}, '
|
||||
f'成功: {succeeded}, '
|
||||
f'失败: {failed}, '
|
||||
f'({datetime.now().strftime("%Y-%m-%d %H:%M:%S")}) '
|
||||
)
|
||||
print('\033[0m')
|
||||
@@ -1,3 +1,5 @@
|
||||
import copy
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
@@ -14,13 +16,23 @@ class SecretGenerator:
|
||||
|
||||
@staticmethod
|
||||
def generate_ssh_key():
|
||||
private_key, public_key = ssh_key_gen()
|
||||
private_key, __ = ssh_key_gen()
|
||||
return private_key
|
||||
|
||||
def generate_password(self):
|
||||
length = int(self.password_rules.get('length', 0))
|
||||
length = length if length else DEFAULT_PASSWORD_RULES['length']
|
||||
return random_string(length, special_char=True)
|
||||
password_rules = self.password_rules
|
||||
if not password_rules or not isinstance(password_rules, dict):
|
||||
password_rules = {}
|
||||
rules = copy.deepcopy(DEFAULT_PASSWORD_RULES)
|
||||
rules.update(password_rules)
|
||||
rules = {
|
||||
'length': rules['length'],
|
||||
'lower': rules['lowercase'],
|
||||
'upper': rules['uppercase'],
|
||||
'digit': rules['digit'],
|
||||
'special_char': rules['symbol']
|
||||
}
|
||||
return random_string(**rules)
|
||||
|
||||
def get_secret(self):
|
||||
if self.secret_type == SecretType.SSH_KEY:
|
||||
@@ -39,6 +51,8 @@ def validate_password_for_ansible(password):
|
||||
# Ansible 推送的时候不支持
|
||||
if '{{' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
||||
if '{%' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `{%` '))
|
||||
# Ansible Windows 推送的时候不支持
|
||||
if "'" in password:
|
||||
raise serializers.ValidationError(_("Password can not contains `'` "))
|
||||
|
||||
@@ -10,7 +10,7 @@ __all__ = ['CommandFilterACLViewSet', 'CommandGroupViewSet']
|
||||
|
||||
class CommandGroupViewSet(OrgBulkModelViewSet):
|
||||
model = models.CommandGroup
|
||||
filterset_fields = ('name',)
|
||||
filterset_fields = ('name', 'command_filters')
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.CommandGroupSerializer
|
||||
|
||||
|
||||
@@ -7,3 +7,4 @@ class ActionChoices(models.TextChoices):
|
||||
accept = 'accept', _('Accept')
|
||||
review = 'review', _('Review')
|
||||
warning = 'warning', _('Warning')
|
||||
notice = 'notice', _('Notifications')
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.10 on 2023-10-18 10:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('acls', '0017_alter_connectmethodacl_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='commandfilteracl',
|
||||
name='command_groups',
|
||||
field=models.ManyToManyField(related_name='command_filters', to='acls.commandgroup', verbose_name='Command group'),
|
||||
),
|
||||
]
|
||||
@@ -103,25 +103,27 @@ class UserAssetAccountBaseACL(OrgModelMixin, UserBaseACL):
|
||||
abstract = True
|
||||
|
||||
@classmethod
|
||||
def filter_queryset(cls, user=None, asset=None, account=None, account_username=None, **kwargs):
|
||||
def _get_filter_queryset(cls, user=None, asset=None, account=None, account_username=None, **kwargs):
|
||||
queryset = cls.objects.all()
|
||||
|
||||
if user:
|
||||
q = cls.users.get_filter_q(user)
|
||||
queryset = queryset.filter(q)
|
||||
q = models.Q()
|
||||
|
||||
if asset:
|
||||
org_id = asset.org_id
|
||||
with tmp_to_org(org_id):
|
||||
q = cls.assets.get_filter_q(asset)
|
||||
queryset = queryset.filter(q)
|
||||
q &= cls.assets.get_filter_q(asset)
|
||||
if user:
|
||||
q &= cls.users.get_filter_q(user)
|
||||
if account and not account_username:
|
||||
account_username = account.username
|
||||
if account_username:
|
||||
q = models.Q(accounts__contains=account_username) | \
|
||||
models.Q(accounts__contains='*') | \
|
||||
models.Q(accounts__contains='@ALL')
|
||||
queryset = queryset.filter(q)
|
||||
q &= models.Q(accounts__contains=account_username) | \
|
||||
models.Q(accounts__contains='*') | \
|
||||
models.Q(accounts__contains='@ALL')
|
||||
if kwargs:
|
||||
queryset = queryset.filter(**kwargs)
|
||||
q &= models.Q(**kwargs)
|
||||
queryset = queryset.filter(q)
|
||||
return queryset.valid().distinct()
|
||||
|
||||
@classmethod
|
||||
def filter_queryset(cls, asset=None, **kwargs):
|
||||
org_id = asset.org_id if asset else ''
|
||||
with tmp_to_org(org_id):
|
||||
return cls._get_filter_queryset(asset=asset, **kwargs)
|
||||
|
||||
@@ -93,7 +93,10 @@ class CommandGroup(JMSOrgBaseModel):
|
||||
|
||||
|
||||
class CommandFilterACL(UserAssetAccountBaseACL):
|
||||
command_groups = models.ManyToManyField(CommandGroup, verbose_name=_('Command group'))
|
||||
command_groups = models.ManyToManyField(
|
||||
CommandGroup, verbose_name=_('Command group'),
|
||||
related_name='command_filters'
|
||||
)
|
||||
|
||||
class Meta(UserAssetAccountBaseACL.Meta):
|
||||
abstract = False
|
||||
|
||||
68
apps/acls/notifications.py
Normal file
68
apps/acls/notifications.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.models import Asset
|
||||
from audits.models import UserLoginLog
|
||||
from notifications.notifications import UserMessage
|
||||
from users.models import User
|
||||
|
||||
|
||||
class UserLoginReminderMsg(UserMessage):
|
||||
subject = _('User login reminder')
|
||||
|
||||
def __init__(self, user, user_log: UserLoginLog):
|
||||
self.user_log = user_log
|
||||
super().__init__(user)
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
user_log = self.user_log
|
||||
|
||||
context = {
|
||||
'ip': user_log.ip,
|
||||
'city': user_log.city,
|
||||
'username': user_log.username,
|
||||
'recipient': self.user.username,
|
||||
'user_agent': user_log.user_agent,
|
||||
}
|
||||
message = render_to_string('acls/user_login_reminder.html', context)
|
||||
|
||||
return {
|
||||
'subject': str(self.subject),
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
user = User.objects.first()
|
||||
user_log = UserLoginLog.objects.first()
|
||||
return cls(user, user_log)
|
||||
|
||||
|
||||
class AssetLoginReminderMsg(UserMessage):
|
||||
subject = _('Asset login reminder')
|
||||
|
||||
def __init__(self, user, asset: Asset, login_user: User, account_username):
|
||||
self.asset = asset
|
||||
self.login_user = login_user
|
||||
self.account_username = account_username
|
||||
super().__init__(user)
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
context = {
|
||||
'recipient': self.user.username,
|
||||
'username': self.login_user.username,
|
||||
'asset': str(self.asset),
|
||||
'account': self.account_username,
|
||||
}
|
||||
message = render_to_string('acls/asset_login_reminder.html', context)
|
||||
|
||||
return {
|
||||
'subject': str(self.subject),
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
user = User.objects.first()
|
||||
asset = Asset.objects.first()
|
||||
return cls(user, asset, user)
|
||||
@@ -1,9 +1,9 @@
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from acls.models.base import BaseACL
|
||||
from common.serializers.fields import JSONManyToManyField, LabeledChoiceField
|
||||
from jumpserver.utils import has_valid_xpack_license
|
||||
from orgs.models import Organization
|
||||
from ..const import ActionChoices
|
||||
|
||||
@@ -68,7 +68,7 @@ class ActionAclSerializer(serializers.Serializer):
|
||||
field_action = self.fields.get("action")
|
||||
if not field_action:
|
||||
return
|
||||
if not has_valid_xpack_license():
|
||||
if not settings.XPACK_LICENSE_IS_VALID:
|
||||
field_action._choices.pop(ActionChoices.review, None)
|
||||
for choice in self.Meta.action_choices_exclude:
|
||||
field_action._choices.pop(choice, None)
|
||||
|
||||
@@ -8,6 +8,7 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from terminal.models import Session
|
||||
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
|
||||
from ..const import ActionChoices
|
||||
|
||||
__all__ = ["CommandFilterACLSerializer", "CommandGroupSerializer", "CommandReviewSerializer"]
|
||||
|
||||
@@ -31,8 +32,7 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
|
||||
class Meta(BaseSerializer.Meta):
|
||||
model = CommandFilterACL
|
||||
fields = BaseSerializer.Meta.fields + ['command_groups']
|
||||
# 默认都支持所有的 actions
|
||||
action_choices_exclude = []
|
||||
action_choices_exclude = [ActionChoices.notice]
|
||||
|
||||
|
||||
class CommandReviewSerializer(serializers.Serializer):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
|
||||
from ..models import ConnectMethodACL
|
||||
from ..const import ActionChoices
|
||||
from ..models import ConnectMethodACL
|
||||
|
||||
__all__ = ["ConnectMethodACLSerializer"]
|
||||
|
||||
@@ -14,5 +14,5 @@ class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
|
||||
if i not in ['assets', 'accounts']
|
||||
]
|
||||
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
|
||||
ActionChoices.review, ActionChoices.accept
|
||||
ActionChoices.review, ActionChoices.accept, ActionChoices.notice
|
||||
]
|
||||
|
||||
13
apps/acls/templates/acls/asset_login_reminder.html
Normal file
13
apps/acls/templates/acls/asset_login_reminder.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% load i18n %}
|
||||
|
||||
<h3>{% trans 'Respectful' %}{{ recipient }},</h3>
|
||||
<hr>
|
||||
<p><strong>{% trans 'Username' %}:</strong> [{{ username }}]</p>
|
||||
<p><strong>{% trans 'Assets' %}:</strong> [{{ asset }}]</p>
|
||||
<p><strong>{% trans 'Account' %}:</strong> [{{ account }}]</p>
|
||||
<hr>
|
||||
|
||||
<p>{% trans 'The user has just logged in to the asset. Please ensure that this is an authorized operation. If you suspect that this is an unauthorized access, please take appropriate measures immediately.' %}</p>
|
||||
|
||||
<p>{% trans 'Thank you' %}!</p>
|
||||
|
||||
14
apps/acls/templates/acls/user_login_reminder.html
Normal file
14
apps/acls/templates/acls/user_login_reminder.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% load i18n %}
|
||||
|
||||
<h3>{% trans 'Respectful' %}{{ recipient }},</h3>
|
||||
<hr>
|
||||
<p><strong>{% trans 'Username' %}:</strong> [{{ username }}]</p>
|
||||
<p><strong>IP:</strong> [{{ ip }}]</p>
|
||||
<p><strong>{% trans 'Login city' %}:</strong> [{{ city }}]</p>
|
||||
<p><strong>{% trans 'User agent' %}:</strong> [{{ user_agent }}]</p>
|
||||
<hr>
|
||||
|
||||
<p>{% trans 'The user has just successfully logged into the system. Please ensure that this is an authorized operation. If you suspect that this is an unauthorized access, please take appropriate measures immediately.' %}</p>
|
||||
|
||||
<p>{% trans 'Thank you' %}!</p>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
|
||||
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 import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.status import HTTP_200_OK
|
||||
@@ -12,7 +15,7 @@ from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connect
|
||||
from assets import serializers
|
||||
from assets.exceptions import NotSupportedTemporarilyError
|
||||
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend
|
||||
from assets.models import Asset, Gateway, Platform
|
||||
from assets.models import Asset, Gateway, Platform, Protocol
|
||||
from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual
|
||||
from common.api import SuggestionMixin
|
||||
from common.drf.filters import BaseFilterSet, AttrRulesFilterBackend
|
||||
@@ -115,6 +118,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
||||
("gateways", "assets.view_gateway"),
|
||||
("spec_info", "assets.view_asset"),
|
||||
("gathered_info", "assets.view_asset"),
|
||||
("sync_platform_protocols", "assets.change_asset"),
|
||||
)
|
||||
extra_filter_backends = [
|
||||
LabelFilterBackend, IpInFilterBackend,
|
||||
@@ -152,6 +156,39 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
||||
gateways = asset.domain.gateways
|
||||
return self.get_paginated_response_from_queryset(gateways)
|
||||
|
||||
@action(methods=['post'], detail=False, url_path='sync-platform-protocols')
|
||||
def sync_platform_protocols(self, request, *args, **kwargs):
|
||||
platform_id = request.data.get('platform_id')
|
||||
platform = get_object_or_404(Platform, pk=platform_id)
|
||||
assets = platform.assets.all()
|
||||
|
||||
platform_protocols = {
|
||||
p['name']: p['port']
|
||||
for p in platform.protocols.values('name', 'port')
|
||||
}
|
||||
asset_protocols_map = defaultdict(set)
|
||||
protocols = assets.prefetch_related('protocols').values_list(
|
||||
'id', 'protocols__name'
|
||||
)
|
||||
for asset_id, protocol in protocols:
|
||||
asset_id = str(asset_id)
|
||||
asset_protocols_map[asset_id].add(protocol)
|
||||
objs = []
|
||||
for asset_id, protocols in asset_protocols_map.items():
|
||||
protocol_names = set(platform_protocols) - protocols
|
||||
if not protocol_names:
|
||||
continue
|
||||
for name in protocol_names:
|
||||
objs.append(
|
||||
Protocol(
|
||||
name=name,
|
||||
port=platform_protocols[name],
|
||||
asset_id=asset_id,
|
||||
)
|
||||
)
|
||||
Protocol.objects.bulk_create(objs)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
if request.path.find('/api/v1/assets/assets/') > -1:
|
||||
error = _('Cannot create asset directly, you should create a host or other')
|
||||
|
||||
@@ -42,7 +42,7 @@ class SerializeToTreeNodeMixin:
|
||||
'name': _name(node),
|
||||
'title': _name(node),
|
||||
'pId': node.parent_key,
|
||||
'isParent': node.assets_amount > 0,
|
||||
'isParent': True,
|
||||
'open': _open(node),
|
||||
'meta': {
|
||||
'data': {
|
||||
|
||||
@@ -49,13 +49,19 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
||||
@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)
|
||||
asset_ids = set(request.data.get('asset_ids', []))
|
||||
platform_ids = set(request.data.get('platform_ids', []))
|
||||
|
||||
if node_ids:
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||
asset_ids |= set(node_asset_ids)
|
||||
|
||||
if asset_ids:
|
||||
_platform_ids = Asset.objects \
|
||||
.filter(id__in=set(asset_ids)) \
|
||||
.values_list('platform_id', flat=True)
|
||||
platform_ids |= set(_platform_ids)
|
||||
platforms = Platform.objects.filter(id__in=platform_ids)
|
||||
serializer = self.get_serializer(platforms, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.locks import NodeAddChildrenLock
|
||||
from common.exceptions import JMSException
|
||||
from common.tree import TreeNodeSerializer
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
from orgs.mixins import generics
|
||||
from orgs.utils import current_org
|
||||
from .mixin import SerializeToTreeNodeMixin
|
||||
@@ -35,8 +35,8 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
is_initial = False
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
self.instance = self.get_object()
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
with NodeAddChildrenLock(self.instance):
|
||||
|
||||
@@ -175,7 +175,7 @@ class BasePlaybookManager:
|
||||
method = self.method_id_meta_mapper.get(method_id)
|
||||
if not method:
|
||||
logger.error("Method not found: {}".format(method_id))
|
||||
return method
|
||||
return
|
||||
method_playbook_dir_path = method['dir']
|
||||
sub_playbook_path = os.path.join(sub_playbook_dir, 'project', 'main.yml')
|
||||
shutil.copytree(method_playbook_dir_path, os.path.dirname(sub_playbook_path))
|
||||
@@ -196,6 +196,11 @@ class BasePlaybookManager:
|
||||
print(msg)
|
||||
runners = []
|
||||
for platform, assets in assets_group_by_platform.items():
|
||||
if not assets:
|
||||
continue
|
||||
if not platform.automation or not platform.automation.ansible_enabled:
|
||||
print(_(" - Platform {} ansible disabled").format(platform.name))
|
||||
continue
|
||||
assets_bulked = [assets[i:i + self.bulk_size] for i in range(0, len(assets), self.bulk_size)]
|
||||
|
||||
for i, _assets in enumerate(assets_bulked, start=1):
|
||||
@@ -204,6 +209,8 @@ class BasePlaybookManager:
|
||||
inventory_path = os.path.join(self.runtime_dir, sub_dir, 'hosts.json')
|
||||
self.generate_inventory(_assets, inventory_path)
|
||||
playbook_path = self.generate_playbook(_assets, platform, playbook_dir)
|
||||
if not playbook_path:
|
||||
continue
|
||||
|
||||
runer = PlaybookRunner(
|
||||
inventory_path,
|
||||
@@ -309,6 +316,7 @@ class BasePlaybookManager:
|
||||
shutil.rmtree(self.runtime_dir)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
print(">>> 任务准备阶段\n")
|
||||
runners = self.get_runners()
|
||||
if len(runners) > 1:
|
||||
print("### 分次执行任务, 总共 {}\n".format(len(runners)))
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
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 }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
register: db_info
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
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 }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
|
||||
@@ -10,4 +10,8 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
filter: version
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import TextChoices
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from jumpserver.utils import has_valid_xpack_license
|
||||
|
||||
|
||||
class Type:
|
||||
def __init__(self, label, value):
|
||||
@@ -65,14 +64,14 @@ class BaseType(TextChoices):
|
||||
@classmethod
|
||||
def _parse_protocols(cls, protocol, tp):
|
||||
from .protocol import Protocol
|
||||
settings = Protocol.settings()
|
||||
_settings = Protocol.settings()
|
||||
choices = protocol.get('choices', [])
|
||||
if choices == '__self__':
|
||||
choices = [tp]
|
||||
|
||||
protocols = []
|
||||
for name in choices:
|
||||
protocol = {'name': name, **settings.get(name, {})}
|
||||
protocol = {'name': name, **_settings.get(name, {})}
|
||||
setting = protocol.pop('setting', {})
|
||||
setting_values = {k: v.get('default', None) for k, v in setting.items()}
|
||||
protocol['setting'] = setting_values
|
||||
@@ -113,7 +112,7 @@ class BaseType(TextChoices):
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
if not has_valid_xpack_license():
|
||||
if not settings.XPACK_ENABLED:
|
||||
return [
|
||||
(tp.value, tp.label)
|
||||
for tp in cls.get_community_types()
|
||||
|
||||
@@ -7,6 +7,7 @@ class DatabaseTypes(BaseType):
|
||||
POSTGRESQL = 'postgresql', 'PostgreSQL'
|
||||
ORACLE = 'oracle', 'Oracle'
|
||||
SQLSERVER = 'sqlserver', 'SQLServer'
|
||||
DB2 = 'db2', 'DB2'
|
||||
CLICKHOUSE = 'clickhouse', 'ClickHouse'
|
||||
MONGODB = 'mongodb', 'MongoDB'
|
||||
REDIS = 'redis', 'Redis'
|
||||
@@ -45,6 +46,15 @@ class DatabaseTypes(BaseType):
|
||||
'change_secret_enabled': False,
|
||||
'push_account_enabled': False,
|
||||
},
|
||||
cls.DB2: {
|
||||
'ansible_enabled': False,
|
||||
'ping_enabled': False,
|
||||
'gather_facts_enabled': False,
|
||||
'gather_accounts_enabled': False,
|
||||
'verify_account_enabled': False,
|
||||
'change_secret_enabled': False,
|
||||
'push_account_enabled': False,
|
||||
},
|
||||
cls.CLICKHOUSE: {
|
||||
'ansible_enabled': False,
|
||||
'ping_enabled': False,
|
||||
@@ -73,6 +83,7 @@ class DatabaseTypes(BaseType):
|
||||
cls.POSTGRESQL: [{'name': 'PostgreSQL'}],
|
||||
cls.ORACLE: [{'name': 'Oracle'}],
|
||||
cls.SQLSERVER: [{'name': 'SQLServer'}],
|
||||
cls.DB2: [{'name': 'DB2'}],
|
||||
cls.CLICKHOUSE: [{'name': 'ClickHouse'}],
|
||||
cls.MONGODB: [{'name': 'MongoDB'}],
|
||||
cls.REDIS: [
|
||||
|
||||
@@ -24,7 +24,7 @@ class DeviceTypes(BaseType):
|
||||
def _get_protocol_constrains(cls) -> dict:
|
||||
return {
|
||||
'*': {
|
||||
'choices': ['ssh', 'telnet']
|
||||
'choices': ['ssh', 'telnet', 'sftp']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
oracle = 'oracle', 'Oracle'
|
||||
postgresql = 'postgresql', 'PostgreSQL'
|
||||
sqlserver = 'sqlserver', 'SQLServer'
|
||||
db2 = 'db2', 'DB2'
|
||||
clickhouse = 'clickhouse', 'ClickHouse'
|
||||
redis = 'redis', 'Redis'
|
||||
mongodb = 'mongodb', 'MongoDB'
|
||||
@@ -45,7 +46,13 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'sftp_home': {
|
||||
'type': 'str',
|
||||
'default': '/tmp',
|
||||
'label': _('SFTP home')
|
||||
'label': _('SFTP root'),
|
||||
'help_text': _(
|
||||
'SFTP root directory, Support variable: <br>'
|
||||
'- ${ACCOUNT} The connected account username <br>'
|
||||
'- ${HOME} The home directory of the connected account <br>'
|
||||
'- ${USER} The username of the user'
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -154,6 +161,21 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'required': True,
|
||||
'secret_types': ['password'],
|
||||
'xpack': True,
|
||||
'setting': {
|
||||
'version': {
|
||||
'type': 'choice',
|
||||
'choices': [('>=2014', '>= 2014'), ('<2014', '< 2014')],
|
||||
'default': '>=2014',
|
||||
'label': _('Version'),
|
||||
'help_text': _('SQL Server version, Different versions have different connection drivers')
|
||||
}
|
||||
}
|
||||
},
|
||||
cls.db2: {
|
||||
'port': 5000,
|
||||
'required': True,
|
||||
'secret_types': ['password'],
|
||||
'xpack': True,
|
||||
},
|
||||
cls.clickhouse: {
|
||||
'port': 9000,
|
||||
@@ -254,7 +276,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
}
|
||||
}
|
||||
}
|
||||
if settings.XPACK_ENABLED:
|
||||
if settings.XPACK_LICENSE_IS_VALID:
|
||||
choices = protocols[cls.chatgpt]['setting']['api_mode']['choices']
|
||||
choices.extend([
|
||||
('gpt-4', 'GPT-4'),
|
||||
|
||||
@@ -107,8 +107,9 @@ def create_app_nodes(apps, org_id):
|
||||
'key': next_key, 'value': name, 'parent_key': parent_key,
|
||||
'full_value': full_value, 'org_id': org_id
|
||||
}
|
||||
node, created = node_model.objects.get_or_create(
|
||||
node, __ = node_model.objects.get_or_create(
|
||||
defaults=defaults, value=name, org_id=org_id,
|
||||
parent_key=parent_key
|
||||
)
|
||||
node.parent = parent
|
||||
return node
|
||||
|
||||
@@ -25,7 +25,7 @@ def migrate_asset_accounts(apps, schema_editor):
|
||||
count += len(auth_books)
|
||||
# auth book 和 account 相同的属性
|
||||
same_attrs = [
|
||||
'id', 'username', 'comment', 'date_created', 'date_updated',
|
||||
'username', 'comment', 'date_created', 'date_updated',
|
||||
'created_by', 'asset_id', 'org_id',
|
||||
]
|
||||
# 认证的属性,可能是 auth_book 的,可能是 system_user 的
|
||||
|
||||
@@ -49,11 +49,11 @@ def migrate_assets_sftp_protocol(apps, schema_editor):
|
||||
|
||||
count = 0
|
||||
print("\nAsset add sftp protocol: ")
|
||||
asset_ids = asset_cls.objects\
|
||||
asset_ids = list(asset_cls.objects\
|
||||
.filter(platform__in=sftp_platforms)\
|
||||
.exclude(protocols__name='sftp')\
|
||||
.distinct()\
|
||||
.values_list('id', flat=True)
|
||||
.values_list('id', flat=True))
|
||||
while True:
|
||||
_asset_ids = asset_ids[count:count + 1000]
|
||||
if not _asset_ids:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.1.10 on 2023-09-13 10:59
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_device_automation_ansible_enabled(apps, *args):
|
||||
platform_model = apps.get_model('assets', 'Platform')
|
||||
automation_model = apps.get_model('assets', 'PlatformAutomation')
|
||||
ids = platform_model.objects.filter(category='device').values_list('id', flat=True)
|
||||
automation_model.objects.filter(platform_id__in=ids).update(ansible_enabled=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('assets', '0122_auto_20230803_1553'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_device_automation_ansible_enabled)
|
||||
]
|
||||
31
apps/assets/migrations/0124_auto_20231007_1437.py
Normal file
31
apps/assets/migrations/0124_auto_20231007_1437.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.1.10 on 2023-10-07 06:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def add_db2_platform(apps, schema_editor):
|
||||
platform_cls = apps.get_model('assets', 'Platform')
|
||||
automation_cls = apps.get_model('assets', 'PlatformAutomation')
|
||||
platform, _ = platform_cls.objects.update_or_create(
|
||||
name='DB2', defaults={
|
||||
'name': 'DB2', 'category': 'database',
|
||||
'internal': True, 'type': 'db2',
|
||||
'domain_enabled': True, 'su_enabled': False,
|
||||
'su_method': None, 'comment': 'DB2', 'created_by': 'System',
|
||||
'updated_by': 'System', 'custom_fields': []
|
||||
}
|
||||
)
|
||||
platform.protocols.update_or_create(name='db2', defaults={
|
||||
'name': 'db2', 'port': 50000, 'primary': True, 'setting': {}
|
||||
})
|
||||
automation_cls.objects.update_or_create(platform=platform, defaults={'ansible_enabled': False})
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('assets', '0123_device_automation_ansible_enabled'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_db2_platform)
|
||||
]
|
||||
21
apps/assets/migrations/0125_auto_20231011_1053.py
Normal file
21
apps/assets/migrations/0125_auto_20231011_1053.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.1.10 on 2023-10-11 02:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def change_windows_ping_method(apps, schema_editor):
|
||||
platform_automation_cls = apps.get_model('assets', 'PlatformAutomation')
|
||||
automations = platform_automation_cls.objects.filter(platform__name__in=['Windows', 'Windows2016'])
|
||||
automations.update(ping_method='ping_by_rdp')
|
||||
automations.update(verify_account_method='verify_account_by_rdp')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0124_auto_20231007_1437'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(change_windows_ping_method)
|
||||
]
|
||||
@@ -402,12 +402,7 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin):
|
||||
return Asset.objects.filter(q).distinct()
|
||||
|
||||
def get_assets_amount(self):
|
||||
q = Q(node__key__startswith=f'{self.key}:') | Q(node__key=self.key)
|
||||
return self.assets.through.objects.filter(q).count()
|
||||
|
||||
def get_assets_account_by_children(self):
|
||||
children = self.get_all_children().values_list()
|
||||
return self.assets.through.objects.filter(node_id__in=children).count()
|
||||
return self.get_all_assets().count()
|
||||
|
||||
@classmethod
|
||||
def get_node_all_assets_by_key_v2(cls, key):
|
||||
|
||||
@@ -175,6 +175,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
||||
protocols = self.initial_data.get('protocols')
|
||||
if protocols is not None:
|
||||
return
|
||||
if getattr(self, 'instance', None):
|
||||
return
|
||||
|
||||
protocols_required, protocols_default = self._get_protocols_required_default()
|
||||
protocol_map = {str(protocol.id): protocol for protocol in protocols_required + protocols_default}
|
||||
@@ -281,14 +283,52 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
||||
return protocols_data_map.values()
|
||||
|
||||
@staticmethod
|
||||
def accounts_create(accounts_data, asset):
|
||||
def update_account_su_from(accounts, include_su_from_accounts):
|
||||
if not include_su_from_accounts:
|
||||
return
|
||||
name_map = {account.name: account for account in accounts}
|
||||
username_secret_type_map = {
|
||||
(account.username, account.secret_type): account for account in accounts
|
||||
}
|
||||
|
||||
for name, username_secret_type in include_su_from_accounts.items():
|
||||
account = name_map.get(name)
|
||||
if not account:
|
||||
continue
|
||||
su_from_account = username_secret_type_map.get(username_secret_type)
|
||||
if su_from_account:
|
||||
account.su_from = su_from_account
|
||||
account.save()
|
||||
|
||||
def accounts_create(self, accounts_data, asset):
|
||||
from accounts.models import AccountTemplate
|
||||
if not accounts_data:
|
||||
return
|
||||
|
||||
if not isinstance(accounts_data[0], dict):
|
||||
raise serializers.ValidationError({'accounts': _("Invalid data")})
|
||||
|
||||
su_from_name_username_secret_type_map = {}
|
||||
for data in accounts_data:
|
||||
data['asset'] = asset.id
|
||||
name = data.get('name')
|
||||
su_from = data.pop('su_from', None)
|
||||
template_id = data.get('template', None)
|
||||
if template_id:
|
||||
template = AccountTemplate.objects.get(id=template_id)
|
||||
if template and template.su_from:
|
||||
su_from_name_username_secret_type_map[template.name] = (
|
||||
template.su_from.username, template.su_from.secret_type
|
||||
)
|
||||
elif isinstance(su_from, dict):
|
||||
su_from = Account.objects.get(id=su_from.get('id'))
|
||||
su_from_name_username_secret_type_map[name] = (
|
||||
su_from.username, su_from.secret_type
|
||||
)
|
||||
s = AssetAccountSerializer(data=accounts_data, many=True)
|
||||
s.is_valid(raise_exception=True)
|
||||
s.save()
|
||||
accounts = s.save()
|
||||
self.update_account_su_from(accounts, su_from_name_username_secret_type_map)
|
||||
|
||||
@atomic
|
||||
def create(self, validated_data):
|
||||
@@ -298,10 +338,37 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
||||
self.perform_nodes_display_create(instance, nodes_display)
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def sync_platform_protocols(instance, old_platform):
|
||||
platform = instance.platform
|
||||
|
||||
if str(old_platform.id) == str(instance.platform_id):
|
||||
return
|
||||
|
||||
platform_protocols = {
|
||||
p['name']: p['port']
|
||||
for p in platform.protocols.values('name', 'port')
|
||||
}
|
||||
|
||||
protocols = set(instance.protocols.values_list('name', flat=True))
|
||||
protocol_names = set(platform_protocols) - protocols
|
||||
objs = []
|
||||
for name in protocol_names:
|
||||
objs.append(
|
||||
Protocol(
|
||||
name=name,
|
||||
port=platform_protocols[name],
|
||||
asset_id=instance.id,
|
||||
)
|
||||
)
|
||||
Protocol.objects.bulk_create(objs)
|
||||
|
||||
@atomic
|
||||
def update(self, instance, validated_data):
|
||||
old_platform = instance.platform
|
||||
nodes_display = validated_data.pop('nodes_display', '')
|
||||
instance = super().update(instance, validated_data)
|
||||
self.sync_platform_protocols(instance, old_platform)
|
||||
self.perform_nodes_display_create(instance, nodes_display)
|
||||
return instance
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from assets.models import Database
|
||||
from assets.models import Database, Platform
|
||||
from assets.serializers.gateway import GatewayWithAccountSecretSerializer
|
||||
from .common import AssetSerializer
|
||||
|
||||
@@ -20,13 +20,44 @@ class DatabaseSerializer(AssetSerializer):
|
||||
]
|
||||
fields = AssetSerializer.Meta.fields + extra_fields
|
||||
|
||||
def validate(self, attrs):
|
||||
platform = attrs.get('platform')
|
||||
db_type_required = ('mongodb', 'postgresql')
|
||||
if platform and getattr(platform, 'type') in db_type_required \
|
||||
and not attrs.get('db_name'):
|
||||
raise ValidationError({'db_name': _('This field is required.')})
|
||||
return attrs
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_db_name_required()
|
||||
|
||||
def get_platform(self):
|
||||
platform = None
|
||||
platform_id = None
|
||||
|
||||
if getattr(self, 'initial_data', None):
|
||||
platform_id = self.initial_data.get('platform')
|
||||
if isinstance(platform_id, dict):
|
||||
platform_id = platform_id.get('id') or platform_id.get('pk')
|
||||
if not platform_id and self.instance:
|
||||
platform = self.instance.platform
|
||||
elif getattr(self, 'instance', None):
|
||||
if isinstance(self.instance, (list, QuerySet)):
|
||||
return
|
||||
platform = self.instance.platform
|
||||
elif self.context.get('request'):
|
||||
platform_id = self.context['request'].query_params.get('platform')
|
||||
|
||||
if not platform and platform_id:
|
||||
platform = Platform.objects.filter(id=platform_id).first()
|
||||
return platform
|
||||
|
||||
def set_db_name_required(self):
|
||||
db_field = self.fields.get('db_name')
|
||||
if not db_field:
|
||||
return
|
||||
|
||||
platform = self.get_platform()
|
||||
if not platform:
|
||||
return
|
||||
|
||||
if platform.type in ['mysql', 'mariadb']:
|
||||
db_field.required = False
|
||||
db_field.allow_blank = True
|
||||
db_field.allow_null = True
|
||||
|
||||
|
||||
class DatabaseWithGatewaySerializer(DatabaseSerializer):
|
||||
|
||||
@@ -30,8 +30,9 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
|
||||
if '/' in data:
|
||||
error = _("Can't contains: " + "/")
|
||||
raise serializers.ValidationError(error)
|
||||
if self.instance:
|
||||
instance = self.instance
|
||||
view = self.context['view']
|
||||
instance = self.instance or getattr(view, 'instance', None)
|
||||
if instance:
|
||||
siblings = instance.get_siblings()
|
||||
else:
|
||||
instance = Node.org_root()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueValidator
|
||||
|
||||
from common.serializers import (
|
||||
WritableNestedModelSerializer, type_field_map, MethodSerializer,
|
||||
@@ -123,6 +124,10 @@ class PlatformSerializer(WritableNestedModelSerializer):
|
||||
("super", "super 15"),
|
||||
("super_level", "super level 15")
|
||||
]
|
||||
id = serializers.IntegerField(
|
||||
label='ID', required=False,
|
||||
validators=[UniqueValidator(queryset=Platform.objects.all())]
|
||||
)
|
||||
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"))
|
||||
@@ -213,7 +218,7 @@ class PlatformSerializer(WritableNestedModelSerializer):
|
||||
def validate_automation(self, automation):
|
||||
automation = automation or {}
|
||||
ansible_enabled = automation.get('ansible_enabled', False) \
|
||||
and self.constraints['automation'].get('ansible_enabled', False)
|
||||
and self.constraints['automation'].get('ansible_enabled', False)
|
||||
automation['ansible_enable'] = ansible_enabled
|
||||
return automation
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class KubernetesClient:
|
||||
|
||||
remote_bind_address = (
|
||||
urlparse(asset.address).hostname,
|
||||
urlparse(asset.address).port
|
||||
urlparse(asset.address).port or 443
|
||||
)
|
||||
server = SSHTunnelForwarder(
|
||||
(gateway.address, gateway.port),
|
||||
|
||||
@@ -1,50 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import F, Value, CharField, Q
|
||||
from django.http import HttpResponse, FileResponse
|
||||
from django.utils.encoding import escape_uri_path
|
||||
from rest_framework import generics
|
||||
from rest_framework import status
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from common.api import AsyncApiMixin
|
||||
from common.drf.filters import DatetimeRangeFilter
|
||||
from common.api import CommonApiMixin
|
||||
from common.const.http import GET, POST
|
||||
from common.drf.filters import DatetimeRangeFilterBackend
|
||||
from common.permissions import IsServiceAccount
|
||||
from common.plugins.es import QuerySet as ESQuerySet
|
||||
from common.utils import is_uuid, get_logger, lazyproperty
|
||||
from common.const.http import GET, POST
|
||||
from common.storage.ftp_file import FTPFileStorageHandler
|
||||
from common.utils import is_uuid, get_logger, lazyproperty
|
||||
from orgs.mixins.api import OrgReadonlyModelViewSet, OrgModelViewSet
|
||||
from orgs.utils import current_org, tmp_to_root_org
|
||||
from orgs.models import Organization
|
||||
from orgs.utils import current_org, tmp_to_root_org
|
||||
from rbac.permissions import RBACPermission
|
||||
from terminal.models import default_storage
|
||||
from users.models import User
|
||||
from .backends import TYPE_ENGINE_MAPPING
|
||||
from .const import ActivityChoices
|
||||
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog, ActivityLog, JobLog
|
||||
from .models import (
|
||||
FTPLog, UserLoginLog, OperateLog, PasswordChangeLog,
|
||||
ActivityLog, JobLog, UserSession
|
||||
)
|
||||
from .serializers import (
|
||||
FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer,
|
||||
OperateLogSerializer, OperateLogActionDetailSerializer,
|
||||
PasswordChangeLogSerializer, ActivityUnionLogSerializer,
|
||||
FileSerializer
|
||||
FileSerializer, UserSessionSerializer
|
||||
)
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class JobAuditViewSet(OrgReadonlyModelViewSet):
|
||||
model = JobLog
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||
date_range_filter_fields = [
|
||||
('date_start', ('date_from', 'date_to'))
|
||||
]
|
||||
@@ -57,7 +58,7 @@ class JobAuditViewSet(OrgReadonlyModelViewSet):
|
||||
class FTPLogViewSet(OrgModelViewSet):
|
||||
model = FTPLog
|
||||
serializer_class = FTPLogSerializer
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||
date_range_filter_fields = [
|
||||
('date_start', ('date_from', 'date_to'))
|
||||
]
|
||||
@@ -113,7 +114,7 @@ class FTPLogViewSet(OrgModelViewSet):
|
||||
class UserLoginCommonMixin:
|
||||
model = UserLoginLog
|
||||
serializer_class = UserLoginLogSerializer
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||
date_range_filter_fields = [
|
||||
('datetime', ('date_from', 'date_to'))
|
||||
]
|
||||
@@ -183,6 +184,8 @@ class ResourceActivityAPIView(generics.ListAPIView):
|
||||
'r_user', 'r_action', 'r_type'
|
||||
)
|
||||
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
|
||||
if resource_id:
|
||||
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
|
||||
with tmp_to_root_org():
|
||||
qs1 = self.get_operate_log_qs(fields, limit, org_q, resource_id=resource_id)
|
||||
qs2 = self.get_activity_log_qs(fields, limit, org_q, resource_id=resource_id)
|
||||
@@ -193,7 +196,7 @@ class ResourceActivityAPIView(generics.ListAPIView):
|
||||
class OperateLogViewSet(OrgReadonlyModelViewSet):
|
||||
model = OperateLog
|
||||
serializer_class = OperateLogSerializer
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||
date_range_filter_fields = [
|
||||
('datetime', ('date_from', 'date_to'))
|
||||
]
|
||||
@@ -214,11 +217,10 @@ class OperateLogViewSet(OrgReadonlyModelViewSet):
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_queryset(self):
|
||||
org_q = Q(org_id=current_org.id)
|
||||
qs = OperateLog.objects.all()
|
||||
if self.is_action_detail:
|
||||
org_q |= Q(org_id=Organization.SYSTEM_ID)
|
||||
with tmp_to_root_org():
|
||||
qs = OperateLog.objects.filter(org_q)
|
||||
with tmp_to_root_org():
|
||||
qs |= OperateLog.objects.filter(org_id=Organization.SYSTEM_ID)
|
||||
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
|
||||
if es_config:
|
||||
engine_mod = import_module(TYPE_ENGINE_MAPPING['es'])
|
||||
@@ -232,7 +234,7 @@ class OperateLogViewSet(OrgReadonlyModelViewSet):
|
||||
class PasswordChangeLogViewSet(OrgReadonlyModelViewSet):
|
||||
model = PasswordChangeLog
|
||||
serializer_class = PasswordChangeLogSerializer
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||
date_range_filter_fields = [
|
||||
('datetime', ('date_from', 'date_to'))
|
||||
]
|
||||
@@ -248,3 +250,43 @@ class PasswordChangeLogViewSet(OrgReadonlyModelViewSet):
|
||||
user__in=[str(user) for user in users]
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
||||
http_method_names = ('get', 'post', 'head', 'options', 'trace')
|
||||
serializer_class = UserSessionSerializer
|
||||
filterset_fields = ['id', 'ip', 'city', 'type']
|
||||
search_fields = ['id', 'ip', 'city']
|
||||
rbac_perms = {
|
||||
'offline': ['audits.offline_usersession']
|
||||
}
|
||||
|
||||
@property
|
||||
def org_user_ids(self):
|
||||
user_ids = current_org.get_members().values_list('id', flat=True)
|
||||
return user_ids
|
||||
|
||||
def get_queryset(self):
|
||||
keys = UserSession.get_keys()
|
||||
queryset = UserSession.objects.filter(key__in=keys)
|
||||
if current_org.is_root():
|
||||
return queryset
|
||||
user_ids = self.org_user_ids
|
||||
queryset = queryset.filter(user_id__in=user_ids)
|
||||
return queryset
|
||||
|
||||
@action(['POST'], detail=False, url_path='offline')
|
||||
def offline(self, request, *args, **kwargs):
|
||||
ids = request.data.get('ids', [])
|
||||
queryset = self.get_queryset()
|
||||
session_key = request.session.session_key
|
||||
queryset = queryset.exclude(key=session_key).filter(id__in=ids)
|
||||
if not queryset.exists():
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
keys = queryset.values_list('key', flat=True)
|
||||
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
for key in keys:
|
||||
session_store_cls(key).delete()
|
||||
queryset.delete()
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@@ -58,7 +58,7 @@ class OperateLogStore(object):
|
||||
return diff_list
|
||||
|
||||
def save(self, **kwargs):
|
||||
log_id = kwargs.get('id', '')
|
||||
log_id = kwargs.pop('id', None)
|
||||
before = kwargs.pop('before') or {}
|
||||
after = kwargs.pop('after') or {}
|
||||
|
||||
|
||||
@@ -25,10 +25,18 @@ class ActionChoices(TextChoices):
|
||||
delete = "delete", _("Delete")
|
||||
create = "create", _("Create")
|
||||
# Activities action
|
||||
download = "download", _("Download")
|
||||
connect = "connect", _("Connect")
|
||||
login = "login", _("Login")
|
||||
change_auth = "change_password", _("Change password")
|
||||
|
||||
accept = 'accept', _('Accept')
|
||||
review = 'review', _('Review')
|
||||
notice = 'notice', _('Notifications')
|
||||
reject = 'reject', _('Reject')
|
||||
approve = 'approve', _('Approve')
|
||||
close = 'close', _('Close')
|
||||
|
||||
|
||||
class LoginTypeChoices(TextChoices):
|
||||
web = "W", _("Web")
|
||||
|
||||
29
apps/audits/migrations/0023_auto_20230906_1322.py
Normal file
29
apps/audits/migrations/0023_auto_20230906_1322.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 4.1.10 on 2023-09-06 05:31
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('audits', '0022_auto_20230605_1555'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ftplog',
|
||||
name='date_start',
|
||||
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Date start'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='operatelog',
|
||||
name='action',
|
||||
field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create'), ('download', 'Download'), ('connect', 'Connect'), ('login', 'Login'), ('change_password', 'Change password'), ('accept', 'Accept'), ('review', 'Review'), ('notice', 'Notifications'), ('reject', 'Reject'), ('approve', 'Approve'), ('close', 'Close')], max_length=16, verbose_name='Action'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userloginlog',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date login'),
|
||||
),
|
||||
]
|
||||
37
apps/audits/migrations/0024_usersession.py
Normal file
37
apps/audits/migrations/0024_usersession.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 4.1.10 on 2023-09-15 08:58
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('audits', '0023_auto_20230906_1322'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserSession',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('ip', models.GenericIPAddressField(verbose_name='Login IP')),
|
||||
('key', models.CharField(max_length=128, verbose_name='Session key')),
|
||||
('city', models.CharField(blank=True, max_length=254, null=True, verbose_name='Login city')),
|
||||
('user_agent', models.CharField(blank=True, max_length=254, null=True, verbose_name='User agent')),
|
||||
('type', models.CharField(choices=[('W', 'Web'), ('T', 'Terminal'), ('U', 'Unknown')], max_length=2, verbose_name='Login type')),
|
||||
('backend', models.CharField(default='', max_length=32, verbose_name='Authentication backend')),
|
||||
('date_created', models.DateTimeField(blank=True, null=True, verbose_name='Date created')),
|
||||
('date_expired', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Date expired')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User session',
|
||||
'ordering': ['-date_created'],
|
||||
'permissions': [('offline_usersession', 'Offline ussr session')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.1.10 on 2023-10-18 08:01
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('audits', '0024_usersession'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='usersession',
|
||||
name='date_expired',
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user