Compare commits
125 Commits
v4.10.6-lt
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
e617245b26 | |
|
|
d309d11a8f | |
|
|
4771693a56 | |
|
|
cefc820ac1 | |
|
|
d007afdb43 | |
|
|
e8921a43be | |
|
|
a9b44103d4 | |
|
|
4abf2bded6 | |
|
|
54693089a0 | |
|
|
0b859dd502 | |
|
|
3fb27f969a | |
|
|
45627a1d92 | |
|
|
245e2dab66 | |
|
|
8f0a41b1a8 | |
|
|
1a9e56c520 | |
|
|
67c2f471b4 | |
|
|
b04f96f5f2 | |
|
|
30f03b7d89 | |
|
|
28a97d0b5a | |
|
|
3410686690 | |
|
|
6860e2327f | |
|
|
20253e760c | |
|
|
a63cfde8d2 | |
|
|
92e250e03b | |
|
|
098f0950cb | |
|
|
39b0830a6b | |
|
|
2e847bc2bc | |
|
|
f82f31876a | |
|
|
cde182c015 | |
|
|
b990cdf561 | |
|
|
c9a062823d | |
|
|
643ba4fc15 | |
|
|
d16a55bbe2 | |
|
|
ae31554729 | |
|
|
53b47980a2 | |
|
|
d31b5ee570 | |
|
|
65aea1ea36 | |
|
|
5abb5c5d5a | |
|
|
93e41a5004 | |
|
|
95f51bbe48 | |
|
|
0184d292ec | |
|
|
23a6d320c7 | |
|
|
b16304c48a | |
|
|
7cd1e4d3a0 | |
|
|
64a9987c3f | |
|
|
18bfe312fa | |
|
|
9280884c1c | |
|
|
c593f91d77 | |
|
|
46da05652a | |
|
|
9249aba1a9 | |
|
|
eca637c120 | |
|
|
ddacd5fce1 | |
|
|
3ca5c04099 | |
|
|
6603a073ec | |
|
|
d745f7495a | |
|
|
76f1667c89 | |
|
|
1ab1954299 | |
|
|
c8335999a4 | |
|
|
5b4a67362d | |
|
|
e025073da2 | |
|
|
2155bc6862 | |
|
|
953b515817 | |
|
|
7f7a354b2d | |
|
|
2b2f7ea3f0 | |
|
|
529123e1b5 | |
|
|
e156ab6ad8 | |
|
|
3c1fd134ae | |
|
|
b15f663c87 | |
|
|
93906dff0a | |
|
|
307befdacd | |
|
|
dbfc4d3981 | |
|
|
849edd33c1 | |
|
|
37cceec8fe | |
|
|
d2494c25cc | |
|
|
023952582e | |
|
|
863fe95100 | |
|
|
4b0bdb18c9 | |
|
|
10da053a95 | |
|
|
c40bc46520 | |
|
|
a732cc614e | |
|
|
bb29d519c6 | |
|
|
b56c3a76a7 | |
|
|
ab908d24a7 | |
|
|
79cabe1b3c | |
|
|
231b7287c1 | |
|
|
be7a4c0d6e | |
|
|
009da19050 | |
|
|
dfda6b1e08 | |
|
|
59b40578d8 | |
|
|
e5db28c014 | |
|
|
6d1f26b0f8 | |
|
|
2333dbbe33 | |
|
|
16461b0fa9 | |
|
|
528b0ea1ba | |
|
|
60f06adaa9 | |
|
|
7a6187b95f | |
|
|
aacaf3a174 | |
|
|
3c9d2534fa | |
|
|
4f79abe678 | |
|
|
ae9956ff91 | |
|
|
429677e0ce | |
|
|
034ee65157 | |
|
|
fdd7d9b6b1 | |
|
|
db0e21f5d9 | |
|
|
468b84eb3d | |
|
|
28d5475d0f | |
|
|
b9c60d856f | |
|
|
bd1d73c6dd | |
|
|
bf92c756d4 | |
|
|
62ebe0d636 | |
|
|
0b1fea8492 | |
|
|
65b5f573f8 | |
|
|
bb639e1fe7 | |
|
|
395b868dcf | |
|
|
1350b774b3 | |
|
|
af7a00c1b1 | |
|
|
965ec7007c | |
|
|
1372fd7535 | |
|
|
3b0ef4cca7 | |
|
|
6832abdaad | |
|
|
c6bf290dbb | |
|
|
23ab66c11a | |
|
|
1debaa5547 | |
|
|
47413966c9 | |
|
|
703f39607c |
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: Build and Push Python Base Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Tag to build'
|
||||||
|
required: true
|
||||||
|
default: '3.11-slim-bullseye-v1'
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
image: tonistiigi/binfmt:qemu-v7.0.0-28
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract repository name
|
||||||
|
id: repo
|
||||||
|
run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build and push multi-arch image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
file: Dockerfile-python
|
||||||
|
tags: jumpserver/core-base:python-${{ inputs.tag }}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM jumpserver/core-base:20250819_064003 AS stage-build
|
FROM jumpserver/core-base:20251014_095903 AS stage-build
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ RUN set -ex \
|
||||||
&& python manage.py compilemessages
|
&& python manage.py compilemessages
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.11-slim-bullseye
|
FROM jumpserver/core-base:python-3.11-slim-bullseye-v1
|
||||||
ENV LANG=en_US.UTF-8 \
|
ENV LANG=en_US.UTF-8 \
|
||||||
PATH=/opt/py3/bin:$PATH
|
PATH=/opt/py3/bin:$PATH
|
||||||
|
|
||||||
|
|
@ -33,6 +33,7 @@ ARG TOOLS=" \
|
||||||
default-libmysqlclient-dev \
|
default-libmysqlclient-dev \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
sshpass \
|
sshpass \
|
||||||
|
nmap \
|
||||||
bubblewrap"
|
bubblewrap"
|
||||||
|
|
||||||
ARG APT_MIRROR=http://deb.debian.org
|
ARG APT_MIRROR=http://deb.debian.org
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.11-slim-bullseye
|
FROM jumpserver/core-base:python-3.11-slim-bullseye-v1
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /usr/local/bin/
|
COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /usr/local/bin/
|
||||||
# Install APT dependencies
|
# Install APT dependencies
|
||||||
|
|
@ -28,7 +28,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||||
&& echo "no" | dpkg-reconfigure dash
|
&& echo "no" | dpkg-reconfigure dash
|
||||||
|
|
||||||
# Install bin tools
|
# Install bin tools
|
||||||
ARG CHECK_VERSION=v1.0.4
|
ARG CHECK_VERSION=v1.0.5
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
||||||
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM python:3.11-slim-bullseye
|
||||||
|
ARG TARGETARCH
|
||||||
|
# Install APT dependencies
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
RUN set -eux; \
|
||||||
|
apt-get update; \
|
||||||
|
apt-get -y --no-install-recommends upgrade; \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# upgrade pip and setuptools
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip setuptools wheel
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<a name="readme-top"></a>
|
<a name="readme-top"></a>
|
||||||
<a href="https://jumpserver.com" target="_blank"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
|
<a href="https://jumpserver.com" target="_blank"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
|
||||||
|
|
||||||
## An open-source PAM tool (Bastion Host)
|
## An open-source PAM platform (Bastion Host)
|
||||||
|
|
||||||
[![][license-shield]][license-link]
|
[![][license-shield]][license-link]
|
||||||
[![][docs-shield]][docs-link]
|
[![][docs-shield]][docs-link]
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
## What is JumpServer?
|
## What is JumpServer?
|
||||||
|
|
||||||
JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
|
JumpServer is an open-source Privileged Access Management (PAM) platform that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
|
||||||
|
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from accounts.const import ChangeSecretRecordStatusChoice
|
||||||
from accounts.filters import AccountFilterSet, NodeFilterBackend
|
from accounts.filters import AccountFilterSet, NodeFilterBackend
|
||||||
from accounts.mixins import AccountRecordViewLogMixin
|
from accounts.mixins import AccountRecordViewLogMixin
|
||||||
from accounts.models import Account, ChangeSecretRecord
|
from accounts.models import Account, ChangeSecretRecord
|
||||||
|
from assets.const.gpt import create_or_update_chatx_resources
|
||||||
from assets.models import Asset, Node
|
from assets.models import Asset, Node
|
||||||
from authentication.permissions import UserConfirmation, ConfirmType
|
from authentication.permissions import UserConfirmation, ConfirmType
|
||||||
from common.api.mixin import ExtraFilterFieldsMixin
|
from common.api.mixin import ExtraFilterFieldsMixin
|
||||||
|
|
@ -18,6 +19,7 @@ from common.drf.filters import AttrRulesFilterBackend
|
||||||
from common.permissions import IsValidUser
|
from common.permissions import IsValidUser
|
||||||
from common.utils import lazyproperty, get_logger
|
from common.utils import lazyproperty, get_logger
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
from orgs.utils import tmp_to_root_org
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
@ -43,6 +45,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||||
'clear_secret': 'accounts.change_account',
|
'clear_secret': 'accounts.change_account',
|
||||||
'move_to_assets': 'accounts.delete_account',
|
'move_to_assets': 'accounts.delete_account',
|
||||||
'copy_to_assets': 'accounts.add_account',
|
'copy_to_assets': 'accounts.add_account',
|
||||||
|
'chat': 'accounts.view_account',
|
||||||
}
|
}
|
||||||
export_as_zip = True
|
export_as_zip = True
|
||||||
|
|
||||||
|
|
@ -152,6 +155,13 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||||
def copy_to_assets(self, request, *args, **kwargs):
|
def copy_to_assets(self, request, *args, **kwargs):
|
||||||
return self._copy_or_move_to_assets(request, move=False)
|
return self._copy_or_move_to_assets(request, move=False)
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=False, url_path='chat')
|
||||||
|
def chat(self, request, *args, **kwargs):
|
||||||
|
with tmp_to_root_org():
|
||||||
|
__, account = create_or_update_chatx_resources()
|
||||||
|
serializer = self.get_serializer(account)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
||||||
"""
|
"""
|
||||||
|
|
@ -190,6 +200,7 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'GET': 'accounts.view_accountsecret',
|
'GET': 'accounts.view_accountsecret',
|
||||||
}
|
}
|
||||||
|
queryset = Account.history.model.objects.none()
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def account(self) -> Account:
|
def account(self) -> Account:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ class VirtualAccountViewSet(OrgBulkModelViewSet):
|
||||||
filterset_fields = ('alias',)
|
filterset_fields = ('alias',)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
if getattr(self, "swagger_fake_view", False):
|
||||||
|
return VirtualAccount.objects.none()
|
||||||
return VirtualAccount.get_or_init_queryset()
|
return VirtualAccount.get_or_init_queryset()
|
||||||
|
|
||||||
def get_object(self, ):
|
def get_object(self, ):
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class AutomationAssetsListApi(generics.ListAPIView):
|
||||||
|
|
||||||
class AutomationRemoveAssetApi(generics.UpdateAPIView):
|
class AutomationRemoveAssetApi(generics.UpdateAPIView):
|
||||||
model = BaseAutomation
|
model = BaseAutomation
|
||||||
|
queryset = BaseAutomation.objects.all()
|
||||||
serializer_class = serializers.UpdateAssetSerializer
|
serializer_class = serializers.UpdateAssetSerializer
|
||||||
http_method_names = ['patch']
|
http_method_names = ['patch']
|
||||||
|
|
||||||
|
|
@ -59,6 +60,7 @@ class AutomationRemoveAssetApi(generics.UpdateAPIView):
|
||||||
|
|
||||||
class AutomationAddAssetApi(generics.UpdateAPIView):
|
class AutomationAddAssetApi(generics.UpdateAPIView):
|
||||||
model = BaseAutomation
|
model = BaseAutomation
|
||||||
|
queryset = BaseAutomation.objects.all()
|
||||||
serializer_class = serializers.UpdateAssetSerializer
|
serializer_class = serializers.UpdateAssetSerializer
|
||||||
http_method_names = ['patch']
|
http_method_names = ['patch']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,12 +154,10 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi):
|
||||||
model = ChangeSecretAutomation
|
model = ChangeSecretAutomation
|
||||||
serializer_class = serializers.ChangeSecretUpdateAssetSerializer
|
serializer_class = serializers.ChangeSecretUpdateAssetSerializer
|
||||||
|
|
||||||
|
|
||||||
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
|
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
|
||||||
model = ChangeSecretAutomation
|
model = ChangeSecretAutomation
|
||||||
serializer_class = serializers.ChangeSecretUpdateNodeSerializer
|
serializer_class = serializers.ChangeSecretUpdateNodeSerializer
|
||||||
|
|
||||||
|
|
||||||
class ChangeSecretStatusViewSet(OrgBulkModelViewSet):
|
class ChangeSecretStatusViewSet(OrgBulkModelViewSet):
|
||||||
perm_model = ChangeSecretAutomation
|
perm_model = ChangeSecretAutomation
|
||||||
filterset_class = ChangeSecretStatusFilterSet
|
filterset_class = ChangeSecretStatusFilterSet
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,8 @@ class ChangeSecretDashboardApi(APIView):
|
||||||
status_counts = defaultdict(lambda: defaultdict(int))
|
status_counts = defaultdict(lambda: defaultdict(int))
|
||||||
|
|
||||||
for date_finished, status in results:
|
for date_finished, status in results:
|
||||||
date_str = str(date_finished.date())
|
dt_local = timezone.localtime(date_finished)
|
||||||
|
date_str = str(dt_local.date())
|
||||||
if status == ChangeSecretRecordStatusChoice.failed:
|
if status == ChangeSecretRecordStatusChoice.failed:
|
||||||
status_counts[date_str]['failed'] += 1
|
status_counts[date_str]['failed'] += 1
|
||||||
elif status == ChangeSecretRecordStatusChoice.success:
|
elif status == ChangeSecretRecordStatusChoice.success:
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,9 @@ class CheckAccountEngineViewSet(JMSModelViewSet):
|
||||||
http_method_names = ['get', 'options']
|
http_method_names = ['get', 'options']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
if getattr(self, "swagger_fake_view", False):
|
||||||
|
return CheckAccountEngine.objects.none()
|
||||||
|
|
||||||
return CheckAccountEngine.get_default_engines()
|
return CheckAccountEngine.get_default_engines()
|
||||||
|
|
||||||
def filter_queryset(self, queryset: list):
|
def filter_queryset(self, queryset: list):
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,10 @@ class PushAccountRemoveAssetApi(AutomationRemoveAssetApi):
|
||||||
model = PushAccountAutomation
|
model = PushAccountAutomation
|
||||||
serializer_class = serializers.PushAccountUpdateAssetSerializer
|
serializer_class = serializers.PushAccountUpdateAssetSerializer
|
||||||
|
|
||||||
|
|
||||||
class PushAccountAddAssetApi(AutomationAddAssetApi):
|
class PushAccountAddAssetApi(AutomationAddAssetApi):
|
||||||
model = PushAccountAutomation
|
model = PushAccountAutomation
|
||||||
serializer_class = serializers.PushAccountUpdateAssetSerializer
|
serializer_class = serializers.PushAccountUpdateAssetSerializer
|
||||||
|
|
||||||
|
|
||||||
class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi):
|
class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi):
|
||||||
model = PushAccountAutomation
|
model = PushAccountAutomation
|
||||||
serializer_class = serializers.PushAccountUpdateNodeSerializer
|
serializer_class = serializers.PushAccountUpdateNodeSerializer
|
||||||
|
|
@ -235,8 +235,8 @@ class AccountBackupHandler:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error = str(e)
|
error = str(e)
|
||||||
print(f'\033[31m>>> {error}\033[0m')
|
print(f'\033[31m>>> {error}\033[0m')
|
||||||
self.execution.status = Status.error
|
self.manager.status = Status.error
|
||||||
self.execution.summary['error'] = error
|
self.manager.summary['error'] = error
|
||||||
|
|
||||||
def backup_by_obj_storage(self):
|
def backup_by_obj_storage(self):
|
||||||
object_id = self.execution.snapshot.get('id')
|
object_id = self.execution.snapshot.get('id')
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,16 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||||
if host.get('error'):
|
if host.get('error'):
|
||||||
return host
|
return host
|
||||||
|
|
||||||
|
inventory_hosts = []
|
||||||
|
if asset.type == HostTypes.WINDOWS:
|
||||||
|
if self.secret_type == SecretType.SSH_KEY:
|
||||||
|
host['error'] = _("Windows does not support SSH key authentication")
|
||||||
|
return host
|
||||||
|
new_secret = self.get_secret(account)
|
||||||
|
if '>' in new_secret or '^' in new_secret:
|
||||||
|
host['error'] = _("Windows password cannot contain special characters like > ^")
|
||||||
|
return host
|
||||||
|
|
||||||
host['ssh_params'] = {}
|
host['ssh_params'] = {}
|
||||||
|
|
||||||
accounts = self.get_accounts(account)
|
accounts = self.get_accounts(account)
|
||||||
|
|
@ -130,11 +140,6 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||||
if asset.type == HostTypes.WINDOWS:
|
if asset.type == HostTypes.WINDOWS:
|
||||||
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
|
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
|
||||||
|
|
||||||
inventory_hosts = []
|
|
||||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
|
||||||
print(f'Windows {asset} does not support ssh key push')
|
|
||||||
return inventory_hosts
|
|
||||||
|
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
h = deepcopy(host)
|
h = deepcopy(host)
|
||||||
h['name'] += '(' + account.username + ')' # To distinguish different accounts
|
h['name'] += '(' + account.username + ')' # To distinguish different accounts
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test SQLServer connection
|
- name: Test SQLServer connection
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: |
|
script: |
|
||||||
SELECT @@version
|
SELECT @@version
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
@ -23,45 +25,53 @@
|
||||||
var: info
|
var: info
|
||||||
|
|
||||||
- name: Check whether SQLServer User exist
|
- name: Check whether SQLServer User exist
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
|
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
|
||||||
when: db_info is succeeded
|
when: db_info is succeeded
|
||||||
register: user_exist
|
register: user_exist
|
||||||
|
|
||||||
- name: Change SQLServer password
|
- name: Change SQLServer password
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
|
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: user_exist.query_results[0] | length != 0
|
when: user_exist.query_results[0] | length != 0
|
||||||
|
|
||||||
- name: Add SQLServer user
|
- name: Add SQLServer user
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
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"
|
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
|
ignore_errors: true
|
||||||
when: user_exist.query_results[0] | length == 0
|
when: user_exist.query_results[0] | length == 0
|
||||||
|
|
||||||
- name: Verify password
|
- name: Verify password
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: |
|
script: |
|
||||||
SELECT @@version
|
SELECT @@version
|
||||||
when: check_conn_after_change
|
when: check_conn_after_change
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||||
|
group: "{{ params.group if params.group | length > 0 else omit }}"
|
||||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||||
append: "{{ true if params.groups | length > 0 else false }}"
|
append: "{{ true if params.groups | length > 0 else false }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@ params:
|
||||||
default: ''
|
default: ''
|
||||||
help_text: "{{ 'Params home help text' | trans }}"
|
help_text: "{{ 'Params home help text' | trans }}"
|
||||||
|
|
||||||
|
- name: group
|
||||||
|
type: str
|
||||||
|
label: "{{ 'Params group label' | trans }}"
|
||||||
|
default: ''
|
||||||
|
help_text: "{{ 'Params group help text' | trans }}"
|
||||||
|
|
||||||
- name: groups
|
- name: groups
|
||||||
type: str
|
type: str
|
||||||
label: "{{ 'Params groups label' | trans }}"
|
label: "{{ 'Params groups label' | trans }}"
|
||||||
|
|
@ -61,6 +67,11 @@ i18n:
|
||||||
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||||
en: 'Default home directory /home/{account username}'
|
en: 'Default home directory /home/{account username}'
|
||||||
|
|
||||||
|
Params group help text:
|
||||||
|
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
|
||||||
|
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
|
||||||
|
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
|
||||||
|
|
||||||
Params groups help text:
|
Params groups help text:
|
||||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||||
|
|
@ -86,6 +97,11 @@ i18n:
|
||||||
ja: 'グループ'
|
ja: 'グループ'
|
||||||
en: 'Groups'
|
en: 'Groups'
|
||||||
|
|
||||||
|
Params group label:
|
||||||
|
zh: '主组'
|
||||||
|
ja: '主组'
|
||||||
|
en: 'Main group'
|
||||||
|
|
||||||
Params uid label:
|
Params uid label:
|
||||||
zh: '用户ID'
|
zh: '用户ID'
|
||||||
ja: 'ユーザーID'
|
ja: 'ユーザーID'
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||||
|
group: "{{ params.group if params.group | length > 0 else omit }}"
|
||||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||||
append: "{{ true if params.groups | length > 0 else false }}"
|
append: "{{ true if params.groups | length > 0 else false }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ params:
|
||||||
default: ''
|
default: ''
|
||||||
help_text: "{{ 'Params home help text' | trans }}"
|
help_text: "{{ 'Params home help text' | trans }}"
|
||||||
|
|
||||||
|
- name: group
|
||||||
|
type: str
|
||||||
|
label: "{{ 'Params group label' | trans }}"
|
||||||
|
default: ''
|
||||||
|
help_text: "{{ 'Params group help text' | trans }}"
|
||||||
|
|
||||||
- name: groups
|
- name: groups
|
||||||
type: str
|
type: str
|
||||||
label: "{{ 'Params groups label' | trans }}"
|
label: "{{ 'Params groups label' | trans }}"
|
||||||
|
|
@ -63,6 +69,11 @@ i18n:
|
||||||
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||||
en: 'Default home directory /home/{account username}'
|
en: 'Default home directory /home/{account username}'
|
||||||
|
|
||||||
|
Params group help text:
|
||||||
|
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
|
||||||
|
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
|
||||||
|
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
|
||||||
|
|
||||||
Params groups help text:
|
Params groups help text:
|
||||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||||
|
|
@ -88,6 +99,11 @@ i18n:
|
||||||
ja: 'グループ'
|
ja: 'グループ'
|
||||||
en: 'Groups'
|
en: 'Groups'
|
||||||
|
|
||||||
|
Params group label:
|
||||||
|
zh: '主组'
|
||||||
|
ja: '主组'
|
||||||
|
en: 'Main group'
|
||||||
|
|
||||||
Params uid label:
|
Params uid label:
|
||||||
zh: '用户ID'
|
zh: '用户ID'
|
||||||
ja: 'ユーザーID'
|
ja: 'ユーザーID'
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from accounts.const import (
|
||||||
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
|
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
|
||||||
)
|
)
|
||||||
from accounts.models import ChangeSecretRecord
|
from accounts.models import ChangeSecretRecord
|
||||||
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
|
from accounts.notifications import ChangeSecretExecutionTaskMsg
|
||||||
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.utils.file import encrypt_and_compress_zip_file
|
from common.utils.file import encrypt_and_compress_zip_file
|
||||||
|
|
@ -94,10 +94,6 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
|
||||||
if not recipients:
|
if not recipients:
|
||||||
return
|
return
|
||||||
|
|
||||||
context = self.get_report_context()
|
|
||||||
for user in recipients:
|
|
||||||
ChangeSecretReportMsg(user, context).publish()
|
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test SQLServer connection
|
- name: Test SQLServer connection
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: |
|
script: |
|
||||||
SELECT
|
SELECT
|
||||||
l.name,
|
l.name,
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test SQLServer connection
|
- name: Test SQLServer connection
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: |
|
script: |
|
||||||
SELECT @@version
|
SELECT @@version
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
@ -23,47 +25,55 @@
|
||||||
var: info
|
var: info
|
||||||
|
|
||||||
- name: Check whether SQLServer User exist
|
- name: Check whether SQLServer User exist
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
|
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
|
||||||
when: db_info is succeeded
|
when: db_info is succeeded
|
||||||
register: user_exist
|
register: user_exist
|
||||||
|
|
||||||
- name: Change SQLServer password
|
- name: Change SQLServer password
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
|
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: user_exist.query_results[0] | length != 0
|
when: user_exist.query_results[0] | length != 0
|
||||||
register: change_info
|
register: change_info
|
||||||
|
|
||||||
- name: Add SQLServer user
|
- name: Add SQLServer user
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version"
|
script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version"
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: user_exist.query_results[0] | length == 0
|
when: user_exist.query_results[0] | length == 0
|
||||||
register: change_info
|
register: change_info
|
||||||
|
|
||||||
- name: Verify password
|
- name: Verify password
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: |
|
script: |
|
||||||
SELECT @@version
|
SELECT @@version
|
||||||
when: check_conn_after_change
|
when: check_conn_after_change
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||||
|
group: "{{ params.group if params.group | length > 0 else omit }}"
|
||||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||||
append: "{{ true if params.groups | length > 0 else false }}"
|
append: "{{ true if params.groups | length > 0 else false }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@ params:
|
||||||
default: ''
|
default: ''
|
||||||
help_text: "{{ 'Params home help text' | trans }}"
|
help_text: "{{ 'Params home help text' | trans }}"
|
||||||
|
|
||||||
|
- name: group
|
||||||
|
type: str
|
||||||
|
label: "{{ 'Params group label' | trans }}"
|
||||||
|
default: ''
|
||||||
|
help_text: "{{ 'Params group help text' | trans }}"
|
||||||
|
|
||||||
- name: groups
|
- name: groups
|
||||||
type: str
|
type: str
|
||||||
label: "{{ 'Params groups label' | trans }}"
|
label: "{{ 'Params groups label' | trans }}"
|
||||||
|
|
@ -61,6 +67,11 @@ i18n:
|
||||||
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||||
en: 'Default home directory /home/{account username}'
|
en: 'Default home directory /home/{account username}'
|
||||||
|
|
||||||
|
Params group help text:
|
||||||
|
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
|
||||||
|
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
|
||||||
|
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
|
||||||
|
|
||||||
Params groups help text:
|
Params groups help text:
|
||||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||||
|
|
@ -86,6 +97,11 @@ i18n:
|
||||||
ja: 'グループ'
|
ja: 'グループ'
|
||||||
en: 'Groups'
|
en: 'Groups'
|
||||||
|
|
||||||
|
Params group label:
|
||||||
|
zh: '主组'
|
||||||
|
ja: '主组'
|
||||||
|
en: 'Main group'
|
||||||
|
|
||||||
Params uid label:
|
Params uid label:
|
||||||
zh: '用户ID'
|
zh: '用户ID'
|
||||||
ja: 'ユーザーID'
|
ja: 'ユーザーID'
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||||
|
group: "{{ params.group if params.group | length > 0 else omit }}"
|
||||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||||
append: "{{ true if params.groups | length > 0 else false }}"
|
append: "{{ true if params.groups | length > 0 else false }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ params:
|
||||||
default: ''
|
default: ''
|
||||||
help_text: "{{ 'Params home help text' | trans }}"
|
help_text: "{{ 'Params home help text' | trans }}"
|
||||||
|
|
||||||
|
- name: group
|
||||||
|
type: str
|
||||||
|
label: "{{ 'Params group label' | trans }}"
|
||||||
|
default: ''
|
||||||
|
help_text: "{{ 'Params group help text' | trans }}"
|
||||||
|
|
||||||
- name: groups
|
- name: groups
|
||||||
type: str
|
type: str
|
||||||
label: "{{ 'Params groups label' | trans }}"
|
label: "{{ 'Params groups label' | trans }}"
|
||||||
|
|
@ -63,6 +69,11 @@ i18n:
|
||||||
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||||
en: 'Default home directory /home/{account username}'
|
en: 'Default home directory /home/{account username}'
|
||||||
|
|
||||||
|
Params group help text:
|
||||||
|
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
|
||||||
|
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
|
||||||
|
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
|
||||||
|
|
||||||
Params groups help text:
|
Params groups help text:
|
||||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||||
|
|
@ -84,9 +95,14 @@ i18n:
|
||||||
en: 'Home'
|
en: 'Home'
|
||||||
|
|
||||||
Params groups label:
|
Params groups label:
|
||||||
zh: '用户组'
|
zh: '附加组'
|
||||||
ja: 'グループ'
|
ja: '追加グループ'
|
||||||
en: 'Groups'
|
en: 'Additional Group'
|
||||||
|
|
||||||
|
Params group label:
|
||||||
|
zh: '主组'
|
||||||
|
ja: '主组'
|
||||||
|
en: 'Main group'
|
||||||
|
|
||||||
Params uid label:
|
Params uid label:
|
||||||
zh: '用户ID'
|
zh: '用户ID'
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: "Remove account"
|
- name: "Remove account"
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: "{{ jms_asset.spec_info.db_name }}"
|
name: "{{ jms_asset.spec_info.db_name }}"
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: "DROP LOGIN {{ account.username }}; select @@version"
|
script: "DROP LOGIN {{ account.username }}; select @@version"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: |
|
script: |
|
||||||
SELECT @@version
|
SELECT @@version
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
|
|
||||||
from azure.identity import ClientSecretCredential
|
|
||||||
from azure.keyvault.secrets import SecretClient
|
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
|
||||||
|
|
@ -14,6 +11,9 @@ __all__ = ['AZUREVaultClient']
|
||||||
class AZUREVaultClient(object):
|
class AZUREVaultClient(object):
|
||||||
|
|
||||||
def __init__(self, vault_url, tenant_id, client_id, client_secret):
|
def __init__(self, vault_url, tenant_id, client_id, client_secret):
|
||||||
|
from azure.identity import ClientSecretCredential
|
||||||
|
from azure.keyvault.secrets import SecretClient
|
||||||
|
|
||||||
authentication_endpoint = 'https://login.microsoftonline.com/' \
|
authentication_endpoint = 'https://login.microsoftonline.com/' \
|
||||||
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
|
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
|
||||||
|
|
||||||
|
|
@ -23,6 +23,8 @@ class AZUREVaultClient(object):
|
||||||
self.client = SecretClient(vault_url=vault_url, credential=credentials)
|
self.client = SecretClient(vault_url=vault_url, credential=credentials)
|
||||||
|
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
|
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.client.set_secret('jumpserver', '666')
|
self.client.set_secret('jumpserver', '666')
|
||||||
except (ResourceNotFoundError, ClientAuthenticationError) as e:
|
except (ResourceNotFoundError, ClientAuthenticationError) as e:
|
||||||
|
|
@ -32,6 +34,8 @@ class AZUREVaultClient(object):
|
||||||
return True, ''
|
return True, ''
|
||||||
|
|
||||||
def get(self, name, version=None):
|
def get(self, name, version=None):
|
||||||
|
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
secret = self.client.get_secret(name, version)
|
secret = self.client.get_secret(name, version)
|
||||||
return secret.value
|
return secret.value
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
|
||||||
return self.asset.platform
|
return self.asset.platform
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def alias(self):
|
def alias(self) -> str:
|
||||||
"""
|
"""
|
||||||
别称,因为有虚拟账号,@INPUT @MANUAL @USER, 否则为 id
|
别称,因为有虚拟账号,@INPUT @MANUAL @USER, 否则为 id
|
||||||
"""
|
"""
|
||||||
|
|
@ -140,13 +140,13 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
|
||||||
return self.username
|
return self.username
|
||||||
return str(self.id)
|
return str(self.id)
|
||||||
|
|
||||||
def is_virtual(self):
|
def is_virtual(self) -> bool:
|
||||||
"""
|
"""
|
||||||
不要用 username 去判断,因为可能是构造的 account 对象,设置了同名账号的用户名,
|
不要用 username 去判断,因为可能是构造的 account 对象,设置了同名账号的用户名,
|
||||||
"""
|
"""
|
||||||
return self.alias.startswith('@')
|
return self.alias.startswith('@')
|
||||||
|
|
||||||
def is_ds_account(self):
|
def is_ds_account(self) -> bool:
|
||||||
if self.is_virtual():
|
if self.is_virtual():
|
||||||
return ''
|
return ''
|
||||||
if not self.asset.is_directory_service:
|
if not self.asset.is_directory_service:
|
||||||
|
|
@ -160,7 +160,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
|
||||||
return self.asset.ds
|
return self.asset.ds
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def ds_domain(self):
|
def ds_domain(self) -> str:
|
||||||
"""这个不能去掉,perm_account 会动态设置这个值,以更改 full_username"""
|
"""这个不能去掉,perm_account 会动态设置这个值,以更改 full_username"""
|
||||||
if self.is_virtual():
|
if self.is_virtual():
|
||||||
return ''
|
return ''
|
||||||
|
|
@ -172,17 +172,17 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
|
||||||
return '@' in self.username or '\\' in self.username
|
return '@' in self.username or '\\' in self.username
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_username(self):
|
def full_username(self) -> str:
|
||||||
if not self.username_has_domain() and self.ds_domain:
|
if not self.username_has_domain() and self.ds_domain:
|
||||||
return '{}@{}'.format(self.username, self.ds_domain)
|
return '{}@{}'.format(self.username, self.ds_domain)
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def has_secret(self):
|
def has_secret(self) -> bool:
|
||||||
return bool(self.secret)
|
return bool(self.secret)
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def versions(self):
|
def versions(self) -> int:
|
||||||
return self.history.count()
|
return self.history.count()
|
||||||
|
|
||||||
def get_su_from_accounts(self):
|
def get_su_from_accounts(self):
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class IntegrationApplication(JMSOrgBaseModel):
|
||||||
return qs.filter(*query)
|
return qs.filter(*query)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def accounts_amount(self):
|
def accounts_amount(self) -> int:
|
||||||
return self.get_accounts().count()
|
return self.get_accounts().count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,11 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||||
return bool(self.secret)
|
return bool(self.secret)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_username(self):
|
def has_username(self) -> bool:
|
||||||
return bool(self.username)
|
return bool(self.username)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def spec_info(self):
|
def spec_info(self) -> dict:
|
||||||
data = {}
|
data = {}
|
||||||
if self.secret_type != SecretType.SSH_KEY:
|
if self.secret_type != SecretType.SSH_KEY:
|
||||||
return data
|
return data
|
||||||
|
|
@ -87,13 +87,13 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def password(self):
|
def password(self) -> str:
|
||||||
if self.secret_type == SecretType.PASSWORD:
|
if self.secret_type == SecretType.PASSWORD:
|
||||||
return self.secret
|
return self.secret
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def private_key(self):
|
def private_key(self) -> str:
|
||||||
if self.secret_type == SecretType.SSH_KEY:
|
if self.secret_type == SecretType.SSH_KEY:
|
||||||
return self.secret
|
return self.secret
|
||||||
return None
|
return None
|
||||||
|
|
@ -110,7 +110,7 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ssh_key_fingerprint(self):
|
def ssh_key_fingerprint(self) -> str:
|
||||||
if self.public_key:
|
if self.public_key:
|
||||||
public_key = self.public_key
|
public_key = self.public_key
|
||||||
elif self.private_key:
|
elif self.private_key:
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ class VaultModelMixin(models.Model):
|
||||||
__secret = None
|
__secret = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def secret(self):
|
def secret(self) -> str:
|
||||||
if self.__secret:
|
if self.__secret:
|
||||||
return self.__secret
|
return self.__secret
|
||||||
from accounts.backends import vault_client
|
from accounts.backends import vault_client
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ class VirtualAccount(JMSOrgBaseModel):
|
||||||
verbose_name = _('Virtual account')
|
verbose_name = _('Virtual account')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
return self.get_alias_display()
|
return self.get_alias_display()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def username(self):
|
def username(self) -> str:
|
||||||
usernames_map = {
|
usernames_map = {
|
||||||
AliasAccount.INPUT: _("Manual input"),
|
AliasAccount.INPUT: _("Manual input"),
|
||||||
AliasAccount.USER: _("Same with user"),
|
AliasAccount.USER: _("Same with user"),
|
||||||
|
|
@ -32,7 +32,7 @@ class VirtualAccount(JMSOrgBaseModel):
|
||||||
return usernames_map.get(self.alias, '')
|
return usernames_map.get(self.alias, '')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def comment(self):
|
def comment(self) -> str:
|
||||||
comments_map = {
|
comments_map = {
|
||||||
AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'),
|
AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'),
|
||||||
AliasAccount.USER: _('The account username name same with user on connect'),
|
AliasAccount.USER: _('The account username name same with user on connect'),
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,8 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||||
'source_id': {'required': False, 'allow_null': True},
|
'source_id': {'required': False, 'allow_null': True},
|
||||||
}
|
}
|
||||||
fields_unimport_template = ['params']
|
fields_unimport_template = ['params']
|
||||||
|
# 手动判断唯一性校验
|
||||||
|
validators = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_eager_loading(cls, queryset):
|
def setup_eager_loading(cls, queryset):
|
||||||
|
|
@ -263,6 +265,21 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
instance = getattr(self, "instance", None)
|
||||||
|
if instance:
|
||||||
|
return super().validate(attrs)
|
||||||
|
|
||||||
|
field_errors = {}
|
||||||
|
for _fields in Account._meta.unique_together:
|
||||||
|
lookup = {field: attrs.get(field) for field in _fields}
|
||||||
|
if Account.objects.filter(**lookup).exists():
|
||||||
|
verbose_names = ', '.join([str(Account._meta.get_field(f).verbose_name) for f in _fields])
|
||||||
|
msg_template = _('Account already exists. Field(s): {fields} must be unique.')
|
||||||
|
field_errors[_fields[0]] = msg_template.format(fields=verbose_names)
|
||||||
|
raise serializers.ValidationError(field_errors)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class AccountDetailSerializer(AccountSerializer):
|
class AccountDetailSerializer(AccountSerializer):
|
||||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||||
|
|
@ -456,6 +473,8 @@ class AssetAccountBulkSerializer(
|
||||||
|
|
||||||
|
|
||||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||||
|
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
|
||||||
|
|
||||||
class Meta(AccountSerializer.Meta):
|
class Meta(AccountSerializer.Meta):
|
||||||
fields = AccountSerializer.Meta.fields + ['spec_info']
|
fields = AccountSerializer.Meta.fields + ['spec_info']
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
|
@ -470,6 +489,7 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||||
|
|
||||||
class AccountHistorySerializer(serializers.ModelSerializer):
|
class AccountHistorySerializer(serializers.ModelSerializer):
|
||||||
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
|
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
|
||||||
|
secret = serializers.CharField(label=_('Secret'), read_only=True)
|
||||||
id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
|
id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,8 @@ class AuthValidateMixin(serializers.Serializer):
|
||||||
class BaseAccountSerializer(
|
class BaseAccountSerializer(
|
||||||
AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer
|
AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer
|
||||||
):
|
):
|
||||||
|
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BaseAccount
|
model = BaseAccount
|
||||||
fields_mini = ["id", "name", "username"]
|
fields_mini = ["id", "name", "username"]
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_is_success(obj):
|
def get_is_success(obj) -> bool:
|
||||||
return obj.status == ChangeSecretRecordStatusChoice.success
|
return obj.status == ChangeSecretRecordStatusChoice.success
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -157,7 +157,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_asset(instance):
|
def get_asset(instance) -> str:
|
||||||
return str(instance.asset)
|
return str(instance.asset)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -165,7 +165,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
|
||||||
return str(instance.account)
|
return str(instance.account)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_is_success(obj):
|
def get_is_success(obj) -> str:
|
||||||
if obj.status == ChangeSecretRecordStatusChoice.success.value:
|
if obj.status == ChangeSecretRecordStatusChoice.success.value:
|
||||||
return _("Success")
|
return _("Success")
|
||||||
return _("Failed")
|
return _("Failed")
|
||||||
|
|
@ -196,9 +196,9 @@ class ChangeSecretAccountSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_meta(obj):
|
def get_meta(obj) -> dict:
|
||||||
return account_secret_task_status.get(str(obj.id))
|
return account_secret_task_status.get(str(obj.id))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_ttl(obj):
|
def get_ttl(obj) -> int:
|
||||||
return account_secret_task_status.get_ttl(str(obj.id))
|
return account_secret_task_status.get_ttl(str(obj.id))
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ class AssetRiskSerializer(serializers.Serializer):
|
||||||
risk_summary = serializers.SerializerMethodField()
|
risk_summary = serializers.SerializerMethodField()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_risk_summary(obj):
|
def get_risk_summary(obj) -> dict:
|
||||||
summary = {}
|
summary = {}
|
||||||
for risk in RiskChoice.choices:
|
for risk in RiskChoice.choices:
|
||||||
summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0)
|
summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0)
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
<h3>{% trans 'Task name' %}: {{ name }}</h3>
|
|
||||||
<h3>{% trans 'Task execution id' %}: {{ execution_id }}</h3>
|
|
||||||
<p>{% trans 'Respectful' %} {{ recipient }}</p>
|
|
||||||
<p>{% trans 'Hello! The following is the failure of changing the password of your assets or pushing the account. Please check and handle it in time.' %}</p>
|
|
||||||
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
|
|
||||||
<caption></caption>
|
|
||||||
<thead>
|
|
||||||
<tr style="background-color: #f2f2f2;">
|
|
||||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Asset' %}</th>
|
|
||||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Account' %}</th>
|
|
||||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Error' %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for asset_name, account_username, error in asset_account_errors %}
|
|
||||||
<tr>
|
|
||||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ asset_name }}</td>
|
|
||||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ account_username }}</td>
|
|
||||||
<td style="border: 1px solid #ddd; padding: 10px;">
|
|
||||||
<div style="
|
|
||||||
max-width: 90%;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: block;"
|
|
||||||
title="{{ error }}"
|
|
||||||
>
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
@ -3,3 +3,4 @@ from .connect_method import *
|
||||||
from .login_acl import *
|
from .login_acl import *
|
||||||
from .login_asset_acl import *
|
from .login_asset_acl import *
|
||||||
from .login_asset_check import *
|
from .login_asset_check import *
|
||||||
|
from .data_masking import *
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
|
||||||
|
from .common import ACLUserFilterMixin
|
||||||
|
from ..models import DataMaskingRule
|
||||||
|
from .. import serializers
|
||||||
|
|
||||||
|
__all__ = ['DataMaskingRuleViewSet']
|
||||||
|
|
||||||
|
|
||||||
|
class DataMaskingRuleFilter(ACLUserFilterMixin):
|
||||||
|
class Meta:
|
||||||
|
model = DataMaskingRule
|
||||||
|
fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
class DataMaskingRuleViewSet(OrgBulkModelViewSet):
|
||||||
|
model = DataMaskingRule
|
||||||
|
filterset_class = DataMaskingRuleFilter
|
||||||
|
search_fields = ('name',)
|
||||||
|
serializer_class = serializers.DataMaskingRuleSerializer
|
||||||
|
|
@ -8,7 +8,7 @@ __all__ = ['LoginAssetACLViewSet']
|
||||||
class LoginAssetACLFilter(ACLUserAssetFilterMixin):
|
class LoginAssetACLFilter(ACLUserAssetFilterMixin):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.LoginAssetACL
|
model = models.LoginAssetACL
|
||||||
fields = ['name', ]
|
fields = ['name', 'action']
|
||||||
|
|
||||||
|
|
||||||
class LoginAssetACLViewSet(OrgBulkModelViewSet):
|
class LoginAssetACLViewSet(OrgBulkModelViewSet):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Generated by Django 4.1.13 on 2025-10-07 16:16
|
||||||
|
|
||||||
|
import common.db.fields
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('acls', '0002_auto_20210926_1047'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DataMaskingRule',
|
||||||
|
fields=[
|
||||||
|
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
|
||||||
|
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
|
||||||
|
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||||
|
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||||
|
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
|
||||||
|
('action', models.CharField(default='reject', max_length=64, verbose_name='Action')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||||
|
('users', common.db.fields.JSONManyToManyField(default=dict, to='users.User', verbose_name='Users')),
|
||||||
|
('assets', common.db.fields.JSONManyToManyField(default=dict, to='assets.Asset', verbose_name='Assets')),
|
||||||
|
('accounts', models.JSONField(default=list, verbose_name='Accounts')),
|
||||||
|
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||||
|
('fields_pattern', models.CharField(default='password', max_length=128, verbose_name='Fields pattern')),
|
||||||
|
('masking_method', models.CharField(choices=[('fixed_char', 'Fixed Character Replacement'), ('hide_middle', 'Hide Middle Characters'), ('keep_prefix', 'Keep Prefix Only'), ('keep_suffix', 'Keep Suffix Only')], default='fixed_char', max_length=32, verbose_name='Masking Method')),
|
||||||
|
('mask_pattern', models.CharField(blank=True, default='######', max_length=128, null=True, verbose_name='Mask Pattern')),
|
||||||
|
('reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Data Masking Rule',
|
||||||
|
'unique_together': {('org_id', 'name')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -2,3 +2,4 @@ from .command_acl import *
|
||||||
from .connect_method import *
|
from .connect_method import *
|
||||||
from .login_acl import *
|
from .login_acl import *
|
||||||
from .login_asset_acl import *
|
from .login_asset_acl import *
|
||||||
|
from .data_masking import *
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from acls.models import UserAssetAccountBaseACL
|
||||||
|
from common.utils import get_logger
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
__all__ = ['MaskingMethod', 'DataMaskingRule']
|
||||||
|
|
||||||
|
|
||||||
|
class MaskingMethod(models.TextChoices):
|
||||||
|
fixed_char = "fixed_char", _("Fixed Character Replacement") # 固定字符替换
|
||||||
|
hide_middle = "hide_middle", _("Hide Middle Characters") # 隐藏中间几位
|
||||||
|
keep_prefix = "keep_prefix", _("Keep Prefix Only") # 只保留前缀
|
||||||
|
keep_suffix = "keep_suffix", _("Keep Suffix Only") # 只保留后缀
|
||||||
|
|
||||||
|
|
||||||
|
class DataMaskingRule(UserAssetAccountBaseACL):
|
||||||
|
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||||
|
fields_pattern = models.CharField(max_length=128, default='password', verbose_name=_("Fields pattern"))
|
||||||
|
|
||||||
|
masking_method = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
choices=MaskingMethod.choices,
|
||||||
|
default=MaskingMethod.fixed_char,
|
||||||
|
verbose_name=_("Masking Method"),
|
||||||
|
)
|
||||||
|
mask_pattern = models.CharField(
|
||||||
|
max_length=128,
|
||||||
|
verbose_name=_("Mask Pattern"),
|
||||||
|
default="######",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [('org_id', 'name')]
|
||||||
|
verbose_name = _("Data Masking Rule")
|
||||||
|
|
@ -1,30 +1,52 @@
|
||||||
from django.template.loader import render_to_string
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
from acls.models import LoginACL, LoginAssetACL
|
||||||
from assets.models import Asset
|
from assets.models import Asset
|
||||||
from audits.models import UserLoginLog
|
from audits.models import UserLoginLog
|
||||||
|
from common.views.template import custom_render_to_string
|
||||||
from notifications.notifications import UserMessage
|
from notifications.notifications import UserMessage
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
|
|
||||||
class UserLoginReminderMsg(UserMessage):
|
class UserLoginReminderMsg(UserMessage):
|
||||||
subject = _('User login reminder')
|
subject = _('User login reminder')
|
||||||
|
template_name = 'acls/user_login_reminder.html'
|
||||||
|
contexts = [
|
||||||
|
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
|
||||||
|
{"name": "username", "label": _('User'), "default": "john"},
|
||||||
|
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
|
||||||
|
{"name": "recipient_name", "label": _("Recipient name"), "default": "John"},
|
||||||
|
{"name": "recipient_username", "label": _("Recipient username"), "default": "john"},
|
||||||
|
{"name": "user_agent", "label": _('User agent'), "default": "Mozilla/5.0"},
|
||||||
|
{"name": "acl_name", "label": _('ACL name'), "default": "login acl"},
|
||||||
|
{"name": "login_from", "label": _('Login from'), "default": "web"},
|
||||||
|
{"name": "time", "label": _('Login time'), "default": "2025-01-01 12:00:00"},
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, user, user_log: UserLoginLog):
|
def __init__(self, user, user_log: UserLoginLog, acl: LoginACL):
|
||||||
self.user_log = user_log
|
self.user_log = user_log
|
||||||
|
self.acl_name = str(acl)
|
||||||
|
self.login_from = user_log.get_type_display()
|
||||||
|
now = timezone.localtime(user_log.datetime)
|
||||||
|
self.time = now.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
super().__init__(user)
|
super().__init__(user)
|
||||||
|
|
||||||
def get_html_msg(self) -> dict:
|
def get_html_msg(self) -> dict:
|
||||||
user_log = self.user_log
|
user_log = self.user_log
|
||||||
context = {
|
context = {
|
||||||
'ip': user_log.ip,
|
'ip': user_log.ip,
|
||||||
|
'time': self.time,
|
||||||
'city': user_log.city,
|
'city': user_log.city,
|
||||||
|
'acl_name': self.acl_name,
|
||||||
|
'login_from': self.login_from,
|
||||||
'username': user_log.username,
|
'username': user_log.username,
|
||||||
'recipient': self.user,
|
'recipient_name': self.user.name,
|
||||||
|
'recipient_username': self.user.username,
|
||||||
'user_agent': user_log.user_agent,
|
'user_agent': user_log.user_agent,
|
||||||
}
|
}
|
||||||
message = render_to_string('acls/user_login_reminder.html', context)
|
message = custom_render_to_string(self.template_name, context)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'subject': str(self.subject),
|
'subject': str(self.subject),
|
||||||
|
|
@ -40,24 +62,55 @@ class UserLoginReminderMsg(UserMessage):
|
||||||
|
|
||||||
class AssetLoginReminderMsg(UserMessage):
|
class AssetLoginReminderMsg(UserMessage):
|
||||||
subject = _('User login alert for asset')
|
subject = _('User login alert for asset')
|
||||||
|
template_name = 'acls/asset_login_reminder.html'
|
||||||
|
contexts = [
|
||||||
|
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
|
||||||
|
{"name": "username", "label": _('User'), "default": "john"},
|
||||||
|
{"name": "name", "label": _('Name'), "default": "John"},
|
||||||
|
{"name": "asset", "label": _('Asset'), "default": "dev server"},
|
||||||
|
{"name": "recipient_name", "label": _('Recipient name'), "default": "John"},
|
||||||
|
{"name": "recipient_username", "label": _('Recipient username'), "default": "john"},
|
||||||
|
{"name": "account", "label": _('Account Input username'), "default": "root"},
|
||||||
|
{"name": "account_name", "label": _('Account name'), "default": "root"},
|
||||||
|
{"name": "acl_name", "label": _('ACL name'), "default": "login acl"},
|
||||||
|
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
|
||||||
|
{"name": "login_from", "label": _('Login from'), "default": "web"},
|
||||||
|
{"name": "time", "label": _('Login time'), "default": "2025-01-01 12:00:00"}
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, user, asset: Asset, login_user: User, account: Account, input_username):
|
def __init__(
|
||||||
|
self, user, asset: Asset, login_user: User,
|
||||||
|
account: Account, acl: LoginAssetACL,
|
||||||
|
ip, input_username, login_from
|
||||||
|
):
|
||||||
|
self.ip = ip
|
||||||
self.asset = asset
|
self.asset = asset
|
||||||
self.login_user = login_user
|
self.login_user = login_user
|
||||||
self.account = account
|
self.account = account
|
||||||
|
self.acl_name = str(acl)
|
||||||
|
self.login_from = login_from
|
||||||
|
self.login_user = login_user
|
||||||
self.input_username = input_username
|
self.input_username = input_username
|
||||||
|
|
||||||
|
now = timezone.localtime(timezone.now())
|
||||||
|
self.time = now.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
super().__init__(user)
|
super().__init__(user)
|
||||||
|
|
||||||
def get_html_msg(self) -> dict:
|
def get_html_msg(self) -> dict:
|
||||||
context = {
|
context = {
|
||||||
'recipient': self.user,
|
'ip': self.ip,
|
||||||
|
'time': self.time,
|
||||||
|
'login_from': self.login_from,
|
||||||
|
'recipient_name': self.user.name,
|
||||||
|
'recipient_username': self.user.username,
|
||||||
'username': self.login_user.username,
|
'username': self.login_user.username,
|
||||||
'name': self.login_user.name,
|
'name': self.login_user.name,
|
||||||
'asset': str(self.asset),
|
'asset': str(self.asset),
|
||||||
'account': self.input_username,
|
'account': self.input_username,
|
||||||
'account_name': self.account.name,
|
'account_name': self.account.name,
|
||||||
|
'acl_name': self.acl_name,
|
||||||
}
|
}
|
||||||
message = render_to_string('acls/asset_login_reminder.html', context)
|
message = custom_render_to_string(self.template_name, context)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'subject': str(self.subject),
|
'subject': str(self.subject),
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ from .connect_method import *
|
||||||
from .login_acl import *
|
from .login_acl import *
|
||||||
from .login_asset_acl import *
|
from .login_asset_acl import *
|
||||||
from .login_asset_check import *
|
from .login_asset_check import *
|
||||||
|
from .data_masking import *
|
||||||
|
|
@ -90,7 +90,7 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
"is_active", "priority", "action",
|
"is_active", "priority", "action",
|
||||||
"date_created", "date_updated",
|
"date_created", "date_updated",
|
||||||
"comment", "created_by", "org_id",
|
"comment", "created_by"
|
||||||
]
|
]
|
||||||
fields_m2m = ["reviewers", ]
|
fields_m2m = ["reviewers", ]
|
||||||
fields = fields_small + fields_m2m
|
fields = fields_small + fields_m2m
|
||||||
|
|
@ -100,6 +100,20 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
|
||||||
'reviewers': {'label': _('Recipients')},
|
'reviewers': {'label': _('Recipients')},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BaseUserACLSerializer(BaseACLSerializer):
|
||||||
|
users = JSONManyToManyField(label=_('User'))
|
||||||
|
|
||||||
|
class Meta(BaseACLSerializer.Meta):
|
||||||
|
fields = BaseACLSerializer.Meta.fields + ['users']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
|
||||||
|
assets = JSONManyToManyField(label=_('Asset'))
|
||||||
|
accounts = serializers.ListField(label=_('Account'))
|
||||||
|
|
||||||
|
class Meta(BaseUserACLSerializer.Meta):
|
||||||
|
fields = BaseUserACLSerializer.Meta.fields + ['assets', 'accounts', 'org_id']
|
||||||
|
|
||||||
def validate_reviewers(self, reviewers):
|
def validate_reviewers(self, reviewers):
|
||||||
action = self.initial_data.get('action')
|
action = self.initial_data.get('action')
|
||||||
if not action and self.instance:
|
if not action and self.instance:
|
||||||
|
|
@ -119,18 +133,3 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
|
||||||
)
|
)
|
||||||
raise serializers.ValidationError(error)
|
raise serializers.ValidationError(error)
|
||||||
return valid_reviewers
|
return valid_reviewers
|
||||||
|
|
||||||
|
|
||||||
class BaseUserACLSerializer(BaseACLSerializer):
|
|
||||||
users = JSONManyToManyField(label=_('User'))
|
|
||||||
|
|
||||||
class Meta(BaseACLSerializer.Meta):
|
|
||||||
fields = BaseACLSerializer.Meta.fields + ['users']
|
|
||||||
|
|
||||||
|
|
||||||
class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
|
|
||||||
assets = JSONManyToManyField(label=_('Asset'))
|
|
||||||
accounts = serializers.ListField(label=_('Account'))
|
|
||||||
|
|
||||||
class Meta(BaseUserACLSerializer.Meta):
|
|
||||||
fields = BaseUserACLSerializer.Meta.fields + ['assets', 'accounts']
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from common.serializers.mixin import CommonBulkModelSerializer
|
||||||
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
|
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
|
||||||
from ..const import ActionChoices
|
from ..const import ActionChoices
|
||||||
from ..models import ConnectMethodACL
|
from ..models import ConnectMethodACL
|
||||||
|
|
@ -6,16 +6,15 @@ from ..models import ConnectMethodACL
|
||||||
__all__ = ["ConnectMethodACLSerializer"]
|
__all__ = ["ConnectMethodACLSerializer"]
|
||||||
|
|
||||||
|
|
||||||
class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer):
|
class ConnectMethodACLSerializer(BaseSerializer, CommonBulkModelSerializer):
|
||||||
class Meta(BaseSerializer.Meta):
|
class Meta(BaseSerializer.Meta):
|
||||||
model = ConnectMethodACL
|
model = ConnectMethodACL
|
||||||
fields = [
|
fields = [
|
||||||
i for i in BaseSerializer.Meta.fields + ['connect_methods']
|
i for i in BaseSerializer.Meta.fields + ['connect_methods']
|
||||||
if i not in ['assets', 'accounts']
|
if i not in ['assets', 'accounts', 'org_id']
|
||||||
]
|
]
|
||||||
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
|
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
|
||||||
ActionChoices.review,
|
ActionChoices.review,
|
||||||
ActionChoices.accept,
|
|
||||||
ActionChoices.notice,
|
ActionChoices.notice,
|
||||||
ActionChoices.face_verify,
|
ActionChoices.face_verify,
|
||||||
ActionChoices.face_online,
|
ActionChoices.face_online,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from acls.models import MaskingMethod, DataMaskingRule
|
||||||
|
from common.serializers.fields import LabeledChoiceField
|
||||||
|
from common.serializers.mixin import CommonBulkModelSerializer
|
||||||
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
|
||||||
|
|
||||||
|
__all__ = ['DataMaskingRuleSerializer']
|
||||||
|
|
||||||
|
|
||||||
|
class DataMaskingRuleSerializer(BaseSerializer, BulkOrgResourceModelSerializer):
|
||||||
|
masking_method = LabeledChoiceField(
|
||||||
|
choices=MaskingMethod.choices, default=MaskingMethod.fixed_char, label=_('Masking Method')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseSerializer.Meta):
|
||||||
|
model = DataMaskingRule
|
||||||
|
fields = BaseSerializer.Meta.fields + ['fields_pattern', 'masking_method', 'mask_pattern']
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from common.serializers import CommonBulkModelSerializer
|
||||||
from common.serializers import MethodSerializer
|
from common.serializers import MethodSerializer
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
|
||||||
from .base import BaseUserACLSerializer
|
from .base import BaseUserACLSerializer
|
||||||
from .rules import RuleSerializer
|
from .rules import RuleSerializer
|
||||||
from ..const import ActionChoices
|
from ..const import ActionChoices
|
||||||
|
|
@ -12,12 +12,12 @@ __all__ = ["LoginACLSerializer"]
|
||||||
common_help_text = _("With * indicating a match all. ")
|
common_help_text = _("With * indicating a match all. ")
|
||||||
|
|
||||||
|
|
||||||
class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
|
class LoginACLSerializer(BaseUserACLSerializer, CommonBulkModelSerializer):
|
||||||
rules = MethodSerializer(label=_('Rule'))
|
rules = MethodSerializer(label=_('Rule'))
|
||||||
|
|
||||||
class Meta(BaseUserACLSerializer.Meta):
|
class Meta(BaseUserACLSerializer.Meta):
|
||||||
model = LoginACL
|
model = LoginACL
|
||||||
fields = BaseUserACLSerializer.Meta.fields + ['rules', ]
|
fields = list((set(BaseUserACLSerializer.Meta.fields) | {'rules'}))
|
||||||
action_choices_exclude = [
|
action_choices_exclude = [
|
||||||
ActionChoices.warning,
|
ActionChoices.warning,
|
||||||
ActionChoices.notify_and_warn,
|
ActionChoices.notify_and_warn,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<h3>{% trans 'Dear' %}: {{ recipient.name }}[{{ recipient.username }}]</h3>
|
<h3>{% trans 'Dear' %}: {{ recipient_name }}[{{ recipient_username }}]</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<p>{% trans 'We would like to inform you that a user has recently logged into the following asset:' %}<p>
|
<p>{% trans 'We would like to inform you that a user has recently logged into the following asset:' %}<p>
|
||||||
<p><strong>{% trans 'Asset details' %}:</strong></p>
|
<p><strong>{% trans 'Asset details' %}:</strong></p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>{% trans 'User' %}:</strong> [{{ name }}({{ username }})]</li>
|
<li><strong>{% trans 'User' %}:</strong> [{{ name }}({{ username }})]</li>
|
||||||
|
<li><strong>IP:</strong> [{{ ip }}]</li>
|
||||||
<li><strong>{% trans 'Assets' %}:</strong> [{{ asset }}]</li>
|
<li><strong>{% trans 'Assets' %}:</strong> [{{ asset }}]</li>
|
||||||
<li><strong>{% trans 'Account' %}:</strong> [{{ account_name }}({{ account }})]</li>
|
<li><strong>{% trans 'Account' %}:</strong> [{{ account_name }}({{ account }})]</li>
|
||||||
|
<li><strong>{% trans 'Login asset acl' %}:</strong> [{{ acl_name }}]</li>
|
||||||
|
<li><strong>{% trans 'Login from' %}:</strong> [{{ login_from }}]</li>
|
||||||
|
<li><strong>{% trans 'Time' %}:</strong> [{{ time }}]</li>
|
||||||
</ul>
|
</ul>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<h3>{% trans 'Dear' %}: {{ recipient.name }}[{{ recipient.username }}]</h3>
|
<h3>{% trans 'Dear' %}: {{ recipient_name }}[{{ recipient_username }}]</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<p>{% trans 'We would like to inform you that a user has recently logged:' %}<p>
|
<p>{% trans 'We would like to inform you that a user has recently logged:' %}<p>
|
||||||
<p><strong>{% trans 'User details' %}:</strong></p>
|
<p><strong>{% trans 'User details' %}:</strong></p>
|
||||||
|
|
@ -8,7 +8,10 @@
|
||||||
<li><strong>{% trans 'User' %}:</strong> [{{ username }}]</li>
|
<li><strong>{% trans 'User' %}:</strong> [{{ username }}]</li>
|
||||||
<li><strong>IP:</strong> [{{ ip }}]</li>
|
<li><strong>IP:</strong> [{{ ip }}]</li>
|
||||||
<li><strong>{% trans 'Login city' %}:</strong> [{{ city }}]</li>
|
<li><strong>{% trans 'Login city' %}:</strong> [{{ city }}]</li>
|
||||||
|
<li><strong>{% trans 'Login from' %}:</strong> [{{ login_from }}]</li>
|
||||||
<li><strong>{% trans 'User agent' %}:</strong> [{{ user_agent }}]</li>
|
<li><strong>{% trans 'User agent' %}:</strong> [{{ user_agent }}]</li>
|
||||||
|
<li><strong>{% trans 'Login acl' %}:</strong> [{{ acl_name }}]</li>
|
||||||
|
<li><strong>{% trans 'Time' %}:</strong> [{{ time }}]</li>
|
||||||
</ul>
|
</ul>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl'
|
||||||
router.register(r'command-filter-acls', api.CommandFilterACLViewSet, 'command-filter-acl')
|
router.register(r'command-filter-acls', api.CommandFilterACLViewSet, 'command-filter-acl')
|
||||||
router.register(r'command-groups', api.CommandGroupViewSet, 'command-group')
|
router.register(r'command-groups', api.CommandGroupViewSet, 'command-group')
|
||||||
router.register(r'connect-method-acls', api.ConnectMethodACLViewSet, 'connect-method-acl')
|
router.register(r'connect-method-acls', api.ConnectMethodACLViewSet, 'connect-method-acl')
|
||||||
|
router.register(r'data-masking-rules', api.DataMaskingRuleViewSet, 'data-masking-rule')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),
|
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ class FavoriteAssetViewSet(BulkModelViewSet):
|
||||||
serializer_class = FavoriteAssetSerializer
|
serializer_class = FavoriteAssetSerializer
|
||||||
permission_classes = (IsValidUser,)
|
permission_classes = (IsValidUser,)
|
||||||
filterset_fields = ['asset']
|
filterset_fields = ['asset']
|
||||||
|
page_no_limit = True
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
with tmp_to_root_org():
|
with tmp_to_root_org():
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from assets.const import AllTypes
|
from assets.const import AllTypes
|
||||||
from assets.models import Platform, Node, Asset, PlatformProtocol
|
from assets.models import Platform, Node, Asset, PlatformProtocol, PlatformAutomation
|
||||||
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer, PlatformListSerializer
|
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer, PlatformListSerializer
|
||||||
from common.api import JMSModelViewSet
|
from common.api import JMSModelViewSet
|
||||||
from common.permissions import IsValidUser
|
from common.permissions import IsValidUser
|
||||||
|
|
@ -43,6 +43,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
||||||
'ops_methods': 'assets.view_platform',
|
'ops_methods': 'assets.view_platform',
|
||||||
'filter_nodes_assets': 'assets.view_platform',
|
'filter_nodes_assets': 'assets.view_platform',
|
||||||
}
|
}
|
||||||
|
page_no_limit = True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# 因为没有走分页逻辑,所以需要这里 prefetch
|
# 因为没有走分页逻辑,所以需要这里 prefetch
|
||||||
|
|
@ -112,6 +113,7 @@ class PlatformProtocolViewSet(JMSModelViewSet):
|
||||||
|
|
||||||
class PlatformAutomationMethodsApi(generics.ListAPIView):
|
class PlatformAutomationMethodsApi(generics.ListAPIView):
|
||||||
permission_classes = (IsValidUser,)
|
permission_classes = (IsValidUser,)
|
||||||
|
queryset = PlatformAutomation.objects.none()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def automation_methods():
|
def automation_methods():
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,7 @@ class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView):
|
||||||
'GET': 'assets.view_asset',
|
'GET': 'assets.view_asset',
|
||||||
'list': 'assets.view_asset',
|
'list': 'assets.view_asset',
|
||||||
}
|
}
|
||||||
|
queryset = Node.objects.none()
|
||||||
|
|
||||||
def get_assets(self):
|
def get_assets(self):
|
||||||
key = self.request.query_params.get('key')
|
key = self.request.query_params.get('key')
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test SQLServer connection
|
- name: Test SQLServer connection
|
||||||
community.general.mssql_script:
|
mssql_script:
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
|
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||||
|
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||||
script: |
|
script: |
|
||||||
SELECT @@version
|
SELECT @@version
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from orgs.models import Organization
|
||||||
from .base import BaseType
|
from .base import BaseType
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -52,3 +53,41 @@ class GPTTypes(BaseType):
|
||||||
return [
|
return [
|
||||||
cls.CHATGPT,
|
cls.CHATGPT,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
CHATX_NAME = 'ChatX'
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_update_chatx_resources(chatx_name=CHATX_NAME, org_id=Organization.SYSTEM_ID):
|
||||||
|
from django.apps import apps
|
||||||
|
|
||||||
|
platform_model = apps.get_model('assets', 'Platform')
|
||||||
|
asset_model = apps.get_model('assets', 'Asset')
|
||||||
|
account_model = apps.get_model('accounts', 'Account')
|
||||||
|
|
||||||
|
platform, __ = platform_model.objects.get_or_create(
|
||||||
|
name=chatx_name,
|
||||||
|
defaults={
|
||||||
|
'internal': True,
|
||||||
|
'type': chatx_name,
|
||||||
|
'category': 'ai',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
asset, __ = asset_model.objects.get_or_create(
|
||||||
|
address=chatx_name,
|
||||||
|
defaults={
|
||||||
|
'name': chatx_name,
|
||||||
|
'platform': platform,
|
||||||
|
'org_id': org_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
account, __ = account_model.objects.get_or_create(
|
||||||
|
username=chatx_name,
|
||||||
|
defaults={
|
||||||
|
'name': chatx_name,
|
||||||
|
'asset': asset,
|
||||||
|
'org_id': org_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return asset, account
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||||
'default': False,
|
'default': False,
|
||||||
'label': _('Auth username')
|
'label': _('Auth username')
|
||||||
},
|
},
|
||||||
|
'enable_cluster_mode': {
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
'label': _('Enable cluster mode'),
|
||||||
|
'help_text': _('Enable if this Redis instance is part of a cluster')
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ class Protocol(models.Model):
|
||||||
return protocols[0] if len(protocols) > 0 else {}
|
return protocols[0] if len(protocols) > 0 else {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def setting(self):
|
def setting(self) -> dict:
|
||||||
if self._setting is not None:
|
if self._setting is not None:
|
||||||
return self._setting
|
return self._setting
|
||||||
return self.asset_platform_protocol.get('setting', {})
|
return self.asset_platform_protocol.get('setting', {})
|
||||||
|
|
@ -122,7 +122,7 @@ class Protocol(models.Model):
|
||||||
self._setting = value
|
self._setting = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public(self):
|
def public(self) -> bool:
|
||||||
return self.asset_platform_protocol.get('public', True)
|
return self.asset_platform_protocol.get('public', True)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -210,7 +210,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
|
||||||
return self.category == const.Category.DS and hasattr(self, 'ds')
|
return self.category == const.Category.DS and hasattr(self, 'ds')
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def spec_info(self):
|
def spec_info(self) -> dict:
|
||||||
instance = getattr(self, self.category, None)
|
instance = getattr(self, self.category, None)
|
||||||
if not instance:
|
if not instance:
|
||||||
return {}
|
return {}
|
||||||
|
|
@ -240,7 +240,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
|
||||||
return info
|
return info
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def auto_config(self):
|
def auto_config(self) -> dict:
|
||||||
platform = self.platform
|
platform = self.platform
|
||||||
auto_config = {
|
auto_config = {
|
||||||
'su_enabled': platform.su_enabled,
|
'su_enabled': platform.su_enabled,
|
||||||
|
|
@ -343,11 +343,11 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
|
||||||
return names
|
return names
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def type(self):
|
def type(self) -> str:
|
||||||
return self.platform.type
|
return self.platform.type
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def category(self):
|
def category(self) -> str:
|
||||||
return self.platform.category
|
return self.platform.category
|
||||||
|
|
||||||
def is_category(self, category):
|
def is_category(self, category):
|
||||||
|
|
|
||||||
|
|
@ -573,7 +573,7 @@ class Node(JMSOrgBaseModel, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||||
return not self.__gt__(other)
|
return not self.__gt__(other)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def computed_full_value(self):
|
def computed_full_value(self):
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class PlatformProtocol(models.Model):
|
||||||
return '{}/{}'.format(self.name, self.port)
|
return '{}/{}'.format(self.name, self.port)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def secret_types(self):
|
def secret_types(self) -> list:
|
||||||
return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
|
return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
|
||||||
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
|
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
|
||||||
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts'))
|
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts'))
|
||||||
nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path"))
|
nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path"))
|
||||||
|
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
|
||||||
platform = ObjectRelatedField(queryset=Platform.objects, required=True, label=_('Platform'),
|
platform = ObjectRelatedField(queryset=Platform.objects, required=True, label=_('Platform'),
|
||||||
attrs=('id', 'name', 'type'))
|
attrs=('id', 'name', 'type'))
|
||||||
accounts_amount = serializers.IntegerField(read_only=True, label=_('Accounts amount'))
|
accounts_amount = serializers.IntegerField(read_only=True, label=_('Accounts amount'))
|
||||||
|
|
@ -425,6 +426,18 @@ class DetailMixin(serializers.Serializer):
|
||||||
gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
|
gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
|
||||||
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
|
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_auto_config(obj) -> dict:
|
||||||
|
return obj.auto_config
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_gathered_info(obj) -> dict:
|
||||||
|
return obj.gathered_info
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_spec_info(obj) -> dict:
|
||||||
|
return obj.spec_info
|
||||||
|
|
||||||
def get_instance(self):
|
def get_instance(self):
|
||||||
request = self.context.get('request')
|
request = self.context.get('request')
|
||||||
if not self.instance and UUID_PATTERN.findall(request.path):
|
if not self.instance and UUID_PATTERN.findall(request.path):
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,10 @@ class DatabaseSerializer(AssetSerializer):
|
||||||
if not platform:
|
if not platform:
|
||||||
return
|
return
|
||||||
|
|
||||||
if platform.type in ['mysql', 'mariadb']:
|
if platform.type in [
|
||||||
|
'mysql', 'mariadb', 'oracle', 'sqlserver',
|
||||||
|
'db2', 'dameng', 'clickhouse', 'redis'
|
||||||
|
]:
|
||||||
db_field.required = False
|
db_field.required = False
|
||||||
db_field.allow_blank = True
|
db_field.allow_blank = True
|
||||||
db_field.allow_null = True
|
db_field.allow_null = True
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,13 @@ class WebSerializer(AssetSerializer):
|
||||||
'submit_selector': {
|
'submit_selector': {
|
||||||
'default': 'id=login_button',
|
'default': 'id=login_button',
|
||||||
},
|
},
|
||||||
|
'script': {
|
||||||
|
'default': [],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
data = data.copy()
|
||||||
|
if data.get('script') in ("", None):
|
||||||
|
data.pop('script', None)
|
||||||
|
return super().to_internal_value(data)
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,13 @@ __all__ = [
|
||||||
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
|
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
|
||||||
assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets'))
|
assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets'))
|
||||||
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
|
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
|
||||||
|
executed_amount = serializers.IntegerField(read_only=True, label=_('Executed amount'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'date_created', 'date_updated', 'created_by',
|
'date_created', 'date_updated', 'created_by',
|
||||||
'periodic_display', 'executed_amount', 'type', 'last_execution_date'
|
'periodic_display', 'executed_amount', 'type',
|
||||||
|
'last_execution_date',
|
||||||
]
|
]
|
||||||
mini_fields = [
|
mini_fields = [
|
||||||
'id', 'name', 'type', 'is_periodic', 'interval',
|
'id', 'name', 'type', 'is_periodic', 'interval',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import timedelta
|
from datetime import timedelta, datetime
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -40,7 +40,7 @@ __all__ = [
|
||||||
|
|
||||||
class JobLog(JobExecution):
|
class JobLog(JobExecution):
|
||||||
@property
|
@property
|
||||||
def creator_name(self):
|
def creator_name(self) -> str:
|
||||||
return self.creator.name
|
return self.creator.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -232,7 +232,7 @@ class UserLoginLog(models.Model):
|
||||||
return '%s(%s)' % (self.username, self.city)
|
return '%s(%s)' % (self.username, self.city)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def backend_display(self):
|
def backend_display(self) -> str:
|
||||||
return gettext(self.backend)
|
return gettext(self.backend)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -258,7 +258,7 @@ class UserLoginLog(models.Model):
|
||||||
return login_logs
|
return login_logs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reason_display(self):
|
def reason_display(self) -> str:
|
||||||
from authentication.errors import reason_choices, old_reason_choices
|
from authentication.errors import reason_choices, old_reason_choices
|
||||||
|
|
||||||
reason = reason_choices.get(self.reason)
|
reason = reason_choices.get(self.reason)
|
||||||
|
|
@ -300,15 +300,15 @@ class UserSession(models.Model):
|
||||||
return '%s(%s)' % (self.user, self.ip)
|
return '%s(%s)' % (self.user, self.ip)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def backend_display(self):
|
def backend_display(self) -> str:
|
||||||
return gettext(self.backend)
|
return gettext(self.backend)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_active(self):
|
def is_active(self) -> bool:
|
||||||
return user_session_manager.check_active(self.key)
|
return user_session_manager.check_active(self.key)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_expired(self):
|
def date_expired(self) -> datetime:
|
||||||
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
session_store = session_store_cls(session_key=self.key)
|
session_store = session_store_cls(session_key=self.key)
|
||||||
cache_key = session_store.cache_key
|
cache_key = session_store.cache_key
|
||||||
|
|
|
||||||
|
|
@ -119,11 +119,11 @@ class OperateLogSerializer(BulkOrgResourceModelSerializer):
|
||||||
fields = fields_small
|
fields = fields_small
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_resource_type(instance):
|
def get_resource_type(instance) -> str:
|
||||||
return _(instance.resource_type)
|
return _(instance.resource_type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_resource(instance):
|
def get_resource(instance) -> str:
|
||||||
return i18n_trans(instance.resource)
|
return i18n_trans(instance.resource)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -147,11 +147,11 @@ class ActivityUnionLogSerializer(serializers.Serializer):
|
||||||
r_type = serializers.CharField(read_only=True)
|
r_type = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_timestamp(obj):
|
def get_timestamp(obj) -> str:
|
||||||
return as_current_tz(obj['datetime']).strftime('%Y-%m-%d %H:%M:%S')
|
return as_current_tz(obj['datetime']).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_content(obj):
|
def get_content(obj) -> str:
|
||||||
if not obj['r_detail']:
|
if not obj['r_detail']:
|
||||||
action = obj['r_action'].replace('_', ' ').capitalize()
|
action = obj['r_action'].replace('_', ' ').capitalize()
|
||||||
ctn = _('%s %s this resource') % (obj['r_user'], _(action).lower())
|
ctn = _('%s %s this resource') % (obj['r_user'], _(action).lower())
|
||||||
|
|
@ -160,7 +160,7 @@ class ActivityUnionLogSerializer(serializers.Serializer):
|
||||||
return ctn
|
return ctn
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_detail_url(obj):
|
def get_detail_url(obj) -> str:
|
||||||
detail_url = ''
|
detail_url = ''
|
||||||
detail_id, obj_type = obj['r_detail_id'], obj['r_type']
|
detail_id, obj_type = obj['r_detail_id'], obj['r_type']
|
||||||
if not detail_id:
|
if not detail_id:
|
||||||
|
|
@ -210,7 +210,7 @@ class UserSessionSerializer(serializers.ModelSerializer):
|
||||||
"backend_display": {"label": _("Auth backend display")},
|
"backend_display": {"label": _("Auth backend display")},
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_is_current_user_session(self, obj):
|
def get_is_current_user_session(self, obj) -> bool:
|
||||||
request = self.context.get('request')
|
request = self.context.get('request')
|
||||||
if not request:
|
if not request:
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ def send_login_info_to_reviewers(instance: UserLoginLog | str, auth_acl_id):
|
||||||
|
|
||||||
reviewers = acl.reviewers.all()
|
reviewers = acl.reviewers.all()
|
||||||
for reviewer in reviewers:
|
for reviewer in reviewers:
|
||||||
UserLoginReminderMsg(reviewer, instance).publish_async()
|
UserLoginReminderMsg(reviewer, instance, acl).publish_async()
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_auth_success)
|
@receiver(post_auth_success)
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ class RDPFileClientProtocolURLMixin:
|
||||||
'autoreconnection enabled:i': '1',
|
'autoreconnection enabled:i': '1',
|
||||||
'bookmarktype:i': '3',
|
'bookmarktype:i': '3',
|
||||||
'use redirection server name:i': '0',
|
'use redirection server name:i': '0',
|
||||||
|
'bitmapcachepersistenable:i': '0',
|
||||||
|
'bitmapcachesize:i': '1500',
|
||||||
}
|
}
|
||||||
|
|
||||||
# copy from
|
# copy from
|
||||||
|
|
@ -76,7 +78,6 @@ class RDPFileClientProtocolURLMixin:
|
||||||
rdp_low_speed_broadband_option = {
|
rdp_low_speed_broadband_option = {
|
||||||
"connection type:i": 2,
|
"connection type:i": 2,
|
||||||
"disable wallpaper:i": 1,
|
"disable wallpaper:i": 1,
|
||||||
"bitmapcachepersistenable:i": 1,
|
|
||||||
"disable full window drag:i": 1,
|
"disable full window drag:i": 1,
|
||||||
"disable menu anims:i": 1,
|
"disable menu anims:i": 1,
|
||||||
"allow font smoothing:i": 0,
|
"allow font smoothing:i": 0,
|
||||||
|
|
@ -87,7 +88,6 @@ class RDPFileClientProtocolURLMixin:
|
||||||
rdp_high_speed_broadband_option = {
|
rdp_high_speed_broadband_option = {
|
||||||
"connection type:i": 4,
|
"connection type:i": 4,
|
||||||
"disable wallpaper:i": 0,
|
"disable wallpaper:i": 0,
|
||||||
"bitmapcachepersistenable:i": 1,
|
|
||||||
"disable full window drag:i": 1,
|
"disable full window drag:i": 1,
|
||||||
"disable menu anims:i": 0,
|
"disable menu anims:i": 0,
|
||||||
"allow font smoothing:i": 0,
|
"allow font smoothing:i": 0,
|
||||||
|
|
@ -375,7 +375,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
|
||||||
for name in default_name_opts.keys():
|
for name in default_name_opts.keys():
|
||||||
value = preferences.get(name, default_name_opts[name])
|
value = preferences.get(name, default_name_opts[name])
|
||||||
connect_options[name] = value
|
connect_options[name] = value
|
||||||
connect_options['lang'] = getattr(user, 'lang', settings.LANGUAGE_CODE)
|
connect_options['lang'] = getattr(user, 'lang') or settings.LANGUAGE_CODE
|
||||||
data['connect_options'] = connect_options
|
data['connect_options'] = connect_options
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -431,7 +431,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
|
||||||
if account.username != AliasAccount.INPUT:
|
if account.username != AliasAccount.INPUT:
|
||||||
data['input_username'] = ''
|
data['input_username'] = ''
|
||||||
|
|
||||||
ticket = self._validate_acl(user, asset, account, connect_method)
|
ticket = self._validate_acl(user, asset, account, connect_method, protocol)
|
||||||
if ticket:
|
if ticket:
|
||||||
data['from_ticket'] = ticket
|
data['from_ticket'] = ticket
|
||||||
|
|
||||||
|
|
@ -470,7 +470,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
|
||||||
after=after, object_name=object_name
|
after=after, object_name=object_name
|
||||||
)
|
)
|
||||||
|
|
||||||
def _validate_acl(self, user, asset, account, connect_method):
|
def _validate_acl(self, user, asset, account, connect_method, protocol):
|
||||||
from acls.models import LoginAssetACL
|
from acls.models import LoginAssetACL
|
||||||
kwargs = {'user': user, 'asset': asset, 'account': account}
|
kwargs = {'user': user, 'asset': asset, 'account': account}
|
||||||
if account.username == AliasAccount.INPUT:
|
if account.username == AliasAccount.INPUT:
|
||||||
|
|
@ -523,9 +523,15 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
|
||||||
return
|
return
|
||||||
|
|
||||||
self._record_operate_log(acl, asset)
|
self._record_operate_log(acl, asset)
|
||||||
|
os = get_request_os(self.request) if self.request else 'windows'
|
||||||
|
method = ConnectMethodUtil.get_connect_method(
|
||||||
|
connect_method, protocol=protocol, os=os
|
||||||
|
)
|
||||||
|
login_from = method['label'] if method else connect_method
|
||||||
for reviewer in reviewers:
|
for reviewer in reviewers:
|
||||||
AssetLoginReminderMsg(
|
AssetLoginReminderMsg(
|
||||||
reviewer, asset, user, account, self.input_username
|
reviewer, asset, user, account, acl,
|
||||||
|
ip, self.input_username, login_from
|
||||||
).publish_async()
|
).publish_async()
|
||||||
|
|
||||||
def create_face_verify(self, response):
|
def create_face_verify(self, response):
|
||||||
|
|
@ -618,6 +624,8 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||||
|
|
||||||
token_id = request.data.get('id') or ''
|
token_id = request.data.get('id') or ''
|
||||||
token = ConnectionToken.get_typed_connection_token(token_id)
|
token = ConnectionToken.get_typed_connection_token(token_id)
|
||||||
|
if not token:
|
||||||
|
raise PermissionDenied('Token {} is not valid'.format(token))
|
||||||
token.is_valid()
|
token.is_valid()
|
||||||
serializer = self.get_serializer(instance=token)
|
serializer = self.get_serializer(instance=token)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,9 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
|
||||||
|
|
||||||
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
|
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
|
||||||
subject = '%s: %s' % (get_login_title(), _('Forgot password'))
|
subject = '%s: %s' % (get_login_title(), _('Forgot password'))
|
||||||
|
tip = _('The validity period of the verification code is {} minute').format(settings.VERIFY_CODE_TTL // 60)
|
||||||
context = {
|
context = {
|
||||||
'user': user, 'title': subject, 'code': code,
|
'user': user, 'title': subject, 'code': code, 'tip': tip,
|
||||||
}
|
}
|
||||||
message = render_to_string('authentication/_msg_reset_password_code.html', context)
|
message = render_to_string('authentication/_msg_reset_password_code.html', context)
|
||||||
content = {'subject': subject, 'message': message}
|
content = {'subject': subject, 'message': message}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,10 @@ class JMSBaseAuthBackend:
|
||||||
"""
|
"""
|
||||||
# 三方用户认证完成后,在后续的 get_user 获取逻辑中,也应该需要检查用户是否有效
|
# 三方用户认证完成后,在后续的 get_user 获取逻辑中,也应该需要检查用户是否有效
|
||||||
is_valid = getattr(user, 'is_valid', None)
|
is_valid = getattr(user, 'is_valid', None)
|
||||||
return is_valid or is_valid is None
|
if not is_valid:
|
||||||
|
logger.info("User %s is not valid", getattr(user, "username", "<unknown>"))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
# allow user to authenticate
|
# allow user to authenticate
|
||||||
def username_allow_authenticate(self, username):
|
def username_allow_authenticate(self, username):
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
||||||
# example implementation:
|
# example implementation:
|
||||||
try:
|
try:
|
||||||
key = AccessKey.objects.get(id=key_id)
|
key = AccessKey.objects.get(id=key_id)
|
||||||
if not key.is_active:
|
if not key.is_valid:
|
||||||
return None, None
|
return None, None
|
||||||
user, secret = key.user, str(key.secret)
|
user, secret = key.user, str(key.secret)
|
||||||
after_authenticate_update_date(user, key)
|
after_authenticate_update_date(user, key)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ class TempTokenAuthBackend(JMSBaseAuthBackend):
|
||||||
return settings.AUTH_TEMP_TOKEN
|
return settings.AUTH_TEMP_TOKEN
|
||||||
|
|
||||||
def authenticate(self, request, username='', password=''):
|
def authenticate(self, request, username='', password=''):
|
||||||
token = self.model.objects.filter(username=username, secret=password).first()
|
tokens = self.model.objects.filter(username=username).order_by('-date_created')[:500]
|
||||||
|
token = next((t for t in tokens if t.secret == password), None)
|
||||||
|
|
||||||
if not token:
|
if not token:
|
||||||
return None
|
return None
|
||||||
if not token.is_valid:
|
if not token.is_valid:
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class BaseMFA(abc.ABC):
|
||||||
if not ok:
|
if not ok:
|
||||||
return False, msg
|
return False, msg
|
||||||
|
|
||||||
cache.set(cache_key, code, 60)
|
cache.set(cache_key, code, settings.VERIFY_CODE_TTL)
|
||||||
return True, msg
|
return True, msg
|
||||||
|
|
||||||
def is_authenticated(self):
|
def is_authenticated(self):
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,14 @@ class MFAEmail(BaseMFA):
|
||||||
def send_challenge(self):
|
def send_challenge(self):
|
||||||
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
|
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
|
||||||
subject = '%s: %s' % (get_login_title(), _('MFA code'))
|
subject = '%s: %s' % (get_login_title(), _('MFA code'))
|
||||||
|
tip = _('The validity period of the verification code is {} minute').format(settings.VERIFY_CODE_TTL // 60)
|
||||||
context = {
|
context = {
|
||||||
'user': self.user, 'title': subject, 'code': code,
|
'user': self.user, 'title': subject, 'code': code, 'tip': tip,
|
||||||
}
|
}
|
||||||
message = render_to_string('authentication/_msg_mfa_email_code.html', context)
|
message = render_to_string('authentication/_msg_mfa_email_code.html', context)
|
||||||
content = {'subject': subject, 'message': message}
|
content = {'subject': subject, 'message': message}
|
||||||
sender_util = SendAndVerifyCodeUtil(
|
sender_util = SendAndVerifyCodeUtil(
|
||||||
self.user.email, code=code, backend=self.name, timeout=60, **content
|
self.user.email, code=code, backend=self.name, **content
|
||||||
)
|
)
|
||||||
sender_util.gen_and_send_async()
|
sender_util.gen_and_send_async()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ def save_access_key_secrets(apps, schema_editor):
|
||||||
ak.secret = old_value
|
ak.secret = old_value
|
||||||
ak.save(update_fields=["secret"])
|
ak.save(update_fields=["secret"])
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ class AccessKey(models.Model):
|
||||||
date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used'))
|
date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used'))
|
||||||
date_created = models.DateTimeField(auto_now_add=True)
|
date_created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_valid(self):
|
||||||
|
return self.is_active and self.user.is_valid
|
||||||
|
|
||||||
def get_id(self):
|
def get_id(self):
|
||||||
return str(self.id)
|
return str(self.id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from datetime import timedelta
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
@ -76,7 +77,10 @@ class ConnectionToken(JMSOrgBaseModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_typed_connection_token(cls, token_id):
|
def get_typed_connection_token(cls, token_id):
|
||||||
token = get_object_or_404(cls, id=token_id)
|
try:
|
||||||
|
token = get_object_or_404(cls, id=token_id)
|
||||||
|
except ValidationError:
|
||||||
|
return None
|
||||||
|
|
||||||
if token.type == ConnectionTokenType.ADMIN.value:
|
if token.type == ConnectionTokenType.ADMIN.value:
|
||||||
token = AdminConnectionToken.objects.get(id=token_id)
|
token = AdminConnectionToken.objects.get(id=token_id)
|
||||||
|
|
@ -85,11 +89,11 @@ class ConnectionToken(JMSOrgBaseModel):
|
||||||
return token
|
return token
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self):
|
def is_expired(self) -> bool:
|
||||||
return self.date_expired < timezone.now()
|
return self.date_expired < timezone.now()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expire_time(self):
|
def expire_time(self) -> int:
|
||||||
interval = self.date_expired - timezone.now()
|
interval = self.date_expired - timezone.now()
|
||||||
seconds = interval.total_seconds()
|
seconds = interval.total_seconds()
|
||||||
if seconds < 0:
|
if seconds < 0:
|
||||||
|
|
@ -161,7 +165,7 @@ class ConnectionToken(JMSOrgBaseModel):
|
||||||
def expire_at(self):
|
def expire_at(self):
|
||||||
return self.permed_account.date_expired.timestamp()
|
return self.permed_account.date_expired.timestamp()
|
||||||
|
|
||||||
def is_valid(self):
|
def is_valid(self) -> bool:
|
||||||
if not self.is_active:
|
if not self.is_active:
|
||||||
error = _('Connection token inactive')
|
error = _('Connection token inactive')
|
||||||
raise PermissionDenied(error)
|
raise PermissionDenied(error)
|
||||||
|
|
@ -334,6 +338,18 @@ class ConnectionToken(JMSOrgBaseModel):
|
||||||
acls = CommandFilterACL.filter_queryset(**kwargs).valid()
|
acls = CommandFilterACL.filter_queryset(**kwargs).valid()
|
||||||
return acls
|
return acls
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def data_masking_rules(self):
|
||||||
|
from acls.models import DataMaskingRule
|
||||||
|
kwargs = {
|
||||||
|
'user': self.user,
|
||||||
|
'asset': self.asset,
|
||||||
|
'account': self.account_object,
|
||||||
|
}
|
||||||
|
with tmp_to_org(self.asset.org_id):
|
||||||
|
rules = DataMaskingRule.filter_queryset(**kwargs).valid()
|
||||||
|
return rules
|
||||||
|
|
||||||
|
|
||||||
class SuperConnectionToken(ConnectionToken):
|
class SuperConnectionToken(ConnectionToken):
|
||||||
_type = ConnectionTokenType.SUPER
|
_type = ConnectionTokenType.SUPER
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,24 @@
|
||||||
from django.template.loader import render_to_string
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.utils.timezone import local_now_display
|
from common.utils.timezone import local_now_display
|
||||||
|
from common.views.template import custom_render_to_string
|
||||||
from notifications.notifications import UserMessage
|
from notifications.notifications import UserMessage
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class DifferentCityLoginMessage(UserMessage):
|
class DifferentCityLoginMessage(UserMessage):
|
||||||
|
subject = _('Different city login reminder')
|
||||||
|
template_name = 'authentication/_msg_different_city.html'
|
||||||
|
contexts = [
|
||||||
|
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
|
||||||
|
{"name": "username", "label": _('User'), "default": "john"},
|
||||||
|
{"name": "name", "label": _('Name'), "default": "John"},
|
||||||
|
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
|
||||||
|
{"name": "time", "label": _('Login Date'), "default": "2025-01-01 12:00:00"},
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, user, ip, city):
|
def __init__(self, user, ip, city):
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.city = city
|
self.city = city
|
||||||
|
|
@ -16,18 +26,16 @@ class DifferentCityLoginMessage(UserMessage):
|
||||||
|
|
||||||
def get_html_msg(self) -> dict:
|
def get_html_msg(self) -> dict:
|
||||||
now = local_now_display()
|
now = local_now_display()
|
||||||
subject = _('Different city login reminder')
|
|
||||||
context = dict(
|
context = dict(
|
||||||
subject=subject,
|
|
||||||
name=self.user.name,
|
name=self.user.name,
|
||||||
username=self.user.username,
|
username=self.user.username,
|
||||||
ip=self.ip,
|
ip=self.ip,
|
||||||
time=now,
|
time=now,
|
||||||
city=self.city,
|
city=self.city,
|
||||||
)
|
)
|
||||||
message = render_to_string('authentication/_msg_different_city.html', context)
|
message = custom_render_to_string(self.template_name, context)
|
||||||
return {
|
return {
|
||||||
'subject': subject,
|
'subject': str(self.subject),
|
||||||
'message': message
|
'message': message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,6 +49,16 @@ class DifferentCityLoginMessage(UserMessage):
|
||||||
|
|
||||||
|
|
||||||
class OAuthBindMessage(UserMessage):
|
class OAuthBindMessage(UserMessage):
|
||||||
|
subject = _('OAuth binding reminder')
|
||||||
|
template_name = 'authentication/_msg_oauth_bind.html'
|
||||||
|
contexts = [
|
||||||
|
{"name": "username", "label": _('User'), "default": "john"},
|
||||||
|
{"name": "name", "label": _('Name'), "default": "John"},
|
||||||
|
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
|
||||||
|
{"name": "oauth_name", "label": _('OAuth name'), "default": "WeCom"},
|
||||||
|
{"name": "oauth_id", "label": _('OAuth ID'), "default": "000001"},
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, user, ip, oauth_name, oauth_id):
|
def __init__(self, user, ip, oauth_name, oauth_id):
|
||||||
super().__init__(user)
|
super().__init__(user)
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
|
@ -51,7 +69,6 @@ class OAuthBindMessage(UserMessage):
|
||||||
now = local_now_display()
|
now = local_now_display()
|
||||||
subject = self.oauth_name + ' ' + _('binding reminder')
|
subject = self.oauth_name + ' ' + _('binding reminder')
|
||||||
context = dict(
|
context = dict(
|
||||||
subject=subject,
|
|
||||||
name=self.user.name,
|
name=self.user.name,
|
||||||
username=self.user.username,
|
username=self.user.username,
|
||||||
ip=self.ip,
|
ip=self.ip,
|
||||||
|
|
@ -59,9 +76,9 @@ class OAuthBindMessage(UserMessage):
|
||||||
oauth_name=self.oauth_name,
|
oauth_name=self.oauth_name,
|
||||||
oauth_id=self.oauth_id
|
oauth_id=self.oauth_id
|
||||||
)
|
)
|
||||||
message = render_to_string('authentication/_msg_oauth_bind.html', context)
|
message = custom_render_to_string(self.template_name, context)
|
||||||
return {
|
return {
|
||||||
'subject': subject,
|
'subject': str(subject),
|
||||||
'message': message
|
'message': message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,10 @@ class UserConfirmation(permissions.BasePermission):
|
||||||
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
confirm_level = request.session.get('CONFIRM_LEVEL')
|
session = getattr(request, 'session', {})
|
||||||
confirm_type = request.session.get('CONFIRM_TYPE')
|
confirm_level = session.get('CONFIRM_LEVEL')
|
||||||
confirm_time = request.session.get('CONFIRM_TIME')
|
confirm_type = session.get('CONFIRM_TYPE')
|
||||||
|
confirm_time = session.get('CONFIRM_TIME')
|
||||||
|
|
||||||
ttl = self.get_ttl(confirm_type)
|
ttl = self.get_ttl(confirm_type)
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import SecretType
|
from accounts.const import SecretType
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
from acls.models import CommandGroup, CommandFilterACL
|
from acls.models import CommandGroup, CommandFilterACL, DataMaskingRule
|
||||||
from assets.models import Asset, Platform, Gateway, Zone
|
from assets.models import Asset, Platform, Gateway, Zone
|
||||||
from assets.serializers.asset import AssetProtocolsSerializer
|
from assets.serializers.asset import AssetProtocolsSerializer
|
||||||
from assets.serializers.platform import PlatformSerializer
|
from assets.serializers.platform import PlatformSerializer
|
||||||
|
|
@ -60,7 +60,7 @@ class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_su_from(account):
|
def get_su_from(account) -> dict:
|
||||||
if not hasattr(account, 'asset'):
|
if not hasattr(account, 'asset'):
|
||||||
return {}
|
return {}
|
||||||
su_enabled = account.asset.platform.su_enabled
|
su_enabled = account.asset.platform.su_enabled
|
||||||
|
|
@ -83,6 +83,14 @@ class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _ConnectionTokenDataMaskingRuleSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = DataMaskingRule
|
||||||
|
fields = ['id', 'name', 'fields_pattern',
|
||||||
|
'masking_method', 'mask_pattern',
|
||||||
|
'is_active', 'priority']
|
||||||
|
|
||||||
|
|
||||||
class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
|
class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
|
||||||
command_groups = ObjectRelatedField(
|
command_groups = ObjectRelatedField(
|
||||||
many=True, required=False, queryset=CommandGroup.objects,
|
many=True, required=False, queryset=CommandGroup.objects,
|
||||||
|
|
@ -105,7 +113,7 @@ class _ConnectionTokenPlatformSerializer(PlatformSerializer):
|
||||||
class Meta(PlatformSerializer.Meta):
|
class Meta(PlatformSerializer.Meta):
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = [field for field in PlatformSerializer.Meta.fields
|
fields = [field for field in PlatformSerializer.Meta.fields
|
||||||
if field not in PlatformSerializer.Meta.fields_m2m]
|
if field not in PlatformSerializer.Meta.fields_m2m]
|
||||||
|
|
||||||
def get_field_names(self, declared_fields, info):
|
def get_field_names(self, declared_fields, info):
|
||||||
names = super().get_field_names(declared_fields, info)
|
names = super().get_field_names(declared_fields, info)
|
||||||
|
|
@ -139,6 +147,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
||||||
platform = _ConnectionTokenPlatformSerializer(read_only=True)
|
platform = _ConnectionTokenPlatformSerializer(read_only=True)
|
||||||
zone = ObjectRelatedField(queryset=Zone.objects, required=False, label=_('Domain'))
|
zone = ObjectRelatedField(queryset=Zone.objects, required=False, label=_('Domain'))
|
||||||
command_filter_acls = _ConnectionTokenCommandFilterACLSerializer(read_only=True, many=True)
|
command_filter_acls = _ConnectionTokenCommandFilterACLSerializer(read_only=True, many=True)
|
||||||
|
data_masking_rules = _ConnectionTokenDataMaskingRuleSerializer(read_only=True, many=True)
|
||||||
expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True)
|
expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True)
|
||||||
connect_method = _ConnectTokenConnectMethodSerializer(read_only=True, source='connect_method_object')
|
connect_method = _ConnectTokenConnectMethodSerializer(read_only=True, source='connect_method_object')
|
||||||
connect_options = serializers.JSONField(read_only=True)
|
connect_options = serializers.JSONField(read_only=True)
|
||||||
|
|
@ -149,7 +158,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
||||||
model = ConnectionToken
|
model = ConnectionToken
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'value', 'user', 'asset', 'account',
|
'id', 'value', 'user', 'asset', 'account',
|
||||||
'platform', 'command_filter_acls', 'protocol',
|
'platform', 'command_filter_acls', 'data_masking_rules', 'protocol',
|
||||||
'zone', 'gateway', 'actions', 'expire_at',
|
'zone', 'gateway', 'actions', 'expire_at',
|
||||||
'from_ticket', 'expire_now', 'connect_method',
|
'from_ticket', 'expire_now', 'connect_method',
|
||||||
'connect_options', 'face_monitor_token'
|
'connect_options', 'face_monitor_token'
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from common.serializers import CommonModelSerializer
|
||||||
from common.serializers.fields import EncryptedField
|
from common.serializers.fields import EncryptedField
|
||||||
from perms.serializers.permission import ActionChoicesField
|
from perms.serializers.permission import ActionChoicesField
|
||||||
from ..models import ConnectionToken, AdminConnectionToken
|
from ..models import ConnectionToken, AdminConnectionToken
|
||||||
|
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ConnectionTokenSerializer', 'SuperConnectionTokenSerializer',
|
'ConnectionTokenSerializer', 'SuperConnectionTokenSerializer',
|
||||||
|
|
@ -13,7 +14,7 @@ __all__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTokenSerializer(CommonModelSerializer):
|
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||||
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
|
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
|
||||||
input_secret = EncryptedField(
|
input_secret = EncryptedField(
|
||||||
label=_("Input secret"), max_length=40960, required=False, allow_blank=True
|
label=_("Input secret"), max_length=40960, required=False, allow_blank=True
|
||||||
|
|
@ -60,7 +61,7 @@ class ConnectionTokenSerializer(CommonModelSerializer):
|
||||||
validated_data['remote_addr'] = get_request_ip(request)
|
validated_data['remote_addr'] = get_request_ip(request)
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
def get_from_ticket_info(self, instance):
|
def get_from_ticket_info(self, instance) -> dict:
|
||||||
if not instance.from_ticket:
|
if not instance.from_ticket:
|
||||||
return {}
|
return {}
|
||||||
user = self.get_request_user()
|
user = self.get_request_user()
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class BearerTokenSerializer(serializers.Serializer):
|
||||||
user = UserProfileSerializer(read_only=True)
|
user = UserProfileSerializer(read_only=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_keyword(obj):
|
def get_keyword(obj) -> str:
|
||||||
return 'Bearer'
|
return 'Bearer'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<td style="height: 50px;">{% trans 'MFA code' %}: <span style="font-weight: bold;">{{ code }}</span></td>
|
<td style="height: 50px;">{% trans 'MFA code' %}: <span style="font-weight: bold;">{{ code }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="border: 1px solid #eee">
|
<tr style="border: 1px solid #eee">
|
||||||
<td style="height: 30px;">{% trans 'The validity period of the verification code is one minute' %}</td>
|
<td style="height: 30px;">{{ tip }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@
|
||||||
<b>{% trans 'Time' %}:</b> {{ time }}<br>
|
<b>{% trans 'Time' %}:</b> {{ time }}<br>
|
||||||
<b>{% trans 'IP' %}:</b> {{ ip }}
|
<b>{% trans 'IP' %}:</b> {{ ip }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
-
|
|
||||||
<p>
|
<p>
|
||||||
{% trans 'If the operation is not your own, unbind and change the password.' %}
|
{% trans 'If the operation is not your own, unbind and change the password.' %}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@
|
||||||
{% trans 'Please click the link below to reset your password, if not your request, concern your account security' %}
|
{% trans 'Please click the link below to reset your password, if not your request, concern your account security' %}
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<a href="{{ rest_password_url }}?token={{ rest_password_token}}" class='showLink' target="_blank">
|
<a href="{{ rest_password_url }}?token={{ rest_password_token }}" class='showLink' target="_blank">
|
||||||
{% trans 'Click here reset password' %}
|
{% trans 'Click here reset password' %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<br>
|
<br>
|
||||||
<p>
|
<p>
|
||||||
{% trans 'This link is valid for 1 hour. After it expires' %}
|
{% trans 'This link is valid for 1 hour. After it expires' %}
|
||||||
<a href="{{ forget_password_url }}?email={{ user.email }}">{% trans 'request new one' %}</a>
|
<a href="{{ forget_password_url }}?email={{ email }}">{% trans 'request new one' %}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<td style="height: 30px;"> {% trans 'Copy the verification code to the Reset Password page to reset the password.' %} </td>
|
<td style="height: 30px;"> {% trans 'Copy the verification code to the Reset Password page to reset the password.' %} </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="border: 1px solid #eee">
|
<tr style="border: 1px solid #eee">
|
||||||
<td style="height: 30px;">{% trans 'The validity period of the verification code is one minute' %}</td>
|
<td style="height: 30px;">{{ tip }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse, urlsplit, urlunsplit, urlencode
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||||
|
|
@ -155,9 +155,18 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
|
||||||
auth_name, redirect_url = auth_method['name'], auth_method['url']
|
auth_name, redirect_url = auth_method['name'], auth_method['url']
|
||||||
next_url = request.GET.get('next') or '/'
|
next_url = request.GET.get('next') or '/'
|
||||||
next_url = safe_next_url(next_url, request=request)
|
next_url = safe_next_url(next_url, request=request)
|
||||||
query_string = request.GET.urlencode()
|
|
||||||
redirect_url = '{}?next={}&{}'.format(redirect_url, next_url, query_string)
|
|
||||||
|
|
||||||
|
merged_qs_items = dict(request.GET.lists())
|
||||||
|
merged_qs_items.pop('next', None)
|
||||||
|
|
||||||
|
merged = {}
|
||||||
|
for k, v_list in merged_qs_items.items():
|
||||||
|
merged[k] = v_list if len(v_list) > 1 else (v_list[0] if v_list else '')
|
||||||
|
|
||||||
|
merged['next'] = next_url
|
||||||
|
query = urlencode(merged, doseq=True)
|
||||||
|
u = urlsplit(redirect_url)
|
||||||
|
redirect_url = urlunsplit((u.scheme, u.netloc, u.path, query, u.fragment))
|
||||||
if settings.LOGIN_REDIRECT_MSG_ENABLED:
|
if settings.LOGIN_REDIRECT_MSG_ENABLED:
|
||||||
message_data = {
|
message_data = {
|
||||||
'title': _('Redirecting'),
|
'title': _('Redirecting'),
|
||||||
|
|
@ -165,7 +174,7 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
|
||||||
'redirect_url': redirect_url,
|
'redirect_url': redirect_url,
|
||||||
'interval': 3,
|
'interval': 3,
|
||||||
'has_cancel': True,
|
'has_cancel': True,
|
||||||
'cancel_url': reverse('authentication:login') + f'?admin=1&{query_string}'
|
'cancel_url': reverse('authentication:login') + f'?admin=1&{query}'
|
||||||
}
|
}
|
||||||
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
||||||
return redirect_url
|
return redirect_url
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from contextlib import nullcontext
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.signals import m2m_changed
|
from django.db.models.signals import m2m_changed
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
@ -16,6 +17,7 @@ from common.drf.filters import (
|
||||||
IDNotFilterBackend, NotOrRelFilterBackend, LabelFilterBackend
|
IDNotFilterBackend, NotOrRelFilterBackend, LabelFilterBackend
|
||||||
)
|
)
|
||||||
from common.utils import get_logger, lazyproperty
|
from common.utils import get_logger, lazyproperty
|
||||||
|
from common.utils import is_uuid
|
||||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||||
from .action import RenderToJsonMixin
|
from .action import RenderToJsonMixin
|
||||||
from .serializer import SerializerMixin
|
from .serializer import SerializerMixin
|
||||||
|
|
@ -95,9 +97,33 @@ class QuerySetMixin:
|
||||||
request: Request
|
request: Request
|
||||||
get_serializer_class: Callable
|
get_serializer_class: Callable
|
||||||
get_queryset: Callable
|
get_queryset: Callable
|
||||||
|
slug_field = 'name'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_object(self):
|
||||||
return super().get_queryset()
|
pk = self.kwargs.get(self.lookup_field)
|
||||||
|
if not pk or is_uuid(pk) or pk.isdigit():
|
||||||
|
return super().get_object()
|
||||||
|
return self.get_queryset().get(**{self.slug_field: pk})
|
||||||
|
|
||||||
|
def limit_queryset_if_no_page(self, queryset):
|
||||||
|
if self.request.query_params.get('format') in ['csv', 'xlsx']:
|
||||||
|
return queryset
|
||||||
|
action = getattr(self, 'action', None)
|
||||||
|
if action != 'list':
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
# 如果分页器有设置 limit,则不限制
|
||||||
|
if self.paginator and self.paginator.get_limit(self.request):
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
# 如果分页器没有设置 limit,则不限制
|
||||||
|
if getattr(self, 'page_no_limit', False):
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
if not settings.DEFAULT_PAGE_SIZE:
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
return queryset[:settings.DEFAULT_PAGE_SIZE]
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
@ -106,6 +132,7 @@ class QuerySetMixin:
|
||||||
if self.action == 'metadata':
|
if self.action == 'metadata':
|
||||||
queryset = queryset.none()
|
queryset = queryset.none()
|
||||||
queryset = self.setup_eager_loading(queryset)
|
queryset = self.setup_eager_loading(queryset)
|
||||||
|
queryset = self.limit_queryset_if_no_page(queryset)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def setup_eager_loading(self, queryset, is_paginated=False):
|
def setup_eager_loading(self, queryset, is_paginated=False):
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ class Language(models.TextChoices):
|
||||||
es = 'es', 'Español'
|
es = 'es', 'Español'
|
||||||
ru = 'ru', 'Русский'
|
ru = 'ru', 'Русский'
|
||||||
ko = 'ko', '한국어'
|
ko = 'ko', '한국어'
|
||||||
|
vi = 'vi', 'Tiếng Việt'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_code_mapper(cls):
|
def get_code_mapper(cls):
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,8 @@ class EncryptMixin:
|
||||||
if value is None:
|
if value is None:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
plain_value = Encryptor(value).decrypt()
|
encryptor = Encryptor(value)
|
||||||
|
plain_value = encryptor.decrypt()
|
||||||
|
|
||||||
# 可能和Json mix,所以要先解密,再json
|
# 可能和Json mix,所以要先解密,再json
|
||||||
sp = super()
|
sp = super()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
import base64
|
||||||
|
|
||||||
from django.db import connections, transaction, connection
|
from django.db import connections, transaction, connection
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
@ -102,6 +103,54 @@ class Encryptor:
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
self.value = force_str(value)
|
self.value = force_str(value)
|
||||||
|
|
||||||
|
def is_encrypted_data(self):
|
||||||
|
"""
|
||||||
|
检测数据是否为加密格式
|
||||||
|
返回 True 表示是加密数据,False 表示是原始数据
|
||||||
|
"""
|
||||||
|
if not self.value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检测 base64 编码格式 (crypto.encrypt 的输出)
|
||||||
|
try:
|
||||||
|
# 尝试不同的 base64 解码方式
|
||||||
|
# 1. 标准 base64
|
||||||
|
try:
|
||||||
|
base64.b64decode(self.value)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2. URL-safe base64
|
||||||
|
try:
|
||||||
|
# 添加必要的填充
|
||||||
|
missing_padding = len(self.value) % 4
|
||||||
|
if missing_padding:
|
||||||
|
padded_value = self.value + '=' * (4 - missing_padding)
|
||||||
|
else:
|
||||||
|
padded_value = self.value
|
||||||
|
base64.urlsafe_b64decode(padded_value)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 检测 AES GCM 格式 (固定72字符metadata)
|
||||||
|
if len(self.value) > 72:
|
||||||
|
try:
|
||||||
|
# 前72字符应该是3个24字符的base64编码
|
||||||
|
metadata = self.value[:72]
|
||||||
|
for i in range(0, 72, 24):
|
||||||
|
part = metadata[i:i+24]
|
||||||
|
base64.b64decode(part)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def decrypt(self):
|
def decrypt(self):
|
||||||
plain_value = crypto.decrypt(self.value)
|
plain_value = crypto.decrypt(self.value)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import ChoiceField, empty
|
from rest_framework.fields import empty
|
||||||
|
|
||||||
from common.db.fields import TreeChoices, JSONManyToManyField as ModelJSONManyToManyField
|
from common.db.fields import TreeChoices, JSONManyToManyField as ModelJSONManyToManyField
|
||||||
from common.utils import decrypt_password, is_uuid
|
from common.utils import decrypt_password, is_uuid
|
||||||
|
|
@ -53,7 +53,7 @@ class EncryptedField(serializers.CharField):
|
||||||
return decrypt_password(value)
|
return decrypt_password(value)
|
||||||
|
|
||||||
|
|
||||||
class LabeledChoiceField(ChoiceField):
|
class LabeledChoiceField(serializers.ChoiceField):
|
||||||
def to_representation(self, key):
|
def to_representation(self, key):
|
||||||
if key is None:
|
if key is None:
|
||||||
return key
|
return key
|
||||||
|
|
@ -180,6 +180,122 @@ class ObjectRelatedField(serializers.RelatedField):
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
self.fail("incorrect_type", data_type=type(pk).__name__)
|
self.fail("incorrect_type", data_type=type(pk).__name__)
|
||||||
|
|
||||||
|
def get_schema(self):
|
||||||
|
"""
|
||||||
|
为 drf-spectacular 提供 OpenAPI schema
|
||||||
|
"""
|
||||||
|
# 获取字段的基本信息
|
||||||
|
field_type = 'array' if self.many else 'object'
|
||||||
|
|
||||||
|
if field_type == 'array':
|
||||||
|
# 如果是多对多关系
|
||||||
|
return {
|
||||||
|
'type': 'array',
|
||||||
|
'items': self._get_openapi_item_schema(),
|
||||||
|
'description': getattr(self, 'help_text', ''),
|
||||||
|
'title': getattr(self, 'label', ''),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 如果是一对一关系
|
||||||
|
return {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': self._get_openapi_properties_schema(),
|
||||||
|
'description': getattr(self, 'help_text', ''),
|
||||||
|
'title': getattr(self, 'label', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_openapi_item_schema(self):
|
||||||
|
"""
|
||||||
|
获取数组项的 OpenAPI schema
|
||||||
|
"""
|
||||||
|
return self._get_openapi_object_schema()
|
||||||
|
|
||||||
|
def _get_openapi_object_schema(self):
|
||||||
|
"""
|
||||||
|
获取对象的 OpenAPI schema
|
||||||
|
"""
|
||||||
|
properties = {}
|
||||||
|
|
||||||
|
# 动态分析 attrs 中的属性类型
|
||||||
|
for attr in self.attrs:
|
||||||
|
# 尝试从 queryset 的 model 中获取字段信息
|
||||||
|
field_type = self._infer_field_type(attr)
|
||||||
|
properties[attr] = {
|
||||||
|
'type': field_type,
|
||||||
|
'description': f'{attr} field'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': properties,
|
||||||
|
'required': ['id'] if 'id' in self.attrs else []
|
||||||
|
}
|
||||||
|
|
||||||
|
def _infer_field_type(self, attr_name):
|
||||||
|
"""
|
||||||
|
智能推断字段类型
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 如果有 queryset,尝试从 model 中获取字段信息
|
||||||
|
if hasattr(self, 'queryset') and self.queryset is not None:
|
||||||
|
model = self.queryset.model
|
||||||
|
if hasattr(model, '_meta') and hasattr(model._meta, 'fields'):
|
||||||
|
field = model._meta.get_field(attr_name)
|
||||||
|
if field:
|
||||||
|
return self._map_django_field_type(field)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 如果没有 queryset 或无法获取字段信息,使用启发式规则
|
||||||
|
return self._heuristic_field_type(attr_name)
|
||||||
|
|
||||||
|
def _map_django_field_type(self, field):
|
||||||
|
"""
|
||||||
|
将 Django 字段类型映射到 OpenAPI 类型
|
||||||
|
"""
|
||||||
|
field_type = type(field).__name__
|
||||||
|
|
||||||
|
# 整数类型
|
||||||
|
if 'Integer' in field_type or 'BigInteger' in field_type or 'SmallInteger' in field_type:
|
||||||
|
return 'integer'
|
||||||
|
# 浮点数类型
|
||||||
|
elif 'Float' in field_type or 'Decimal' in field_type:
|
||||||
|
return 'number'
|
||||||
|
# 布尔类型
|
||||||
|
elif 'Boolean' in field_type:
|
||||||
|
return 'boolean'
|
||||||
|
# 日期时间类型
|
||||||
|
elif 'DateTime' in field_type or 'Date' in field_type or 'Time' in field_type:
|
||||||
|
return 'string'
|
||||||
|
# 文件类型
|
||||||
|
elif 'File' in field_type or 'Image' in field_type:
|
||||||
|
return 'string'
|
||||||
|
# 其他类型默认为字符串
|
||||||
|
else:
|
||||||
|
return 'string'
|
||||||
|
|
||||||
|
def _heuristic_field_type(self, attr_name):
|
||||||
|
"""
|
||||||
|
启发式推断字段类型
|
||||||
|
"""
|
||||||
|
# 基于属性名的启发式规则
|
||||||
|
|
||||||
|
if attr_name in ['is_active', 'enabled', 'visible'] or attr_name.startswith('is_'):
|
||||||
|
return 'boolean'
|
||||||
|
elif attr_name in ['count', 'number', 'size', 'amount']:
|
||||||
|
return 'integer'
|
||||||
|
elif attr_name in ['price', 'rate', 'percentage']:
|
||||||
|
return 'number'
|
||||||
|
else:
|
||||||
|
# 默认返回字符串类型
|
||||||
|
return 'string'
|
||||||
|
|
||||||
|
def _get_openapi_properties_schema(self):
|
||||||
|
"""
|
||||||
|
获取对象属性的 OpenAPI schema
|
||||||
|
"""
|
||||||
|
return self._get_openapi_object_schema()['properties']
|
||||||
|
|
||||||
|
|
||||||
class TreeChoicesField(serializers.MultipleChoiceField):
|
class TreeChoicesField(serializers.MultipleChoiceField):
|
||||||
def __init__(self, choice_cls, **kwargs):
|
def __init__(self, choice_cls, **kwargs):
|
||||||
|
|
@ -238,6 +354,23 @@ class BitChoicesField(TreeChoicesField):
|
||||||
value |= name_value_map[name]
|
value |= name_value_map[name]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def get_schema(self):
|
||||||
|
"""
|
||||||
|
为 drf-spectacular 提供 OpenAPI schema
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'value': {'type': 'string'},
|
||||||
|
'label': {'type': 'string'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'description': getattr(self, 'help_text', ''),
|
||||||
|
'title': getattr(self, 'label', ''),
|
||||||
|
}
|
||||||
|
|
||||||
def run_validation(self, data=empty):
|
def run_validation(self, data=empty):
|
||||||
"""
|
"""
|
||||||
备注:
|
备注:
|
||||||
|
|
|
||||||
|
|
@ -185,3 +185,9 @@ def check_migrations_file_prefix_conflict(*args, **kwargs):
|
||||||
print(f'{msg_left}{msg_right1}\n{msg_right2}\n')
|
print(f'{msg_left}{msg_right1}\n{msg_right2}\n')
|
||||||
|
|
||||||
print('=' * 80)
|
print('=' * 80)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(django_ready)
|
||||||
|
def clear_response_cache(sender, **kwargs):
|
||||||
|
from django.core.cache import cache
|
||||||
|
cache.delete_pattern('views.decorators.cache.cache_page:*')
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from azure.storage.blob import BlobServiceClient
|
|
||||||
|
|
||||||
from .base import ObjectStorage
|
from .base import ObjectStorage
|
||||||
|
|
||||||
|
|
||||||
class AzureStorage(ObjectStorage):
|
class AzureStorage(ObjectStorage):
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
|
from azure.storage.blob import BlobServiceClient
|
||||||
|
|
||||||
self.account_name = config.get("ACCOUNT_NAME", None)
|
self.account_name = config.get("ACCOUNT_NAME", None)
|
||||||
self.account_key = config.get("ACCOUNT_KEY", None)
|
self.account_key = config.get("ACCOUNT_KEY", None)
|
||||||
self.container_name = config.get("CONTAINER_NAME", None)
|
self.container_name = config.get("CONTAINER_NAME", None)
|
||||||
|
|
|
||||||
|
|
@ -97,10 +97,7 @@ def send_mail_attachment_async(subject, message, recipient_list, attachment_list
|
||||||
for attachment in attachment_list:
|
for attachment in attachment_list:
|
||||||
email.attach_file(attachment)
|
email.attach_file(attachment)
|
||||||
os.remove(attachment)
|
os.remove(attachment)
|
||||||
try:
|
return email.send()
|
||||||
return email.send()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Sending mail attachment error: {}".format(e))
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task(
|
@shared_task(
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import socket
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from functools import wraps
|
from functools import wraps, cached_property
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
import html2text
|
import html2text
|
||||||
|
|
@ -246,17 +246,19 @@ def dict_get_any(d, keys):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class lazyproperty:
|
# class lazyproperty:
|
||||||
def __init__(self, func):
|
# def __init__(self, func):
|
||||||
self.func = func
|
# self.func = func
|
||||||
|
|
||||||
def __get__(self, instance, cls):
|
# def __get__(self, instance, cls):
|
||||||
if instance is None:
|
# if instance is None:
|
||||||
return self
|
# return self
|
||||||
else:
|
# else:
|
||||||
value = self.func(instance)
|
# value = self.func(instance)
|
||||||
setattr(instance, self.func.__name__, value)
|
# setattr(instance, self.func.__name__, value)
|
||||||
return value
|
# return value
|
||||||
|
|
||||||
|
lazyproperty = cached_property
|
||||||
|
|
||||||
|
|
||||||
def get_disk_usage(path):
|
def get_disk_usage(path):
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ class Subscription:
|
||||||
self.ch = pb.ch
|
self.ch = pb.ch
|
||||||
self.sub = sub
|
self.sub = sub
|
||||||
self.unsubscribed = False
|
self.unsubscribed = False
|
||||||
logger.info(f"Subscribed to channel: {sub}")
|
|
||||||
|
|
||||||
def _handle_msg(self, _next, error, complete):
|
def _handle_msg(self, _next, error, complete):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue