mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-16 09:02:49 +00:00
Compare commits
36 Commits
pr@dev@ter
...
v4.10.4-lt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71766418bb | ||
|
|
a9399dd709 | ||
|
|
d0cb9e5432 | ||
|
|
558188da90 | ||
|
|
ad5460dab8 | ||
|
|
4d37dca0de | ||
|
|
2ca4002624 | ||
|
|
053d640e4c | ||
|
|
f3acc28ded | ||
|
|
25987545db | ||
|
|
6720ecc6e0 | ||
|
|
0b3a7bb020 | ||
|
|
56373e362b | ||
|
|
02fc045370 | ||
|
|
e4ac73896f | ||
|
|
1518f792d6 | ||
|
|
67277dd622 | ||
|
|
82e7f020ea | ||
|
|
f20b9e01ab | ||
|
|
8cf8a3701b | ||
|
|
7ba24293d1 | ||
|
|
f10114c9ed | ||
|
|
cf31cbfb07 | ||
|
|
0edad24d5d | ||
|
|
1f1c1a9157 | ||
|
|
6c9d271ae1 | ||
|
|
6ff852e225 | ||
|
|
baa75dc735 | ||
|
|
8a9f0436b8 | ||
|
|
a9620a3cbe | ||
|
|
769e7dc8a0 | ||
|
|
2a70449411 | ||
|
|
8df720f19e | ||
|
|
dabbb45f6e | ||
|
|
ce24c1c3fd | ||
|
|
3c54c82ce9 |
@@ -1,4 +1,4 @@
|
||||
FROM jumpserver/core-base:20250827_025554 AS stage-build
|
||||
FROM jumpserver/core-base:20250509_094529 AS stage-build
|
||||
|
||||
ARG VERSION
|
||||
|
||||
@@ -33,7 +33,6 @@ ARG TOOLS=" \
|
||||
default-libmysqlclient-dev \
|
||||
openssh-client \
|
||||
sshpass \
|
||||
nmap \
|
||||
bubblewrap"
|
||||
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
|
||||
@@ -14,8 +14,7 @@ ARG TOOLS=" \
|
||||
telnet \
|
||||
vim \
|
||||
postgresql-client-13 \
|
||||
wget \
|
||||
poppler-utils"
|
||||
wget"
|
||||
|
||||
RUN set -ex \
|
||||
&& apt-get update \
|
||||
@@ -28,5 +27,5 @@ WORKDIR /opt/jumpserver
|
||||
ARG PIP_MIRROR=https://pypi.org/simple
|
||||
|
||||
RUN set -ex \
|
||||
&& uv pip install -i${PIP_MIRROR} --group xpack \
|
||||
&& playwright install chromium --with-deps --only-shell
|
||||
&& uv pip install -i${PIP_MIRROR} --group xpack
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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>
|
||||
|
||||
## An open-source PAM platform (Bastion Host)
|
||||
## An open-source PAM tool (Bastion Host)
|
||||
|
||||
[![][license-shield]][license-link]
|
||||
[![][docs-shield]][docs-link]
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
## What is JumpServer?
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
<picture>
|
||||
|
||||
@@ -41,8 +41,8 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
'partial_update': ['accounts.change_account'],
|
||||
'su_from_accounts': 'accounts.view_account',
|
||||
'clear_secret': 'accounts.change_account',
|
||||
'move_to_assets': 'accounts.delete_account',
|
||||
'copy_to_assets': 'accounts.add_account',
|
||||
'move_to_assets': 'accounts.create_account',
|
||||
'copy_to_assets': 'accounts.create_account',
|
||||
}
|
||||
export_as_zip = True
|
||||
|
||||
@@ -190,7 +190,6 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
|
||||
rbac_perms = {
|
||||
'GET': 'accounts.view_accountsecret',
|
||||
}
|
||||
queryset = Account.history.model.objects.none()
|
||||
|
||||
@lazyproperty
|
||||
def account(self) -> Account:
|
||||
|
||||
@@ -20,7 +20,7 @@ __all__ = ['PamDashboardApi']
|
||||
class PamDashboardApi(APIView):
|
||||
http_method_names = ['get']
|
||||
rbac_perms = {
|
||||
'GET': 'rbac.view_pam',
|
||||
'GET': 'accounts.view_account',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -12,8 +12,6 @@ class VirtualAccountViewSet(OrgBulkModelViewSet):
|
||||
filterset_fields = ('alias',)
|
||||
|
||||
def get_queryset(self):
|
||||
if getattr(self, "swagger_fake_view", False):
|
||||
return VirtualAccount.objects.none()
|
||||
return VirtualAccount.get_or_init_queryset()
|
||||
|
||||
def get_object(self, ):
|
||||
|
||||
@@ -41,7 +41,6 @@ class AutomationAssetsListApi(generics.ListAPIView):
|
||||
|
||||
class AutomationRemoveAssetApi(generics.UpdateAPIView):
|
||||
model = BaseAutomation
|
||||
queryset = BaseAutomation.objects.all()
|
||||
serializer_class = serializers.UpdateAssetSerializer
|
||||
http_method_names = ['patch']
|
||||
|
||||
@@ -60,7 +59,6 @@ class AutomationRemoveAssetApi(generics.UpdateAPIView):
|
||||
|
||||
class AutomationAddAssetApi(generics.UpdateAPIView):
|
||||
model = BaseAutomation
|
||||
queryset = BaseAutomation.objects.all()
|
||||
serializer_class = serializers.UpdateAssetSerializer
|
||||
http_method_names = ['patch']
|
||||
|
||||
|
||||
@@ -154,10 +154,12 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi):
|
||||
model = ChangeSecretAutomation
|
||||
serializer_class = serializers.ChangeSecretUpdateAssetSerializer
|
||||
|
||||
|
||||
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
|
||||
model = ChangeSecretAutomation
|
||||
serializer_class = serializers.ChangeSecretUpdateNodeSerializer
|
||||
|
||||
|
||||
class ChangeSecretStatusViewSet(OrgBulkModelViewSet):
|
||||
perm_model = ChangeSecretAutomation
|
||||
filterset_class = ChangeSecretStatusFilterSet
|
||||
|
||||
@@ -62,8 +62,7 @@ class ChangeSecretDashboardApi(APIView):
|
||||
status_counts = defaultdict(lambda: defaultdict(int))
|
||||
|
||||
for date_finished, status in results:
|
||||
dt_local = timezone.localtime(date_finished)
|
||||
date_str = str(dt_local.date())
|
||||
date_str = str(date_finished.date())
|
||||
if status == ChangeSecretRecordStatusChoice.failed:
|
||||
status_counts[date_str]['failed'] += 1
|
||||
elif status == ChangeSecretRecordStatusChoice.success:
|
||||
@@ -91,10 +90,10 @@ class ChangeSecretDashboardApi(APIView):
|
||||
|
||||
def get_change_secret_asset_queryset(self):
|
||||
qs = self.change_secrets_queryset
|
||||
node_ids = qs.values_list('nodes', flat=True).distinct()
|
||||
nodes = Node.objects.filter(id__in=node_ids).only('id', 'key')
|
||||
node_ids = qs.filter(nodes__isnull=False).values_list('nodes', flat=True).distinct()
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||
direct_asset_ids = qs.values_list('assets', flat=True).distinct()
|
||||
direct_asset_ids = qs.filter(assets__isnull=False).values_list('assets', flat=True).distinct()
|
||||
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
|
||||
return Asset.objects.filter(id__in=asset_ids)
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
|
||||
class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_checkaccountexecution"),
|
||||
("retrieve", "accounts.view_checkaccountexecution"),
|
||||
("retrieve", "accounts.view_checkaccountsexecution"),
|
||||
("create", "accounts.add_checkaccountexecution"),
|
||||
("adhoc", "accounts.add_checkaccountexecution"),
|
||||
("report", "accounts.view_checkaccountexecution"),
|
||||
("report", "accounts.view_checkaccountsexecution"),
|
||||
)
|
||||
ordering = ("-date_created",)
|
||||
tp = AutomationTypes.check_account
|
||||
@@ -150,9 +150,6 @@ class CheckAccountEngineViewSet(JMSModelViewSet):
|
||||
http_method_names = ['get', 'options']
|
||||
|
||||
def get_queryset(self):
|
||||
if getattr(self, "swagger_fake_view", False):
|
||||
return CheckAccountEngine.objects.none()
|
||||
|
||||
return CheckAccountEngine.get_default_engines()
|
||||
|
||||
def filter_queryset(self, queryset: list):
|
||||
|
||||
@@ -63,10 +63,12 @@ class PushAccountRemoveAssetApi(AutomationRemoveAssetApi):
|
||||
model = PushAccountAutomation
|
||||
serializer_class = serializers.PushAccountUpdateAssetSerializer
|
||||
|
||||
|
||||
class PushAccountAddAssetApi(AutomationAddAssetApi):
|
||||
model = PushAccountAutomation
|
||||
serializer_class = serializers.PushAccountUpdateAssetSerializer
|
||||
|
||||
|
||||
class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi):
|
||||
model = PushAccountAutomation
|
||||
serializer_class = serializers.PushAccountUpdateNodeSerializer
|
||||
serializer_class = serializers.PushAccountUpdateNodeSerializer
|
||||
|
||||
@@ -113,18 +113,6 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||
if host.get('error'):
|
||||
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
|
||||
|
||||
if self.secret_strategy == SecretStrategy.custom:
|
||||
new_secret = self.execution.snapshot['secret']
|
||||
if '>' in new_secret or '^' in new_secret:
|
||||
host['error'] = _("Windows password cannot contain special characters like > ^")
|
||||
return host
|
||||
|
||||
host['ssh_params'] = {}
|
||||
|
||||
accounts = self.get_accounts(account)
|
||||
@@ -142,6 +130,11 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||
if asset.type == HostTypes.WINDOWS:
|
||||
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
|
||||
|
||||
inventory_hosts = []
|
||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
||||
print(f'Windows {asset} does not support ssh key push')
|
||||
return inventory_hosts
|
||||
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
h['name'] += '(' + account.username + ')' # To distinguish different accounts
|
||||
|
||||
@@ -53,6 +53,4 @@
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
when: check_conn_after_change
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
when: check_conn_after_change
|
||||
@@ -39,8 +39,7 @@
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
|
||||
append_privs: "{{ db_name != '' | bool }}"
|
||||
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
|
||||
|
||||
@@ -56,5 +56,3 @@
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
when: check_conn_after_change
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -5,14 +5,12 @@
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: |
|
||||
SELECT @@version
|
||||
register: db_info
|
||||
@@ -25,53 +23,45 @@
|
||||
var: info
|
||||
|
||||
- name: Check whether SQLServer User exist
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
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 }}';"
|
||||
when: db_info is succeeded
|
||||
register: user_exist
|
||||
|
||||
- name: Change SQLServer password
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
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"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length != 0
|
||||
|
||||
- name: Add SQLServer user
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
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"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length == 0
|
||||
|
||||
- name: Verify password
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: |
|
||||
SELECT @@version
|
||||
when: check_conn_after_change
|
||||
|
||||
@@ -8,7 +8,7 @@ type:
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
@@ -24,7 +24,3 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
@@ -9,7 +9,7 @@ type:
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
@@ -25,8 +25,3 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ priority: 49
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
@@ -25,8 +25,3 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from accounts.const import (
|
||||
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
|
||||
)
|
||||
from accounts.models import ChangeSecretRecord
|
||||
from accounts.notifications import ChangeSecretExecutionTaskMsg
|
||||
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
|
||||
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
||||
from common.utils import get_logger
|
||||
from common.utils.file import encrypt_and_compress_zip_file
|
||||
@@ -94,6 +94,10 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
|
||||
if not recipients:
|
||||
return
|
||||
|
||||
context = self.get_report_context()
|
||||
for user in recipients:
|
||||
ChangeSecretReportMsg(user, context).publish()
|
||||
|
||||
if not records:
|
||||
return
|
||||
|
||||
|
||||
@@ -240,11 +240,6 @@ class CheckAccountManager(BaseManager):
|
||||
|
||||
print("Check: {} => {}".format(account, msg))
|
||||
if not error:
|
||||
AccountRisk.objects.filter(
|
||||
asset=account.asset,
|
||||
username=account.username,
|
||||
risk=handler.risk
|
||||
).delete()
|
||||
continue
|
||||
self.add_risk(handler.risk, account)
|
||||
self.commit_risks(_assets)
|
||||
|
||||
@@ -5,14 +5,12 @@
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: |
|
||||
SELECT
|
||||
l.name,
|
||||
|
||||
@@ -54,5 +54,3 @@
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
when: check_conn_after_change
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -39,8 +39,7 @@
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
|
||||
append_privs: "{{ db_name != '' | bool }}"
|
||||
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
|
||||
|
||||
@@ -5,14 +5,12 @@
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: |
|
||||
SELECT @@version
|
||||
register: db_info
|
||||
@@ -25,55 +23,47 @@
|
||||
var: info
|
||||
|
||||
- name: Check whether SQLServer User exist
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
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 }}';"
|
||||
when: db_info is succeeded
|
||||
register: user_exist
|
||||
|
||||
- name: Change SQLServer password
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
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"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length != 0
|
||||
register: change_info
|
||||
|
||||
- name: Add SQLServer user
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
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"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length == 0
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: |
|
||||
SELECT @@version
|
||||
when: check_conn_after_change
|
||||
|
||||
@@ -8,7 +8,7 @@ type:
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
@@ -22,8 +22,3 @@ i18n:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
@@ -9,7 +9,7 @@ type:
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
@@ -23,8 +23,3 @@ i18n:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
@@ -9,7 +9,7 @@ priority: 49
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
@@ -23,8 +23,3 @@ i18n:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
@@ -5,13 +5,11 @@
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: "{{ jms_asset.spec_info.db_name }}"
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: "DROP LOGIN {{ account.username }}; select @@version"
|
||||
|
||||
|
||||
@@ -16,5 +16,3 @@
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -5,13 +5,11 @@
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: |
|
||||
SELECT @@version
|
||||
|
||||
@@ -1,5 +1,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
|
||||
|
||||
@@ -11,9 +14,6 @@ __all__ = ['AZUREVaultClient']
|
||||
class AZUREVaultClient(object):
|
||||
|
||||
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/' \
|
||||
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
|
||||
|
||||
@@ -23,8 +23,6 @@ class AZUREVaultClient(object):
|
||||
self.client = SecretClient(vault_url=vault_url, credential=credentials)
|
||||
|
||||
def is_active(self):
|
||||
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
|
||||
|
||||
try:
|
||||
self.client.set_secret('jumpserver', '666')
|
||||
except (ResourceNotFoundError, ClientAuthenticationError) as e:
|
||||
@@ -34,8 +32,6 @@ class AZUREVaultClient(object):
|
||||
return True, ''
|
||||
|
||||
def get(self, name, version=None):
|
||||
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
|
||||
|
||||
try:
|
||||
secret = self.client.get_secret(name, version)
|
||||
return secret.value
|
||||
|
||||
@@ -46,16 +46,11 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Account',
|
||||
'permissions': [
|
||||
('view_accountsecret', 'Can view asset account secret'),
|
||||
('view_historyaccount', 'Can view asset history account'),
|
||||
('view_historyaccountsecret', 'Can view asset history account secret'),
|
||||
('verify_account', 'Can verify account'),
|
||||
('push_account', 'Can push account'),
|
||||
('remove_account', 'Can remove account'),
|
||||
('view_accountsession', 'Can view session'),
|
||||
('view_accountactivity', 'Can view activity')
|
||||
],
|
||||
'permissions': [('view_accountsecret', 'Can view asset account secret'),
|
||||
('view_historyaccount', 'Can view asset history account'),
|
||||
('view_historyaccountsecret', 'Can view asset history account secret'),
|
||||
('verify_account', 'Can verify account'), ('push_account', 'Can push account'),
|
||||
('remove_account', 'Can remove account')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
|
||||
@@ -116,8 +116,6 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
|
||||
('verify_account', _('Can verify account')),
|
||||
('push_account', _('Can push account')),
|
||||
('remove_account', _('Can remove account')),
|
||||
('view_accountsession', _('Can view session')),
|
||||
('view_accountactivity', _('Can view activity')),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
@@ -132,7 +130,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
|
||||
return self.asset.platform
|
||||
|
||||
@lazyproperty
|
||||
def alias(self) -> str:
|
||||
def alias(self):
|
||||
"""
|
||||
别称,因为有虚拟账号,@INPUT @MANUAL @USER, 否则为 id
|
||||
"""
|
||||
@@ -140,13 +138,13 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
|
||||
return self.username
|
||||
return str(self.id)
|
||||
|
||||
def is_virtual(self) -> bool:
|
||||
def is_virtual(self):
|
||||
"""
|
||||
不要用 username 去判断,因为可能是构造的 account 对象,设置了同名账号的用户名,
|
||||
"""
|
||||
return self.alias.startswith('@')
|
||||
|
||||
def is_ds_account(self) -> bool:
|
||||
def is_ds_account(self):
|
||||
if self.is_virtual():
|
||||
return ''
|
||||
if not self.asset.is_directory_service:
|
||||
@@ -160,7 +158,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
|
||||
return self.asset.ds
|
||||
|
||||
@lazyproperty
|
||||
def ds_domain(self) -> str:
|
||||
def ds_domain(self):
|
||||
"""这个不能去掉,perm_account 会动态设置这个值,以更改 full_username"""
|
||||
if self.is_virtual():
|
||||
return ''
|
||||
@@ -172,17 +170,17 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
|
||||
return '@' in self.username or '\\' in self.username
|
||||
|
||||
@property
|
||||
def full_username(self) -> str:
|
||||
def full_username(self):
|
||||
if not self.username_has_domain() and self.ds_domain:
|
||||
return '{}@{}'.format(self.username, self.ds_domain)
|
||||
return self.username
|
||||
|
||||
@lazyproperty
|
||||
def has_secret(self) -> bool:
|
||||
def has_secret(self):
|
||||
return bool(self.secret)
|
||||
|
||||
@lazyproperty
|
||||
def versions(self) -> int:
|
||||
def versions(self):
|
||||
return self.history.count()
|
||||
|
||||
def get_su_from_accounts(self):
|
||||
|
||||
@@ -33,7 +33,7 @@ class IntegrationApplication(JMSOrgBaseModel):
|
||||
return qs.filter(*query)
|
||||
|
||||
@property
|
||||
def accounts_amount(self) -> int:
|
||||
def accounts_amount(self):
|
||||
return self.get_accounts().count()
|
||||
|
||||
@property
|
||||
|
||||
@@ -68,10 +68,8 @@ class AccountRisk(JMSOrgBaseModel):
|
||||
related_name='risks', null=True
|
||||
)
|
||||
risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices)
|
||||
status = models.CharField(
|
||||
max_length=32, choices=ConfirmOrIgnore.choices, default=ConfirmOrIgnore.pending,
|
||||
blank=True, verbose_name=_('Status')
|
||||
)
|
||||
status = models.CharField(max_length=32, choices=ConfirmOrIgnore.choices, default=ConfirmOrIgnore.pending,
|
||||
blank=True, verbose_name=_('Status'))
|
||||
details = models.JSONField(default=list, verbose_name=_('Detail'))
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -75,11 +75,11 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||
return bool(self.secret)
|
||||
|
||||
@property
|
||||
def has_username(self) -> bool:
|
||||
def has_username(self):
|
||||
return bool(self.username)
|
||||
|
||||
@property
|
||||
def spec_info(self) -> dict:
|
||||
def spec_info(self):
|
||||
data = {}
|
||||
if self.secret_type != SecretType.SSH_KEY:
|
||||
return data
|
||||
@@ -87,13 +87,13 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||
return data
|
||||
|
||||
@property
|
||||
def password(self) -> str:
|
||||
def password(self):
|
||||
if self.secret_type == SecretType.PASSWORD:
|
||||
return self.secret
|
||||
return None
|
||||
|
||||
@property
|
||||
def private_key(self) -> str:
|
||||
def private_key(self):
|
||||
if self.secret_type == SecretType.SSH_KEY:
|
||||
return self.secret
|
||||
return None
|
||||
@@ -110,7 +110,7 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||
return None
|
||||
|
||||
@property
|
||||
def ssh_key_fingerprint(self) -> str:
|
||||
def ssh_key_fingerprint(self):
|
||||
if self.public_key:
|
||||
public_key = self.public_key
|
||||
elif self.private_key:
|
||||
|
||||
@@ -56,7 +56,7 @@ class VaultModelMixin(models.Model):
|
||||
__secret = None
|
||||
|
||||
@property
|
||||
def secret(self) -> str:
|
||||
def secret(self):
|
||||
if self.__secret:
|
||||
return self.__secret
|
||||
from accounts.backends import vault_client
|
||||
|
||||
@@ -18,11 +18,11 @@ class VirtualAccount(JMSOrgBaseModel):
|
||||
verbose_name = _('Virtual account')
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
def name(self):
|
||||
return self.get_alias_display()
|
||||
|
||||
@property
|
||||
def username(self) -> str:
|
||||
def username(self):
|
||||
usernames_map = {
|
||||
AliasAccount.INPUT: _("Manual input"),
|
||||
AliasAccount.USER: _("Same with user"),
|
||||
@@ -32,7 +32,7 @@ class VirtualAccount(JMSOrgBaseModel):
|
||||
return usernames_map.get(self.alias, '')
|
||||
|
||||
@property
|
||||
def comment(self) -> str:
|
||||
def comment(self):
|
||||
comments_map = {
|
||||
AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'),
|
||||
AliasAccount.USER: _('The account username name same with user on connect'),
|
||||
|
||||
@@ -253,8 +253,6 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
'source_id': {'required': False, 'allow_null': True},
|
||||
}
|
||||
fields_unimport_template = ['params']
|
||||
# 手动判断唯一性校验
|
||||
validators = []
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
@@ -265,21 +263,6 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
)
|
||||
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):
|
||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||
@@ -473,8 +456,6 @@ class AssetAccountBulkSerializer(
|
||||
|
||||
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
|
||||
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields = AccountSerializer.Meta.fields + ['spec_info']
|
||||
extra_kwargs = {
|
||||
@@ -489,7 +470,6 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
|
||||
class AccountHistorySerializer(serializers.ModelSerializer):
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -70,8 +70,6 @@ class AuthValidateMixin(serializers.Serializer):
|
||||
class BaseAccountSerializer(
|
||||
AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer
|
||||
):
|
||||
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = BaseAccount
|
||||
fields_mini = ["id", "name", "username"]
|
||||
|
||||
@@ -130,7 +130,7 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = fields
|
||||
|
||||
@staticmethod
|
||||
def get_is_success(obj) -> bool:
|
||||
def get_is_success(obj):
|
||||
return obj.status == ChangeSecretRecordStatusChoice.success
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = fields
|
||||
|
||||
@staticmethod
|
||||
def get_asset(instance) -> str:
|
||||
def get_asset(instance):
|
||||
return str(instance.asset)
|
||||
|
||||
@staticmethod
|
||||
@@ -165,7 +165,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
|
||||
return str(instance.account)
|
||||
|
||||
@staticmethod
|
||||
def get_is_success(obj) -> str:
|
||||
def get_is_success(obj):
|
||||
if obj.status == ChangeSecretRecordStatusChoice.success.value:
|
||||
return _("Success")
|
||||
return _("Failed")
|
||||
@@ -196,9 +196,9 @@ class ChangeSecretAccountSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = fields
|
||||
|
||||
@staticmethod
|
||||
def get_meta(obj) -> dict:
|
||||
def get_meta(obj):
|
||||
return account_secret_task_status.get(str(obj.id))
|
||||
|
||||
@staticmethod
|
||||
def get_ttl(obj) -> int:
|
||||
def get_ttl(obj):
|
||||
return account_secret_task_status.get_ttl(str(obj.id))
|
||||
|
||||
@@ -69,7 +69,7 @@ class AssetRiskSerializer(serializers.Serializer):
|
||||
risk_summary = serializers.SerializerMethodField()
|
||||
|
||||
@staticmethod
|
||||
def get_risk_summary(obj) -> dict:
|
||||
def get_risk_summary(obj):
|
||||
summary = {}
|
||||
for risk in RiskChoice.choices:
|
||||
summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{% 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>
|
||||
@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from common.db.fields import JSONManyToManyField
|
||||
from common.db.models import JMSBaseModel
|
||||
from common.utils import contains_ip
|
||||
from common.utils.timezone import contains_time_period
|
||||
from common.utils.time_period import contains_time_period
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from ..const import ActionChoices
|
||||
|
||||
|
||||
@@ -34,16 +34,16 @@ class CommandGroup(JMSOrgBaseModel):
|
||||
|
||||
@lazyproperty
|
||||
def pattern(self):
|
||||
content = self.content.replace('\r\n', '\n')
|
||||
if self.type == 'command':
|
||||
s = self.construct_command_regex(content)
|
||||
s = self.construct_command_regex(self.content)
|
||||
else:
|
||||
s = r'{0}'.format(r'{}'.format('|'.join(content.split('\n'))))
|
||||
s = r'{0}'.format(self.content)
|
||||
return s
|
||||
|
||||
@classmethod
|
||||
def construct_command_regex(cls, content):
|
||||
regex = []
|
||||
content = content.replace('\r\n', '\n')
|
||||
for _cmd in content.split('\n'):
|
||||
cmd = re.sub(r'\s+', ' ', _cmd)
|
||||
cmd = re.escape(cmd)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from common.serializers.mixin import CommonBulkModelSerializer
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
|
||||
from ..const import ActionChoices
|
||||
from ..models import ConnectMethodACL
|
||||
@@ -6,15 +6,16 @@ from ..models import ConnectMethodACL
|
||||
__all__ = ["ConnectMethodACLSerializer"]
|
||||
|
||||
|
||||
class ConnectMethodACLSerializer(BaseSerializer, CommonBulkModelSerializer):
|
||||
class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer):
|
||||
class Meta(BaseSerializer.Meta):
|
||||
model = ConnectMethodACL
|
||||
fields = [
|
||||
i for i in BaseSerializer.Meta.fields + ['connect_methods']
|
||||
if i not in ['assets', 'accounts', 'org_id']
|
||||
if i not in ['assets', 'accounts']
|
||||
]
|
||||
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
|
||||
ActionChoices.review,
|
||||
ActionChoices.accept,
|
||||
ActionChoices.notice,
|
||||
ActionChoices.face_verify,
|
||||
ActionChoices.face_online,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from common.serializers import CommonBulkModelSerializer
|
||||
from common.serializers import MethodSerializer
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .base import BaseUserACLSerializer
|
||||
from .rules import RuleSerializer
|
||||
from ..const import ActionChoices
|
||||
@@ -12,12 +12,12 @@ __all__ = ["LoginACLSerializer"]
|
||||
common_help_text = _("With * indicating a match all. ")
|
||||
|
||||
|
||||
class LoginACLSerializer(BaseUserACLSerializer, CommonBulkModelSerializer):
|
||||
class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
|
||||
rules = MethodSerializer(label=_('Rule'))
|
||||
|
||||
class Meta(BaseUserACLSerializer.Meta):
|
||||
model = LoginACL
|
||||
fields = list((set(BaseUserACLSerializer.Meta.fields) | {'rules'}) - {'org_id'})
|
||||
fields = BaseUserACLSerializer.Meta.fields + ['rules', ]
|
||||
action_choices_exclude = [
|
||||
ActionChoices.warning,
|
||||
ActionChoices.notify_and_warn,
|
||||
|
||||
@@ -16,7 +16,6 @@ class CategoryViewSet(ListModelMixin, JMSGenericViewSet):
|
||||
'types': TypeSerializer,
|
||||
}
|
||||
permission_classes = (IsValidUser,)
|
||||
default_limit = None
|
||||
|
||||
def get_queryset(self):
|
||||
return AllTypes.categories()
|
||||
|
||||
@@ -14,7 +14,6 @@ class FavoriteAssetViewSet(BulkModelViewSet):
|
||||
serializer_class = FavoriteAssetSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
filterset_fields = ['asset']
|
||||
default_limit = None
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with tmp_to_root_org():
|
||||
|
||||
@@ -7,18 +7,15 @@ from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from assets.const import AllTypes
|
||||
from assets.models import Platform, Node, Asset, PlatformProtocol, PlatformAutomation
|
||||
from assets.models import Platform, Node, Asset, PlatformProtocol
|
||||
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer, PlatformListSerializer
|
||||
from common.api import JMSModelViewSet
|
||||
from common.permissions import IsValidUser
|
||||
from common.serializers import GroupedChoiceSerializer
|
||||
from rbac.models import RoleBinding
|
||||
|
||||
__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi', 'PlatformProtocolViewSet']
|
||||
|
||||
|
||||
|
||||
|
||||
class PlatformFilter(filters.FilterSet):
|
||||
name__startswith = filters.CharFilter(field_name='name', lookup_expr='istartswith')
|
||||
|
||||
@@ -43,7 +40,6 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
||||
'ops_methods': 'assets.view_platform',
|
||||
'filter_nodes_assets': 'assets.view_platform',
|
||||
}
|
||||
default_limit = None
|
||||
|
||||
def get_queryset(self):
|
||||
# 因为没有走分页逻辑,所以需要这里 prefetch
|
||||
@@ -67,13 +63,6 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
||||
return super().get_object()
|
||||
return self.get_queryset().get(name=pk)
|
||||
|
||||
|
||||
def check_permissions(self, request):
|
||||
if self.action == 'list' and RoleBinding.is_org_admin(request.user):
|
||||
return True
|
||||
else:
|
||||
return super().check_permissions(request)
|
||||
|
||||
def check_object_permissions(self, request, obj):
|
||||
if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal:
|
||||
self.permission_denied(
|
||||
@@ -113,7 +102,6 @@ class PlatformProtocolViewSet(JMSModelViewSet):
|
||||
|
||||
class PlatformAutomationMethodsApi(generics.ListAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
queryset = PlatformAutomation.objects.none()
|
||||
|
||||
@staticmethod
|
||||
def automation_methods():
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from rest_framework.generics import ListAPIView
|
||||
|
||||
from assets import serializers
|
||||
from assets.const import Protocol
|
||||
from common.permissions import IsValidUser
|
||||
from assets.models import Protocol
|
||||
|
||||
__all__ = ['ProtocolListApi']
|
||||
|
||||
@@ -13,13 +13,3 @@ class ProtocolListApi(ListAPIView):
|
||||
|
||||
def get_queryset(self):
|
||||
return list(Protocol.protocols())
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
search = self.request.query_params.get("search", "").lower().strip()
|
||||
if not search:
|
||||
return queryset
|
||||
queryset = [
|
||||
p for p in queryset
|
||||
if search in p['label'].lower() or search in p['value'].lower()
|
||||
]
|
||||
return queryset
|
||||
|
||||
@@ -161,7 +161,6 @@ class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView):
|
||||
'GET': 'assets.view_asset',
|
||||
'list': 'assets.view_asset',
|
||||
}
|
||||
queryset = Node.objects.none()
|
||||
|
||||
def get_assets(self):
|
||||
key = self.request.query_params.get('key')
|
||||
|
||||
@@ -11,20 +11,15 @@ class FormatAssetInfo:
|
||||
@staticmethod
|
||||
def get_cpu_model_count(cpus):
|
||||
try:
|
||||
if len(cpus) % 3 == 0:
|
||||
step = 3
|
||||
models = [cpus[i + 2] for i in range(0, len(cpus), step)]
|
||||
elif len(cpus) % 2 == 0:
|
||||
step = 2
|
||||
models = [cpus[i + 1] for i in range(0, len(cpus), step)]
|
||||
else:
|
||||
raise ValueError("CPU list format not recognized")
|
||||
models = [cpus[i + 1] + " " + cpus[i + 2] for i in range(0, len(cpus), 3)]
|
||||
|
||||
model_counts = Counter(models)
|
||||
|
||||
result = ', '.join([f"{model} x{count}" for model, count in model_counts.items()])
|
||||
except Exception as e:
|
||||
print(f"Error processing CPU model list: {e}")
|
||||
result = ''
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
ansible_shell_type: sh
|
||||
ansible_connection: local
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
ansible_timeout: 30
|
||||
|
||||
tasks:
|
||||
- name: Test asset connection (pyfreerdp)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
ansible_connection: local
|
||||
ansible_shell_type: sh
|
||||
ansible_become: false
|
||||
ansible_timeout: 30
|
||||
|
||||
tasks:
|
||||
- name: Test asset connection (paramiko)
|
||||
ssh_ping:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_shell_type: sh
|
||||
ansible_timeout: 30
|
||||
|
||||
tasks:
|
||||
- name: Test asset connection (telnet)
|
||||
telnet_ping:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
ansible_timeout: 30
|
||||
|
||||
tasks:
|
||||
- name: Test MongoDB connection
|
||||
@@ -17,5 +16,3 @@
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
ansible_timeout: 30
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
ansible_timeout: 30
|
||||
|
||||
tasks:
|
||||
- name: Test Oracle connection
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
ansible_timeout: 30
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
community.postgresql.postgresql_ping:
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
ansible_timeout: 30
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: |
|
||||
SELECT @@version
|
||||
|
||||
@@ -14,10 +14,6 @@ class Connectivity(TextChoices):
|
||||
NTLM_ERR = 'ntlm_err', _('NTLM credentials rejected error')
|
||||
CREATE_TEMPORARY_ERR = 'create_temp_err', _('Create temporary error')
|
||||
|
||||
@classmethod
|
||||
def as_dict(cls):
|
||||
return {choice.value: choice.label for choice in cls}
|
||||
|
||||
|
||||
class AutomationTypes(TextChoices):
|
||||
ping = 'ping', _('Ping')
|
||||
|
||||
@@ -20,7 +20,3 @@ class Category(ChoicesMixin, models.TextChoices):
|
||||
_category = getattr(cls, category.upper(), None)
|
||||
choices = [(_category.value, _category.label)] if _category else cls.choices
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def as_dict(cls):
|
||||
return {choice.value: choice.label for choice in cls}
|
||||
|
||||
@@ -250,12 +250,6 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'default': False,
|
||||
'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')
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -355,7 +349,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
for protocol, config in cls.settings().items():
|
||||
if not xpack_enabled and config.get('xpack', False):
|
||||
continue
|
||||
protocols.append({'label': protocol.label, 'value': protocol.value})
|
||||
protocols.append(protocol)
|
||||
|
||||
from assets.models.platform import PlatformProtocol
|
||||
custom_protocols = (
|
||||
|
||||
@@ -112,7 +112,7 @@ class Protocol(models.Model):
|
||||
return protocols[0] if len(protocols) > 0 else {}
|
||||
|
||||
@property
|
||||
def setting(self) -> dict:
|
||||
def setting(self):
|
||||
if self._setting is not None:
|
||||
return self._setting
|
||||
return self.asset_platform_protocol.get('setting', {})
|
||||
@@ -122,7 +122,7 @@ class Protocol(models.Model):
|
||||
self._setting = value
|
||||
|
||||
@property
|
||||
def public(self) -> bool:
|
||||
def public(self):
|
||||
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')
|
||||
|
||||
@lazyproperty
|
||||
def spec_info(self) -> dict:
|
||||
def spec_info(self):
|
||||
instance = getattr(self, self.category, None)
|
||||
if not instance:
|
||||
return {}
|
||||
@@ -240,7 +240,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
|
||||
return info
|
||||
|
||||
@lazyproperty
|
||||
def auto_config(self) -> dict:
|
||||
def auto_config(self):
|
||||
platform = self.platform
|
||||
auto_config = {
|
||||
'su_enabled': platform.su_enabled,
|
||||
@@ -343,11 +343,11 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
|
||||
return names
|
||||
|
||||
@lazyproperty
|
||||
def type(self) -> str:
|
||||
def type(self):
|
||||
return self.platform.type
|
||||
|
||||
@lazyproperty
|
||||
def category(self) -> str:
|
||||
def category(self):
|
||||
return self.platform.category
|
||||
|
||||
def is_category(self, category):
|
||||
|
||||
@@ -53,7 +53,7 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
return name
|
||||
|
||||
def get_all_assets(self):
|
||||
nodes = self.nodes.only("id", "key")
|
||||
nodes = self.nodes.all()
|
||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list("id", flat=True)
|
||||
direct_asset_ids = self.assets.all().values_list("id", flat=True)
|
||||
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
|
||||
|
||||
@@ -573,7 +573,7 @@ class Node(JMSOrgBaseModel, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||
return not self.__gt__(other)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
def name(self):
|
||||
return self.value
|
||||
|
||||
def computed_full_value(self):
|
||||
|
||||
@@ -25,7 +25,7 @@ class PlatformProtocol(models.Model):
|
||||
return '{}/{}'.format(self.name, self.port)
|
||||
|
||||
@property
|
||||
def secret_types(self) -> list:
|
||||
def secret_types(self):
|
||||
return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
|
||||
|
||||
@lazyproperty
|
||||
|
||||
@@ -147,7 +147,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
|
||||
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
|
||||
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"))
|
||||
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
|
||||
platform = ObjectRelatedField(queryset=Platform.objects, required=True, label=_('Platform'),
|
||||
attrs=('id', 'name', 'type'))
|
||||
accounts_amount = serializers.IntegerField(read_only=True, label=_('Accounts amount'))
|
||||
@@ -426,18 +425,6 @@ class DetailMixin(serializers.Serializer):
|
||||
gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
|
||||
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):
|
||||
request = self.context.get('request')
|
||||
if not self.instance and UUID_PATTERN.findall(request.path):
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext_lazy as _, get_language
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.models import Custom, Platform, Asset
|
||||
from common.const import UUID_PATTERN
|
||||
from common.serializers import create_serializer_class
|
||||
from common.serializers.common import DictSerializer, MethodSerializer
|
||||
from terminal.models import Applet
|
||||
from .common import AssetSerializer
|
||||
|
||||
__all__ = ['CustomSerializer']
|
||||
@@ -48,38 +47,8 @@ class CustomSerializer(AssetSerializer):
|
||||
|
||||
if not platform:
|
||||
return default_field
|
||||
|
||||
custom_fields = platform.custom_fields
|
||||
|
||||
if not custom_fields:
|
||||
return default_field
|
||||
name = platform.name.title() + 'CustomSerializer'
|
||||
|
||||
applet = Applet.objects.filter(
|
||||
name=platform.created_by.replace('Applet:', '')
|
||||
).first()
|
||||
|
||||
if not applet:
|
||||
return create_serializer_class(name, custom_fields)()
|
||||
|
||||
i18n = applet.manifest.get('i18n', {})
|
||||
|
||||
lang = get_language()
|
||||
lang_short = lang[:2]
|
||||
|
||||
def translate_text(key):
|
||||
return (
|
||||
i18n.get(key, {}).get(lang)
|
||||
or i18n.get(key, {}).get(lang_short)
|
||||
or key
|
||||
)
|
||||
|
||||
for field in custom_fields:
|
||||
label = field.get('label')
|
||||
help_text = field.get('help_text')
|
||||
if label:
|
||||
field['label'] = translate_text(label)
|
||||
if help_text:
|
||||
field['help_text'] = translate_text(help_text)
|
||||
|
||||
return create_serializer_class(name, custom_fields)()
|
||||
|
||||
@@ -19,13 +19,11 @@ __all__ = [
|
||||
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets'))
|
||||
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
|
||||
executed_amount = serializers.IntegerField(read_only=True, label=_('Executed amount'))
|
||||
|
||||
class Meta:
|
||||
read_only_fields = [
|
||||
'date_created', 'date_updated', 'created_by',
|
||||
'periodic_display', 'executed_amount', 'type',
|
||||
'last_execution_date',
|
||||
'periodic_display', 'executed_amount', 'type', 'last_execution_date'
|
||||
]
|
||||
mini_fields = [
|
||||
'id', 'name', 'type', 'is_periodic', 'interval',
|
||||
|
||||
@@ -172,7 +172,10 @@ class UserLoginLogViewSet(UserLoginCommonMixin, OrgReadonlyModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.model.filter_queryset_by_org(queryset)
|
||||
if current_org.is_root() or not settings.XPACK_ENABLED:
|
||||
return queryset
|
||||
users = self.get_org_member_usernames()
|
||||
queryset = queryset.filter(username__in=users)
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -294,7 +297,12 @@ class PasswordChangeLogViewSet(OrgReadonlyModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
return self.model.filter_queryset_by_org(queryset)
|
||||
if not current_org.is_root():
|
||||
users = current_org.get_members()
|
||||
queryset = queryset.filter(
|
||||
user__in=[str(user) for user in users]
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import uuid
|
||||
from datetime import timedelta, datetime
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
@@ -40,7 +40,7 @@ __all__ = [
|
||||
|
||||
class JobLog(JobExecution):
|
||||
@property
|
||||
def creator_name(self) -> str:
|
||||
def creator_name(self):
|
||||
return self.creator.name
|
||||
|
||||
class Meta:
|
||||
@@ -189,15 +189,6 @@ class PasswordChangeLog(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Password change log")
|
||||
|
||||
@staticmethod
|
||||
def filter_queryset_by_org(queryset):
|
||||
if not current_org.is_root():
|
||||
users = current_org.get_members()
|
||||
queryset = queryset.filter(
|
||||
user__in=[str(user) for user in users]
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class UserLoginLog(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
@@ -232,7 +223,7 @@ class UserLoginLog(models.Model):
|
||||
return '%s(%s)' % (self.username, self.city)
|
||||
|
||||
@property
|
||||
def backend_display(self) -> str:
|
||||
def backend_display(self):
|
||||
return gettext(self.backend)
|
||||
|
||||
@classmethod
|
||||
@@ -258,7 +249,7 @@ class UserLoginLog(models.Model):
|
||||
return login_logs
|
||||
|
||||
@property
|
||||
def reason_display(self) -> str:
|
||||
def reason_display(self):
|
||||
from authentication.errors import reason_choices, old_reason_choices
|
||||
|
||||
reason = reason_choices.get(self.reason)
|
||||
@@ -267,15 +258,6 @@ class UserLoginLog(models.Model):
|
||||
reason = old_reason_choices.get(self.reason, self.reason)
|
||||
return reason
|
||||
|
||||
@staticmethod
|
||||
def filter_queryset_by_org(queryset):
|
||||
from audits.utils import construct_userlogin_usernames
|
||||
if current_org.is_root() or not settings.XPACK_ENABLED:
|
||||
return queryset
|
||||
user_queryset = current_org.get_members()
|
||||
users = construct_userlogin_usernames(user_queryset)
|
||||
return queryset.filter(username__in=users)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-datetime", "username"]
|
||||
verbose_name = _("User login log")
|
||||
@@ -300,15 +282,15 @@ class UserSession(models.Model):
|
||||
return '%s(%s)' % (self.user, self.ip)
|
||||
|
||||
@property
|
||||
def backend_display(self) -> str:
|
||||
def backend_display(self):
|
||||
return gettext(self.backend)
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
def is_active(self):
|
||||
return user_session_manager.check_active(self.key)
|
||||
|
||||
@property
|
||||
def date_expired(self) -> datetime:
|
||||
def date_expired(self):
|
||||
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
session_store = session_store_cls(session_key=self.key)
|
||||
cache_key = session_store.cache_key
|
||||
|
||||
@@ -119,11 +119,11 @@ class OperateLogSerializer(BulkOrgResourceModelSerializer):
|
||||
fields = fields_small
|
||||
|
||||
@staticmethod
|
||||
def get_resource_type(instance) -> str:
|
||||
def get_resource_type(instance):
|
||||
return _(instance.resource_type)
|
||||
|
||||
@staticmethod
|
||||
def get_resource(instance) -> str:
|
||||
def get_resource(instance):
|
||||
return i18n_trans(instance.resource)
|
||||
|
||||
|
||||
@@ -147,11 +147,11 @@ class ActivityUnionLogSerializer(serializers.Serializer):
|
||||
r_type = serializers.CharField(read_only=True)
|
||||
|
||||
@staticmethod
|
||||
def get_timestamp(obj) -> str:
|
||||
def get_timestamp(obj):
|
||||
return as_current_tz(obj['datetime']).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
@staticmethod
|
||||
def get_content(obj) -> str:
|
||||
def get_content(obj):
|
||||
if not obj['r_detail']:
|
||||
action = obj['r_action'].replace('_', ' ').capitalize()
|
||||
ctn = _('%s %s this resource') % (obj['r_user'], _(action).lower())
|
||||
@@ -160,7 +160,7 @@ class ActivityUnionLogSerializer(serializers.Serializer):
|
||||
return ctn
|
||||
|
||||
@staticmethod
|
||||
def get_detail_url(obj) -> str:
|
||||
def get_detail_url(obj):
|
||||
detail_url = ''
|
||||
detail_id, obj_type = obj['r_detail_id'], obj['r_type']
|
||||
if not detail_id:
|
||||
@@ -210,7 +210,7 @@ class UserSessionSerializer(serializers.ModelSerializer):
|
||||
"backend_display": {"label": _("Auth backend display")},
|
||||
}
|
||||
|
||||
def get_is_current_user_session(self, obj) -> bool:
|
||||
def get_is_current_user_session(self, obj):
|
||||
request = self.context.get('request')
|
||||
if not request:
|
||||
return False
|
||||
|
||||
@@ -2,19 +2,18 @@
|
||||
#
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from celery import shared_task
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from django.utils._os import safe_join
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.const.crontab import CRONTAB_AT_AM_TWO
|
||||
from common.storage.ftp_file import FTPFileStorageHandler
|
||||
from common.utils import get_log_keep_day, get_logger
|
||||
from common.utils.safe import safe_run_cmd
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from ops.models import CeleryTaskExecution
|
||||
from orgs.utils import tmp_to_root_org
|
||||
@@ -58,12 +57,14 @@ def clean_ftp_log_period():
|
||||
now = timezone.now()
|
||||
days = get_log_keep_day('FTP_LOG_KEEP_DAYS')
|
||||
expired_day = now - datetime.timedelta(days=days)
|
||||
file_store_dir = safe_join(default_storage.base_location, FTPLog.upload_to)
|
||||
file_store_dir = os.path.join(default_storage.base_location, FTPLog.upload_to)
|
||||
FTPLog.objects.filter(date_start__lt=expired_day).delete()
|
||||
command = "find %s -mtime +%s -type f -exec rm -f {} \\;"
|
||||
safe_run_cmd(command, (file_store_dir, days))
|
||||
command = "find %s -type d -empty -delete;"
|
||||
safe_run_cmd(command, (file_store_dir,))
|
||||
command = "find %s -mtime +%s -type f -exec rm -f {} \\;" % (
|
||||
file_store_dir, days
|
||||
)
|
||||
subprocess.call(command, shell=True)
|
||||
command = "find %s -type d -empty -delete;" % file_store_dir
|
||||
subprocess.call(command, shell=True)
|
||||
logger.info("Clean FTP file done")
|
||||
|
||||
|
||||
@@ -75,11 +76,12 @@ def clean_celery_tasks_period():
|
||||
tasks.delete()
|
||||
tasks = CeleryTaskExecution.objects.filter(date_start__isnull=True)
|
||||
tasks.delete()
|
||||
command = "find %s -mtime +%s -name '*.log' -type f -exec rm -f {} \\;"
|
||||
safe_run_cmd(command, (settings.CELERY_LOG_DIR, expire_days))
|
||||
celery_log_path = safe_join(settings.LOG_DIR, 'celery.log')
|
||||
command = "echo > %s"
|
||||
safe_run_cmd(command, (celery_log_path,))
|
||||
command = "find %s -mtime +%s -name '*.log' -type f -exec rm -f {} \\;" % (
|
||||
settings.CELERY_LOG_DIR, expire_days
|
||||
)
|
||||
subprocess.call(command, shell=True)
|
||||
command = "echo > {}".format(os.path.join(settings.LOG_DIR, 'celery.log'))
|
||||
subprocess.call(command, shell=True)
|
||||
|
||||
|
||||
def batch_delete(queryset, batch_size=3000):
|
||||
@@ -117,15 +119,15 @@ def clean_expired_session_period():
|
||||
expired_sessions = Session.objects.filter(date_start__lt=expire_date)
|
||||
timestamp = expire_date.timestamp()
|
||||
expired_commands = Command.objects.filter(timestamp__lt=timestamp)
|
||||
replay_dir = safe_join(default_storage.base_location, 'replay')
|
||||
replay_dir = os.path.join(default_storage.base_location, 'replay')
|
||||
|
||||
batch_delete(expired_sessions)
|
||||
logger.info("Clean session item done")
|
||||
batch_delete(expired_commands)
|
||||
logger.info("Clean session command done")
|
||||
remove_files_by_days(replay_dir, days)
|
||||
command = "find %s -type d -empty -delete;"
|
||||
safe_run_cmd(command, (replay_dir,))
|
||||
command = "find %s -type d -empty -delete;" % replay_dir
|
||||
subprocess.call(command, shell=True)
|
||||
logger.info("Clean session replay done")
|
||||
|
||||
|
||||
|
||||
@@ -618,8 +618,6 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
|
||||
token_id = request.data.get('id') or ''
|
||||
token = ConnectionToken.get_typed_connection_token(token_id)
|
||||
if not token:
|
||||
raise PermissionDenied('Token {} is not valid'.format(token))
|
||||
token.is_valid()
|
||||
serializer = self.get_serializer(instance=token)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.views import View
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from common.utils import get_logger
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
logger = get_logger(__file__)
|
||||
@@ -61,13 +61,4 @@ class JMSBaseAuthBackend:
|
||||
|
||||
|
||||
class JMSModelBackend(JMSBaseAuthBackend, ModelBackend):
|
||||
def user_can_authenticate(self, user):
|
||||
return True
|
||||
|
||||
|
||||
class BaseAuthCallbackClientView(View):
|
||||
http_method_names = ['get']
|
||||
|
||||
def get(self, request):
|
||||
from authentication.views.utils import redirect_to_guard_view
|
||||
return redirect_to_guard_view(query_string='next=client')
|
||||
pass
|
||||
|
||||
@@ -1,51 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import threading
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django_cas_ng.backends import CASBackend as _CASBackend
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from ..base import JMSBaseAuthBackend
|
||||
|
||||
__all__ = ['CASBackend', 'CASUserDoesNotExist']
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class CASUserDoesNotExist(Exception):
|
||||
"""Exception raised when a CAS user does not exist."""
|
||||
pass
|
||||
__all__ = ['CASBackend']
|
||||
|
||||
|
||||
class CASBackend(JMSBaseAuthBackend, _CASBackend):
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_CAS
|
||||
|
||||
def authenticate(self, request, ticket, service):
|
||||
UserModel = get_user_model()
|
||||
manager = UserModel._default_manager
|
||||
original_get_by_natural_key = manager.get_by_natural_key
|
||||
thread_local = threading.local()
|
||||
thread_local.thread_id = threading.get_ident()
|
||||
logger.debug(f"CASBackend.authenticate: thread_id={thread_local.thread_id}")
|
||||
|
||||
def get_by_natural_key(self, username):
|
||||
logger.debug(f"CASBackend.get_by_natural_key: thread_id={threading.get_ident()}, username={username}")
|
||||
if threading.get_ident() != thread_local.thread_id:
|
||||
return original_get_by_natural_key(username)
|
||||
|
||||
try:
|
||||
user = original_get_by_natural_key(username)
|
||||
except UserModel.DoesNotExist:
|
||||
raise CASUserDoesNotExist(username)
|
||||
return user
|
||||
|
||||
try:
|
||||
manager.get_by_natural_key = get_by_natural_key.__get__(manager, type(manager))
|
||||
user = super().authenticate(request, ticket=ticket, service=service)
|
||||
finally:
|
||||
manager.get_by_natural_key = original_get_by_natural_key
|
||||
return user
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import View
|
||||
from django_cas_ng.views import LoginView
|
||||
|
||||
from authentication.backends.base import BaseAuthCallbackClientView
|
||||
from common.utils import FlashMessageUtil
|
||||
from .backends import CASUserDoesNotExist
|
||||
|
||||
__all__ = ['LoginView']
|
||||
|
||||
from authentication.views.utils import redirect_to_guard_view
|
||||
|
||||
|
||||
class CASLoginView(LoginView):
|
||||
def get(self, request):
|
||||
try:
|
||||
resp = super().get(request)
|
||||
return resp
|
||||
return super().get(request)
|
||||
except PermissionDenied:
|
||||
return HttpResponseRedirect('/')
|
||||
except CASUserDoesNotExist as e:
|
||||
message_data = {
|
||||
'title': _('User does not exist: {}').format(e),
|
||||
'error': _(
|
||||
'CAS login was successful, but no corresponding local user was found in the system, and automatic '
|
||||
'user creation is disabled in the CAS authentication configuration. Login failed.'),
|
||||
'interval': 10,
|
||||
'redirect_url': '/',
|
||||
}
|
||||
return FlashMessageUtil.gen_and_redirect_to(message_data)
|
||||
|
||||
|
||||
class CASCallbackClientView(BaseAuthCallbackClientView):
|
||||
pass
|
||||
class CASCallbackClientView(View):
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
return redirect_to_guard_view(query_string='next=client')
|
||||
|
||||
@@ -5,10 +5,10 @@ from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.views import View
|
||||
|
||||
from authentication.backends.base import BaseAuthCallbackClientView
|
||||
from authentication.mixins import authenticate
|
||||
from authentication.utils import build_absolute_uri
|
||||
from authentication.views.mixins import FlashMessageMixin
|
||||
from authentication.views.utils import redirect_to_guard_view
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -67,8 +67,11 @@ class OAuth2AuthCallbackView(View, FlashMessageMixin):
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
|
||||
|
||||
class OAuth2AuthCallbackClientView(BaseAuthCallbackClientView):
|
||||
pass
|
||||
class OAuth2AuthCallbackClientView(View):
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
return redirect_to_guard_view(query_string='next=client')
|
||||
|
||||
|
||||
class OAuth2EndSessionView(View):
|
||||
|
||||
@@ -29,7 +29,7 @@ from authentication.utils import build_absolute_uri_for_oidc
|
||||
from authentication.views.mixins import FlashMessageMixin
|
||||
from common.utils import safe_next_url
|
||||
from .utils import get_logger
|
||||
from ..base import BaseAuthCallbackClientView
|
||||
from ...views.utils import redirect_to_guard_view
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -210,8 +210,11 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
|
||||
return HttpResponseRedirect(settings.AUTH_OPENID_AUTHENTICATION_FAILURE_REDIRECT_URI)
|
||||
|
||||
|
||||
class OIDCAuthCallbackClientView(BaseAuthCallbackClientView):
|
||||
pass
|
||||
class OIDCAuthCallbackClientView(View):
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
return redirect_to_guard_view(query_string='next=client')
|
||||
|
||||
|
||||
class OIDCEndSessionView(View):
|
||||
|
||||
@@ -71,8 +71,7 @@ class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet):
|
||||
return self.redirect_to_error(_('Auth failed'))
|
||||
|
||||
confirm_mfa = request.session.get('passkey_confirm_mfa')
|
||||
# 如果开启了安全模式,Passkey 不能作为 MFA
|
||||
if confirm_mfa and not settings.SAFE_MODE:
|
||||
if confirm_mfa:
|
||||
request.session['CONFIRM_LEVEL'] = ConfirmType.values.index('mfa') + 1
|
||||
request.session['CONFIRM_TIME'] = int(time.time())
|
||||
request.session['CONFIRM_TYPE'] = ConfirmType.MFA
|
||||
@@ -81,9 +80,7 @@ class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet):
|
||||
|
||||
try:
|
||||
self.check_oauth2_auth(user, settings.AUTH_BACKEND_PASSKEY)
|
||||
# 如果开启了安全模式,passkey 不能作为 MFA
|
||||
if not settings.SAFE_MODE:
|
||||
self.mark_mfa_ok('passkey', user)
|
||||
self.mark_mfa_ok('passkey', user)
|
||||
return self.redirect_to_guard_view()
|
||||
except Exception as e:
|
||||
msg = getattr(e, 'msg', '') or str(e)
|
||||
|
||||
@@ -19,7 +19,7 @@ from onelogin.saml2.idp_metadata_parser import (
|
||||
from authentication.views.mixins import FlashMessageMixin
|
||||
from common.utils import get_logger
|
||||
from .settings import JmsSaml2Settings
|
||||
from ..base import BaseAuthCallbackClientView
|
||||
from ...views.utils import redirect_to_guard_view
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -300,8 +300,11 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin, FlashMessageMixin):
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
class Saml2AuthCallbackClientView(BaseAuthCallbackClientView):
|
||||
pass
|
||||
class Saml2AuthCallbackClientView(View):
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
return redirect_to_guard_view(query_string='next=client')
|
||||
|
||||
|
||||
class Saml2AuthMetadataView(View, PrepareRequestMixin):
|
||||
|
||||
@@ -14,9 +14,7 @@ class TempTokenAuthBackend(JMSBaseAuthBackend):
|
||||
return settings.AUTH_TEMP_TOKEN
|
||||
|
||||
def authenticate(self, request, username='', password=''):
|
||||
tokens = self.model.objects.filter(username=username).order_by('-date_created')[:500]
|
||||
token = next((t for t in tokens if t.secret == password), None)
|
||||
|
||||
token = self.model.objects.filter(username=username, secret=password).first()
|
||||
if not token:
|
||||
return None
|
||||
if not token.is_valid:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import abc
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -24,21 +23,17 @@ class BaseMFA(abc.ABC):
|
||||
|
||||
cache_key = f'{self.name}_{self.user.username}'
|
||||
cache_code = cache.get(cache_key)
|
||||
|
||||
is_match = cache_code == code
|
||||
|
||||
if settings.SAFE_MODE and is_match:
|
||||
if cache_code == code:
|
||||
return False, _(
|
||||
"The two-factor code you entered has either already been used or has expired. "
|
||||
"Please request a new one."
|
||||
)
|
||||
|
||||
ok, msg = self._check_code(code)
|
||||
|
||||
if not ok:
|
||||
return False, msg
|
||||
|
||||
cache.set(cache_key, code, 60)
|
||||
cache.set(cache_key, code, 60 * 5)
|
||||
return True, msg
|
||||
|
||||
def is_authenticated(self):
|
||||
|
||||
@@ -19,8 +19,6 @@ class MFAPasskey(BaseMFA):
|
||||
def is_active(self):
|
||||
if not self.is_authenticated():
|
||||
return True
|
||||
if settings.SAFE_MODE:
|
||||
return False
|
||||
return self.user.passkey_set.count()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 4.1.13 on 2025-08-04 06:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentication", "0006_connectiontoken_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="connectiontoken",
|
||||
name="remote_addr",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=128, null=True, verbose_name="Remote addr"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,50 +0,0 @@
|
||||
# Generated by Django 4.1.13 on 2025-08-14 06:39
|
||||
|
||||
import authentication.models.access_key
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
old_access_key_secrets_mapper = {}
|
||||
|
||||
def fetch_access_key_secrets(apps, schema_editor):
|
||||
AccessKey = apps.get_model("authentication", "AccessKey")
|
||||
|
||||
for id, secret in AccessKey.objects.all().values_list('id', 'secret'):
|
||||
old_access_key_secrets_mapper[str(id)] = secret
|
||||
|
||||
|
||||
def save_access_key_secrets(apps, schema_editor):
|
||||
AccessKey = apps.get_model("authentication", "AccessKey")
|
||||
aks = AccessKey.objects.filter(id__in=list(old_access_key_secrets_mapper.keys()))
|
||||
for ak in aks:
|
||||
old_value = old_access_key_secrets_mapper.get(str(ak.id))
|
||||
if not old_value:
|
||||
continue
|
||||
ak.secret = old_value
|
||||
ak.save(update_fields=["secret"])
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentication", "0007_connectiontoken_remote_addr"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(fetch_access_key_secrets),
|
||||
migrations.AlterField(
|
||||
model_name="accesskey",
|
||||
name="secret",
|
||||
field=common.db.fields.EncryptTextField(
|
||||
default=authentication.models.access_key.default_secret,
|
||||
verbose_name="AccessKeySecret",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="temptoken",
|
||||
name="secret",
|
||||
field=common.db.fields.EncryptTextField(
|
||||
verbose_name="Secret"
|
||||
),
|
||||
),
|
||||
migrations.RunPython(save_access_key_secrets),
|
||||
]
|
||||
@@ -4,7 +4,6 @@ from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db.fields import EncryptTextField
|
||||
import common.db.models
|
||||
|
||||
from common.db.utils import default_ip_group
|
||||
@@ -17,7 +16,7 @@ def default_secret():
|
||||
|
||||
class AccessKey(models.Model):
|
||||
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, default=uuid.uuid4, editable=False)
|
||||
secret = EncryptTextField(verbose_name='AccessKeySecret', default=default_secret)
|
||||
secret = models.CharField(verbose_name='AccessKeySecret', default=default_secret, max_length=36)
|
||||
ip_group = models.JSONField(default=default_ip_group, verbose_name=_('IP group'))
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User',
|
||||
on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='access_keys')
|
||||
|
||||
@@ -4,7 +4,6 @@ from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
@@ -58,9 +57,6 @@ class ConnectionToken(JMSOrgBaseModel):
|
||||
)
|
||||
face_monitor_token = models.CharField(max_length=128, null=True, blank=True, verbose_name=_("Face monitor token"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||||
remote_addr = models.CharField(
|
||||
max_length=128, verbose_name=_("Remote addr"), blank=True, null=True
|
||||
)
|
||||
|
||||
type = models.CharField(
|
||||
max_length=16, choices=ConnectionTokenType.choices,
|
||||
@@ -77,10 +73,7 @@ class ConnectionToken(JMSOrgBaseModel):
|
||||
|
||||
@classmethod
|
||||
def get_typed_connection_token(cls, token_id):
|
||||
try:
|
||||
token = get_object_or_404(cls, id=token_id)
|
||||
except ValidationError:
|
||||
return None
|
||||
token = get_object_or_404(cls, id=token_id)
|
||||
|
||||
if token.type == ConnectionTokenType.ADMIN.value:
|
||||
token = AdminConnectionToken.objects.get(id=token_id)
|
||||
@@ -89,11 +82,11 @@ class ConnectionToken(JMSOrgBaseModel):
|
||||
return token
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
def is_expired(self):
|
||||
return self.date_expired < timezone.now()
|
||||
|
||||
@property
|
||||
def expire_time(self) -> int:
|
||||
def expire_time(self):
|
||||
interval = self.date_expired - timezone.now()
|
||||
seconds = interval.total_seconds()
|
||||
if seconds < 0:
|
||||
@@ -165,7 +158,7 @@ class ConnectionToken(JMSOrgBaseModel):
|
||||
def expire_at(self):
|
||||
return self.permed_account.date_expired.timestamp()
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
def is_valid(self):
|
||||
if not self.is_active:
|
||||
error = _('Connection token inactive')
|
||||
raise PermissionDenied(error)
|
||||
|
||||
@@ -3,12 +3,11 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db.models import JMSBaseModel
|
||||
from common.db.fields import EncryptTextField
|
||||
|
||||
|
||||
class TempToken(JMSBaseModel):
|
||||
username = models.CharField(max_length=128, verbose_name=_("Username"))
|
||||
secret = EncryptTextField(verbose_name=_("Secret"))
|
||||
secret = models.CharField(max_length=64, verbose_name=_("Secret"))
|
||||
verified = models.BooleanField(default=False, verbose_name=_("Verified"))
|
||||
date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified"))
|
||||
date_expired = models.DateTimeField(verbose_name=_("Date expired"))
|
||||
|
||||
@@ -20,10 +20,9 @@ class UserConfirmation(permissions.BasePermission):
|
||||
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
return True
|
||||
|
||||
session = getattr(request, 'session', {})
|
||||
confirm_level = session.get('CONFIRM_LEVEL')
|
||||
confirm_type = session.get('CONFIRM_TYPE')
|
||||
confirm_time = session.get('CONFIRM_TIME')
|
||||
confirm_level = request.session.get('CONFIRM_LEVEL')
|
||||
confirm_type = request.session.get('CONFIRM_TYPE')
|
||||
confirm_time = request.session.get('CONFIRM_TIME')
|
||||
|
||||
ttl = self.get_ttl(confirm_type)
|
||||
now = int(time.time())
|
||||
|
||||
@@ -60,7 +60,7 @@ class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_su_from(account) -> dict:
|
||||
def get_su_from(account):
|
||||
if not hasattr(account, 'asset'):
|
||||
return {}
|
||||
su_enabled = account.asset.platform.su_enabled
|
||||
@@ -104,8 +104,6 @@ class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
|
||||
class _ConnectionTokenPlatformSerializer(PlatformSerializer):
|
||||
class Meta(PlatformSerializer.Meta):
|
||||
model = Platform
|
||||
fields = [field for field in PlatformSerializer.Meta.fields
|
||||
if field not in PlatformSerializer.Meta.fields_m2m]
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
names = super().get_field_names(declared_fields, info)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import get_request_ip
|
||||
from common.serializers import CommonModelSerializer
|
||||
from common.serializers.fields import EncryptedField
|
||||
from perms.serializers.permission import ActionChoicesField
|
||||
from ..models import ConnectionToken, AdminConnectionToken
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
|
||||
__all__ = [
|
||||
'ConnectionTokenSerializer', 'SuperConnectionTokenSerializer',
|
||||
@@ -14,7 +12,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||
class ConnectionTokenSerializer(CommonModelSerializer):
|
||||
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
|
||||
input_secret = EncryptedField(
|
||||
label=_("Input secret"), max_length=40960, required=False, allow_blank=True
|
||||
@@ -31,7 +29,6 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||
'is_active', 'is_reusable', 'from_ticket', 'from_ticket_info',
|
||||
'date_expired', 'date_created', 'date_updated', 'created_by',
|
||||
'updated_by', 'org_id', 'org_name', 'face_monitor_token',
|
||||
'remote_addr',
|
||||
]
|
||||
read_only_fields = [
|
||||
# 普通 Token 不支持指定 user
|
||||
@@ -55,13 +52,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||
def get_user(self, attrs):
|
||||
return self.get_request_user()
|
||||
|
||||
def create(self, validated_data):
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
validated_data['remote_addr'] = get_request_ip(request)
|
||||
return super().create(validated_data)
|
||||
|
||||
def get_from_ticket_info(self, instance) -> dict:
|
||||
def get_from_ticket_info(self, instance):
|
||||
if not instance.from_ticket:
|
||||
return {}
|
||||
user = self.get_request_user()
|
||||
|
||||
@@ -46,7 +46,7 @@ class BearerTokenSerializer(serializers.Serializer):
|
||||
user = UserProfileSerializer(read_only=True)
|
||||
|
||||
@staticmethod
|
||||
def get_keyword(obj) -> str:
|
||||
def get_keyword(obj):
|
||||
return 'Bearer'
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<style>
|
||||
.login-content {
|
||||
{% comment %} box-shadow: 0 5px 5px -3px rgb(0 0 0 / 15%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%); {% endcomment %}
|
||||
{#box-shadow: 0 5px 5px -3px rgb(0 0 0 / 15%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%);#}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
@@ -69,27 +69,23 @@
|
||||
}
|
||||
|
||||
.login-content {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 500px;
|
||||
width: 1000px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #f3f3f3;
|
||||
{#height: calc(100vh - (100vh - 470px) / 3);#}
|
||||
}
|
||||
|
||||
|
||||
.captcha {
|
||||
float: right;
|
||||
}
|
||||
@@ -142,7 +138,7 @@
|
||||
}
|
||||
|
||||
.jms-title {
|
||||
{#padding: 22px 10px 10px;#}
|
||||
{#padding: 22px 10px 10px;#}
|
||||
}
|
||||
|
||||
.more-login-items {
|
||||
@@ -322,7 +318,7 @@
|
||||
</div>
|
||||
<div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}">
|
||||
<div class="mobile-logo" style="padding-bottom: 45px; box-sizing: border-box">
|
||||
<div class="jms-title">
|
||||
<div class="jms-title">
|
||||
<img style="width: 60px; height: 60px" src="{{ INTERFACE.logo_logout }}" alt="Logo"/>
|
||||
<span style="padding-left: 10px">{{ INTERFACE.login_title }}</span>
|
||||
</div>
|
||||
@@ -334,18 +330,17 @@
|
||||
</h2>
|
||||
<ul class=" nav navbar-top-links navbar-right">
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle login-page-language" data-bs-toggle="dropdown" href="#"
|
||||
target="_blank">
|
||||
<a class="dropdown-toggle login-page-language" data-bs-toggle="dropdown" href="#" target="_blank">
|
||||
<i class="fa fa-globe fa-lg" style="margin-right: 2px"></i>
|
||||
<span>{{ current_lang.title }}<b class="caret"></b></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu profile-dropdown dropdown-menu-right">
|
||||
{% for lang in langs %}
|
||||
<li>
|
||||
<a href="{% url 'i18n-switch' lang=lang.code %}">
|
||||
<span>{{ lang.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<a href="{% url 'i18n-switch' lang=lang.code %}">
|
||||
<span>{{ lang.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
@@ -355,11 +350,11 @@
|
||||
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
|
||||
{% if form.non_field_errors %}
|
||||
<p class="help-block red-fonts">
|
||||
{{ form.non_field_errors.as_text }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if form.non_field_errors %}
|
||||
<p class="help-block red-fonts">
|
||||
{{ form.non_field_errors.as_text }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% bootstrap_field form.username show_label=False %}
|
||||
@@ -370,15 +365,15 @@
|
||||
name="{{ form.password.html_name }}">
|
||||
{% if form.password.errors %}
|
||||
<p class="help-block" style="text-align: left">
|
||||
{{ form.password.errors.as_text }}
|
||||
</p>
|
||||
{{ form.password.errors.as_text }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.challenge %}
|
||||
{% bootstrap_field form.challenge show_label=False %}
|
||||
{% elif form.mfa_type %}
|
||||
<div class="form-group" style="display: flex">
|
||||
{% include '_mfa_login_field.html' %}
|
||||
{% include '_mfa_login_field.html' %}
|
||||
</div>
|
||||
{% elif form.captcha %}
|
||||
<div class="captcha-field">
|
||||
@@ -408,25 +403,25 @@
|
||||
</div>
|
||||
|
||||
{% if demo_mode %}
|
||||
<div>
|
||||
<p class="red-fonts" style='text-align: center;'>
|
||||
{% trans 'Username' %}: demo {% trans 'Password' %}: jumpserver
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="red-fonts" style='text-align: center;'>
|
||||
{% trans 'Username' %}: demo {% trans 'Password' %}: jumpserver
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="more-login">
|
||||
{% if auth_methods %}
|
||||
<div class="more-methods-title {{ current_lang.code }}">
|
||||
{% trans "More login options" %}
|
||||
{% trans "More login options" %}
|
||||
</div>
|
||||
<div class="more-login-items">
|
||||
{% for method in auth_methods %}
|
||||
<a href="{{ method.url }}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ method.logo }}" height="15" width="15"/> </i>
|
||||
{{ method.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% for method in auth_methods %}
|
||||
<a href="{{ method.url }}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ method.logo }}" height="15" width="15"/> </i>
|
||||
{{ method.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center" style="display: inline-block;">
|
||||
@@ -452,16 +447,14 @@
|
||||
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
|
||||
$('#login-form').submit(); //post提交
|
||||
}
|
||||
|
||||
function checkHealth() {
|
||||
let url = "{% url 'health' %}";
|
||||
let url = "{% url 'health' %}";
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "GET",
|
||||
flash_message: false,
|
||||
})
|
||||
}
|
||||
|
||||
setInterval(checkHealth, 30 * 1000);
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -15,33 +15,6 @@
|
||||
.btn-sm i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.passwordBox2 {
|
||||
max-width: 560px;
|
||||
margin-bottom: auto;
|
||||
padding: 100px 20px 20px 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ibox-content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.ibox-context-margin {
|
||||
margin: 20px 0 0 0;
|
||||
}
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
||||
import datetime
|
||||
import os
|
||||
from typing import Callable
|
||||
from urllib.parse import urlparse, urlsplit, urlunsplit, urlencode
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
@@ -16,7 +16,7 @@ from django.http import HttpRequest
|
||||
from django.shortcuts import reverse, redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext as _, get_language, get_language_from_request
|
||||
from django.utils.translation import gettext as _, get_language
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
@@ -155,18 +155,9 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
|
||||
auth_name, redirect_url = auth_method['name'], auth_method['url']
|
||||
next_url = request.GET.get('next') or '/'
|
||||
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:
|
||||
message_data = {
|
||||
'title': _('Redirecting'),
|
||||
@@ -174,7 +165,7 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
|
||||
'redirect_url': redirect_url,
|
||||
'interval': 3,
|
||||
'has_cancel': True,
|
||||
'cancel_url': reverse('authentication:login') + f'?admin=1&{query}'
|
||||
'cancel_url': reverse('authentication:login') + f'?admin=1&{query_string}'
|
||||
}
|
||||
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
||||
return redirect_url
|
||||
@@ -307,11 +298,9 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
||||
from django.utils import timezone
|
||||
response = super().get(request, *args, **kwargs)
|
||||
try:
|
||||
|
||||
lang = request.user.lang if request.user.lang else get_language_from_request(request, check_path=False)
|
||||
response.set_cookie(
|
||||
settings.LANGUAGE_COOKIE_NAME,
|
||||
lang,
|
||||
request.user.lang,
|
||||
expires=timezone.now() + timezone.timedelta(days=365)
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -143,9 +143,7 @@ class EncryptMixin:
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
encryptor = Encryptor(value)
|
||||
plain_value = encryptor.decrypt()
|
||||
|
||||
plain_value = Encryptor(value).decrypt()
|
||||
# 可能和Json mix,所以要先解密,再json
|
||||
sp = super()
|
||||
if hasattr(sp, "from_db_value"):
|
||||
@@ -168,6 +166,9 @@ class EncryptMixin:
|
||||
class EncryptTextField(EncryptMixin, models.TextField):
|
||||
description = _("Encrypt field using Secret Key")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class EncryptCharField(EncryptMixin, models.CharField):
|
||||
@staticmethod
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user