Compare commits

...

148 Commits

Author SHA1 Message Date
Bryan
71766418bb Merge pull request #15742 from jumpserver/dev
merge: v4.10.4-lts
2025-07-17 15:12:58 +08:00
feng
8f91cb1473 perf: Translate 2025-07-17 15:12:01 +08:00
feng
b72e8eba7c perf: Change the secret and retry in batches 2025-07-17 14:21:31 +08:00
feng
d1d6f3fe9c perf: string_punctuation remove > ^ 2025-07-17 14:02:19 +08:00
wangruidong
6095c9865f fix: Action tips translate 2025-07-17 11:48:28 +08:00
wangruidong
6c374cb41f fix: View replay generate multiple operation logs 2025-07-17 11:24:16 +08:00
Eric
df64145adc perf: lion i18n 2025-07-16 19:49:07 +08:00
ibuler
44d77ba03f perf: random password exclude some char 2025-07-16 19:35:04 +08:00
wangruidong
3af188492f fix: Gather account failed 2025-07-16 19:19:43 +08:00
feng
9e798cd0b6 perf: Translate and tools version 2025-07-16 17:43:35 +08:00
feng
4d22c0722b fix: Exclude special char failed 2025-07-16 16:10:17 +08:00
Eric
e6a1662780 perf: add lion i18n 2025-07-15 18:58:42 +08:00
wangruidong
cc4be36752 perf: Log IntegrityError details during user authentication 2025-07-15 18:58:16 +08:00
wangruidong
e1f5d3c737 fix: Delete user failed(DoesNotExist) when user create share session 2025-07-15 18:43:43 +08:00
wangruidong
c0adc1fe74 fix: Gather account error 2025-07-15 18:43:14 +08:00
feng
613715135b perf: Translate 2025-07-15 11:46:39 +08:00
Eric
fe1d5f9828 perf: add en i18n 2025-07-11 15:34:24 +08:00
Eric
1d375e15c5 perf: add i18n keys 2025-07-11 15:34:24 +08:00
Eric
ac21d260ea perf: add lion i18n 2025-07-11 15:34:24 +08:00
wangruidong
accde77307 fix: Add third party login check is block 2025-07-11 15:33:48 +08:00
ibuler
c7dcf1ba59 perf: playbook task db save if conn timeout 2025-07-11 11:00:20 +08:00
wangruidong
b564bbebb3 perf: Translate 2025-07-11 10:30:40 +08:00
Eric
9440c855f4 perf: add lion i18n 2025-07-10 12:50:11 +08:00
w940853815
f282b2079e Update comment 2025-07-10 11:39:37 +08:00
wangruidong
1790cd8345 fix: Add additional third-party authentication backends and adjust MFA check 2025-07-10 11:39:37 +08:00
ibuler
7da74dc6e8 fix: integrate with azure oidc 2025-07-10 11:33:41 +08:00
Ewall555
33b0068f49 feat: exclude SSO token permissions for change and delete actions 2025-07-10 11:29:18 +08:00
Ewall555
9a446c118b feat: support rbac SSO token 2025-07-10 11:29:18 +08:00
Eric
4bf337b2b4 perf: add VNC terminal type 2025-07-10 11:28:32 +08:00
wangruidong
2acbb80920 perf: Add account date_expired 2025-07-09 10:47:06 +08:00
gerry-f2c
ae859c5562 perf: dbeaver uses a fixed driver directory (#15689) 2025-07-08 18:02:24 +08:00
Eric
a9bc716af5 perf: add encrypt field for sqlserver 2008 2025-07-08 18:01:31 +08:00
feng
2d5401e76e perf: Translate 2025-07-08 16:01:52 +08:00
Gerry.tan
d933e296bc perf: ES command log supports fuzzy search 2025-07-08 11:25:44 +08:00
wangruidong
1e5a995917 fix: Ticket filter error 2025-07-08 10:42:40 +08:00
wangruidong
baaaf83ab9 perf: Translate 2025-07-08 10:35:04 +08:00
wangruidong
ab06ac1f1f perf: Update IP group validation to include address validation 2025-07-08 10:34:34 +08:00
jiangweidong
99c4622ccb fix: SSO access to web assets with encrypted password auto-filling 2025-07-08 10:19:32 +08:00
Eric
9bdfab966f perf: add replay_size on session 2025-07-08 10:18:54 +08:00
老广
1a1acb62de Update README.md 2025-07-08 10:16:43 +08:00
wanghe-fit2cloud
2a128ea01b docs: Add GitCode badges 2025-07-07 15:33:08 +08:00
王贺
5a720b41bf docs: Add GitCode badge 2025-07-07 13:42:15 +08:00
feng
726c5cf34d fix: View replay record operate log 2025-07-07 10:37:29 +08:00
wangruidong
06afc8a0e1 perf: Translate 2025-07-02 19:04:15 +08:00
ibuler
276fd928a7 perf: add pg client 2025-07-01 16:18:25 +08:00
dependabot[bot]
05c6272d7e chore(deps): bump requests from 2.31.0 to 2.32.4
Bumps [requests](https://github.com/psf/requests) from 2.31.0 to 2.32.4.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.31.0...v2.32.4)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 15:59:44 +08:00
Bai
c3f877d116 fix: check count 2025-07-01 15:35:17 +08:00
wangruidong
60deef2abf fix: Org admin cannot use system tools. 2025-07-01 15:23:34 +08:00
wangruidong
058754dc1b perf: Translate time cost 2025-06-30 10:05:13 +08:00
feng
a238c5d34b perf: operate record 2025-06-27 19:03:24 +08:00
feng626
76c6ed0f95 Merge pull request #15649 from jumpserver/pr@dev@translate
perf: Translate
2025-06-27 14:01:16 +08:00
feng626
0d07f7421b Merge branch 'dev' into pr@dev@translate 2025-06-27 14:01:05 +08:00
Ewall555
b07d4e207c perf: Translate 2025-06-27 13:59:57 +08:00
feng
dc92963059 perf: Translate 2025-06-27 13:15:07 +08:00
feng
9abd708a0a fix: ES search session count 2025-06-27 10:32:28 +08:00
jiangweidong
c9270877eb fix: According to the CMPP2.0 protocol standard, modify the attribute alignment. 2025-06-26 18:41:30 +08:00
feng
b5518dd2ba perf: Pam perm tree 2025-06-26 18:14:57 +08:00
wangruidong
1d40f5ecbc fix: Handle ValidationError in account_obj property 2025-06-26 15:23:16 +08:00
ibuler
91fee6c034 perf: change some 18n 2025-06-26 14:54:06 +08:00
feng
1b65055c5e perf: Account backup backup_type limit 2025-06-23 18:27:27 +08:00
feng
e79ef516a5 perf: Change secret windoes translate 2025-06-23 15:59:55 +08:00
Ewall555
8843f247d6 fix: Use local Python interpreter variable in RDP automation scripts 2025-06-23 14:15:46 +08:00
ibuler
cb42df542d fix: bitwardne request data encode 2025-06-23 14:13:15 +08:00
Ewall555
46ddad1d59 perf: Update metismenu plugin to version 3.0.7 2025-06-23 14:11:23 +08:00
Bryan
a9399dd709 Merge pull request #15608 from jumpserver/dev
v4.10.2
2025-06-19 20:14:21 +08:00
feng
f55a6ae364 perf: Translate 2025-06-19 20:09:06 +08:00
feng
34772b8fb4 fix: client login get session_key 2025-06-19 18:48:46 +08:00
feng
286891061b perf: face acl 2025-06-19 17:58:08 +08:00
feng
269bf1283e perf: Translate 2025-06-19 17:07:25 +08:00
Bai
d32c11bced fix: login page i18n same with personal settings 2025-06-19 16:47:53 +08:00
feng
fb64af2eb2 perf: Translate 2025-06-19 16:17:09 +08:00
Eric
82f32cbba3 perf: replace koko category to luna 2025-06-19 16:09:41 +08:00
wangruidong
411b485448 fix: The title of the risk detection email and server performance check notification i18n 2025-06-19 15:33:46 +08:00
feng
60608e92ea fix: Connection failure after directly connecting to asset ACL 2025-06-19 15:28:12 +08:00
feng
6922c62b50 perf: Translate 2025-06-19 11:49:47 +08:00
Eric
531c23d983 perf: add lina i18n 2025-06-19 10:22:24 +08:00
feng
df9e6cf866 perf: Translate 2025-06-18 16:37:33 +08:00
halo
65f2b92eb3 perf: Update client version and perf translate 2025-06-17 19:26:59 +08:00
feng626
bac621991e Merge pull request #15592 from jumpserver/pr@dev@translate
perf: Translate
2025-06-17 19:26:52 +08:00
feng626
db24d34b64 Merge branch 'dev' into pr@dev@translate 2025-06-17 19:25:59 +08:00
feng
265c066054 perf: Translate 2025-06-17 19:23:55 +08:00
feng
dad6b5def0 perf: Translate 2025-06-17 18:51:39 +08:00
feng
00f6c3a5de perf: Change secret record 2025-06-17 17:11:25 +08:00
wangruidong
cfbd162890 fix: Correct language retrieval in profile to use the provided object 2025-06-16 19:04:45 +08:00
wangruidong
17e8f25cb4 fix: Update language preference setting to include category 2025-06-16 19:04:45 +08:00
wangruidong
71bf8c8699 fix: The luna page cannot switch language settings 2025-06-16 19:04:45 +08:00
wangruidong
98342e0b70 fix: The login page cannot switch language settings 2025-06-16 19:04:45 +08:00
wangruidong
70aaa9cf8f fix: Activate user language when sending emails 2025-06-16 19:04:45 +08:00
Aaron3S
70b2d28760 feat: remove fuzzy 2025-06-16 18:40:18 +08:00
Aaron3S
8265a069e2 feat: translate 2025-06-16 18:40:18 +08:00
feng
9ec48aae0c perf: Translate 2025-06-16 14:55:23 +08:00
feng
41658af8fd perf: Suggestion api 2025-06-16 14:03:27 +08:00
fit2bot
7dfb31840e tinkner request ak first 2025-06-16 11:39:20 +08:00
ibuler
2f55db60ec perf: change redirect client auth to session 2025-06-13 10:41:28 +08:00
ibuler
551e6d0479 perf: client login redirect 2025-06-13 10:41:28 +08:00
feng
61c54314d7 perf: Face translate 2025-06-12 18:55:35 +08:00
wangruidong
4e7cd37c1d fix: Ensure user language is activated when sending notifications 2025-06-12 18:30:22 +08:00
wangruidong
e89f43dcd3 perf: Translate 2025-06-12 18:30:22 +08:00
wangruidong
259ead4c6e fix: Prevent nested resource issues in type nodes tree API 2025-06-12 18:29:22 +08:00
feng
348b2a833a perf: Translate 2025-06-12 18:28:33 +08:00
feng
8aec1604ce perf: Change secret clear account queue status 2025-06-12 16:55:04 +08:00
feng
be28a6954a perf: Login to change password and filter out useless accounts 2025-06-11 19:16:29 +08:00
feng
79c2284a01 perf: Change secret after successful login 2025-06-11 18:41:31 +08:00
feng
c2b44cfd84 perf: Translate 2025-06-11 16:36:55 +08:00
ibuler
1e07cba545 perf: open svc account register on deploy 2025-06-11 13:32:00 +08:00
ibuler
48a9b2664a perf: change ftplog asset length 2025-06-11 13:27:23 +08:00
ZhaoJiSen
b3bfbf5046 Merge pull request #15550 from jumpserver/pr@dev@send_mail_async
perf: send_mail_async func log subject recipients info
2025-06-11 11:17:25 +08:00
feng
08aa1e48b9 perf: send_mail_async func log subject recipients info 2025-06-11 11:16:03 +08:00
ZhaoJiSen
97d7427090 Merge pull request #15549 from jumpserver/pr@dev@translate
perf: Translate
2025-06-10 19:14:21 +08:00
feng
9f9d5855c4 perf: Translate 2025-06-10 19:12:31 +08:00
ZhaoJiSen
2db8f0f444 Merge pull request #15546 from jumpserver/pr@dev@send_mail_async
perf: send_mail_async add log
2025-06-10 17:47:22 +08:00
feng
b75210b0c3 perf: send_mail_async add log 2025-06-10 17:45:59 +08:00
wangruidong
4713c6ddf6 fix: Task search error 2025-06-10 16:58:37 +08:00
feng
b70fb58faf perf: Change secret after successful login 2025-06-10 16:57:28 +08:00
Aaron3S
3991976a00 feat: magnus support mongodb 2025-06-10 15:51:12 +08:00
ewall555
90256208dd perf: Update jsencrypt library version 2025-06-09 18:43:18 +08:00
wangruidong
bbd3b32aa1 perf: Remove username hint 2025-06-09 16:58:51 +08:00
wangruidong
ec20a4fd02 fix: Failed to update database assets 2025-06-09 15:04:27 +08:00
wangruidong
d179ce1cd4 perf: Add celery worker count config 2025-06-09 14:02:31 +08:00
ZhaoJiSen
caf23f5b05 Merge pull request #15529 from jumpserver/pr@dev@translate
perf: Translate
2025-06-06 18:23:10 +08:00
feng626
4bb19d59ef Merge branch 'dev' into pr@dev@translate 2025-06-06 18:22:24 +08:00
feng
74ed693a95 perf: Translate 2025-06-06 18:19:44 +08:00
feng
4a7a1fd95c perf: Optimize the results returned by the suggestion api for different organizations 2025-06-06 18:09:05 +08:00
wangruidong
56268433e0 perf: Translate adhoc 2025-06-06 17:56:53 +08:00
ibuler
ea59677b13 perf: swagger auth required 2025-06-06 17:56:25 +08:00
ibuler
94ed26e115 perf: change i18n 2025-06-06 17:56:25 +08:00
wangruidong
284d793253 perf: leak password can bulk delete 2025-06-06 17:05:13 +08:00
wangruidong
570566d9dd perf: set ansible_timeout for account connectivity tasks 2025-06-04 18:41:41 +08:00
wangruidong
3f85c67aee perf: Add retention period for expired user tokens and implement cleanup task 2025-06-04 18:39:49 +08:00
wangruidong
53a84850dc fix: Ensure platform_id is a digit before querying Platform 2025-06-04 18:37:05 +08:00
feng
e4be9621bb perf: Custom push account 2025-06-03 14:52:06 +08:00
Eric
f8b778ada2 perf: tinker to v0.2.2 2025-06-03 13:54:21 +08:00
fit2bot
5c28b15e39 perf: update chrome applet to support language setting (#15509)
* perf: update chrome applet to support language setting

* perf: fix field name

---------

Co-authored-by: Eric <xplzv@126.com>
2025-06-03 13:54:04 +08:00
wangruidong
5e0babdba8 perf: Language settings in personal settings 2025-05-29 11:13:04 +08:00
feng
8a3acb649e fix: ES non-global organizations cannot be queried 2025-05-27 14:31:27 +08:00
Eric
1ade652381 perf: upgrade tinker to v0.2.1 2025-05-23 11:20:17 +08:00
github-actions[bot]
7472f83d7a Auto-translate README 2025-05-22 18:34:57 +08:00
Bai
c56a3d0a2e perf: add ko readme 2025-05-22 18:10:34 +08:00
feng
1a10225823 perf: view task log 2025-05-22 17:44:19 +08:00
feng
56c94d7b3c fix: The account suggestions api cannot find the account associated with the DS 2025-05-22 11:50:17 +08:00
ibuler
16e7a12974 perf: static file download and catch 2025-05-20 13:14:47 +08:00
ibuler
1364889083 fix: aggregate resource api 2025-05-20 13:14:21 +08:00
feng
4f19954640 perf: SSO add mfa 2025-05-20 13:12:13 +08:00
feng
1b2e376681 perf: Account list not display spec_info field 2025-05-19 16:13:36 +08:00
ibuler
14c5162153 perf: client auth changed 2025-05-19 11:27:31 +08:00
Bai
f9245e17cd perf: readme 2025-05-16 18:43:39 +08:00
Aaron3S
6bd1ec960b feat: add a new piico gm alg 2025-05-16 15:07:39 +08:00
ibuler
77cc02ae60 perf: change google authenticator apk download 2025-05-15 17:41:49 +08:00
211 changed files with 8592 additions and 5622 deletions

11
.prettierrc Normal file
View File

@@ -0,0 +1,11 @@
{
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 100,
"endOfLine": "lf"
}

View File

@@ -13,6 +13,7 @@ ARG TOOLS=" \
nmap \
telnet \
vim \
postgresql-client-13 \
wget"
RUN set -ex \

View File

@@ -12,7 +12,7 @@
[![][github-release-shield]][github-release-link]
[![][github-stars-shield]][github-stars-link]
[English](/README.md) · [中文(简体)](/readmes/README.zh-hans.md) · [中文(繁體)](/readmes/README.zh-hant.md) · [日本語](/readmes/README.ja.md) · [Português (Brasil)](/readmes/README.pt-br.md) · [Español](/readmes/README.es.md) · [Русский](/readmes/README.ru.md)
[English](/README.md) · [中文(简体)](/readmes/README.zh-hans.md) · [中文(繁體)](/readmes/README.zh-hant.md) · [日本語](/readmes/README.ja.md) · [Português (Brasil)](/readmes/README.pt-br.md) · [Español](/readmes/README.es.md) · [Русский](/readmes/README.ru.md) · [한국어](/readmes/README.ko.md)
</div>
<br/>
@@ -23,8 +23,8 @@ JumpServer is an open-source Privileged Access Management (PAM) tool that provid
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/dd612f3d-c958-4f84-b164-f31b75454d7f">
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/28676212-2bc4-4a9f-ae10-3be9320647e3">
<source media="(prefers-color-scheme: light)" srcset="https://www.jumpserver.com/images/jumpserver-arch-light.png">
<source media="(prefers-color-scheme: dark)" srcset="https://www.jumpserver.com/images/jumpserver-arch-dark.png">
<img src="https://github.com/user-attachments/assets/dd612f3d-c958-4f84-b164-f31b75454d7f" alt="Theme-based Image">
</picture>
@@ -85,6 +85,8 @@ JumpServer consists of multiple key components, which collectively form the func
| [Nec](https://github.com/jumpserver/nec) | <img alt="Nec" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE VNC Proxy Connector |
| [Facelive](https://github.com/jumpserver/facelive) | <img alt="Facelive" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Facial Recognition |
## Third-party projects
- [jumpserver-grafana-dashboard](https://github.com/acerrah/jumpserver-grafana-dashboard) JumpServer with grafana dashboard
## Contributing

View File

@@ -78,18 +78,25 @@ class AccountViewSet(OrgBulkModelViewSet):
permission_classes=[IsValidUser]
)
def username_suggestions(self, request, *args, **kwargs):
asset_ids = request.data.get('assets', [])
raw_asset_ids = request.data.get('assets', [])
node_ids = request.data.get('nodes', [])
username = request.data.get('username', '')
accounts = Account.objects.all()
asset_ids = set(raw_asset_ids)
if node_ids:
nodes = Node.objects.filter(id__in=node_ids)
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
asset_ids.extend(node_asset_ids)
node_asset_qs = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
asset_ids |= {str(u) for u in node_asset_qs}
if asset_ids:
accounts = accounts.filter(asset_id__in=list(set(asset_ids)))
through = Asset.directory_services.through
ds_qs = through.objects.filter(asset_id__in=asset_ids) \
.values_list('directoryservice_id', flat=True)
asset_ids |= {str(u) for u in ds_qs}
accounts = Account.objects.filter(asset_id__in=list(asset_ids))
else:
accounts = Account.objects.all()
if username:
accounts = accounts.filter(username__icontains=username)

View File

@@ -43,6 +43,7 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
search_fields = ('username', 'name')
serializer_classes = {
'default': serializers.AccountTemplateSerializer,
'retrieve': serializers.AccountDetailTemplateSerializer,
}
rbac_perms = {
'su_from_account_templates': 'accounts.view_accounttemplate',

View File

@@ -6,10 +6,13 @@ from rest_framework.decorators import action
from rest_framework.response import Response
from accounts import serializers
from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice
from accounts.filters import ChangeSecretRecordFilterSet
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
from accounts.const import (
AutomationTypes, ChangeSecretRecordStatusChoice
)
from accounts.filters import ChangeSecretRecordFilterSet, ChangeSecretStatusFilterSet
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, Account
from accounts.tasks import execute_automation_record_task
from accounts.utils import account_secret_task_status
from authentication.permissions import UserConfirmation, ConfirmType
from common.permissions import IsValidLicense
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
@@ -23,7 +26,7 @@ __all__ = [
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet',
'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi',
'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi',
'ChangSecretNodeAddRemoveApi'
'ChangSecretNodeAddRemoveApi', 'ChangeSecretStatusViewSet'
]
@@ -94,12 +97,13 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
def execute(self, request, *args, **kwargs):
record_ids = request.data.get('record_ids')
records = self.get_queryset().filter(id__in=record_ids)
execution_count = records.values_list('execution_id', flat=True).distinct().count()
if execution_count != 1:
if not records.exists():
return Response(
{'detail': 'Only one execution is allowed to execute'},
{'detail': 'No valid records found'},
status=status.HTTP_400_BAD_REQUEST
)
record_ids = [str(_id) for _id in records.values_list('id', flat=True)]
task = execute_automation_record_task.delay(record_ids, self.tp)
return Response({'task': task.id}, status=status.HTTP_200_OK)
@@ -154,3 +158,25 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi):
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
model = ChangeSecretAutomation
serializer_class = serializers.ChangeSecretUpdateNodeSerializer
class ChangeSecretStatusViewSet(OrgBulkModelViewSet):
perm_model = ChangeSecretAutomation
filterset_class = ChangeSecretStatusFilterSet
serializer_class = serializers.ChangeSecretAccountSerializer
search_fields = ('username',)
permission_classes = [RBACPermission, IsValidLicense]
http_method_names = ["get", "delete", "options"]
def get_queryset(self):
account_ids = list(account_secret_task_status.account_ids)
return Account.objects.filter(id__in=account_ids).select_related('asset')
def bulk_destroy(self, request, *args, **kwargs):
account_ids = request.data.get('account_ids')
if isinstance(account_ids, str):
account_ids = [account_ids]
for _id in account_ids:
account_secret_task_status.clear(_id)
return Response(status=status.HTTP_200_OK)

View File

@@ -5,9 +5,10 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.automations.methods import platform_automation_methods
from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice
from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice, \
ChangeSecretAccountStatus
from accounts.models import BaseAccountQuerySet
from accounts.utils import SecretGenerator
from accounts.utils import SecretGenerator, account_secret_task_status
from assets.automations.base.manager import BasePlaybookManager
from assets.const import HostTypes
from common.db.utils import safe_atomic_db_connection
@@ -36,7 +37,7 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
)
self.account_ids = self.execution.snapshot['accounts']
self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试
self.name_recorder_mapper = {} # 做个映射,方便后面处理
self.name_record_mapper = {} # 做个映射,方便后面处理
def gen_account_inventory(self, account, asset, h, path_dir):
raise NotImplementedError
@@ -112,10 +113,15 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
if host.get('error'):
return host
host['check_conn_after_change'] = self.execution.snapshot.get('check_conn_after_change', True)
host['ssh_params'] = {}
accounts = self.get_accounts(account)
existing_ids = set(map(str, accounts.values_list('id', flat=True)))
missing_ids = set(map(str, self.account_ids)) - existing_ids
for account_id in missing_ids:
self.clear_account_queue_status(account_id)
error_msg = _("No pending accounts found")
if not accounts:
print(f'{asset}: {error_msg}')
@@ -132,31 +138,50 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
for account in accounts:
h = deepcopy(host)
h['name'] += '(' + account.username + ')' # To distinguish different accounts
account_status = account_secret_task_status.get_status(account.id)
if account_status == ChangeSecretAccountStatus.PROCESSING:
h['error'] = f'Account is already being processed, skipping: {account}'
inventory_hosts.append(h)
continue
try:
h = self.gen_account_inventory(account, asset, h, path_dir)
h, record = self.gen_account_inventory(account, asset, h, path_dir)
h['check_conn_after_change'] = record.execution.snapshot.get('check_conn_after_change', True)
account_secret_task_status.set_status(
account.id,
ChangeSecretAccountStatus.PROCESSING,
metadata={'execution_id': self.execution.id}
)
except Exception as e:
h['error'] = str(e)
self.clear_account_queue_status(account.id)
inventory_hosts.append(h)
return inventory_hosts
@staticmethod
def save_record(recorder):
recorder.save(update_fields=['error', 'status', 'date_finished'])
def save_record(record):
record.save(update_fields=['error', 'status', 'date_finished'])
@staticmethod
def clear_account_queue_status(account_id):
account_secret_task_status.clear(account_id)
def on_host_success(self, host, result):
recorder = self.name_recorder_mapper.get(host)
if not recorder:
record = self.name_record_mapper.get(host)
if not record:
return
recorder.status = ChangeSecretRecordStatusChoice.success.value
recorder.date_finished = timezone.now()
record.status = ChangeSecretRecordStatusChoice.success.value
record.date_finished = timezone.now()
account = recorder.account
account = record.account
if not account:
print("Account not found, deleted ?")
return
account.secret = getattr(recorder, 'new_secret', account.secret)
account.secret = getattr(record, 'new_secret', account.secret)
account.date_updated = timezone.now()
account.date_change_secret = timezone.now()
account.change_secret_status = ChangeSecretRecordStatusChoice.success
@@ -172,16 +197,17 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
with safe_atomic_db_connection():
account.save(update_fields=['secret', 'date_updated', 'date_change_secret', 'change_secret_status'])
self.save_record(recorder)
self.save_record(record)
self.clear_account_queue_status(account.id)
def on_host_error(self, host, error, result):
recorder = self.name_recorder_mapper.get(host)
if not recorder:
record = self.name_record_mapper.get(host)
if not record:
return
recorder.status = ChangeSecretRecordStatusChoice.failed.value
recorder.date_finished = timezone.now()
recorder.error = error
account = recorder.account
record.status = ChangeSecretRecordStatusChoice.failed.value
record.date_finished = timezone.now()
record.error = error
account = record.account
if not account:
print("Account not found, deleted ?")
return
@@ -192,12 +218,13 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
self.summary['fail_accounts'] += 1
self.result['fail_accounts'].append(
{
"asset": str(recorder.asset),
"username": recorder.account.username,
"asset": str(record.asset),
"username": record.account.username,
}
)
super().on_host_error(host, error, result)
with safe_atomic_db_connection():
account.save(update_fields=['change_secret_status', 'date_change_secret', 'date_updated'])
self.save_record(recorder)
self.save_record(record)
self.clear_account_queue_status(account.id)

View File

@@ -16,9 +16,9 @@ params:
i18n:
Windows account change secret rdp verify:
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密 RDP 协议测试最后的可连接性'
ja: 'Ansibleモジュールwin_userWindowsアカウントの改密RDPプロトコルテストの最後の接続性を実行する'
en: 'Using the Ansible module win_user performs Windows account encryption RDP protocol testing for final connectivity'
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密(最后使用 Python 模块 pyfreerdp 验证账号的可连接性'
ja: 'Ansible モジュール win_user を使用して Windows アカウントのパスワードを変更します (最後に Python モジュール pyfreerdp を使用してアカウントの接続を確認します)'
en: 'Use the Ansible module win_user to change the Windows account password (finally use the Python module pyfreerdp to verify the account connectivity)'
Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'

View File

@@ -30,28 +30,28 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
record = self.get_or_create_record(asset, account, h['name'])
new_secret, private_key_path = self.handle_ssh_secret(account.secret_type, record.new_secret, path_dir)
h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
return h
return h, record
def get_or_create_record(self, asset, account, name):
asset_account_id = f'{asset.id}-{account.id}'
if asset_account_id in self.record_map:
record_id = self.record_map[asset_account_id]
recorder = ChangeSecretRecord.objects.filter(id=record_id).first()
record = ChangeSecretRecord.objects.filter(id=record_id).first()
else:
new_secret = self.get_secret(account)
recorder = self.create_record(asset, account, new_secret)
record = self.create_record(asset, account, new_secret)
self.name_recorder_mapper[name] = recorder
return recorder
self.name_record_mapper[name] = record
return record
def create_record(self, asset, account, new_secret):
recorder = ChangeSecretRecord(
record = ChangeSecretRecord(
asset=asset, account=account, execution=self.execution,
old_secret=account.secret, new_secret=new_secret,
comment=f'{account.username}@{asset.address}'
)
return recorder
return record
def check_secret(self):
if self.secret_strategy == SecretStrategy.custom \
@@ -61,10 +61,10 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
return True
@staticmethod
def get_summary(recorders):
def get_summary(records):
total, succeed, failed = 0, 0, 0
for recorder in recorders:
if recorder.status == ChangeSecretRecordStatusChoice.success.value:
for record in records:
if record.status == ChangeSecretRecordStatusChoice.success.value:
succeed += 1
else:
failed += 1
@@ -73,8 +73,8 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
return summary
def print_summary(self):
recorders = list(self.name_recorder_mapper.values())
summary = self.get_summary(recorders)
records = list(self.name_record_mapper.values())
summary = self.get_summary(records)
print('\n\n' + '-' * 80)
plan_execution_end = _('Plan execution end')
print('{} {}\n'.format(plan_execution_end, local_now_filename()))
@@ -86,7 +86,7 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
if self.secret_type and not self.check_secret():
return
recorders = list(self.name_recorder_mapper.values())
records = list(self.name_record_mapper.values())
if self.record_map:
return
@@ -98,17 +98,17 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
for user in recipients:
ChangeSecretReportMsg(user, context).publish()
if not recorders:
if not records:
return
summary = self.get_summary(recorders)
self.send_recorder_mail(recipients, recorders, summary)
summary = self.get_summary(records)
self.send_record_mail(recipients, records, summary)
def send_recorder_mail(self, recipients, recorders, summary):
def send_record_mail(self, recipients, records, summary):
name = self.execution.snapshot['name']
path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx')
if not self.create_file(recorders, filename):
if not self.create_file(records, filename):
return
for user in recipients:
@@ -121,9 +121,9 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
os.remove(filename)
@staticmethod
def create_file(recorders, filename):
def create_file(records, filename):
serializer_cls = ChangeSecretRecordBackUpSerializer
serializer = serializer_cls(recorders, many=True)
serializer = serializer_cls(records, many=True)
header = [str(v.label) for v in serializer.child.fields.values()]
rows = [[str(i) for i in row.values()] for row in serializer.data]

View File

@@ -15,11 +15,13 @@ from common.decorators import bulk_create_decorator, bulk_update_decorator
from settings.models import LeakPasswords
# 已设置手动 finish
@bulk_create_decorator(AccountRisk)
def create_risk(data):
return AccountRisk(**data)
# 已设置手动 finish
@bulk_update_decorator(AccountRisk, update_fields=["details", "status"])
def update_risk(risk):
return risk
@@ -217,6 +219,9 @@ class CheckAccountManager(BaseManager):
"details": [{"datetime": now, 'type': 'init'}],
})
create_risk.finish()
update_risk.finish()
def pre_run(self):
super().pre_run()
self.assets = self.execution.get_all_assets()
@@ -264,7 +269,7 @@ class CheckAccountManager(BaseManager):
handler.clean()
def get_report_subject(self):
return "Check account report of %s" % self.execution.id
return _("Check account report of {}").format(self.execution.id)
def get_report_template(self):
return "accounts/check_account_report.html"

View File

@@ -30,6 +30,16 @@ common_risk_items = [
diff_items = risk_items + common_risk_items
@bulk_create_decorator(AccountRisk)
def _create_risk(data):
return AccountRisk(**data)
@bulk_update_decorator(AccountRisk, update_fields=["details"])
def _update_risk(account):
return account
def format_datetime(value):
if isinstance(value, timezone.datetime):
return value.strftime("%Y-%m-%d %H:%M:%S")
@@ -141,25 +151,17 @@ class AnalyseAccountRisk:
found = assets_risks.get(key)
if not found:
self._create_risk(dict(**d, details=[detail]))
_create_risk(dict(**d, details=[detail]))
continue
found.details.append(detail)
self._update_risk(found)
@bulk_create_decorator(AccountRisk)
def _create_risk(self, data):
return AccountRisk(**data)
@bulk_update_decorator(AccountRisk, update_fields=["details"])
def _update_risk(self, account):
return account
_update_risk(found)
def lost_accounts(self, asset, lost_users):
if not self.check_risk:
return
for user in lost_users:
self._create_risk(
_create_risk(
dict(
asset_id=str(asset.id),
username=user,
@@ -176,7 +178,7 @@ class AnalyseAccountRisk:
self._analyse_item_changed(ga, d)
if not sys_found:
basic = {"asset": asset, "username": d["username"], 'gathered_account': ga}
self._create_risk(
_create_risk(
dict(
**basic,
risk=RiskChoice.new_found,
@@ -388,6 +390,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
self.update_gathered_account(ori_account, d)
ori_found = username in ori_users
need_analyser_gather_account.append((asset, ga, d, ori_found))
# 这里顺序不能调整risk 外键关联了 gathered_account 主键 id所以在创建 risk 需要保证 gathered_account 已经创建完成
self.create_gathered_account.finish()
self.update_gathered_account.finish()
for analysis_data in need_analyser_gather_account:
@@ -403,6 +406,9 @@ class GatherAccountsManager(AccountBasePlaybookManager):
present=True
)
# 因为有 bulk create, bulk update, 所以这里需要 sleep 一下,等待数据同步
_update_risk.finish()
_create_risk.finish()
time.sleep(0.5)
def get_report_template(self):

View File

@@ -20,10 +20,11 @@
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
register: ping_info
delegate_to: localhost
- name: Change asset password (paramiko)
- name: Push asset password (paramiko)
custom_command:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
@@ -39,7 +40,10 @@
name: "{{ account.username }}"
password: "{{ account.secret }}"
commands: "{{ params.commands }}"
first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}"
answers: "{{ params.answers }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
delay_time: "{{ params.delay_time | default(2) }}"
prompt: "{{ params.prompt | default('.*') }}"
ignore_errors: true
when: ping_info is succeeded and check_conn_after_change
register: change_info
@@ -58,5 +62,6 @@
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
delegate_to: localhost
when: check_conn_after_change

View File

@@ -10,10 +10,30 @@ protocol: ssh
priority: 50
params:
- name: commands
type: list
type: text
label: "{{ 'Params commands label' | trans }}"
default: [ '' ]
default: ''
help_text: "{{ 'Params commands help text' | trans }}"
- name: recv_timeout
type: int
label: "{{ 'Params recv_timeout label' | trans }}"
default: 30
help_text: "{{ 'Params recv_timeout help text' | trans }}"
- name: delay_time
type: int
label: "{{ 'Params delay_time label' | trans }}"
default: 2
help_text: "{{ 'Params delay_time help text' | trans }}"
- name: prompt
type: str
label: "{{ 'Params prompt label' | trans }}"
default: '.*'
help_text: "{{ 'Params prompt help text' | trans }}"
- name: answers
type: text
label: "{{ 'Params answer label' | trans }}"
default: '.*'
help_text: "{{ 'Params answer help text' | trans }}"
i18n:
SSH account push:
@@ -22,11 +42,91 @@ i18n:
en: 'Custom push using SSH command line'
Params commands help text:
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 &#123;username&#125;、&#123;password&#125;、&#123;login_password&#125;格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. &#123;login_password&#125;<br />3. configure terminal<br />4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end'
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />&#123;ユーザー名&#125;、&#123;パスワード&#125;、&#123;login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.&#123;login_password&#125;<br />3 .ターミナルの設定<br / >4. ユーザー名 &#123;ユーザー名&#125; 権限 0 パスワード &#123;パスワード&#125; <br />5. 終了'
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use &#123;username&#125;, &#123;password&#125;, &#123;login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. &#123;login_password&#125;<br />3. configure terminal<br / >4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end'
zh: |
请将命令中的指定位置改成特殊符号 <br />
1. 推送账号 -> {username} <br />
2. 推送密码 -> {password} <br />
3. 登录用户密码 -> {login_password} <br />
<strong>多条命令使用换行分割,</strong>执行任务时系统会根据特殊符号替换真实数据。<br />
比如针对 Cisco 主机进行推送,一般需要配置五条命令:<br />
enable <br />
{login_password} <br />
configure terminal <br />
username {username} privilege 0 password {password} <br />
end <br />
ja: |
コマンド内の指定された位置を特殊記号に変更してください。<br />
新しいパスワード(アカウント押す) -> {username} <br />
新しいパスワード(パスワード押す) -> {password} <br />
ログインユーザーパスワード -> {login_password} <br />
<strong>複数のコマンドは改行で区切り、</strong>タスクを実行するときにシステムは特殊記号を使用して実際のデータを置き換えます。<br />
例えば、Cisco機器のパスワードを変更する場合、一般的には5つのコマンドを設定する必要があります<br />
enable <br />
{login_password} <br />
configure terminal <br />
username {username} privilege 0 password {password} <br />
end <br />
en: |
Please change the specified positions in the command to special symbols. <br />
Change password account -> {username} <br />
Change password -> {password} <br />
Login user password -> {login_password} <br />
<strong>Multiple commands are separated by new lines,</strong> and when executing tasks, <br />
the system will replace the special symbols with real data. <br />
For example, to push the password for a Cisco device, you generally need to configure five commands: <br />
enable <br />
{login_password} <br />
configure terminal <br />
username {username} privilege 0 password {password} <br />
end <br />
Params commands label:
zh: '自定义命令'
ja: 'カスタムコマンド'
en: 'Custom command'
Params recv_timeout label:
zh: '超时时间'
ja: 'タイムアウト'
en: 'Timeout'
Params recv_timeout help text:
zh: '等待命令结果返回的超时时间(秒)'
ja: 'コマンドの結果を待つタイムアウト時間(秒)'
en: 'The timeout for waiting for the command result to return (Seconds)'
Params delay_time label:
zh: '延迟发送时间'
ja: '遅延送信時間'
en: 'Delayed send time'
Params delay_time help text:
zh: '每条命令延迟发送的时间间隔(秒)'
ja: '各コマンド送信の遅延間隔(秒)'
en: 'Time interval for each command delay in sending (Seconds)'
Params prompt label:
zh: '提示符'
ja: 'ヒント'
en: 'Prompt'
Params prompt help text:
zh: '终端连接后显示的提示符信息(正则表达式)'
ja: 'ターミナル接続後に表示されるプロンプト情報(正規表現)'
en: 'Prompt information displayed after terminal connection (Regular expression)'
Params answer label:
zh: '命令结果'
ja: 'コマンド結果'
en: 'Command result'
Params answer help text:
zh: |
根据结果匹配度决定是否执行下一条命令,输入框的内容和上方 “自定义命令” 内容按行一一对应(正则表达式)
ja: |
結果の一致度に基づいて次のコマンドを実行するかどうかを決定します。
入力欄の内容は、上の「カスタムコマンド」の内容と行ごとに対応しています(せいきひょうげん)
en: |
Decide whether to execute the next command based on the result match.
The input content corresponds line by line with the content
of the `Custom command` above. (Regular expression)

View File

@@ -12,7 +12,7 @@ logger = get_logger(__name__)
class PushAccountManager(BaseChangeSecretPushManager):
@staticmethod
def require_update_version(account, recorder):
def require_update_version(account, record):
account.skip_history_when_saving = True
return False
@@ -31,29 +31,29 @@ class PushAccountManager(BaseChangeSecretPushManager):
secret_type = account.secret_type
if not secret:
raise ValueError(_('Secret cannot be empty'))
self.get_or_create_record(asset, account, h['name'])
record = self.get_or_create_record(asset, account, h['name'])
new_secret, private_key_path = self.handle_ssh_secret(secret_type, secret, path_dir)
h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
return h
return h, record
def get_or_create_record(self, asset, account, name):
asset_account_id = f'{asset.id}-{account.id}'
if asset_account_id in self.record_map:
record_id = self.record_map[asset_account_id]
recorder = PushSecretRecord.objects.filter(id=record_id).first()
record = PushSecretRecord.objects.filter(id=record_id).first()
else:
recorder = self.create_record(asset, account)
record = self.create_record(asset, account)
self.name_recorder_mapper[name] = recorder
return recorder
self.name_record_mapper[name] = record
return record
def create_record(self, asset, account):
recorder = PushSecretRecord(
record = PushSecretRecord(
asset=asset, account=account, execution=self.execution,
comment=f'{account.username}@{asset.address}'
)
return recorder
return record
def print_summary(self):
print('\n\n' + '-' * 80)

View File

@@ -3,7 +3,7 @@
vars:
ansible_shell_type: sh
ansible_connection: local
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Verify account (pyfreerdp)

View File

@@ -8,6 +8,7 @@
ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}"
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
ansible_timeout: 30
when: not account.become.ansible_become
- name: Verify account connectivity(Switch)
@@ -20,4 +21,5 @@
ansible_become_method: "{{ account.become.ansible_become_method }}"
ansible_become_user: "{{ account.become.ansible_become_user }}"
ansible_become_password: "{{ account.become.ansible_become_password }}"
ansible_timeout: 30
when: account.become.ansible_become

View File

@@ -9,3 +9,4 @@
vars:
ansible_user: "{{ account.full_username }}"
ansible_password: "{{ account.secret }}"
ansible_timeout: 30

View File

@@ -17,7 +17,7 @@ __all__ = [
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
'GatherAccountDetailField'
'GatherAccountDetailField', 'ChangeSecretAccountStatus'
]
@@ -117,6 +117,12 @@ class ChangeSecretRecordStatusChoice(models.TextChoices):
pending = 'pending', _('Pending')
class ChangeSecretAccountStatus(models.TextChoices):
QUEUED = 'queued', _('Queued')
READY = 'ready', _('Ready')
PROCESSING = 'processing', _('Processing')
class GatherAccountDetailField(models.TextChoices):
can_login = 'can_login', _('Can login')
superuser = 'superuser', _('Superuser')

View File

@@ -17,6 +17,7 @@ from common.utils.timezone import local_zero_hour, local_now
from .const.automation import ChangeSecretRecordStatusChoice
from .models import Account, GatheredAccount, ChangeSecretRecord, PushSecretRecord, IntegrationApplication, \
AutomationExecution
from .utils import account_secret_task_status
logger = get_logger(__file__)
@@ -233,7 +234,7 @@ class AutomationExecutionFilterSet(DaysExecutionFilterMixin, BaseFilterSet):
class Meta:
model = AutomationExecution
fields = ["days", 'trigger', 'automation_id', 'automation__name']
fields = ["days", 'trigger', 'automation__name']
class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterSet):
@@ -242,3 +243,25 @@ class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterS
class Meta:
model = PushSecretRecord
fields = ["id", "status", "asset_id", "execution_id"]
class ChangeSecretStatusFilterSet(BaseFilterSet):
asset_name = drf_filters.CharFilter(
field_name="asset__name", lookup_expr="icontains"
)
status = drf_filters.CharFilter(method='filter_dynamic')
execution_id = drf_filters.CharFilter(method='filter_dynamic')
class Meta:
model = Account
fields = ["username"]
@staticmethod
def filter_dynamic(queryset, name, value):
_ids = list(queryset.values_list('id', flat=True))
data_map = {
_id: account_secret_task_status.get(str(_id)).get(name)
for _id in _ids
}
matched = [_id for _id, v in data_map.items() if v == value]
return queryset.filter(id__in=matched)

View File

@@ -335,6 +335,7 @@ class Migration(migrations.Migration):
],
options={
"abstract": False,
"verbose_name": "Check engine",
},
),
migrations.CreateModel(

View File

@@ -119,6 +119,9 @@ class CheckAccountEngine(JMSBaseModel):
def __str__(self):
return self.name
class Meta:
verbose_name = _('Check engine')
@staticmethod
def get_default_engines():
data = [
@@ -128,7 +131,7 @@ class CheckAccountEngine(JMSBaseModel):
"name": _("Check the discovered accounts"),
"comment": _(
"Perform checks and analyses based on automatically discovered account results, "
"including user groups, public keys, sudoers, and other information"
"including user groups, public keys, sudoers, and other information."
)
},
{
@@ -144,13 +147,13 @@ class CheckAccountEngine(JMSBaseModel):
"id": "00000000-0000-0000-0000-000000000003",
"slug": "check_account_repeat",
"name": _("Check if the account and password are repeated"),
"comment": _("Check if the account is the same as other accounts")
"comment": _("Check if the account is the same as other accounts.")
},
{
"id": "00000000-0000-0000-0000-000000000004",
"slug": "check_account_leak",
"name": _("Check whether the account password is a common password"),
"comment": _("Check whether the account password is a commonly leaked password")
"comment": _("Check whether the account password is a commonly leaked password.")
},
]
return data

View File

@@ -6,6 +6,7 @@ from common.tasks import send_mail_attachment_async, upload_backup_to_obj_storag
from notifications.notifications import UserMessage
from terminal.models.component.storage import ReplayStorage
from users.models import User
from users.utils import activate_user_language
class AccountBackupExecutionTaskMsg:
@@ -28,9 +29,10 @@ class AccountBackupExecutionTaskMsg:
).format(name)
def publish(self, attachment_list=None):
send_mail_attachment_async(
self.subject, self.message, [self.user.email], attachment_list
)
with activate_user_language(self.user):
send_mail_attachment_async(
self.subject, self.message, [self.user.email], attachment_list
)
class AccountBackupByObjStorageExecutionTaskMsg:
@@ -74,9 +76,10 @@ class ChangeSecretExecutionTaskMsg:
return self.summary + '\n' + default_message
def publish(self, attachments=None):
send_mail_attachment_async(
self.subject, self.message, [self.user.email], attachments
)
with activate_user_language(self.user):
send_mail_attachment_async(
self.subject, self.message, [self.user.email], attachments
)
class GatherAccountChangeMsg(UserMessage):

View File

@@ -23,7 +23,7 @@ TYPE_CHOICES = [
("delete_both", _("Delete remote")),
("add_account", _("Add account")),
("change_password_add", _("Change password and Add")),
("change_password", _("Change password")),
("change_password", _("Change secret")),
]

View File

@@ -246,6 +246,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
'source', 'source_id', 'secret_reset',
] + AccountCreateUpdateSerializerMixin.Meta.fields + automation_fields
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + automation_fields
fields = [f for f in fields if f not in ['spec_info']]
extra_kwargs = {
**BaseAccountSerializer.Meta.extra_kwargs,
'name': {'required': False},
@@ -268,7 +269,7 @@ class AccountDetailSerializer(AccountSerializer):
class Meta(AccountSerializer.Meta):
model = Account
fields = AccountSerializer.Meta.fields + ['has_secret']
fields = AccountSerializer.Meta.fields + ['has_secret', 'spec_info']
read_only_fields = AccountSerializer.Meta.read_only_fields + ['has_secret']

View File

@@ -75,7 +75,7 @@ class BaseAccountSerializer(
fields_mini = ["id", "name", "username"]
fields_small = fields_mini + [
"secret_type", "secret", "passphrase",
"privileged", "is_active", "spec_info",
"privileged", "is_active",
]
fields_other = ["created_by", "date_created", "date_updated", "comment"]
fields = fields_small + fields_other + ["labels"]

View File

@@ -57,11 +57,15 @@ class AccountTemplateSerializer(BaseAccountSerializer):
fields_unimport_template = ['push_params']
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer):
class AccountDetailTemplateSerializer(AccountTemplateSerializer):
class Meta(AccountTemplateSerializer.Meta):
fields = AccountTemplateSerializer.Meta.fields + ['spec_info']
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountDetailTemplateSerializer):
class Meta(AccountDetailTemplateSerializer.Meta):
fields = AccountDetailTemplateSerializer.Meta.fields
extra_kwargs = {
**AccountTemplateSerializer.Meta.extra_kwargs,
**AccountDetailTemplateSerializer.Meta.extra_kwargs,
'secret': {'write_only': False},
'spec_info': {'label': _('Spec info')},
}

View File

@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
#
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes
from accounts.const import AutomationTypes, AccountBackupType
from accounts.models import BackupAccountAutomation
from common.serializers.fields import EncryptedField
from common.utils import get_logger
@@ -41,6 +42,17 @@ class BackupAccountSerializer(BaseAutomationSerializer):
'types': {'label': _('Asset type')}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_backup_type_choices()
def set_backup_type_choices(self):
field_backup_type = self.fields.get("backup_type")
if not field_backup_type:
return
if not settings.XPACK_LICENSE_IS_VALID:
field_backup_type._choices.pop(AccountBackupType.object_storage, None)
@property
def model_type(self):
return AutomationTypes.backup_account

View File

@@ -16,6 +16,7 @@ from assets.models import Asset
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import get_logger
from .base import BaseAutomationSerializer
from ...utils import account_secret_task_status
logger = get_logger(__file__)
@@ -26,6 +27,7 @@ __all__ = [
'ChangeSecretRecordBackUpSerializer',
'ChangeSecretUpdateAssetSerializer',
'ChangeSecretUpdateNodeSerializer',
'ChangeSecretAccountSerializer'
]
@@ -179,3 +181,24 @@ class ChangeSecretUpdateNodeSerializer(serializers.ModelSerializer):
class Meta:
model = ChangeSecretAutomation
fields = ['id', 'nodes']
class ChangeSecretAccountSerializer(serializers.ModelSerializer):
asset = ObjectRelatedField(
queryset=Asset.objects.all(), required=False, label=_("Asset")
)
ttl = serializers.SerializerMethodField(label=_('TTL'))
meta = serializers.SerializerMethodField(label=_('Meta'))
class Meta:
model = Account
fields = ['id', 'username', 'asset', 'meta', 'ttl']
read_only_fields = fields
@staticmethod
def get_meta(obj):
return account_secret_task_status.get(str(obj.id))
@staticmethod
def get_ttl(obj):
return account_secret_task_status.get_ttl(str(obj.id))

View File

@@ -28,7 +28,7 @@ class DiscoverAccountAutomationSerializer(BaseAutomationSerializer):
+ read_only_fields)
extra_kwargs = {
'check_risk': {
'help_text': _('Whether to check the risk of the gathered accounts.'),
'help_text': _('Whether to check the risk of the discovered accounts.'),
},
**BaseAutomationSerializer.Meta.extra_kwargs
}

View File

@@ -1,4 +1,5 @@
import datetime
from collections import defaultdict
from celery import shared_task
from django.db.models import Q
@@ -72,24 +73,43 @@ def execute_automation_record_task(record_ids, tp):
task_name = gettext_noop('Execute automation record')
with tmp_to_root_org():
records = ChangeSecretRecord.objects.filter(id__in=record_ids)
records = ChangeSecretRecord.objects.filter(id__in=record_ids).order_by('-date_updated')
if not records:
logger.error('No automation record found: {}'.format(record_ids))
logger.error(f'No automation record found: {record_ids}')
return
record = records[0]
record_map = {f'{record.asset_id}-{record.account_id}': str(record.id) for record in records}
task_snapshot = {
'params': {},
'record_map': record_map,
'secret': record.new_secret,
'secret_type': record.execution.snapshot.get('secret_type'),
'assets': [str(instance.asset_id) for instance in records],
'accounts': [str(instance.account_id) for instance in records],
}
with tmp_to_org(record.execution.org_id):
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
seen_accounts = set()
unique_records = []
for rec in records:
acct = str(rec.account_id)
if acct not in seen_accounts:
seen_accounts.add(acct)
unique_records.append(rec)
exec_groups = defaultdict(list)
for rec in unique_records:
exec_groups[rec.execution_id].append(rec)
for __, group in exec_groups.items():
latest_rec = group[0]
snapshot = getattr(latest_rec.execution, 'snapshot', {}) or {}
record_map = {f"{r.asset_id}-{r.account_id}": str(r.id) for r in group}
assets = [str(r.asset_id) for r in group]
accounts = [str(r.account_id) for r in group]
task_snapshot = {
'params': {},
'record_map': record_map,
'secret': latest_rec.new_secret,
'secret_type': snapshot.get('secret_type'),
'assets': assets,
'accounts': accounts,
}
with tmp_to_org(latest_rec.execution.org_id):
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
@shared_task(

View File

@@ -1,37 +1,107 @@
from collections import defaultdict
from celery import shared_task
from django.utils.translation import gettext_noop, gettext_lazy as _
from accounts.const import AutomationTypes
from accounts.const import AutomationTypes, ChangeSecretAccountStatus
from accounts.tasks.common import quickstart_automation_by_snapshot
from accounts.utils import account_secret_task_status
from common.utils import get_logger
from orgs.utils import tmp_to_org
logger = get_logger(__file__)
__all__ = [
'push_accounts_to_assets_task',
'push_accounts_to_assets_task', 'change_secret_accounts_to_assets_task'
]
def _process_accounts(account_ids, automation_model, default_name, automation_type, snapshot=None):
from accounts.models import Account
accounts = Account.objects.filter(id__in=account_ids)
if not accounts:
logger.warning(
"No accounts found for automation task %s with ids %s",
automation_type, account_ids
)
return
task_name = automation_model.generate_unique_name(gettext_noop(default_name))
snapshot = snapshot or {}
snapshot.update({
'accounts': [str(a.id) for a in accounts],
'assets': [str(a.asset_id) for a in accounts],
})
quickstart_automation_by_snapshot(task_name, automation_type, snapshot)
@shared_task(
queue="ansible",
verbose_name=_('Push accounts to assets'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
description=_(
"When creating or modifying an account requires account push, this task is executed"
"Whenever an account is created or modified and needs pushing to assets, run this task"
)
)
def push_accounts_to_assets_task(account_ids, params=None):
from accounts.models import PushAccountAutomation
from accounts.models import Account
accounts = Account.objects.filter(id__in=account_ids)
task_name = gettext_noop("Push accounts to assets")
task_name = PushAccountAutomation.generate_unique_name(task_name)
task_snapshot = {
'accounts': [str(account.id) for account in accounts],
'assets': [str(account.asset_id) for account in accounts],
snapshot = {
'params': params or {},
}
_process_accounts(
account_ids,
PushAccountAutomation,
_("Push accounts to assets"),
AutomationTypes.push_account,
snapshot=snapshot
)
tp = AutomationTypes.push_account
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
@shared_task(
queue="ansible",
verbose_name=_('Change secret accounts to assets'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
description=_(
"When a secret on an account changes and needs pushing to assets, run this task"
)
)
def change_secret_accounts_to_assets_task(account_ids, params=None, snapshot=None, trigger='manual'):
from accounts.models import ChangeSecretAutomation, Account
manager = account_secret_task_status
if trigger == 'delay':
for _id in manager.account_ids:
status = manager.get_status(_id)
# Check if the account is in QUEUED status
if status == ChangeSecretAccountStatus.QUEUED:
account_ids.append(_id)
manager.set_status(_id, ChangeSecretAccountStatus.READY)
if not account_ids:
return
accounts = Account.objects.filter(id__in=account_ids)
if not accounts:
logger.warning(
"No accounts found for change secret automation task with ids %s",
account_ids
)
return
grouped_ids = defaultdict(lambda: defaultdict(list))
for account in accounts:
grouped_ids[account.org_id][account.secret_type].append(str(account.id))
snapshot = snapshot or {}
for org_id, secret_map in grouped_ids.items():
with tmp_to_org(org_id):
for secret_type, ids in secret_map.items():
snapshot['secret_type'] = secret_type
_process_accounts(
ids,
ChangeSecretAutomation,
_("Change secret accounts to assets"),
AutomationTypes.change_secret,
snapshot=snapshot
)

View File

@@ -17,6 +17,7 @@ router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet,
router.register(r'account-backup-plans', api.BackupAccountViewSet, 'account-backup')
router.register(r'account-backup-plan-executions', api.BackupAccountExecutionViewSet, 'account-backup-execution')
router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation')
router.register(r'change-secret-status', api.ChangeSecretStatusViewSet, 'change-secret-status')
router.register(r'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution')
router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record')
router.register(r'gather-account-automations', api.DiscoverAccountsAutomationViewSet, 'gather-account-automation')

View File

@@ -1,10 +1,11 @@
import copy
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
from common.utils import ssh_key_gen, random_string
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
@@ -61,3 +62,80 @@ def validate_ssh_key(ssh_key, passphrase=None):
if not valid:
raise serializers.ValidationError(_("private key invalid or passphrase error"))
return parse_ssh_private_key_str(ssh_key, passphrase)
class AccountSecretTaskStatus:
def __init__(
self,
prefix='queue:change_secret:',
debounce_key='debounce:change_secret:task',
debounce_timeout=10,
queue_status_timeout=60,
default_timeout=3600,
delayed_task_countdown=20,
):
self.prefix = prefix
self.debounce_key = debounce_key
self.debounce_timeout = debounce_timeout
self.queue_status_timeout = queue_status_timeout
self.default_timeout = default_timeout
self.delayed_task_countdown = delayed_task_countdown
self.enabled = getattr(settings, 'CHANGE_SECRET_AFTER_SESSION_END', False)
def _key(self, identifier):
return f"{self.prefix}{identifier}"
@property
def account_ids(self):
for key in cache.iter_keys(f"{self.prefix}*"):
yield key.split(':')[-1]
def is_debounced(self):
return cache.add(self.debounce_key, True, self.debounce_timeout)
def get_queue_key(self, identifier):
return self._key(identifier)
def set_status(
self,
identifier,
status,
timeout=None,
metadata=None,
use_add=False
):
if not self.enabled:
return
key = self._key(identifier)
data = {"status": status}
if metadata:
data.update(metadata)
if use_add:
return cache.add(key, data, timeout or self.queue_status_timeout)
cache.set(key, data, timeout or self.default_timeout)
def get(self, identifier):
return cache.get(self._key(identifier), {})
def get_status(self, identifier):
if not self.enabled:
return
record = cache.get(self._key(identifier), {})
return record.get("status")
def get_ttl(self, identifier):
return cache.ttl(self._key(identifier))
def clear(self, identifier):
if not self.enabled:
return
cache.delete(self._key(identifier))
account_secret_task_status = AccountSecretTaskStatus()

View File

@@ -9,5 +9,6 @@ class ActionChoices(models.TextChoices):
warning = 'warning', _('Warn')
notice = 'notice', _('Notify')
notify_and_warn = 'notify_and_warn', _('Prompt and warn')
face_verify = 'face_verify', _('Face Verify')
face_online = 'face_online', _('Face Online')
face_verify = 'face_verify', _('Face verify')
face_online = 'face_online', _('Face online')
change_secret = 'change_secret', _('Secret rotation')

View File

@@ -79,6 +79,8 @@ class ActionAclSerializer(serializers.Serializer):
field_action._choices.pop(ActionChoices.face_online, None)
for choice in self.Meta.action_choices_exclude:
field_action._choices.pop(choice, None)
if not settings.XPACK_LICENSE_IS_VALID or not settings.CHANGE_SECRET_AFTER_SESSION_END:
field_action._choices.pop(ActionChoices.change_secret, None)
class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):

View File

@@ -33,7 +33,10 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
model = CommandFilterACL
fields = BaseSerializer.Meta.fields + ['command_groups']
action_choices_exclude = [
ActionChoices.notice, ActionChoices.face_verify, ActionChoices.face_online
ActionChoices.notice,
ActionChoices.face_verify,
ActionChoices.face_online,
ActionChoices.change_secret
]

View File

@@ -14,6 +14,10 @@ class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
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
ActionChoices.review,
ActionChoices.accept,
ActionChoices.notice,
ActionChoices.face_verify,
ActionChoices.face_online,
ActionChoices.change_secret
]

View File

@@ -22,7 +22,8 @@ class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
ActionChoices.warning,
ActionChoices.notify_and_warn,
ActionChoices.face_online,
ActionChoices.face_verify
ActionChoices.face_verify,
ActionChoices.change_secret
]
def get_rules_serializer(self):

View File

@@ -1,5 +1,7 @@
# coding: utf-8
#
from urllib.parse import urlparse
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@@ -8,7 +10,7 @@ from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
logger = get_logger(__file__)
__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text']
__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text', 'address_validator']
def ip_group_child_validator(ip_group_child):
@@ -21,6 +23,19 @@ def ip_group_child_validator(ip_group_child):
raise serializers.ValidationError(error)
def address_validator(value):
parsed = urlparse(value)
is_basic_url = parsed.scheme in ('http', 'https') and parsed.netloc
is_valid = value == '*' \
or is_ip_address(value) \
or is_ip_network(value) \
or is_ip_segment(value) \
or is_basic_url
if not is_valid:
error = _('address invalid: `{}`').format(value)
raise serializers.ValidationError(error)
ip_group_help_text = _(
'With * indicating a match all. '
'Such as: '

View File

@@ -22,6 +22,7 @@ from common.tasks import send_mail_async
from common.utils import get_logger, lazyproperty, is_openssh_format_key, ssh_pubkey_gen
from ops.ansible import JMSInventory, DefaultCallback, SuperPlaybookRunner
from ops.ansible.interface import interface
from users.utils import activate_user_language
logger = get_logger(__name__)
@@ -122,9 +123,7 @@ class BaseManager:
self.execution.summary = self.summary
self.execution.result = self.result
self.execution.status = self.status
with safe_atomic_db_connection():
self.execution.save()
self.execution.save()
def print_summary(self):
content = "\nSummery: \n"
@@ -151,12 +150,13 @@ class BaseManager:
if not recipients:
return
print(f"Send report to: {','.join([str(u) for u in recipients])}")
report = self.gen_report()
report = transform(report, cssutils_logging_level="CRITICAL")
subject = self.get_report_subject()
emails = [r.email for r in recipients if r.email]
send_mail_async(subject, report, emails, html_message=report)
for user in recipients:
with activate_user_language(user):
report = self.gen_report()
report = transform(report, cssutils_logging_level="CRITICAL")
subject = self.get_report_subject()
emails = [user.email]
send_mail_async(subject, report, emails, html_message=report)
def gen_report(self):
template_path = self.get_report_template()
@@ -165,9 +165,10 @@ class BaseManager:
return data
def post_run(self):
self.update_execution()
self.print_summary()
self.send_report_if_need()
with safe_atomic_db_connection():
self.update_execution()
self.print_summary()
self.send_report_if_need()
def run(self, *args, **kwargs):
self.pre_run()
@@ -546,7 +547,8 @@ class BasePlaybookManager(PlaybookPrepareMixin, BaseManager):
try:
kwargs.update({"clean_workspace": False})
cb = runner.run(**kwargs)
self.on_runner_success(runner, cb)
with safe_atomic_db_connection():
self.on_runner_success(runner, cb)
except Exception as e:
self.on_runner_failed(runner, e, **info)
finally:

View File

@@ -3,7 +3,7 @@
vars:
ansible_shell_type: sh
ansible_connection: local
ansible_python_interpreter: /opt/py3/bin/python
ansible_python_interpreter: "{{ local_python_interpreter }}"
tasks:
- name: Test asset connection (pyfreerdp)

View File

@@ -1,5 +1,7 @@
- hosts: demo
gather_facts: no
vars:
ansible_timeout: 30
tasks:
- name: Posix ping
ansible.builtin.ping:

View File

@@ -1,5 +1,7 @@
- hosts: windows
gather_facts: no
vars:
ansible_timeout: 30
tasks:
- name: Refresh connection
ansible.builtin.meta: reset_connection

View File

@@ -194,6 +194,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
'default': '>=2014',
'label': _('Version'),
'help_text': _('SQL Server version, Different versions have different connection drivers')
},
'encrypt': {
'type': 'bool',
'default': True,
'label': _('Encrypt'),
'help_text': _('Whether to use TLS encryption.')
}
}
},

View File

@@ -46,7 +46,7 @@ class DatabaseSerializer(AssetSerializer):
elif self.context.get('request'):
platform_id = self.context['request'].query_params.get('platform')
if not platform and platform_id:
if not platform and platform_id and str(platform_id).isdigit():
platform = Platform.objects.filter(id=platform_id).first()
return platform

View File

@@ -32,7 +32,7 @@ from rbac.permissions import RBACPermission
from terminal.models import default_storage
from users.models import User
from .backends import TYPE_ENGINE_MAPPING
from .const import ActivityChoices
from .const import ActivityChoices, ActionChoices
from .filters import UserSessionFilterSet, OperateLogFilterSet
from .models import (
FTPLog, UserLoginLog, OperateLog, PasswordChangeLog,
@@ -45,7 +45,7 @@ from .serializers import (
FileSerializer, UserSessionSerializer, JobsAuditSerializer,
ServiceAccessLogSerializer
)
from .utils import construct_userlogin_usernames
from .utils import construct_userlogin_usernames, record_operate_log_and_activity_log
logger = get_logger(__name__)
@@ -126,6 +126,11 @@ class FTPLogViewSet(OrgModelViewSet):
response['Content-Type'] = 'application/octet-stream'
filename = escape_uri_path(ftp_log.filename)
response["Content-Disposition"] = "attachment; filename*=UTF-8''{}".format(filename)
record_operate_log_and_activity_log(
[ftp_log.id], ActionChoices.download, '', self.model,
resource_display=f'{ftp_log.asset}: {ftp_log.filename}',
)
return response
@action(methods=[POST], detail=True, permission_classes=[IsServiceAccount, ], serializer_class=FileSerializer)

View File

@@ -35,6 +35,7 @@ class OperateLogStore(ES, metaclass=Singleton):
}
}
exact_fields = {}
fuzzy_fields = {}
match_fields = {
'id', 'user', 'action', 'resource_type',
'resource', 'remote_addr', 'org_id'
@@ -44,7 +45,7 @@ class OperateLogStore(ES, metaclass=Singleton):
}
if not config.get('INDEX'):
config['INDEX'] = 'jumpserver_operate_log'
super().__init__(config, properties, keyword_fields, exact_fields, match_fields)
super().__init__(config, properties, keyword_fields, exact_fields, fuzzy_fields, match_fields)
self.pre_use_check()
@staticmethod

View File

@@ -29,7 +29,7 @@ class ActionChoices(TextChoices):
download = "download", _("Download")
connect = "connect", _("Connect")
login = "login", _("Login")
change_auth = "change_password", _("Change password")
change_auth = "change_password", _("Change secret")
accept = 'accept', _('Accept')
review = 'review', _('Review')

View File

@@ -75,7 +75,7 @@ class Migration(migrations.Migration):
("download", "Download"),
("connect", "Connect"),
("login", "Login"),
("change_password", "Change password"),
("change_password", "Change secret"),
("accept", "Accept"),
("review", "Review"),
("notice", "Notifications"),

View File

@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0005_rename_serviceaccesslog'),
]
@@ -18,7 +17,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='ftplog',
name='asset',
field=models.CharField(db_index=True, max_length=1024, verbose_name='Asset'),
field=models.CharField(db_index=True, max_length=767, verbose_name='Asset'),
),
migrations.AlterField(
model_name='ftplog',

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.1.13 on 2025-06-10 09:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("audits", "0006_alter_ftplog_account_alter_ftplog_asset_and_more"),
]
operations = [
migrations.AlterField(
model_name='ftplog',
name='asset',
field=models.CharField(db_index=True, max_length=768, verbose_name='Asset'),
),
]

View File

@@ -56,7 +56,7 @@ class FTPLog(OrgModelMixin):
remote_addr = models.CharField(
max_length=128, verbose_name=_("Remote addr"), blank=True, null=True
)
asset = models.CharField(max_length=1024, verbose_name=_("Asset"), db_index=True)
asset = models.CharField(max_length=768, verbose_name=_("Asset"), db_index=True)
account = models.CharField(max_length=128, verbose_name=_("Account"), db_index=True)
operate = models.CharField(
max_length=16, verbose_name=_("Operate"), choices=OperateChoices.choices
@@ -73,6 +73,9 @@ class FTPLog(OrgModelMixin):
models.Index(fields=['date_start', 'org_id'], name='idx_date_start_org'),
]
def __str__(self):
return "{0.id} of {0.user} to {0.asset}".format(self)
@property
def filepath(self):
return os.path.join(self.upload_to, self.date_start.strftime('%Y-%m-%d'), str(self.id))

View File

@@ -89,6 +89,8 @@ def create_activities(resource_ids, detail, detail_id, action, org_id):
for activity in activities:
create_activity(activity)
create_activity.finish()
@signals.after_task_publish.connect
def after_task_publish_for_activity_log(headers=None, body=None, **kwargs):

View File

@@ -180,7 +180,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'PlatformAutomation', 'PlatformProtocol', 'Protocol',
'HistoricalAccount', 'GatheredUser', 'ApprovalRule',
'BaseAutomation', 'CeleryTask', 'Command', 'JobLog',
'ConnectionToken', 'SessionJoinRecord',
'ConnectionToken', 'SessionJoinRecord', 'SessionSharing',
'HistoricalJob', 'Status', 'TicketStep', 'Ticket',
'UserAssetGrantedTreeNodeRelation', 'TicketAssignee',
'SuperTicket', 'SuperConnectionToken', 'AdminConnectionToken', 'PermNode',

View File

@@ -1,5 +1,6 @@
import copy
from datetime import datetime
from itertools import chain
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.core.exceptions import ObjectDoesNotExist
@@ -7,7 +8,6 @@ from django.db import models
from django.db.models import F, Value, CharField
from django.db.models.functions import Concat
from django.utils import translation
from itertools import chain
from common.db.fields import RelatedManager
from common.utils import validate_ip, get_ip_city, get_logger
@@ -16,7 +16,6 @@ from .const import DEFAULT_CITY, ActivityChoices as LogChoice
from .handler import create_or_update_operate_log
from .models import ActivityLog
logger = get_logger(__name__)
@@ -151,7 +150,7 @@ def record_operate_log_and_activity_log(ids, action, detail, model, **kwargs):
org_id = current_org.id
with translation.override('en'):
resource_type = model._meta.verbose_name
resource_type = kwargs.pop('resource_type', None) or model._meta.verbose_name
create_or_update_operate_log(action, resource_type, force=True, **kwargs)
base_data = {'type': LogChoice.operate_log, 'detail': detail, 'org_id': org_id}
activities = [ActivityLog(resource_id=r_id, **base_data) for r_id in ids]

View File

@@ -37,6 +37,7 @@ class UserConfirmationViewSet(JMSGenericViewSet):
backend_classes = ConfirmType.get_prop_backends(confirm_type)
if not backend_classes:
return
for backend_cls in backend_classes:
backend = backend_cls(self.request.user, self.request)
if not backend.check():
@@ -69,6 +70,7 @@ class UserConfirmationViewSet(JMSGenericViewSet):
ok, msg = backend.authenticate(secret_key, mfa_type)
if ok:
request.session['CONFIRM_LEVEL'] = ConfirmType.values.index(confirm_type) + 1
request.session['CONFIRM_TYPE'] = confirm_type
request.session['CONFIRM_TIME'] = int(time.time())
return Response('ok')
return Response({'error': msg}, status=400)

View File

@@ -369,12 +369,13 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
'terminal_theme_name': 'Default',
}
preferences_query = Preference.objects.filter(
user=user, category='koko', name__in=default_name_opts.keys()
user=user, category='luna', name__in=default_name_opts.keys()
).values_list('name', 'value')
preferences = dict(preferences_query)
for name in default_name_opts.keys():
value = preferences.get(name, default_name_opts[name])
connect_options[name] = value
connect_options['lang'] = getattr(user, 'lang', settings.LANGUAGE_CODE)
data['connect_options'] = connect_options
@staticmethod

View File

@@ -1,20 +1,19 @@
from django.core.cache import cache
from django.utils.translation import gettext as _
from rest_framework.exceptions import NotFound
from rest_framework.generics import CreateAPIView, RetrieveAPIView
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.permissions import AllowAny
from rest_framework.exceptions import NotFound
from common.permissions import IsServiceAccount
from common.utils import get_logger, get_object_or_none
from orgs.utils import tmp_to_root_org
from terminal.api.session.task import create_sessions_tasks
from users.models import User
from .. import serializers
from ..mixins import AuthMixin
from ..const import FACE_CONTEXT_CACHE_KEY_PREFIX, FACE_SESSION_KEY, FACE_CONTEXT_CACHE_TTL, FaceMonitorActionChoices
from ..mixins import AuthMixin
from ..models import ConnectionToken
from ..serializers.face import FaceMonitorCallbackSerializer, FaceMonitorContextSerializer
@@ -93,7 +92,7 @@ class FaceCallbackApi(AuthMixin, CreateAPIView):
connection_token_id = context.get('connection_token_id')
token = ConnectionToken.objects.filter(id=connection_token_id).first()
token.is_active = True
token.save()
token.save(update_fields=['is_active'])
else:
context.update({
'success': False,

View File

@@ -14,7 +14,6 @@ from rest_framework.response import Response
from authentication.errors import ACLError
from common.api import JMSGenericViewSet
from common.const.http import POST, GET
from common.permissions import OnlySuperUser
from common.serializers import EmptySerializer
from common.utils import reverse, safe_next_url
from common.utils.timezone import utc_now
@@ -38,8 +37,11 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
'login_url': SSOTokenSerializer,
'login': EmptySerializer
}
rbac_perms = {
'login_url': 'authentication.add_ssotoken',
}
@action(methods=[POST], detail=False, permission_classes=[OnlySuperUser], url_path='login-url')
@action(methods=[POST], detail=False, url_path='login-url')
def login_url(self, request, *args, **kwargs):
if not settings.AUTH_SSO:
raise SSOAuthClosed()
@@ -103,11 +105,9 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
self.request.session['auth_backend'] = settings.AUTH_BACKEND_SSO
login(self.request, user, settings.AUTH_BACKEND_SSO)
self.send_auth_signal(success=True, user=user)
self.mark_mfa_ok('otp', user)
LoginIpBlockUtil(ip).clean_block_if_need()
LoginBlockUtil(username, ip).clean_failed_count()
self.clear_auth_mark()
except (ACLError, LoginConfirmBaseError): # 无需记录日志
pass
except (AuthFailedError, SSOAuthKeyTTLError) as e:

View File

@@ -224,7 +224,6 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
user_auth_failed.send(
sender=self.__class__, request=request, username=user.username,
reason="User is invalid", backend=settings.AUTH_BACKEND_OIDC_CODE
)
return None

View File

@@ -10,16 +10,15 @@ import datetime as dt
from calendar import timegm
from urllib.parse import urlparse
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.utils.encoding import force_bytes, smart_bytes
from jwkest import JWKESTException
from jwkest.jwk import KEYS
from jwkest.jws import JWS
from django.conf import settings
from common.utils import get_logger
logger = get_logger(__file__)
@@ -99,7 +98,8 @@ def _validate_claims(id_token, nonce=None, validate_nonce=True):
raise SuspiciousOperation('Incorrect id_token: nbf')
# Verifies that the token was issued in the allowed timeframe.
if utc_timestamp > id_token['iat'] + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE:
max_age = settings.AUTH_OPENID_ID_TOKEN_MAX_AGE
if utc_timestamp > id_token['iat'] + max_age:
logger.debug(log_prompt.format('Incorrect id_token: iat'))
raise SuspiciousOperation('Incorrect id_token: iat')

View File

@@ -171,9 +171,10 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
logger.debug(log_prompt.format('Process authenticate'))
try:
user = auth.authenticate(nonce=nonce, request=request, code_verifier=code_verifier)
except IntegrityError:
except IntegrityError as e:
title = _("OpenID Error")
msg = _('Please check if a user with the same username or email already exists')
logger.error(e, exc_info=True)
response = self.get_failed_response('/', title, msg)
return response
if user:

View File

@@ -74,6 +74,7 @@ class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet):
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
request.session['passkey_confirm_mfa'] = ''
return Response('ok')

View File

@@ -278,9 +278,10 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin, FlashMessageMixin):
saml_user_data = self.get_attributes(saml_instance)
try:
user = auth.authenticate(request=request, saml_user_data=saml_user_data)
except IntegrityError:
except IntegrityError as e:
title = _("SAML2 Error")
msg = _('Please check if a user with the same username or email already exists')
logger.error(e, exc_info=True)
response = self.get_failed_response('/', title, msg)
return response
if user and user.is_valid:

View File

@@ -32,7 +32,7 @@ class MFAType(TextChoices):
OTP = 'otp', _('OTP')
SMS = 'sms', _('SMS')
Email = 'email', _('Email')
Face = 'face', _('Face Recognition')
Face = 'face', _('Face recognition')
Radius = 'otp_radius', _('Radius')
Passkey = 'passkey', _('Passkey')
Custom = 'mfa_custom', _('Custom')

View File

@@ -9,7 +9,7 @@ from ..const import MFAType
class MFAFace(BaseMFA, AuthFaceMixin):
name = MFAType.Face.value
display_name = MFAType.Face.name
placeholder = 'Face Recognition'
placeholder = 'Face recognition'
skip_cache_check = True
has_code = False

View File

@@ -35,7 +35,7 @@ class MFAMiddleware:
# 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
white_urls = [
'login/mfa', 'mfa/select', 'face/context','jsi18n/', '/static/',
'login/mfa', 'mfa/select', 'face/context', 'jsi18n/', '/static/',
'/profile/otp', '/logout/',
]
for url in white_urls:
@@ -77,6 +77,7 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
ip = get_request_ip(request)
try:
self.request = request
self.check_is_block()
self._check_third_party_login_acl()
self._check_login_acl(request.user, ip)
except Exception as e:
@@ -120,7 +121,10 @@ class SessionCookieMiddleware(MiddlewareMixin):
USER_LOGIN_ENCRYPTION_KEY_PAIR = 'user_login_encryption_key_pair'
def set_cookie_public_key(self, request, response):
if request.path.startswith('/api'):
whitelist = [
'/api/v1/authentication/sso/login/',
]
if request.path.startswith('/api') and request.path not in whitelist:
return
session_public_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME

View File

@@ -20,6 +20,7 @@ from django.utils.translation import gettext as _
from rest_framework.request import Request
from acls.models import LoginACL
from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY
from common.utils import get_request_ip_or_data, get_request_ip, get_logger, bulk_get, FlashMessageUtil
from users.models import User
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
@@ -227,6 +228,10 @@ class MFAMixin:
self._do_check_user_mfa(code, mfa_type, user=user)
def check_user_mfa_if_need(self, user):
# 扫码登录的认证方式会执行该函数检查 mfa跳转登录认证方式则通过ThirdPartyLoginMiddleware中间件检验 mfa
if not settings.SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY and \
self.request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY:
return
if self.request.session.get('auth_mfa') and \
self.request.session.get('auth_mfa_username') == user.username:
return

View File

@@ -14,23 +14,29 @@ from orgs.utils import tmp_to_root_org
class UserConfirmation(permissions.BasePermission):
ttl = 60 * 5
min_level = 1
confirm_type = 'relogin'
min_type = 'relogin'
def has_permission(self, request, view):
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
return True
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()
if not confirm_level or not confirm_time or \
confirm_level < self.min_level or \
confirm_time < time.time() - ttl:
raise UserConfirmRequired(code=self.confirm_type)
ttl = self.get_ttl(confirm_type)
now = int(time.time())
if not confirm_level or not confirm_time:
raise UserConfirmRequired(code=self.min_type)
if confirm_level < self.min_level or \
confirm_time < now - ttl:
raise UserConfirmRequired(code=self.min_type)
return True
def get_ttl(self):
if self.confirm_type == ConfirmType.MFA:
def get_ttl(self, confirm_type):
if confirm_type == ConfirmType.MFA:
ttl = settings.SECURITY_MFA_VERIFY_TTL
else:
ttl = self.ttl
@@ -40,7 +46,7 @@ class UserConfirmation(permissions.BasePermission):
def require(cls, confirm_type=ConfirmType.RELOGIN, ttl=60 * 5):
min_level = ConfirmType.values.index(confirm_type) + 1
name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl)
return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type})
return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'min_type': confirm_type})
class IsValidUserOrConnectionToken(IsValidUser):

View File

@@ -75,6 +75,7 @@ class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
def get_user(self, attrs):
return attrs.get('user')
class AdminConnectionTokenSerializer(ConnectionTokenSerializer):
class Meta(ConnectionTokenSerializer.Meta):
model = AdminConnectionToken

View File

@@ -1,12 +1,18 @@
# -*- coding: utf-8 -*-
#
import datetime
import logging
from celery import shared_task
from django.conf import settings
from django.contrib.sessions.models import Session
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from authentication.models import ConnectionToken, TempToken
from common.const.crontab import CRONTAB_AT_AM_TWO
from ops.celery.decorator import register_as_period_task
from orgs.utils import tmp_to_root_org
@shared_task(
@@ -18,3 +24,26 @@ from ops.celery.decorator import register_as_period_task
@register_as_period_task(interval=3600 * 24)
def clean_django_sessions():
Session.objects.filter(expire_date__lt=timezone.now()).delete()
@shared_task(
verbose_name=_('Clean expired temporary, connection tokens'),
description=_(
"When connecting to assets or generating temporary passwords, the system creates corresponding connection "
"tokens or temporary credential records. To maintain security and manage storage, the system automatically "
"deletes expired tokens every day at 2:00 AM based on the retention settings configured under System settings "
"> Security > User password > Token Retention Period"
)
)
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
def clean_expire_token():
logging.info('Cleaning expired temporary and connection tokens...')
with tmp_to_root_org():
now = timezone.now()
days = settings.SECURITY_EXPIRED_TOKEN_RECORD_KEEP_DAYS
expired_time = now - datetime.timedelta(days=days)
count = ConnectionToken.objects.filter(date_expired__lt=expired_time).delete()
logging.info('Deleted %d expired connection tokens.', count[0])
count = TempToken.objects.filter(date_expired__lt=expired_time).delete()
logging.info('Deleted %d temporary tokens.', count[0])
logging.info('Cleaned expired temporary and connection tokens.')

View File

@@ -436,7 +436,7 @@
</body>
{% include '_foot_js.html' %}
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.3.3.2.min.js"></script>
<script type="text/javascript" src="/static/js/plugins/cryptojs/crypto-js.min.js"></script>
<script type="text/javascript" src="/static/js/plugins/buffer/buffer.min.js"></script>
<script>

View File

@@ -91,27 +91,30 @@
}
}
const publicKeyCredentialToJSON = (pubKeyCred) => {
if (pubKeyCred instanceof Array) {
const arr = []
for (const i of pubKeyCred) {
arr.push(publicKeyCredentialToJSON(i))
}
return arr
const publicKeyCredentialToJSON = pubKeyCred => {
if (pubKeyCred instanceof Array) {
const arr = []
for (const i of pubKeyCred) {
arr.push(publicKeyCredentialToJSON(i))
}
return arr
}
if (pubKeyCred instanceof ArrayBuffer || pubKeyCred instanceof Uint8Array) {
return encode(pubKeyCred)
}
if (pubKeyCred instanceof Object) {
const obj = {}
for (const key in pubKeyCred) {
obj[key] = publicKeyCredentialToJSON(pubKeyCred[key])
}
if (pubKeyCred instanceof ArrayBuffer) {
return encode(pubKeyCred)
}
return obj
}
if (pubKeyCred instanceof Object) {
const obj = {}
for (const key in pubKeyCred) {
obj[key] = publicKeyCredentialToJSON(pubKeyCred[key])
}
return obj
}
return pubKeyCred
return pubKeyCred
}
function GetAssertReq(getAssert) {

View File

@@ -150,6 +150,7 @@ class BaseBindCallbackView(FlashMessageMixin, IMClientMixin, View):
user.save()
except IntegrityError as e:
msg = _('The %s is already bound to another user') % self.auth_type_label
logger.error(e, exc_info=True)
response = self.get_failed_response(redirect_url, msg, msg)
return response

View File

@@ -144,6 +144,7 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View):
user.save()
except IntegrityError as e:
msg = _('The DingTalk is already bound to another user')
logger.error(e, exc_info=True)
response = self.get_failed_response(redirect_url, msg, msg)
return response

View File

@@ -294,6 +294,19 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
)
return url
def get(self, request, *args, **kwargs):
from django.utils import timezone
response = super().get(request, *args, **kwargs)
try:
response.set_cookie(
settings.LANGUAGE_COOKIE_NAME,
request.user.lang,
expires=timezone.now() + timezone.timedelta(days=365)
)
except Exception:
pass
return response
class UserLoginWaitConfirmView(TemplateView):
template_name = 'authentication/login_wait_confirm.html'

View File

@@ -8,6 +8,8 @@ from rest_framework.request import Request
from rest_framework.response import Response
from common.const.http import POST, PUT
from orgs.models import Organization
from orgs.utils import current_org
__all__ = ['SuggestionMixin', 'RenderToJsonMixin']
@@ -23,7 +25,16 @@ class SuggestionMixin:
@action(methods=['get'], detail=False, url_path='suggestions')
def match(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
queryset = self.get_queryset()
org_id = str(current_org.id)
if (
not request.user.is_superuser and
org_id != Organization.ROOT_ID and
not request.user.orgs.filter(id=org_id).exists()
):
queryset = queryset.none()
queryset = self.filter_queryset(queryset)
queryset = queryset[:self.suggestion_limit]
page = self.paginate_queryset(queryset)

View File

@@ -131,17 +131,18 @@ class LicenseEditionChoices(models.TextChoices):
if choice == key:
return choice
return LicenseEditionChoices.COMMUNITY
@staticmethod
def parse_license_edition(info):
count = info.get('license', {}).get('count', 0)
if 50 >= count > 0:
if 0 < count <= 50:
return LicenseEditionChoices.BASIC
elif count <= 500:
return LicenseEditionChoices.STANDARD
elif count < 5000:
elif count <= 5000:
return LicenseEditionChoices.PROFESSIONAL
elif count >= 5000:
elif count > 5000:
return LicenseEditionChoices.ULTIMATE
else:
return LicenseEditionChoices.COMMUNITY

View File

@@ -50,13 +50,14 @@ def get_objects(model, pks):
# 复制 django.db.close_old_connections, 因为它没有导出ide 提示有问题
def close_old_connections():
for conn in connections.all():
def close_old_connections(**kwargs):
for conn in connections.all(initialized_only=True):
conn.close_if_unusable_or_obsolete()
# 这个要是在 Django 请求周期外使用的,不能影响 Django 的事务管理, 在 api 中使用会影响 api 事务
@contextmanager
def safe_db_connection(auto_close=True):
def safe_db_connection():
close_old_connections()
yield
close_old_connections()
@@ -64,19 +65,25 @@ def safe_db_connection(auto_close=True):
@contextmanager
def safe_atomic_db_connection(auto_close=False):
in_atomic_block = connection.in_atomic_block # 当前是否处于事务中
autocommit = transaction.get_autocommit() # 是否启用了自动提交
created = False
"""
通用数据库连接管理器(线程安全、事务感知):
- 在连接不可用时主动重建连接
- 在非事务环境下自动关闭连接(可选)
- 不影响 Django 请求/事务周期
"""
in_atomic = connection.in_atomic_block # 当前是否在事务中
autocommit = transaction.get_autocommit()
recreated = False
try:
if not connection.is_usable():
connection.close()
connection.connect()
created = True
recreated = True
yield
finally:
# 如果不是事务中API 请求中可能需要提交事务),则关闭连接
if auto_close or (created and not in_atomic_block and autocommit):
# 只在非事务、autocommit 模式下,才考虑主动清理连接
if auto_close or (recreated and not in_atomic and autocommit):
close_old_connections()

View File

@@ -302,16 +302,8 @@ def bulk_handle(handler, batch_size=50, timeout=0.5):
cache = [] # 缓存实例的列表
lock = threading.Lock() # 用于线程安全
timer = [None] # 定时器对象,列表存储以便重置
org_id = None
def reset_timer():
"""重置定时器"""
if timer[0] is not None:
timer[0].cancel()
timer[0] = threading.Timer(timeout, handle_remaining)
timer[0].start()
def handle_it():
from orgs.utils import tmp_to_org
with lock:
@@ -351,17 +343,13 @@ def bulk_handle(handler, batch_size=50, timeout=0.5):
if len(cache) >= batch_size:
handle_it()
reset_timer()
return instance
# 提交剩余实例的方法
def handle_remaining():
if not cache:
return
print("Timer expired. Saving remaining instances.")
from orgs.utils import tmp_to_org
with tmp_to_org(org_id):
handle_it()
handle_it()
wrapper.finish = handle_remaining
return wrapper

View File

@@ -25,3 +25,4 @@ BASE_DIR = os.path.dirname(settings.BASE_DIR)
LOG_DIR = os.path.join(BASE_DIR, 'data', 'logs')
APPS_DIR = os.path.join(BASE_DIR, 'apps')
TMP_DIR = os.path.join(BASE_DIR, 'tmp')
CELERY_WORKER_COUNT = CONFIG.CELERY_WORKER_COUNT or 10

View File

@@ -4,10 +4,10 @@ from ..hands import *
class CeleryBaseService(BaseService):
def __init__(self, queue, num=10, **kwargs):
def __init__(self, queue, **kwargs):
super().__init__(**kwargs)
self.queue = queue
self.num = num
self.num = CELERY_WORKER_COUNT
@property
def cmd(self):

View File

@@ -3,6 +3,7 @@
import time
from django.conf import settings
from django.core.cache import cache
from rest_framework import permissions
@@ -30,6 +31,8 @@ class WithBootstrapToken(permissions.BasePermission):
def check_can_register(self):
enabled = settings.SECURITY_SERVICE_ACCOUNT_REGISTRATION
if enabled == 'auto':
if cache.get(f'APPLET_HOST_DELOYING'):
return True
return time.time() - settings.JUMPSERVER_UPTIME < 300
elif enabled:
return True

View File

@@ -123,7 +123,7 @@ def get_es_client_version(**kwargs):
class ES(object):
def __init__(self, config, properties, keyword_fields, exact_fields=None, match_fields=None):
def __init__(self, config, properties, keyword_fields, exact_fields=None, fuzzy_fields=None, match_fields=None, **kwargs):
self.version = 7
self.config = config
hosts = self.config.get('HOSTS')
@@ -140,7 +140,7 @@ class ES(object):
self.index = None
self.query_index = None
self.properties = properties
self.exact_fields, self.match_fields, self.keyword_fields = set(), set(), set()
self.exact_fields, self.match_fields, self.keyword_fields, self.fuzzy_fields = set(), set(), set(), set()
if isinstance(keyword_fields, Iterable):
self.keyword_fields.update(keyword_fields)
@@ -148,6 +148,8 @@ class ES(object):
self.exact_fields.update(exact_fields)
if isinstance(match_fields, Iterable):
self.match_fields.update(match_fields)
if isinstance(fuzzy_fields, Iterable):
self.fuzzy_fields.update(fuzzy_fields)
self.init_index()
self.doc_type = self.config.get("DOC_TYPE") or '_doc'
@@ -314,6 +316,17 @@ class ES(object):
query: {k: v}
})
return _filter
@staticmethod
def handle_fuzzy_fields(exact):
_filter = []
for k, v in exact.items():
_filter.append({ 'wildcard': { k: f'*{v}*' } })
return _filter
@staticmethod
def is_keyword(props: dict, field: str) -> bool:
return props.get(field, {}).get("type", "keyword") == "keyword"
def get_query_body(self, **kwargs):
new_kwargs = {}
@@ -331,18 +344,37 @@ class ES(object):
keyword_fields = self.keyword_fields
exact_fields = self.exact_fields
match_fields = self.match_fields
fuzzy_fields = self.fuzzy_fields
match = {}
search = []
exact = {}
fuzzy = {}
index = {}
if index_in_field in kwargs:
index['values'] = kwargs[index_in_field]
mapping = self.es.indices.get_mapping(index=self.query_index)
props = (
mapping
.get(self.query_index, {})
.get('mappings', {})
.get('properties', {})
)
common_keyword_able = exact_fields | keyword_fields
for k, v in kwargs.items():
if k in exact_fields.union(keyword_fields):
exact['{}.keyword'.format(k)] = v
if k in ("org_id", "session") and self.is_keyword(props, k):
exact[k] = v
elif k in common_keyword_able:
exact[f"{k}.keyword"] = v
elif k in fuzzy_fields:
fuzzy[f"{k}.keyword"] = v
elif k in match_fields:
match[k] = v
@@ -384,9 +416,10 @@ class ES(object):
'should': should + [
{'match': {k: v}} for k, v in match.items()
] + [
{'match': item} for item in search
],
{'match': item} for item in search
],
'filter': self.handle_exact_fields(exact) +
self.handle_fuzzy_fields(fuzzy) +
[
{
'range': {
@@ -442,7 +475,7 @@ class QuerySet(DJQuerySet):
names, multi_args, multi_kwargs = zip(*filter_calls)
# input 输入
multi_args = tuple(reduce(lambda x, y: x + y, (sub for sub in multi_args if sub),()))
multi_args = tuple(reduce(lambda x, y: x + y, (sub for sub in multi_args if sub), ()))
args = self._grouped_search_args(multi_args)
striped_args = [{k.replace('__icontains', ''): v} for k, values in args.items() for v in values]
@@ -548,4 +581,4 @@ class QuerySet(DJQuerySet):
return iter(self.__execute())
def __len__(self):
return self.count()
return self.count()

View File

@@ -1,6 +1,7 @@
cipher_alg_id = {
"sm4_ebc": 0x00000401,
"sm4_cbc": 0x00000402,
"sm4_mac": 0x00000405,
}

View File

@@ -75,7 +75,7 @@ class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
pk_number = struct.pack('!B', 1)
registered_delivery = struct.pack('!B', 0)
msg_level = struct.pack('!B', 0)
service_id = ((10 - len(service_id)) * '\x00' + service_id).encode('utf-8')
service_id = service_id.ljust(10, '\x00').encode('utf-8')
fee_user_type = struct.pack('!B', 2)
fee_terminal_id = ('0' * 21).encode('utf-8')
tp_pid = struct.pack('!B', 0)
@@ -85,7 +85,7 @@ class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
fee_code = '000000'.encode('utf-8')
valid_time = ('\x00' * 17).encode('utf-8')
at_time = ('\x00' * 17).encode('utf-8')
src_id = ((21 - len(src_id)) * '\x00' + src_id).encode('utf-8')
src_id = src_id.ljust(21, '\x00').encode('utf-8')
reserve = b'\x00' * 8
_msg_length = struct.pack('!B', len(msg_content) * 2)
_msg_src = msg_src.encode('utf-8')

View File

@@ -6,6 +6,7 @@ from django.core.mail import send_mail, EmailMultiAlternatives, get_connection
from django.utils.translation import gettext_lazy as _
from common.storage import jms_storage
from users.models import User
from .utils import get_logger
logger = get_logger(__file__)
@@ -48,6 +49,8 @@ def send_mail_async(*args, **kwargs):
Example:
send_mail_sync.delay(subject, message, recipient_list, fail_silently=False, html_message=None)
"""
from users.utils import activate_user_language
if len(args) == 3:
args = list(args)
args[0] = (settings.EMAIL_SUBJECT_PREFIX or '') + args[0]
@@ -55,8 +58,18 @@ def send_mail_async(*args, **kwargs):
args.insert(2, from_email)
args = tuple(args)
subject = args[0] if len(args) > 0 else kwargs.get('subject')
recipient_list = args[3] if len(args) > 3 else kwargs.get('recipient_list')
logger.info(
"send_mail_async called with subject=%r, recipients=%r", subject, recipient_list
)
try:
return send_mail(connection=get_email_connection(), *args, **kwargs)
users = User.objects.filter(email__in=recipient_list).all()
for user in users:
with activate_user_language(user):
send_mail(connection=get_email_connection(), *args, **kwargs)
except Exception as e:
logger.error("Sending mail error: {}".format(e))

View File

@@ -16,6 +16,8 @@ def get_ip_city_by_geoip(ip):
global reader
if reader is None:
path = os.path.join(os.path.dirname(__file__), 'GeoLite2-City.mmdb')
if not os.path.exists(path):
raise FileNotFoundError(f"IP Database not found, please run `./requirements/static_files.sh`")
reader = geoip2.database.Reader(path)
try:

View File

@@ -12,6 +12,9 @@ def get_ip_city_by_ipip(ip):
global ipip_db
if ipip_db is None:
ipip_db_path = os.path.join(os.path.dirname(__file__), 'ipipfree.ipdb')
if not os.path.exists(ipip_db_path):
raise FileNotFoundError(
f"IP database not found, please run `bash ./requirements/static_files.sh`")
ipip_db = ipdb.City(ipip_db_path)
try:
info = ipip_db.find_info(ip, 'CN')

View File

@@ -6,7 +6,7 @@ import socket
import string
import struct
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
string_punctuation = '!#$%&()*+,-.:;<=?@[]_~'
def random_datetime(date_start, date_end):
@@ -48,7 +48,6 @@ def random_string(
char_list = []
if lower:
lower_chars = remove_exclude_char(string.ascii_lowercase, exclude_chars)
if not lower_chars:
raise ValueError('After excluding characters, no lowercase letters are available.')
@@ -78,7 +77,7 @@ def random_string(
if not special_chars:
raise ValueError('After excluding characters, no special characters are available.')
symbol_num = length // 16 + 1
seq = random_replace_char(seq, symbols, symbol_num)
seq = random_replace_char(seq, special_chars, symbol_num)
secret_chars += seq
secrets.SystemRandom().shuffle(secret_chars)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,8 @@
{
"ActionPerm": "Actions",
"ActionPerm": "Action Permission",
"AlreadyExistsPleaseRename": "File already exists, please rename it",
"AvailableShortcutKey": "Available Shortcut Key",
"Back": "Back",
"Cancel": "Cancel",
"CancelFileUpload": "Cancel file upload",
"Clone Connect": "Clone Connect",
@@ -11,7 +14,8 @@
"Connect": "Connect",
"CopyLink": "Copy Link Address and Code",
"CopyShareURLSuccess": "Copy Share URL Success",
"CreateLink": "Create Share Link",
"CreateFolder": "Create folder",
"CreateLink": "Create link",
"CreateSuccess": "Success",
"CurrentUser": "Current user",
"Custom Setting": "Custom Setting",
@@ -25,12 +29,17 @@
"EndFileTransfer": "File transfer end",
"ExceedTransferSize": "exceed max transfer size",
"Expand": "Expand",
"ExpiredTime": "Expired",
"ExpiredTime": "Expiration time",
"FailedCreateConnection": "Failed to create connection",
"FileAlreadyExists": "File already exists",
"FileListError": "Failed to get file list",
"FileManagement": "File",
"FileManagement": "File Management",
"FileManagementExpired": "The current file management session has expired.",
"FileTransferInterrupted": "File transfer interrupted",
"FileUploadInterrupted": "File upload interrupted",
"Format": "Format",
"General": "General",
"GetFileManagerTokenTimeOut": "Get file manager token timeout",
"GetShareUser": "Enter username",
"Hotkeys": "Hotkeys",
"InputVerifyCode": "Input Verify Code",
@@ -40,7 +49,7 @@
"LastModified": "Last Modified",
"LeaveShare": "Leave Session",
"LeftArrow": "Left arrow",
"LinkAddr": "Link",
"LinkAddr": "Link Address",
"List": "List",
"Minute": "Minute",
"Minutes": "Minutes",
@@ -48,22 +57,26 @@
"MustSelectOneFile": "Must select one file",
"Name": "Name",
"NewFolder": "New Folder",
"NoActiveTerminalTabFound": "No active terminal tab found",
"NoData": "No data",
"NoLink": "No Link",
"NoOnlineUser": "No online user",
"OnlineUser": "Online user",
"OnlineUsers": "Online Users",
"NoRunningTerminalFound": "No running terminal found",
"OnlineUser": "Online User",
"OperationSuccessful": "Operation successful",
"Paste": "Paste",
"PauseSession": "Pause Session",
"PermissionDenied": "Permission denied",
"PermissionExpired": "Permission expired",
"PermissionValid": "Permission valid",
"ReadOnly": "Read-Only",
"PleaseInput": "Please input",
"PleaseInputVerifyCode": "Please input verify code",
"PrimaryUser": "Primary user",
"ReadOnly": "Read Only",
"Reconnect": "Reconnect",
"Refresh": "Refresh",
"Remove": "Remove",
"RemoveShareUser": "You have been removed from the shared session.",
"RemoveShareUserConfirm": "Are you sure to remove the user from the shared session?",
"RemoveUser": "Remove User",
"Rename": "Rename",
"ResumeSession": "Resume Session",
"RightArrow": "Right arrow",
@@ -71,20 +84,25 @@
"SelectAction": "Select",
"SelectTheme": "Select Theme",
"Self": "Self",
"SessionDetail": "Session Detail",
"SessionShare": "Session Share",
"Settings": "Settings",
"Share": "Share",
"ShareUser": "ForUser",
"ShareLink": "Share Link",
"ShareUser": "Share User",
"ShareUserHelpText": "If left blank, everyone could join the session.",
"Size": "Size",
"Sync": "Sync",
"SyncUserPreferenceFailed": "Sync user preference failed",
"SyncUserPreferenceSuccess": "Sync user preference success",
"TerminalInstanceNotFoundForCurrentTab": "Terminal instance not found for current tab",
"TheCurrentTerminalInstanceWasNotFound": "The current terminal instance was not found",
"Theme": "Theme",
"ThemeColors": "Theme Colors",
"ThemeConfig": "Theme",
"ThemeSyncSuccessful": "Theme sync successful",
"TransferHistory": "Transfer history",
"Transfer": "Transfer",
"Type": "Type",
"UnableToGenerateWebSocketURL": "Unable to generate WebSocket URL, missing parameters",
"UpArrow": "Up arrow",
"Upload": "Upload",
"UploadEnd": "Upload completed, please wait for further processing",
@@ -92,11 +110,12 @@
"UploadStart": "Upload start",
"UploadSuccess": "Upload success",
"UploadTips": "Drag file here or click to upload",
"UploadTitle": "file upload",
"UploadTitle": "File upload",
"User": "User",
"VerifyCode": "Verify Code",
"WaitFileTransfer": "Wait file transfer to finish",
"Warning": "Warning",
"WebSocketClosed": "WebSocket closed",
"WebSocketConnectionIsClosedHelpText": "WebSocket connection is closed, please refresh the page or reconnect.",
"Writable": "Writable"
}

View File

@@ -1,5 +1,8 @@
{
"ActionPerm": "Permisos de operación",
"ActionPerm": "Permisos de acción",
"AlreadyExistsPleaseRename": "El archivo ya existe, por favor renombrar",
"AvailableShortcutKey": "Atajos disponibles",
"Back": "Regresar",
"Cancel": "Cancelar",
"CancelFileUpload": "Cancelar la subida del archivo",
"Clone Connect": "Copiar ventana",
@@ -11,7 +14,8 @@
"Connect": "Conectar",
"CopyLink": "Copiar enlace y código de verificación",
"CopyShareURLSuccess": "Dirección de compartición copiada con éxito",
"CreateLink": "Crear enlace compartido",
"CreateFolder": "Crear carpeta",
"CreateLink": "Crear enlace para compartir",
"CreateSuccess": "Creación exitosa",
"CurrentUser": "Usuario actual",
"Custom Setting": "Ajustes personalizados",
@@ -25,12 +29,17 @@
"EndFileTransfer": "Transferencia de archivos finalizada",
"ExceedTransferSize": "Superado el tamaño máximo de transferencia",
"Expand": "Expandir",
"ExpiredTime": "Fecha de caducidad",
"ExpiredTime": "Fecha de validez",
"FailedCreateConnection": "Fallo al crear conexión",
"FileAlreadyExists": "El archivo ya existe",
"FileListError": "No se pudo obtener la información de la lista de archivos",
"FileManagement": "Gestión de archivos",
"FileManagementExpired": "La sesión actual de gestión de archivos ha expirado.",
"FileTransferInterrupted": "Transferencia de archivos interrumpida",
"FileUploadInterrupted": "La subida del archivo se ha interrumpido",
"Format": "Formato",
"General": "General",
"GetFileManagerTokenTimeOut": "Tiempo de espera para obtener el token de gestión de archivos",
"GetShareUser": "Introducir nombre de usuario",
"Hotkeys": "Atajos",
"InputVerifyCode": "Por favor, ingrese el código de verificación",
@@ -43,27 +52,31 @@
"LinkAddr": "Dirección del enlace",
"List": "Lista",
"Minute": "Minutos",
"Minutes": "Minutos",
"Minutes": "Parte",
"MustOneFile": "Solo se puede seleccionar un archivo",
"MustSelectOneFile": "Debe seleccionar un archivo",
"Name": "Nombre",
"NewFolder": "Nueva carpeta",
"NoActiveTerminalTabFound": "No se encontró una pestaña de terminal activa",
"NoData": "Sin datos",
"NoLink": "Sin dirección",
"NoOnlineUser": "No hay usuarios en línea",
"NoRunningTerminalFound": "No se encontró ningún terminal en ejecución",
"OnlineUser": "Usuarios en línea",
"OnlineUsers": "Personas en línea",
"OperationSuccessful": "La acción se realizó con éxito",
"Paste": "Pegar",
"PauseSession": "Pausar esta sesión",
"PermissionDenied": "Sin permiso",
"PermissionExpired": "Los permisos han expirado",
"PermissionValid": "Permisos válidos",
"PleaseInput": "Por favor, ingrese.",
"PleaseInputVerifyCode": "Por favor, ingresa el código de verificación",
"PrimaryUser": "Usuario principal",
"ReadOnly": "Solo lectura",
"Reconnect": "Reconectar",
"Refresh": "Refrescar",
"Remove": "Eliminar",
"RemoveShareUser": "Has sido eliminado de la sesión compartida",
"RemoveShareUserConfirm": "¿Está seguro de que desea eliminar a este usuario?",
"RemoveUser": "Eliminar usuario",
"Rename": "Renombrar",
"ResumeSession": "Restaurar esta sesión",
"RightArrow": "Flecha hacia adelante",
@@ -71,20 +84,25 @@
"SelectAction": "Por favor selecciona",
"SelectTheme": "Por favor, selecciona un tema",
"Self": "Yo",
"SessionDetail": "Detalles de la conversación.",
"SessionShare": "Compartir conversación",
"Settings": "Ajustes",
"Share": "Compartir",
"ShareLink": "Compartir enlace",
"ShareUser": "Compartir usuario",
"ShareUserHelpText": "No se ha seleccionado un usuario, lo que permite la entrada de todos",
"Size": "Tamaño",
"Sync": "Sincronizar",
"SyncUserPreferenceFailed": "Falló la sincronización de ajustes",
"SyncUserPreferenceSuccess": "Sincronización de ajustes exitosa",
"TerminalInstanceNotFoundForCurrentTab": "La pestaña actual no encontró una instancia de terminal",
"TheCurrentTerminalInstanceWasNotFound": "No se encontró la instancia de terminal actual",
"Theme": "Tema",
"ThemeColors": "Color del tema",
"ThemeConfig": "Tema",
"ThemeSyncSuccessful": "Sincronización del tema exitosa",
"TransferHistory": "Transmisión de historial",
"Transfer": "Transmisión",
"Type": "Tipo",
"UnableToGenerateWebSocketURL": "No se puede generar la URL de WebSocket, faltan parámetros",
"UpArrow": "Flecha hacia arriba",
"Upload": "Subir",
"UploadEnd": "La subida ha finalizado, por favor espera el procesamiento posterior",
@@ -94,9 +112,10 @@
"UploadTips": "Arrastra el archivo aquí, o haz clic para subir",
"UploadTitle": "Subir archivo",
"User": "Usuario",
"VerifyCode": "Código de verificación",
"VerifyCode": "Código de verificación.",
"WaitFileTransfer": "Esperando que finalice la transferencia de archivos",
"Warning": "Advertencia",
"WebSocketClosed": "WebSocket cerrado",
"Writable": "Editable"
"WebSocketConnectionIsClosedHelpText": "La conexión WebSocket se ha cerrado, por favor actualiza la página o reconéctate.",
"Writable": "Se puede escribir"
}

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