Compare commits

...

95 Commits

Author SHA1 Message Date
wangruidong
6686afcec1 fix: Password reset is only required for AUTH_BACKEND_MODEL 2024-10-23 11:04:15 +08:00
wangruidong
0918f5c6f6 perf: Translate 2024-10-22 17:49:22 +08:00
wangruidong
891e3d5609 perf: Storage update comment failed 2024-10-22 17:28:47 +08:00
wangruidong
9fad591545 fix: Historical sessions download failed 2024-10-22 16:34:46 +08:00
fit2bot
1ed1c3a536 perf: optimize the connection of operation logs to ES to prevent ES downtime from causing the core component to become unhealthy. (#14283)
* perf: optimize the connection of operation logs to ES to prevent ES downtime from causing the core component to become unhealthy.

* perf: sync publish message

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-10-12 16:18:18 +08:00
wangruidong
63824d3491 fix: adhoc execute alert msg 2024-10-12 16:15:48 +08:00
wangruidong
96eadf060c perf: site msg content optimize 2024-10-11 11:30:59 +08:00
Bai
2c9128b0e7 perf: DEFAULT_PAGE_SIZE same as MAX_LIMIT_PER_PAGE 2024-10-10 18:00:26 +08:00
Bai
7d6fd0f881 fix: Fixed the issue that the workbench user login log only displays failed logs 2024-09-29 14:45:59 +08:00
jiangweidong
4e996afd5e perf: Cloud Sync IP Policy Updated to Preferred Option i18n 2024-09-27 14:29:08 +08:00
feng
1ed745d042 perf: Login encryption key cache added 2024-09-26 15:11:56 +08:00
feng
39ebbfcf10 perf: The locked ip shows the username 2024-09-06 11:00:39 +08:00
wangruidong
d0ec4f798b perf: Optimize asset connection speed with es command storage 2024-08-30 10:53:03 +08:00
feng
1712a9a104 perf: No permission to test asset connectivity 2024-08-21 11:33:03 +08:00
Bryan
951aafcabd fix: v3 apps/authentication migrations won't be applied 2024-08-20 19:05:43 +08:00
wangruidong
e46da9d741 perf: Translate 2024-08-20 16:41:50 +08:00
ibuler
06aaf9e3d0 revert: dockerfile change 2024-08-20 14:32:50 +08:00
ibuler
5ac9fb81dc perf: change docker file 2024-08-20 13:59:49 +08:00
ibuler
fb907e250c perf: change docker file 2024-08-20 13:51:00 +08:00
ibuler
34ea40a14d perf: change docker file 2024-08-19 16:45:57 +08:00
ibuler
8c560b0317 perf: add dockerfile 2024-08-19 16:13:06 +08:00
wangruidong
df9cc4700b perf: Improve performance by optimizing ES index creation 2024-08-16 18:18:33 +08:00
wangruidong
6aa1227e60 fix: call get_verify_state_failed_response NotImplementedError 2024-08-15 20:18:34 +08:00
Bai
296c788e28 fix: job periodic task double run 2024-08-15 20:17:48 +08:00
fit2bot
a1ae29d35e fix: Use only_sudo failed (#13966)
* fix: Use only_sudo failed

* fix: Use only_sudo failed

* fix: Use only_sudo failed

---------

Co-authored-by: feng <1304903146@qq.com>
2024-08-14 16:15:07 +08:00
fit2bot
139ffd0b47 perf: Automation remove account task fail (#13406)
Co-authored-by: feng <1304903146@qq.com>
2024-08-12 18:26:18 +08:00
feng
ff6c1aef7f perf: Activity log no display 2024-08-08 16:39:13 +08:00
Eric
9b6b48a7f1 perf: support only su or sudo 2024-08-07 14:23:26 +08:00
wangruidong
2b133a8085 perf: object storage builtin comment i18n 2024-08-06 10:45:32 +08:00
Eric
81b5f1ce93 perf: Check whether the applet is available. 2024-08-05 18:18:05 +08:00
feng
c646084c51 perf: Ticket set serial number add lock 2024-08-05 17:53:08 +08:00
wangruidong
5e69c03cb7 perf: Remove applets, no longer display remote application connection methods 2024-08-01 15:59:35 +08:00
wangruidong
e2df85bddd fix: stop job failed 2024-07-30 18:49:50 +08:00
feng
d710697fa9 perf: Saml2 callback url miss port 2024-07-26 18:14:49 +08:00
halo
a955fcd682 perf: Email service authentication username is optional 2024-07-26 14:04:47 +08:00
Bai
1816d52d21 perf: Modifying the label matching logic of an AppletHost (random) 2024-07-25 19:02:41 +08:00
feng
d7c26cab7d fix: Console dashboard user login count 2024-07-24 16:10:05 +08:00
wangruidong
dc894fdc2d perf: Modify error message for desktop client login 2024-07-23 14:03:39 +08:00
feng
742ef89bef perf: You can modify sudo permissions multiple times 2024-07-22 17:27:04 +08:00
feng
3d4fc56592 perf: Gpt3 to gpt-4o-mini 2024-07-19 11:56:41 +08:00
feng
45291aba0c perf: The gateway password contains ! Password parsing failed 2024-07-19 10:41:41 +08:00
feng
495ee99e29 perf: Create authorization to add template account Push account parameters 2024-07-18 19:15:59 +08:00
jiangweidong
223eb8ad38 fix: async sms task params can json 2024-07-12 18:38:02 +08:00
gerry-fit
370e959400 perf: Enterprise Edition Hide Footer Copyright Content 2024-07-11 11:47:22 +08:00
fit2bot
b82f007787 perf: Migrate (#13689)
Co-authored-by: feng <1304903146@qq.com>
2024-07-11 10:35:01 +08:00
Eric
1faeb54673 perf: update locale i18n files 2024-07-10 18:42:24 +08:00
wangruidong
04e102cb87 fix: 定时清理任务不生效问题 2024-07-10 15:39:26 +08:00
fit2bot
81027cd561 perf: save_passwd_change filter user source local and passwords not emtpy (#13680)
Co-authored-by: feng <1304903146@qq.com>
2024-07-10 14:25:43 +08:00
fit2bot
cf727d22c0 fix: Account tempale cannot push params (#13671)
Co-authored-by: feng <1304903146@qq.com>
2024-07-09 19:12:24 +08:00
feng
bb6d077645 perf: save_passwd_change filter user source local and passwords not emtpy 2024-07-09 19:07:49 +08:00
halo
a78ccc9667 perf: 优化创建子节点时锁置后 2024-07-09 15:15:36 +08:00
Eric
d70351e6b3 perf: add i18n .mo file 2024-07-09 15:11:21 +08:00
Eric
4e76207adb perf: add i18n 2024-07-09 14:44:59 +08:00
ibuler
7a12c3737f perf: xpack can disable force 2024-07-09 11:10:03 +08:00
吴小白
8450c49e25 Merge pull request #13639 from jumpserver/pr@v3@update_poetry_lock
perf: update poetry lock
2024-07-09 10:59:00 +08:00
吴小白
ab6d8df2f0 perf: update poetry lock 2024-07-09 10:48:04 +08:00
Bai
550115c39f perf: update poetry lock 2024-07-08 19:42:43 +08:00
Eric
9c23512d91 perf: add connection options for mongodb 2024-07-08 18:21:57 +08:00
ibuler
30054b286a perf: change ansible version 2024-07-08 14:29:25 +08:00
Eric
22d7385891 perf: clean mp4 replay file 2024-06-25 19:07:17 +08:00
Bai
1701bedb41 perf: Update poetry lock file 2024-06-24 14:42:05 +08:00
fit2bot
165d030c8e perf: ansible runner in isolated mode (#13434)
perf: use new ansible runner

perf: change lock

Co-authored-by: ibuler <ibuler@qq.com>
2024-06-24 10:21:31 +08:00
feng
9be77cf58f perf: Ansible inventory set jms 2024-06-24 10:15:05 +08:00
吴小白
887724bad4 feat: upgrade poetry.lock 2024-06-24 10:11:49 +08:00
Bai
b283d88781 fix: Clone asset with accounts 2024-06-19 16:00:58 +08:00
gerry-fit
2977323800 perf: 登录重置密码传输进行加密 2024-06-19 14:53:50 +08:00
wangruidong
4a520e9e10 fix: 全局组织,添加标签报错 2024-06-19 09:27:03 +08:00
wangruidong
44f29e166c fix: 一些任务查找不到id和执行者 2024-06-18 16:52:18 +08:00
fit2bot
f42113afb9 fix: Fixed the issue of user login statistics (#13440)
Co-authored-by: feng <1304903146@qq.com>
2024-06-18 14:18:02 +08:00
Bai
ff126f3459 fix: delete account error (DoesNotExist) 2024-06-18 11:06:40 +08:00
wangruidong
66cd6e95a8 fix: 获取账号改密的任务列表超时 2024-06-14 18:54:01 +08:00
wangruidong
b28aec527f perf: 默认关闭作业中心 2024-06-14 18:18:35 +08:00
jiangweidong
496903dfb2 fix: 解决获取用户登录后端的session_key有两种的问题 2024-06-13 17:49:56 +08:00
wangruidong
0a0312695b fix: es使用https报错 2024-06-13 10:33:47 +08:00
wangruidong
3608b025e5 fix: es8会话记录查询不到命令 2024-06-12 15:49:57 +08:00
Bai
68244b2b37 perf: 更新 lock 文件 2024-06-12 14:30:37 +08:00
wangruidong
948e9ecb4b perf: 命令存储支持ES8的版本 2024-06-12 14:15:13 +08:00
wangruidong
7ad4d9116a fix: LDAP定时同步任务设置多个通知人,消息内容分除第一个正常,其它人都不正常 2024-06-11 18:24:43 +08:00
wangruidong
9439035b86 fix: 账号备份,云同步定时任务不执行 2024-06-07 18:33:46 +08:00
halo
2b220d3753 perf: 去掉account序列化中params属性 2024-06-07 15:54:33 +08:00
Bai
440a7ae9cc perf: 添加配置项 FILE_UPLOAD_TEMP_DIR 2024-06-06 16:32:23 +08:00
Bai
40a4efc992 fix: 修复用户登录报错刷新浏览器后依旧报错的问题(登录超时,请重新登录) 2024-06-04 16:33:06 +08:00
老广
15d4fafbdb chrome: change github action 2024-06-04 16:23:25 +08:00
Gerry.tan
48b037ac26 feat: 支持 Dameng 数据库 2024-05-31 14:45:41 +08:00
jiangweidong
dfd133cf5a perf: optimize user operation logs (#13221) 2024-05-31 11:05:35 +08:00
jiangweidong
cdfb11549e fix: 解决OAuth2可以跳过不存在用户不允许登录的规则 2024-05-31 10:43:01 +08:00
fit2bot
0d825927e1 perf: Optimize GitHub labels and update related workflows (#13315)
* perf: Optimize GitHub labels and update related workflows

* perf: Optimize issue template

* perf: Optimize issue template

* Update 1_bug_report.yml

* Update 1_bug_report.yml

* Update 1_bug_report.yml

* Update 1_bug_report.yml

* Update 1_bug_report.yml

* Update 2_feature_request.yml

* Update 2_feature_request.yml

* Update 3_question.yml

* Update 3_question.yml

* Update 3_question.yml

* Update 1_bug_report.yml

* Update 2_feature_request.yml

* Update 1_bug_report_cn.yml

* Update 1_bug_report_cn.yml

* Update 2_feature_request_cn.yml

* Update 1_bug_report_cn.yml

* Update 1_bug_report_cn.yml

* Update 1_bug_report_cn.yml

* Update 3_question_cn.yml

* Update 1_bug_report_cn.yml

* Update 2_feature_request_cn.yml

* Update 3_question_cn.yml

* Update 2_feature_request_cn.yml

* Update 1_bug_report.yml

* Update 1_bug_report_cn.yml

* Update 2_feature_request.yml

* Update 3_question.yml

* perf: Optimize issue template

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
Co-authored-by: Bryan <jiangjie.bai@fit2cloud.com>
2024-05-29 18:15:13 +08:00
Bai
4e8d7df005 fix: v2->v3 The issue of authorized accounts displaying as empty when there are more than 10,000 authorization rules. 2024-05-28 16:09:12 +08:00
Bai
5d1829b998 fix: Disable the applet connection method when all applet hosts have is_active set to False 2024-05-28 11:07:40 +08:00
Bai
75df845024 perf: Remove dependency django-rest-swagger 2024-05-28 10:34:37 +08:00
Bai
c103253867 perf: perm tree search 2024-05-27 18:05:21 +08:00
feng
81da9e018a fix: windows sync remove account fail and applet deploy rbac perm error and job exection log admin auditor cannot view 2024-05-27 11:41:30 +08:00
Bryan
7f90fccc4f perf: The label matching policy is configured with a random selection publisher 2024-05-27 10:26:35 +08:00
fit2bot
4ebcba81e0 perf: dates_metrics api speed (#13266)
Co-authored-by: feng <1304903146@qq.com>
2024-05-22 15:25:38 +08:00
wangruidong
5616d31888 perf: CeleryTaskExecution保存时去掉无用参数 2024-05-22 14:04:11 +08:00
145 changed files with 13333 additions and 2954 deletions

View File

@@ -1,35 +0,0 @@
---
name: 需求建议
about: 提出针对本项目的想法和建议
title: "[Feature] 需求标题"
labels: 类型:需求
assignees:
- ibuler
- baijiangjie
---
## 注意
_针对过于简单的需求描述不予考虑。请确保提供足够的细节和信息以支持功能的开发和实现。_
## 功能名称
[在这里输入功能的名称或标题]
## 功能描述
[在这里描述该功能的详细内容,包括其作用、目的和所需的功能]
## 用户故事(可选)
[如果适用,可以提供用户故事来更好地理解该功能的使用场景和用户期望]
## 功能要求
- [要求1描述该功能的具体要求如界面设计、交互逻辑等]
- [要求2描述该功能的另一个具体要求]
- [以此类推,列出所有相关的功能要求]
## 示例或原型(可选)
[如果有的话,提供该功能的示例或原型图以更好地说明功能的实现方式]
## 优先级
[描述该功能的优先级,如高、中、低,或使用数字等其他标识]
## 备注(可选)
[在这里添加任何其他相关信息或备注]

72
.github/ISSUE_TEMPLATE/1_bug_report.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: '🐛 Bug Report'
description: 'Report an Bug'
title: '[Bug] '
labels: ['🐛 Bug']
assignees:
- baijiangjie
body:
- type: input
attributes:
label: 'Product Version'
description: The versions prior to v2.28 (inclusive) are no longer supported.
validations:
required: true
- type: checkboxes
attributes:
label: 'Product Edition'
options:
- label: 'Community Edition'
- label: 'Enterprise Edition'
- label: 'Enterprise Trial Edition'
validations:
required: true
- type: checkboxes
attributes:
label: 'Installation Method'
options:
- label: 'Online Installation (One-click command installation)'
- label: 'Offline Package Installation'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: 'Source Code'
- type: textarea
attributes:
label: 'Environment Information'
description: Please provide a clear and concise description outlining your environment information.
validations:
required: true
- type: textarea
attributes:
label: '🐛 Bug Description'
description:
Please provide a clear and concise description of the defect. If the issue is complex, please provide detailed explanations. <br/>
Unclear descriptions will not be processed. Please ensure you provide enough detail and information to support replicating and fixing the defect.
validations:
required: true
- type: textarea
attributes:
label: 'Recurrence Steps'
description: Please provide a clear and concise description outlining how to reproduce the issue.
validations:
required: true
- type: textarea
attributes:
label: 'Expected Behavior'
description: Please provide a clear and concise description of what you expect to happen.
- type: textarea
attributes:
label: 'Additional Information'
description: Please add any additional background information about the issue here.
- type: textarea
attributes:
label: 'Attempted Solutions'
description: If you have already attempted to solve the issue, please list the solutions you have tried here.

View File

@@ -0,0 +1,72 @@
name: '🐛 反馈缺陷'
description: '反馈一个缺陷'
title: '[Bug] '
labels: ['🐛 Bug']
assignees:
- baijiangjie
body:
- type: input
attributes:
label: '产品版本'
description: 不再支持 v2.28(含)之前的版本。
validations:
required: true
- type: checkboxes
attributes:
label: '版本类型'
options:
- label: '社区版'
- label: '企业版'
- label: '企业试用版'
validations:
required: true
- type: checkboxes
attributes:
label: '安装方式'
options:
- label: '在线安装 (一键命令安装)'
- label: '离线包安装'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: '源码安装'
- type: textarea
attributes:
label: '环境信息'
description: 请提供一个清晰且简洁的描述,说明你的环境信息。
validations:
required: true
- type: textarea
attributes:
label: '🐛 缺陷描述'
description: |
请提供一个清晰且简洁的缺陷描述,如果问题比较复杂,也请详细说明。<br/>
针对不清晰的描述信息将不予处理。请确保提供足够的细节和信息,以支持对缺陷进行复现和修复。
validations:
required: true
- type: textarea
attributes:
label: '复现步骤'
description: 请提供一个清晰且简洁的描述,说明如何复现问题。
validations:
required: true
- type: textarea
attributes:
label: '期望结果'
description: 请提供一个清晰且简洁的描述,说明你期望发生什么。
- type: textarea
attributes:
label: '补充信息'
description: 在这里添加关于问题的任何其他背景信息。
- type: textarea
attributes:
label: '尝试过的解决方案'
description: 如果你已经尝试解决问题,请在此列出你尝试过的解决方案。

View File

@@ -0,0 +1,56 @@
name: '⭐️ Feature Request'
description: 'Suggest an idea'
title: '[Feature] '
labels: ['⭐️ Feature Request']
assignees:
- baijiangjie
- ibuler
body:
- type: input
attributes:
label: 'Product Version'
description: The versions prior to v2.28 (inclusive) are no longer supported.
validations:
required: true
- type: checkboxes
attributes:
label: 'Product Edition'
options:
- label: 'Community Edition'
- label: 'Enterprise Edition'
- label: 'Enterprise Trial Edition'
validations:
required: true
- type: checkboxes
attributes:
label: 'Installation Method'
options:
- label: 'Online Installation (One-click command installation)'
- label: 'Offline Package Installation'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: 'Source Code'
- type: textarea
attributes:
label: '⭐️ Feature Description'
description: |
Please add a clear and concise description of the problem you aim to solve with this feature request.<br/>
Unclear descriptions will not be processed.
validations:
required: true
- type: textarea
attributes:
label: 'Proposed Solution'
description: Please provide a clear and concise description of the solution you desire.
validations:
required: true
- type: textarea
attributes:
label: 'Additional Information'
description: Please add any additional background information about the issue here.

View File

@@ -0,0 +1,56 @@
name: '⭐️ 功能需求'
description: '提出需求或建议'
title: '[Feature] '
labels: ['⭐️ Feature Request']
assignees:
- baijiangjie
- ibuler
body:
- type: input
attributes:
label: '产品版本'
description: 不再支持 v2.28(含)之前的版本。
validations:
required: true
- type: checkboxes
attributes:
label: '版本类型'
options:
- label: '社区版'
- label: '企业版'
- label: '企业试用版'
validations:
required: true
- type: checkboxes
attributes:
label: '安装方式'
options:
- label: '在线安装 (一键命令安装)'
- label: '离线包安装'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: '源码安装'
- type: textarea
attributes:
label: '⭐️ 需求描述'
description: |
请添加一个清晰且简洁的问题描述,阐述你希望通过这个功能需求解决的问题。<br/>
针对不清晰的描述信息将不予处理。
validations:
required: true
- type: textarea
attributes:
label: '解决方案'
description: 请清晰且简洁地描述你想要的解决方案。
validations:
required: true
- type: textarea
attributes:
label: '补充信息'
description: 在这里添加关于问题的任何其他背景信息。

60
.github/ISSUE_TEMPLATE/3_question.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: '🤔 Question'
description: 'Pose a question'
title: '[Question] '
labels: ['🤔 Question']
assignees:
- baijiangjie
body:
- type: input
attributes:
label: 'Product Version'
description: The versions prior to v2.28 (inclusive) are no longer supported.
validations:
required: true
- type: checkboxes
attributes:
label: 'Product Edition'
options:
- label: 'Community Edition'
- label: 'Enterprise Edition'
- label: 'Enterprise Trial Edition'
validations:
required: true
- type: checkboxes
attributes:
label: 'Installation Method'
options:
- label: 'Online Installation (One-click command installation)'
- label: 'Offline Package Installation'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: 'Source Code'
- type: textarea
attributes:
label: 'Environment Information'
description: Please provide a clear and concise description outlining your environment information.
validations:
required: true
- type: textarea
attributes:
label: '🤔 Question Description'
description: |
Please provide a clear and concise description of the defect. If the issue is complex, please provide detailed explanations. <br/>
Unclear descriptions will not be processed.
validations:
required: true
- type: textarea
attributes:
label: 'Expected Behavior'
description: Please provide a clear and concise description of what you expect to happen.
- type: textarea
attributes:
label: 'Additional Information'
description: Please add any additional background information about the issue here.

View File

@@ -0,0 +1,61 @@
name: '🤔 问题咨询'
description: '提出一个问题'
title: '[Question] '
labels: ['🤔 Question']
assignees:
- baijiangjie
body:
- type: input
attributes:
label: '产品版本'
description: 不再支持 v2.28(含)之前的版本。
validations:
required: true
- type: checkboxes
attributes:
label: '版本类型'
options:
- label: '社区版'
- label: '企业版'
- label: '企业试用版'
validations:
required: true
- type: checkboxes
attributes:
label: '安装方式'
options:
- label: '在线安装 (一键命令安装)'
- label: '离线包安装'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: '源码安装'
- type: textarea
attributes:
label: '环境信息'
description: 请在此详细描述你的环境信息,如操作系统、浏览器和部署架构等。
validations:
required: true
- type: textarea
attributes:
label: '🤔 问题描述'
description: |
请提供一个清晰且简洁的问题描述,如果问题比较复杂,也请详细说明。<br/>
针对不清晰的描述信息将不予处理。
validations:
required: true
- type: textarea
attributes:
label: '期望结果'
description: 请提供一个清晰且简洁的描述,说明你期望发生什么。
- type: textarea
attributes:
label: '补充信息'
description: 在这里添加关于问题的任何其他背景信息。

View File

@@ -1,51 +0,0 @@
---
name: Bug 提交
about: 提交产品缺陷帮助我们更好的改进
title: "[Bug] Bug 标题"
labels: 类型:Bug
assignees:
- baijiangjie
---
## 注意
**JumpServer 版本( v2.28 之前的版本不再支持 )** <br>
_针对过于简单的 Bug 描述不予考虑。请确保提供足够的细节和信息以支持 Bug 的复现和修复。_
## 当前使用的 JumpServer 版本 (必填)
[在这里输入当前使用的 JumpServer 的版本号]
## 使用的版本类型 (必填)
- [ ] 社区版
- [ ] 企业版
- [ ] 企业试用版
## 版本安装方式 (必填)
- [ ] 在线安装 (一键命令)
- [ ] 离线安装 (下载离线包)
- [ ] All-in-One
- [ ] 1Panel 安装
- [ ] Kubernetes 安装
- [ ] 源码安装
## Bug 描述 (详细)
[在这里描述 Bug 的详细情况,包括其影响和出现的具体情况]
## 复现步骤
1. [描述如何复现 Bug 的第一步]
2. [描述如何复现 Bug 的第二步]
3. [以此类推,列出所有复现 Bug 所需的步骤]
## 期望行为
[描述 Bug 出现时期望的系统行为或结果]
## 实际行为
[描述实际上发生了什么,以及 Bug 出现的具体情况]
## 系统环境
- 操作系统:[例如Windows 10, macOS Big Sur]
- 浏览器/应用版本:[如果适用,请提供相关版本信息]
- 其他相关环境信息:[如果有其他相关环境信息,请在此处提供]
## 附加信息(可选)
[在这里添加任何其他相关信息,如截图、错误信息等]

View File

@@ -1,50 +0,0 @@
---
name: 问题咨询
about: 提出针对本项目安装部署、使用及其他方面的相关问题
title: "[Question] 问题标题"
labels: 类型:提问
assignees:
- baijiangjie
---
## 注意
**请描述您的问题.** <br>
**JumpServer 版本( v2.28 之前的版本不再支持 )** <br>
_针对过于简单的 Bug 描述不予考虑。请确保提供足够的细节和信息以支持 Bug 的复现和修复。_
## 当前使用的 JumpServer 版本 (必填)
[在这里输入当前使用的 JumpServer 的版本号]
## 使用的版本类型 (必填)
- [ ] 社区版
- [ ] 企业版
- [ ] 企业试用版
## 版本安装方式 (必填)
- [ ] 在线安装 (一键命令)
- [ ] 离线安装 (下载离线包)
- [ ] All-in-One
- [ ] 1Panel 安装
- [ ] Kubernetes 安装
- [ ] 源码安装
## 问题描述 (详细)
[在这里描述你遇到的问题]
## 背景信息
- 操作系统:[例如Windows 10, macOS Big Sur]
- 浏览器/应用版本:[如果适用,请提供相关版本信息]
- 其他相关环境信息:[如果有其他相关环境信息,请在此处提供]
## 具体问题
[在这里详细描述你的问题,包括任何相关细节或错误信息]
## 尝试过的解决方法
[如果你已经尝试过解决问题,请在这里列出你已经尝试过的解决方法]
## 预期结果
[描述你期望的解决方案或结果]
## 我们的期望
[描述你希望我们提供的帮助或支持]

View File

@@ -12,7 +12,9 @@ jobs:
uses: actions-cool/issues-helper@v2
with:
actions: 'close-issues'
labels: '状态:待反馈'
labels: '⏳ Pending feedback'
inactive-day: 30
body: |
You haven't provided feedback for over 30 days.
We will close this issue. If you have any further needs, you can reopen it or submit a new issue.
您超过 30 天未反馈信息,我们将关闭该 issue如有需求您可以重新打开或者提交新的 issue。

View File

@@ -13,4 +13,4 @@ jobs:
if: ${{ !github.event.issue.pull_request }}
with:
actions: 'remove-labels'
labels: '状态:待处理,状态:待反馈'
labels: '🔔 Pending processing,⏳ Pending feedback'

View File

@@ -13,13 +13,13 @@ jobs:
uses: actions-cool/issues-helper@v2
with:
actions: 'add-labels'
labels: '状态:待处理'
labels: '🔔 Pending processing'
- name: Remove require reply label
uses: actions-cool/issues-helper@v2
with:
actions: 'remove-labels'
labels: '状态:待反馈'
labels: '⏳ Pending feedback'
add-label-if-is-member:
runs-on: ubuntu-latest
@@ -55,11 +55,11 @@ jobs:
uses: actions-cool/issues-helper@v2
with:
actions: 'add-labels'
labels: '状态:待反馈'
labels: '⏳ Pending feedback'
- name: Remove require handle label
if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
uses: actions-cool/issues-helper@v2
with:
actions: 'remove-labels'
labels: '状态:待处理'
labels: '🔔 Pending processing'

View File

@@ -13,4 +13,4 @@ jobs:
if: ${{ !github.event.issue.pull_request }}
with:
actions: 'add-labels'
labels: '状态:待处理'
labels: '🔔 Pending processing'

View File

@@ -10,3 +10,4 @@ jobs:
- uses: jumpserver/action-generic-handler@master
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}

137
Dockerfile Normal file
View File

@@ -0,0 +1,137 @@
FROM python:3.11-slim-bullseye AS stage-1
ARG TARGETARCH
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN echo > /opt/jumpserver/config.yml \
&& cd utils && bash -ixeu build.sh
FROM python:3.11-slim-bullseye as stage-2
ARG TARGETARCH
ARG BUILD_DEPENDENCIES=" \
g++ \
make \
pkg-config"
ARG DEPENDENCIES=" \
freetds-dev \
libffi-dev \
libjpeg-dev \
libkrb5-dev \
libldap2-dev \
libpq-dev \
libsasl2-dev \
libssl-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
freerdp2-dev \
libaio-dev"
ARG TOOLS=" \
ca-certificates \
curl \
default-libmysqlclient-dev \
default-mysql-client \
git \
git-lfs \
unzip \
xz-utils \
wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& echo "no" | dpkg-reconfigure dash
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=poetry.lock,target=/opt/jumpserver/poetry.lock \
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
set -ex \
&& python3 -m venv /opt/py3 \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& . /opt/py3/bin/activate \
&& poetry install
FROM python:3.11-slim-bullseye
ARG TARGETARCH
ENV LANG=zh_CN.UTF-8 \
PATH=/opt/py3/bin:$PATH
ARG DEPENDENCIES=" \
libjpeg-dev \
libpq-dev \
libx11-dev \
freerdp2-dev \
libxmlsec1-openssl"
ARG TOOLS=" \
ca-certificates \
curl \
default-libmysqlclient-dev \
default-mysql-client \
iputils-ping \
locales \
netcat-openbsd \
nmap \
openssh-client \
patch \
sshpass \
telnet \
vim \
bubblewrap \
wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
&& echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
&& sed -i "s@# export @export @g" ~/.bashrc \
&& sed -i "s@# alias @alias @g" ~/.bashrc
ARG RECEPTOR_VERSION=v1.4.5
RUN set -ex \
&& wget -O /opt/receptor.tar.gz https://github.com/ansible/receptor/releases/download/${RECEPTOR_VERSION}/receptor_${RECEPTOR_VERSION/v/}_linux_${TARGETARCH}.tar.gz \
&& tar -xf /opt/receptor.tar.gz -C /usr/local/bin/ \
&& chown root:root /usr/local/bin/receptor \
&& chmod 755 /usr/local/bin/receptor \
&& rm -f /opt/receptor.tar.gz
COPY --from=stage-2 /opt/py3 /opt/py3
COPY --from=stage-1 /opt/jumpserver/release/jumpserver /opt/jumpserver
COPY --from=stage-1 /opt/jumpserver/release/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
WORKDIR /opt/jumpserver
ARG VERSION
ENV VERSION=$VERSION
VOLUME /opt/jumpserver/data
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]

View File

@@ -1,4 +1,4 @@
FROM python:3.11-slim-bullseye as stage-1
FROM python:3.11-slim-bullseye AS stage-1
ARG TARGETARCH
ARG VERSION
@@ -94,6 +94,7 @@ ARG TOOLS=" \
sshpass \
telnet \
vim \
bubblewrap \
wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn

View File

@@ -1,5 +1,5 @@
ARG VERSION
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} AS build-xpack
FROM registry.fit2cloud.com/jumpserver/core-ce:${VERSION}
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack

View File

@@ -13,11 +13,11 @@
login_password: "{{ jms_account.secret }}"
login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}"
become: "{{ custom_become | default(False) }}"
become_method: "{{ custom_become_method | default('su') }}"
become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
become: "{{ jms_custom_become | default(False) }}"
become_method: "{{ jms_custom_become_method | default('su') }}"
become_user: "{{ jms_custom_become_user | default('') }}"
become_password: "{{ jms_custom_become_password | default('') }}"
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) }}"
register: ping_info
@@ -31,11 +31,11 @@
login_port: "{{ jms_asset.port }}"
login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}"
become: "{{ custom_become | default(False) }}"
become_method: "{{ custom_become_method | default('su') }}"
become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
become: "{{ jms_custom_become | default(False) }}"
become_method: "{{ jms_custom_become_method | default('su') }}"
become_user: "{{ jms_custom_become_user | default('') }}"
become_password: "{{ jms_custom_become_password | default('') }}"
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
commands: "{{ params.commands }}"

View File

@@ -35,6 +35,17 @@
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
ansible.builtin.user:
name: "{{ account.username }}"
@@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection
ansible.builtin.meta: reset_connection

View File

@@ -5,6 +5,12 @@ type:
- AIX
method: change_secret
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@@ -34,6 +40,11 @@ i18n:
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)'
en: 'Using Ansible module user to change account secret (DES)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@@ -49,6 +60,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@@ -35,6 +35,17 @@
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
ansible.builtin.user:
name: "{{ account.username }}"
@@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection
ansible.builtin.meta: reset_connection

View File

@@ -6,6 +6,12 @@ type:
- linux
method: change_secret
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@@ -36,6 +42,11 @@ i18n:
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)'
en: 'Using Ansible module user to change account secret (SHA512)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@@ -51,6 +62,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@@ -35,6 +35,17 @@
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
ansible.builtin.user:
name: "{{ account.username }}"
@@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection
ansible.builtin.meta: reset_connection

View File

@@ -5,6 +5,12 @@ type:
- AIX
method: push_account
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@@ -34,6 +40,11 @@ i18n:
ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)'
en: 'Using Ansible module user to push account (DES)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@@ -49,6 +60,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@@ -35,6 +35,17 @@
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
ansible.builtin.user:
name: "{{ account.username }}"
@@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection
ansible.builtin.meta: reset_connection

View File

@@ -6,6 +6,12 @@ type:
- linux
method: push_account
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@@ -36,6 +42,11 @@ i18n:
ja: 'Ansible user モジュールを使用してアカウントをプッシュする (sha512)'
en: 'Using Ansible module user to push account (sha512)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@@ -51,6 +62,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@@ -12,11 +12,13 @@
path: "{{ user_home_dir.stdout }}"
register: home_dir
when: user_home_dir.stdout != ""
ignore_errors: yes
- name: "Rename user home directory if it exists"
ansible.builtin.command:
cmd: "mv {{ user_home_dir.stdout }} {{ user_home_dir.stdout }}.bak"
when: home_dir.stat | default(false) and user_home_dir.stdout != ""
ignore_errors: yes
- name: "Remove account"
ansible.builtin.user:

View File

@@ -4,6 +4,4 @@
- name: "Remove account"
ansible.windows.win_user:
name: "{{ account.username }}"
state: absent
purge: yes
force: yes
state: absent

View File

@@ -53,7 +53,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
on_delete=models.SET_NULL, verbose_name=_("Su from")
)
version = models.IntegerField(default=0, verbose_name=_('Version'))
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'],
verbose_name=_("historical Account"))
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
@@ -119,7 +120,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
return auth
auth.update(self.make_account_ansible_vars(su_from))
become_method = platform.su_method if platform.su_method else 'sudo'
become_method = platform.ansible_become_method
password = su_from.secret if become_method == 'sudo' else self.secret
auth['ansible_become'] = True
auth['ansible_become_method'] = become_method

View File

@@ -79,18 +79,28 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
@staticmethod
def get_template_attr_for_account(template):
# Set initial data from template
field_names = [
'name', 'username', 'secret',
'secret_type', 'privileged', 'is_active'
'name', 'username',
'secret_type', 'secret',
'privileged', 'is_active'
]
field_map = {
'push_params': 'params',
'auto_push': 'push_now'
}
field_names.extend(field_map.keys())
attrs = {}
for name in field_names:
value = getattr(template, name, None)
if value is None:
continue
attrs[name] = value
attr_name = field_map.get(name, name)
attrs[attr_name] = value
attrs['secret'] = template.get_secret()
return attrs
@@ -173,7 +183,8 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
params = validated_data.pop('params', None)
self.clean_auth_fields(validated_data)
instance, stat = self.do_create(validated_data)
self.push_account_if_need(instance, push_now, params, stat)
if instance.source == Source.LOCAL:
self.push_account_if_need(instance, push_now, params, stat)
return instance
def update(self, instance, validated_data):
@@ -275,8 +286,8 @@ class AssetAccountBulkSerializer(
fields = [
'name', 'username', 'secret', 'secret_type', 'passphrase',
'privileged', 'is_active', 'comment', 'template',
'on_invalid', 'push_now', 'assets', 'su_from_username',
'source', 'source_id',
'on_invalid', 'push_now', 'params', 'assets',
'su_from_username', 'source', 'source_id',
]
extra_kwargs = {
'name': {'required': False},
@@ -414,16 +425,23 @@ class AssetAccountBulkSerializer(
return results
@staticmethod
def push_accounts_if_need(results, push_now):
def push_accounts_if_need(results, push_now, params):
if not push_now:
return
accounts = [str(v['instance']) for v in results if v.get('instance')]
push_accounts_to_assets_task.delay(accounts)
account_ids = [v['instance'] for v in results if v.get('instance')]
accounts = Account.objects.filter(id__in=account_ids, source=Source.LOCAL)
if not accounts.exists():
return
account_ids = [str(_id) for _id in accounts.values_list('id', flat=True)]
push_accounts_to_assets_task.delay(account_ids, params)
def create(self, validated_data):
params = validated_data.pop('params', None)
push_now = validated_data.pop('push_now', False)
results = self.perform_bulk_create(validated_data)
self.push_accounts_if_need(results, push_now)
self.push_accounts_if_need(results, push_now, params)
for res in results:
res['asset'] = str(res['asset'])
return results

View File

@@ -6,6 +6,7 @@ from django.dispatch import receiver
from django.utils.translation import gettext_noop
from accounts.backends import vault_client
from accounts.const import Source
from audits.const import ActivityChoices
from audits.signal_handlers import create_activities
from common.decorators import merge_delay_run
@@ -32,7 +33,7 @@ def push_accounts_if_need(accounts=()):
template_accounts = defaultdict(list)
for ac in accounts:
# 再强调一次吧
if ac.source != 'template':
if ac.source != Source.TEMPLATE:
continue
template_accounts[ac.source_id].append(ac)
@@ -61,7 +62,7 @@ def create_accounts_activities(account, action='create'):
@receiver(post_save, sender=Account)
def on_account_create_by_template(sender, instance, created=False, **kwargs):
if not created or instance.source != 'template':
if not created or instance.source != Source.TEMPLATE:
return
push_accounts_if_need.delay(accounts=(instance,))
create_accounts_activities(instance, action='create')

View File

@@ -292,6 +292,7 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
def check_permissions(self, request):
action_perm_require = {
"refresh": "assets.refresh_assethardwareinfo",
"test": "assets.test_assetconnectivity",
}
_action = request.data.get("action")
perm_required = action_perm_require.get(_action)

View File

@@ -39,16 +39,16 @@ class NodeChildrenApi(generics.ListCreateAPIView):
self.instance = self.get_object()
def perform_create(self, serializer):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if value:
children = self.instance.get_children()
if children.filter(value=value).exists():
raise JMSException(_('The same level node name cannot be the same'))
else:
value = self.instance.get_next_child_preset_name()
with NodeAddChildrenLock(self.instance):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if value:
children = self.instance.get_children()
if children.filter(value=value).exists():
raise JMSException(_('The same level node name cannot be the same'))
else:
value = self.instance.get_next_child_preset_name()
node = self.instance.create_child(value=value, _id=_id)
# 避免查询 full value
node._full_value = node.value
@@ -126,7 +126,7 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
include_assets = self.request.query_params.get('assets', '0') == '1'
if not self.instance or not include_assets:
return Asset.objects.none()
if self.instance.is_org_root():
if not self.request.GET.get('search') and self.instance.is_org_root():
return Asset.objects.none()
if query_all:
assets = self.instance.get_all_assets()

View File

@@ -37,7 +37,7 @@ class SSHTunnelManager:
info = self.file_to_json(runner.inventory)
servers, not_valid = [], []
for k, host in info['all']['hosts'].items():
jms_asset, jms_gateway = host.get('jms_asset'), host.get('gateway')
jms_asset, jms_gateway = host.get('jms_asset'), host.get('jms_gateway')
if not jms_gateway:
continue
try:
@@ -113,11 +113,7 @@ class BasePlaybookManager:
if not data:
data = automation_params.get(method_id, {})
params = serializer(data).data
return {
field_name: automation_params.get(field_name, '')
if not params[field_name] else params[field_name]
for field_name in params
}
return params
@property
def platform_automation_methods(self):

View File

@@ -14,11 +14,11 @@
login_port: "{{ jms_asset.port }}"
login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}"
become: "{{ custom_become | default(False) }}"
become_method: "{{ custom_become_method | default('su') }}"
become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
become: "{{ jms_custom_become | default(False) }}"
become_method: "{{ jms_custom_become_method | default('su') }}"
become_user: "{{ jms_custom_become_user | default('') }}"
become_password: "{{ jms_custom_become_password | default('') }}"
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) }}"

View File

@@ -2,5 +2,6 @@ from .automation import *
from .base import *
from .category import *
from .host import *
from .platform import *
from .protocol import *
from .types import *

View File

@@ -8,6 +8,7 @@ class DatabaseTypes(BaseType):
ORACLE = 'oracle', 'Oracle'
SQLSERVER = 'sqlserver', 'SQLServer'
DB2 = 'db2', 'DB2'
DAMENG = 'dameng', 'Dameng'
CLICKHOUSE = 'clickhouse', 'ClickHouse'
MONGODB = 'mongodb', 'MongoDB'
REDIS = 'redis', 'Redis'
@@ -55,6 +56,15 @@ class DatabaseTypes(BaseType):
'change_secret_enabled': False,
'push_account_enabled': False,
},
cls.DAMENG: {
'ansible_enabled': False,
'ping_enabled': False,
'gather_facts_enabled': False,
'gather_accounts_enabled': False,
'verify_account_enabled': False,
'change_secret_enabled': False,
'push_account_enabled': False,
},
cls.CLICKHOUSE: {
'ansible_enabled': False,
'ping_enabled': False,
@@ -84,6 +94,7 @@ class DatabaseTypes(BaseType):
cls.ORACLE: [{'name': 'Oracle'}],
cls.SQLSERVER: [{'name': 'SQLServer'}],
cls.DB2: [{'name': 'DB2'}],
cls.DAMENG: [{'name': 'Dameng'}],
cls.CLICKHOUSE: [{'name': 'ClickHouse'}],
cls.MONGODB: [{'name': 'MongoDB'}],
cls.REDIS: [

View File

@@ -19,7 +19,7 @@ class HostTypes(BaseType):
'charset': 'utf-8', # default
'domain_enabled': True,
'su_enabled': True,
'su_methods': ['sudo', 'su'],
'su_methods': ['sudo', 'su', 'only_sudo', 'only_su'],
},
cls.WINDOWS: {
'su_enabled': False,

View File

@@ -0,0 +1,11 @@
from django.db.models import TextChoices
class SuMethodChoices(TextChoices):
sudo = "sudo", "sudo su -"
su = "su", "su - "
only_sudo = "only_sudo", "sudo su"
only_su = "only_su", "su"
enable = "enable", "enable"
super = "super", "super 15"
super_level = "super_level", "super level 15"

View File

@@ -23,6 +23,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
postgresql = 'postgresql', 'PostgreSQL'
sqlserver = 'sqlserver', 'SQLServer'
db2 = 'db2', 'DB2'
dameng = 'dameng', 'Dameng'
clickhouse = 'clickhouse', 'ClickHouse'
redis = 'redis', 'Redis'
mongodb = 'mongodb', 'MongoDB'
@@ -185,6 +186,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
'secret_types': ['password'],
'xpack': True,
},
cls.dameng: {
'port': 5236,
'required': True,
'secret_types': ['password'],
'xpack': True,
},
cls.clickhouse: {
'port': 9000,
'required': True,
@@ -201,6 +208,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
'default': 'admin',
'label': _('Auth source'),
'help_text': _('The database to authenticate against')
},
'connection_options': {
'type': 'str',
'default': '',
'label': _('Connection options'),
'help_text': _('The connection specific options eg. retryWrites=false&retryReads=false')
}
}
},
@@ -282,23 +295,17 @@ class Protocol(ChoicesMixin, models.TextChoices):
'setting': {
'api_mode': {
'type': 'choice',
'default': 'gpt-3.5-turbo',
'default': 'gpt-4o-mini',
'label': _('API mode'),
'choices': [
('gpt-3.5-turbo', 'GPT-3.5 Turbo'),
('gpt-3.5-turbo-1106', 'GPT-3.5 Turbo 1106'),
('gpt-4o-mini', 'GPT-4o-mini'),
('gpt-4o', 'GPT-4o'),
('gpt-4-turbo', 'GPT-4 Turbo'),
]
}
}
}
}
if settings.XPACK_LICENSE_IS_VALID:
choices = protocols[cls.chatgpt]['setting']['api_mode']['choices']
choices.extend([
('gpt-4', 'GPT-4'),
('gpt-4-turbo', 'GPT-4 Turbo'),
('gpt-4o', 'GPT-4o'),
])
return protocols
@classmethod

View File

@@ -1,10 +1,12 @@
# Generated by Django 3.2.16 on 2022-12-30 08:08
import common.db.fields
from django.db import migrations, models
import django.db.models.deletion
import uuid
import django.db.models.deletion
from django.db import migrations, models
import common.db.fields
class Migration(migrations.Migration):
@@ -53,7 +55,7 @@ class Migration(migrations.Migration):
],
options={
'verbose_name': 'Automation task execution',
'ordering': ('-date_start',),
'ordering': ('org_id', '-date_start',),
},
),
migrations.CreateModel(

View File

@@ -0,0 +1,31 @@
# Generated by Django 4.1.10 on 2023-10-07 06:37
from django.db import migrations
def add_dameng_platform(apps, schema_editor):
platform_cls = apps.get_model('assets', 'Platform')
automation_cls = apps.get_model('assets', 'PlatformAutomation')
platform, _ = platform_cls.objects.update_or_create(
name='Dameng', defaults={
'name': 'Dameng', 'category': 'database',
'internal': True, 'type': 'dameng',
'domain_enabled': True, 'su_enabled': False,
'su_method': None, 'comment': 'Dameng', 'created_by': 'System',
'updated_by': 'System', 'custom_fields': []
}
)
platform.protocols.update_or_create(name='dameng', defaults={
'name': 'dameng', 'port': 5236, 'primary': True, 'setting': {}
})
automation_cls.objects.update_or_create(platform=platform, defaults={'ansible_enabled': False})
class Migration(migrations.Migration):
dependencies = [
('assets', '0127_automation_remove_account'),
]
operations = [
migrations.RunPython(add_dameng_platform)
]

View File

@@ -174,7 +174,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
def get_labels(self):
from labels.models import Label, LabeledResource
res_type = ContentType.objects.get_for_model(self.__class__)
res_type = ContentType.objects.get_for_model(self.__class__.label_model())
label_ids = LabeledResource.objects.filter(res_type=res_type, res_id=self.id) \
.values_list('label_id', flat=True)
return Label.objects.filter(id__in=label_ids)

View File

@@ -123,7 +123,7 @@ class AutomationExecution(OrgModelMixin):
)
class Meta:
ordering = ('-date_start',)
ordering = ('org_id', '-date_start',)
verbose_name = _('Automation task execution')
@property

View File

@@ -1,7 +1,7 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from assets.const import AllTypes, Category, Protocol
from assets.const import AllTypes, Category, Protocol, SuMethodChoices
from common.db.fields import JsonDictTextField
from common.db.models import JMSBaseModel
@@ -127,6 +127,17 @@ class Platform(LabeledMixin, JMSBaseModel):
return True
return False
@property
def ansible_become_method(self):
su_method = self.su_method or SuMethodChoices.sudo
if su_method in [SuMethodChoices.sudo, SuMethodChoices.only_sudo]:
method = SuMethodChoices.sudo
elif su_method in [SuMethodChoices.su, SuMethodChoices.only_su]:
method = SuMethodChoices.su
else:
method = su_method
return method
def __str__(self):
return self.name

View File

@@ -323,7 +323,9 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
template_id = data.get('template', None)
if template_id:
template = AccountTemplate.objects.get(id=template_id)
if template and template.su_from:
template.push_params = data.pop('push_params', {})
data['params'] = template.push_params
if template.su_from:
su_from_name_username_secret_type_map[template.name] = (
template.su_from.username, template.su_from.secret_type
)
@@ -381,6 +383,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
class DetailMixin(serializers.Serializer):
accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts'))
spec_info = MethodSerializer(label=_('Spec info'), read_only=True)
gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
@@ -395,7 +398,7 @@ class DetailMixin(serializers.Serializer):
def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info)
names.extend([
'gathered_info', 'spec_info', 'auto_config',
'accounts', 'gathered_info', 'spec_info', 'auto_config',
])
return names

View File

@@ -9,7 +9,7 @@ from common.serializers import (
)
from common.serializers.fields import LabeledChoiceField
from common.utils import lazyproperty
from ..const import Category, AllTypes, Protocol
from ..const import Category, AllTypes, Protocol, SuMethodChoices
from ..models import Platform, PlatformProtocol, PlatformAutomation
__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer"]
@@ -124,13 +124,6 @@ class PlatformCustomField(serializers.Serializer):
class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
SU_METHOD_CHOICES = [
("sudo", "sudo su -"),
("su", "su - "),
("enable", "enable"),
("super", "super 15"),
("super_level", "super level 15")
]
id = serializers.IntegerField(
label='ID', required=False,
validators=[UniqueValidator(queryset=Platform.objects.all())]
@@ -141,8 +134,8 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
su_method = LabeledChoiceField(
choices=SU_METHOD_CHOICES, label=_("Su method"),
required=False, default="sudo", allow_null=True
choices=SuMethodChoices.choices, label=_("Su method"),
required=False, default=SuMethodChoices.sudo, allow_null=True
)
custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)

View File

@@ -28,7 +28,7 @@ from orgs.utils import current_org, tmp_to_root_org
from rbac.permissions import RBACPermission
from terminal.models import default_storage
from users.models import User
from .backends import TYPE_ENGINE_MAPPING
from .backends import get_operate_log_storage
from .const import ActivityChoices
from .filters import UserSessionFilterSet, OperateLogFilterSet
from .models import (
@@ -146,7 +146,9 @@ class MyLoginLogViewSet(UserLoginCommonMixin, OrgReadonlyModelViewSet):
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(username=self.request.user.username)
username = self.request.user.username
q = Q(username=username) | Q(username__icontains=f'({username})')
qs = qs.filter(q)
return qs
@@ -222,13 +224,11 @@ class OperateLogViewSet(OrgReadonlyModelViewSet):
if self.is_action_detail:
with tmp_to_root_org():
qs |= OperateLog.objects.filter(org_id=Organization.SYSTEM_ID)
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
if es_config:
engine_mod = import_module(TYPE_ENGINE_MAPPING['es'])
store = engine_mod.OperateLogStore(es_config)
if store.ping(timeout=2):
qs = ESQuerySet(store)
qs.model = OperateLog
storage = get_operate_log_storage()
if storage.get_type() == 'es':
qs = ESQuerySet(storage)
qs.model = OperateLog
return qs

View File

@@ -1,18 +1,62 @@
from importlib import import_module
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from common.utils import get_logger
from .base import BaseOperateStorage
from .es import OperateLogStore as ESOperateLogStore
from .db import OperateLogStore as DBOperateLogStore
TYPE_ENGINE_MAPPING = {
'db': 'audits.backends.db',
'es': 'audits.backends.es',
logger = get_logger(__file__)
_global_op_log_storage: None | ESOperateLogStore | DBOperateLogStore = None
op_log_type_mapping = {
'server': DBOperateLogStore, 'es': ESOperateLogStore
}
def get_operate_log_storage(default=False):
engine_mod = import_module(TYPE_ENGINE_MAPPING['db'])
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
if not default and es_config:
engine_mod = import_module(TYPE_ENGINE_MAPPING['es'])
storage = engine_mod.OperateLogStore(es_config)
return storage
def _send_es_unavailable_alarm_msg():
from terminal.notifications import StorageConnectivityMessage
from terminal.const import CommandStorageType
key = 'OPERATE_LOG_ES_UNAVAILABLE_KEY'
if cache.get(key):
return
cache.set(key, 1, 60)
errors = [{
'msg': _("Connect failed"), 'name': f"{_('Operate log')}",
'type': CommandStorageType.es.label
}]
StorageConnectivityMessage(errors).publish_async()
def refresh_log_storage():
global _global_op_log_storage
_global_op_log_storage = None
if settings.OPERATE_LOG_ELASTICSEARCH_CONFIG.get('HOSTS'):
try:
config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
log_storage = op_log_type_mapping['es'](config)
_global_op_log_storage = log_storage
except Exception as e:
_send_es_unavailable_alarm_msg()
logger.warning('Invalid logs storage type: es, error: %s' % str(e))
if not _global_op_log_storage:
_global_op_log_storage = op_log_type_mapping['server']()
def get_operate_log_storage():
if _global_op_log_storage is None:
refresh_log_storage()
log_storage = _global_op_log_storage
if not log_storage.ping(timeout=3):
if log_storage.get_type() == 'es':
_send_es_unavailable_alarm_msg()
logger.warning('Switch default operate log storage.')
log_storage = op_log_type_mapping['server']()
return log_storage

View File

@@ -0,0 +1,15 @@
from perms.const import ActionChoices
class BaseOperateStorage(object):
@staticmethod
def get_type():
return 'base'
@staticmethod
def _get_special_handler(resource_type):
# 根据资源类型,处理特殊字段
resource_map = {
'Asset permission': lambda k, v: ActionChoices.display(int(v)) if k == 'Actions' else v
}
return resource_map.get(resource_type, lambda k, v: v)

View File

@@ -2,14 +2,14 @@
from django.utils.translation import gettext_lazy as _
from audits.models import OperateLog
from perms.const import ActionChoices
from .base import BaseOperateStorage
class OperateLogStore(object):
class OperateLogStore(BaseOperateStorage):
# 用不可见字符分割前后数据,节省存储-> diff: {'key': 'before\0after'}
SEP = '\0'
def __init__(self, config):
def __init__(self, *args, **kwargs):
self.model = OperateLog
self.max_length = 2048
self.max_length_tip_msg = _(
@@ -17,9 +17,13 @@ class OperateLogStore(object):
)
@staticmethod
def ping(timeout=None):
def ping(*args, **kwargs):
return True
@staticmethod
def get_type():
return 'db'
@classmethod
def convert_before_after_to_diff(cls, before, after):
if not isinstance(before, dict):
@@ -46,18 +50,13 @@ class OperateLogStore(object):
before[k], after[k] = before_value, after_value
return before, after
@staticmethod
def _get_special_handler(resource_type):
# 根据资源类型,处理特殊字段
resource_map = {
'Asset permission': lambda k, v: ActionChoices.display(int(v)) if k == 'Actions' else v
}
return resource_map.get(resource_type, lambda k, v: v)
@classmethod
def convert_diff_friendly(cls, op_log):
diff_list = list()
handler = cls._get_special_handler(op_log.resource_type)
# 标记翻译字符串
labels = _("labels")
operate_log_id = _("operate_log_id")
for k, v in op_log.diff.items():
before, after = v.split(cls.SEP, 1)
diff_list.append({

View File

@@ -2,16 +2,17 @@
#
import uuid
from django.utils.translation import gettext_lazy as _
from common.utils.timezone import local_now_display
from common.utils import get_logger
from common.utils.encode import Singleton
from common.plugins.es import ES
from .base import BaseOperateStorage
logger = get_logger(__file__)
class OperateLogStore(ES, metaclass=Singleton):
class OperateLogStore(BaseOperateStorage, ES):
def __init__(self, config):
properties = {
"id": {
@@ -48,7 +49,26 @@ class OperateLogStore(ES, metaclass=Singleton):
self.pre_use_check()
@staticmethod
def make_data(data):
def get_type():
return 'es'
@classmethod
def convert_diff_friendly(cls, op_log):
diff_list = []
handler = cls._get_special_handler(op_log.get('resource_type'))
before = op_log.get('before') or {}
after = op_log.get('after') or {}
keys = set(before.keys()) | set(after.keys())
for key in keys:
before_v, after_v = before.get(key), after.get(key)
diff_list.append({
'field': _(key),
'before': handler(key, before_v) if before_v else _('empty'),
'after': handler(key, after_v) if after_v else _('empty'),
})
return diff_list
def make_data(self, data):
op_id = data.get('id', str(uuid.uuid4()))
datetime_param = data.get('datetime', local_now_display())
data = {

View File

@@ -37,6 +37,9 @@ class ActionChoices(TextChoices):
approve = 'approve', _('Approve')
close = 'close', _('Close')
# Custom action
finished = 'finished', _('Finished')
class LoginTypeChoices(TextChoices):
web = "W", _("Web")

View File

@@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
from common.local import encrypted_field_set
from common.utils import get_request_ip, get_logger
from common.utils.encode import Singleton
from common.utils.timezone import as_current_tz
from jumpserver.utils import current_request
from orgs.models import Organization
@@ -21,17 +20,9 @@ from .backends import get_operate_log_storage
logger = get_logger(__name__)
class OperatorLogHandler(metaclass=Singleton):
class OperatorLogHandler(object):
CACHE_KEY = 'OPERATOR_LOG_CACHE_KEY'
def __init__(self):
self.log_client = self.get_storage_client()
@staticmethod
def get_storage_client():
client = get_operate_log_storage()
return client
@staticmethod
def _consistent_type_to_str(value1, value2):
if isinstance(value1, datetime):
@@ -58,7 +49,7 @@ class OperatorLogHandler(metaclass=Singleton):
return
key = '%s_%s' % (self.CACHE_KEY, instance_id)
cache.set(key, instance_dict, 3 * 60)
cache.set(key, instance_dict, 3)
def get_instance_dict_from_cache(self, instance_id):
if instance_id is None:
@@ -164,13 +155,8 @@ class OperatorLogHandler(metaclass=Singleton):
'remote_addr': remote_addr, 'before': before, 'after': after,
}
with transaction.atomic():
if self.log_client.ping(timeout=1):
client = self.log_client
else:
logger.info('Switch default operate log storage save.')
client = get_operate_log_storage(default=True)
try:
client = get_operate_log_storage()
client.save(**data)
except Exception as e:
error_msg = 'An error occurred saving OperateLog.' \

View File

@@ -19,7 +19,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='operatelog',
name='action',
field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create'), ('download', 'Download'), ('connect', 'Connect'), ('login', 'Login'), ('change_password', 'Change password'), ('accept', 'Accept'), ('review', 'Review'), ('notice', 'Notifications'), ('reject', 'Reject'), ('approve', 'Approve'), ('close', 'Close')], max_length=16, verbose_name='Action'),
field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create'), ('download', 'Download'), ('connect', 'Connect'), ('login', 'Login'), ('change_password', 'Change password'), ('accept', 'Accept'), ('review', 'Review'), ('notice', 'Notifications'), ('reject', 'Reject'), ('approve', 'Approve'), ('close', 'Close'), ('finished', 'Finished')], max_length=16, verbose_name='Action'),
),
migrations.AlterField(
model_name='userloginlog',

View File

@@ -257,6 +257,8 @@ class UserLoginLog(models.Model):
class UserSession(models.Model):
_OPERATE_LOG_ACTION = {'delete': ActionChoices.finished}
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
ip = models.GenericIPAddressField(verbose_name=_("Login IP"))
key = models.CharField(max_length=128, verbose_name=_("Session key"))

View File

@@ -3,7 +3,7 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from audits.backends.db import OperateLogStore
from audits.backends import get_operate_log_storage
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import reverse, i18n_trans
from common.utils.timezone import as_current_tz
@@ -77,7 +77,7 @@ class OperateLogActionDetailSerializer(serializers.ModelSerializer):
fields = ('diff',)
def to_representation(self, instance):
return {'diff': OperateLogStore.convert_diff_friendly(instance)}
return {'diff': get_operate_log_storage().convert_diff_friendly(instance)}
class OperateLogSerializer(BulkOrgResourceModelSerializer):

View File

@@ -3,7 +3,9 @@
import uuid
from django.apps import apps
from django.db.models.signals import post_save, pre_save, m2m_changed, pre_delete
from django.db.models.signals import (
pre_delete, pre_save, m2m_changed, post_delete, post_save
)
from django.dispatch import receiver
from django.utils import translation
@@ -94,7 +96,7 @@ def signal_of_operate_log_whether_continue(
return condition
@receiver(pre_save)
@receiver([pre_save, pre_delete])
def on_object_pre_create_or_update(
sender, instance=None, raw=False, using=None, update_fields=None, **kwargs
):
@@ -103,6 +105,7 @@ def on_object_pre_create_or_update(
)
if not ok:
return
with translation.override('en'):
# users.PrivateToken Model 没有 id 有 pk字段
instance_id = getattr(instance, 'id', getattr(instance, 'pk', None))
@@ -145,7 +148,7 @@ def on_object_created_or_update(
)
@receiver(pre_delete)
@receiver(post_delete)
def on_object_delete(sender, instance=None, **kwargs):
ok = signal_of_operate_log_whether_continue(sender, instance, False)
if not ok:
@@ -153,9 +156,15 @@ def on_object_delete(sender, instance=None, **kwargs):
with translation.override('en'):
resource_type = sender._meta.verbose_name
action = getattr(sender, '_OPERATE_LOG_ACTION', {})
action = action.get('delete', ActionChoices.delete)
instance_id = getattr(instance, 'id', getattr(instance, 'pk', None))
log_id, before = get_instance_dict_from_cache(instance_id)
if not log_id:
log_id, before = None, model_to_dict(instance)
create_or_update_operate_log(
ActionChoices.delete, resource_type,
resource=instance, before=model_to_dict(instance)
action, resource_type, log_id=log_id,
resource=instance, before=before,
)
@@ -166,7 +175,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'django_celery_beat', 'contenttypes', 'sessions', 'auth',
}
exclude_models = {
'UserPasswordHistory', 'ContentType',
'UserPasswordHistory', 'ContentType', 'Asset',
'MessageContent', 'SiteMessage',
'PlatformAutomation', 'PlatformProtocol', 'Protocol',
'HistoricalAccount', 'GatheredUser', 'ApprovalRule',
@@ -178,13 +187,15 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'PermedAsset', 'PermedAccount', 'MenuPermission',
'Permission', 'TicketSession', 'ApplyLoginTicket',
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
'FavoriteAsset',
'FavoriteAsset', 'ChangeSecretRecord'
}
include_models = set()
for i, app in enumerate(apps.get_models(), 1):
app_name = app._meta.app_label
model_name = app._meta.object_name
if app_name in exclude_apps or \
model_name in exclude_models or \
model_name.endswith('Execution'):
continue
if model_name not in include_models:
continue
MODELS_NEED_RECORD.add(model_name)

View File

@@ -16,6 +16,7 @@ from common.storage.ftp_file import FTPFileStorageHandler
from common.utils import get_log_keep_day, get_logger
from ops.celery.decorator import register_as_period_task
from ops.models import CeleryTaskExecution
from orgs.utils import tmp_to_root_org
from terminal.backends import server_replay_storage
from terminal.models import Session, Command
from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog, PasswordChangeLog
@@ -105,8 +106,9 @@ def clean_expired_session_period():
logger.info("Clean session item done")
batch_delete(expired_commands)
logger.info("Clean session command done")
command = "find %s -mtime +%s \\( -name '*.json' -o -name '*.tar' -o -name '*.gz' \\) -exec rm -f {} \\;" % (
replay_dir, days
file_types = "-name '*.json' -o -name '*.tar' -o -name '*.gz' -o -name '*.mp4'"
command = "find %s -mtime +%s \\( %s \\) -exec rm -f {} \\;" % (
replay_dir, days, file_types
)
subprocess.call(command, shell=True)
command = "find %s -type d -empty -delete;" % replay_dir
@@ -118,13 +120,14 @@ def clean_expired_session_period():
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
def clean_audits_log_period():
print("Start clean audit session task log")
clean_login_log_period()
clean_operation_log_period()
clean_ftp_log_period()
clean_activity_log_period()
clean_celery_tasks_period()
clean_expired_session_period()
clean_password_change_log_period()
with tmp_to_root_org():
clean_login_log_period()
clean_operation_log_period()
clean_ftp_log_period()
clean_activity_log_period()
clean_celery_tasks_period()
clean_expired_session_period()
clean_password_change_log_period()
@shared_task(verbose_name=_('Upload FTP file to external storage'))

View File

@@ -49,9 +49,15 @@ def _get_instance_field_value(
continue
value = getattr(instance, f.name, None) or getattr(instance, f.attname, None)
if not isinstance(value, bool) and not value:
if not isinstance(value, (bool, int)) and not value:
continue
choices = getattr(f, 'choices', []) or []
for c_value, c_label in choices:
if c_value == value:
value = c_label
break
if getattr(f, 'primary_key', False):
f.verbose_name = 'id'
elif isinstance(value, list):

View File

@@ -4,7 +4,6 @@ from django.contrib import auth
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from authentication.utils import build_absolute_uri
from authentication.views.mixins import FlashMessageMixin
@@ -55,11 +54,7 @@ class OAuth2AuthCallbackView(View, FlashMessageMixin):
logger.debug(log_prompt.format('Process authenticate'))
user = authenticate(code=callback_params['code'], request=request)
if err_msg := getattr(request, 'error_message', ''):
login_url = reverse('authentication:login') + '?admin=1'
return self.get_failed_response(login_url, title=_('Authentication failed'), msg=err_msg)
if user and user.is_valid:
if user:
logger.debug(log_prompt.format('Login: {}'.format(user)))
auth.login(self.request, user)
logger.debug(log_prompt.format('Redirect'))
@@ -68,8 +63,7 @@ class OAuth2AuthCallbackView(View, FlashMessageMixin):
)
logger.debug(log_prompt.format('Redirect'))
# OAuth2 服务端认证成功, 但是用户被禁用了, 这时候需要调用服务端的logout
redirect_url = settings.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT
redirect_url = settings.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT or '/'
return HttpResponseRedirect(redirect_url)

View File

@@ -1,14 +1,12 @@
import copy
from urllib import parse
from django.views import View
from django.contrib import auth
from django.urls import reverse
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt
from django.contrib import auth
from django.http import HttpResponseRedirect, HttpResponse, HttpResponseServerError
from django.urls import reverse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.errors import OneLogin_Saml2_Error
from onelogin.saml2.idp_metadata_parser import (
@@ -16,23 +14,29 @@ from onelogin.saml2.idp_metadata_parser import (
dict_deep_merge
)
from .settings import JmsSaml2Settings
from common.utils import get_logger
from .settings import JmsSaml2Settings
logger = get_logger(__file__)
class PrepareRequestMixin:
@staticmethod
def is_secure():
url_result = parse.urlparse(settings.SITE_URL)
return 'on' if url_result.scheme == 'https' else 'off'
@property
def parsed_url(self):
return parse.urlparse(settings.SITE_URL)
def is_secure(self):
return 'on' if self.parsed_url.scheme == 'https' else 'off'
def http_host(self):
return f"{self.parsed_url.hostname}:{self.parsed_url.port}" \
if self.parsed_url.port else self.parsed_url.hostname
def prepare_django_request(self, request):
result = {
'https': self.is_secure(),
'http_host': request.META['HTTP_HOST'],
'http_host': self.http_host(),
'script_name': request.META['PATH_INFO'],
'get_data': request.GET.copy(),
'post_data': request.POST.copy()
@@ -275,7 +279,7 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin):
logger.debug(log_prompt.format('Redirect'))
redir = post_data.get('RelayState')
if not redir or len(redir) == 0:
redir = "/"
redir = "/"
next_url = saml_instance.redirect_to(redir)
return HttpResponseRedirect(next_url)

View File

@@ -2,6 +2,7 @@ import base64
from django.conf import settings
from django.contrib.auth import logout as auth_logout
from django.core.cache import cache
from django.http import HttpResponse
from django.shortcuts import redirect, reverse, render
from django.utils.deprecation import MiddlewareMixin
@@ -116,23 +117,43 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
class SessionCookieMiddleware(MiddlewareMixin):
USER_LOGIN_ENCRYPTION_KEY_PAIR = 'user_login_encryption_key_pair'
@staticmethod
def set_cookie_public_key(request, response):
def set_cookie_public_key(self, request, response):
if request.path.startswith('/api'):
return
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
public_key = request.session.get(pub_key_name)
cookie_key = request.COOKIES.get(pub_key_name)
if public_key and public_key == cookie_key:
session_public_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
session_private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
session_public_key = request.session.get(session_public_key_name)
cookie_public_key = request.COOKIES.get(session_public_key_name)
if session_public_key and session_public_key == cookie_public_key:
return
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
private_key, public_key = gen_key_pair()
private_key, public_key = self.get_key_pair()
public_key_decode = base64.b64encode(public_key.encode()).decode()
request.session[pub_key_name] = public_key_decode
request.session[pri_key_name] = private_key
response.set_cookie(pub_key_name, public_key_decode)
request.session[session_public_key_name] = public_key_decode
request.session[session_private_key_name] = private_key
response.set_cookie(session_public_key_name, public_key_decode)
def get_key_pair(self):
key_pair = cache.get(self.USER_LOGIN_ENCRYPTION_KEY_PAIR)
if key_pair:
return key_pair['private_key'], key_pair['public_key']
private_key, public_key = gen_key_pair()
key_pair = {
'private_key': private_key,
'public_key': public_key
}
cache.set(self.USER_LOGIN_ENCRYPTION_KEY_PAIR, key_pair, None)
return private_key, public_key
@staticmethod
def set_cookie_session_prefix(request, response):

View File

@@ -319,20 +319,26 @@ class AuthPostCheckMixin:
@classmethod
def _check_passwd_is_too_simple(cls, user: User, password):
if user.is_superuser and password == 'admin':
if not user.is_auth_backend_model():
return
if user.check_passwd_too_simple(password):
message = _('Your password is too simple, please change it for security')
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
raise errors.PasswordTooSimple(url)
@classmethod
def _check_passwd_need_update(cls, user: User):
if user.need_update_password:
if not user.is_auth_backend_model():
return
if user.check_need_update_password():
message = _('You should to change your password before login')
url = cls.generate_reset_password_url_with_flash_msg(user, message)
raise errors.PasswordNeedUpdate(url)
@classmethod
def _check_password_require_reset_or_not(cls, user: User):
if not user.is_auth_backend_model():
return
if user.password_has_expired:
message = _('Your password has expired, please reset before logging in')
url = cls.generate_reset_password_url_with_flash_msg(user, message)

View File

@@ -3,3 +3,4 @@ from .connection_token import *
from .private_token import *
from .sso_token import *
from .temp_token import *
from ..backends.passkey.models import *

View File

@@ -200,7 +200,7 @@ class ConnectionToken(JMSOrgBaseModel):
host_account = applet.select_host_account(self.user, self.asset)
if not host_account:
raise JMSException({'error': 'No host account available'})
raise JMSException({'error': 'No host account available, please check the applet, host and account'})
host, account, lock_key = bulk_get(host_account, ('host', 'account', 'lock_key'))
gateway = host.domain.select_gateway() if host.domain else None

View File

@@ -1,5 +1,5 @@
from django.conf import settings
from django.contrib.auth import user_logged_in
from django.contrib.auth import user_logged_in, BACKEND_SESSION_KEY
from django.core.cache import cache
from django.dispatch import receiver
from django_cas_ng.signals import cas_user_authenticated
@@ -20,8 +20,9 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
and user.mfa_enabled \
and not request.session.get('auth_mfa'):
request.session['auth_mfa_required'] = 1
auth_backend = request.session.get('auth_backend', request.session.get(BACKEND_SESSION_KEY))
if not request.session.get("auth_third_party_done") and \
request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY:
auth_backend in AUTHENTICATION_BACKENDS_THIRD_PARTY:
request.session['auth_third_party_required'] = 1
user_session_id = request.session.get('user_session_id')

View File

@@ -8,10 +8,8 @@
<p>
<b>{% trans 'Username' %}:</b> {{ username }}<br>
<b>{% trans 'Login time' %}:</b> {{ time }}<br>
<b>{% trans 'Login city' %}:</b> {{ city }}({{ ip }})
<b>{% trans 'Login city' %}:</b> {{ city }}({{ ip }})<br>
</p>
-
<p>
{% trans 'If you suspect that the login behavior is abnormal, please modify the account password in time.' %}
</p>

View File

@@ -10,8 +10,7 @@
{% trans 'Click here reset password' %}
</a>
</p>
-
<br>
<p>
{% trans 'This link is valid for 1 hour. After it expires' %}
<a href="{{ forget_password_url }}?email={{ user.email }}">{% trans 'request new one' %}</a>

View File

@@ -5,11 +5,10 @@
{% trans 'Your password has just been successfully updated' %}
</p>
<p>
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br />
<b>{% trans 'Browser' %}:</b> {{ browser }}
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br/>
<b>{% trans 'Browser' %}:</b> {{ browser }} <br>
</p>
-
<p>
{% trans 'If the password update was not initiated by you, your account may have security issues' %} <br />
{% trans 'If the password update was not initiated by you, your account may have security issues' %} <br/>
{% trans 'If you have any questions, you can contact the administrator' %}
</p>

View File

@@ -5,11 +5,10 @@
{% trans 'Your public key has just been successfully updated' %}
</p>
<p>
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br />
<b>{% trans 'Browser' %}:</b> {{ browser }}
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br>
<b>{% trans 'Browser' %}:</b> {{ browser }}<br>
</p>
-
<p>
{% trans 'If the public key update was not initiated by you, your account may have security issues' %} <br />
{% trans 'If the public key update was not initiated by you, your account may have security issues' %} <br/>
{% trans 'If you have any questions, you can contact the administrator' %}
</p>

View File

@@ -1,4 +1,4 @@
from .utils import gen_key_pair, rsa_decrypt, rsa_encrypt
from common.utils import gen_key_pair, rsa_decrypt, rsa_encrypt
def test_rsa_encrypt_decrypt(message='test-password-$%^&*'):

View File

@@ -46,9 +46,6 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
def verify_state(self):
raise NotImplementedError
def get_verify_state_failed_response(self, redirect_uri):
raise NotImplementedError
def create_user_if_not_exist(self, user_id, **kwargs):
user = None
user_attr = self.client.get_user_detail(user_id, **kwargs)
@@ -122,9 +119,6 @@ class BaseBindCallbackView(FlashMessageMixin, IMClientMixin, View):
def verify_state(self):
raise NotImplementedError
def get_verify_state_failed_response(self, redirect_uri):
raise NotImplementedError
def get_already_bound_response(self, redirect_uri):
raise NotImplementedError
@@ -151,11 +145,9 @@ class BaseBindCallbackView(FlashMessageMixin, IMClientMixin, View):
setattr(user, f'{self.auth_type}_id', auth_user_id)
user.save()
except IntegrityError as e:
if e.args[0] == 1062:
msg = _('The %s is already bound to another user') % self.auth_type_label
response = self.get_failed_response(redirect_url, msg, msg)
return response
raise e
msg = _('The %s is already bound to another user') % self.auth_type_label
response = self.get_failed_response(redirect_url, msg, msg)
return response
ip = get_request_ip(request)
OAuthBindMessage(user, ip, self.auth_type_label, auth_user_id).publish_async()

View File

@@ -47,15 +47,7 @@ class DingTalkBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, Fla
)
def verify_state(self):
state = self.request.GET.get('state')
session_state = self.request.session.get(DINGTALK_STATE_SESSION_KEY)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
return self.verify_state_with_session_key(DINGTALK_STATE_SESSION_KEY)
def get_already_bound_response(self, redirect_url):
msg = _('DingTalk is already bound')

View File

@@ -58,15 +58,7 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe
)
def verify_state(self):
state = self.request.GET.get('state')
session_state = self.request.session.get(self.state_session_key)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
return self.verify_state_with_session_key(self.state_session_key)
def get_qr_url(self, redirect_uri):
state = random_string(16)

View File

@@ -249,6 +249,8 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
def form_valid(self, form):
if not self.request.session.test_cookie_worked():
form.add_error(None, _("Login timeout, please try again."))
# 当 session 过期后,刷新浏览器重新提交依旧会报错,所以需要重新设置 test_cookie
self.request.session.set_test_cookie()
return self.form_invalid(form)
# https://docs.djangoproject.com/en/3.1/topics/http/sessions/#setting-test-cookies

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import gettext_lazy as _
from common.utils import FlashMessageUtil
@@ -32,3 +33,12 @@ class FlashMessageMixin:
def get_failed_response(self, redirect_url, title, msg, interval=10):
return self.get_response(redirect_url, title, msg, 'error', interval)
def get_verify_state_failed_response(self, redirect_uri):
msg = _(
"For your safety, automatic redirection login is not supported on the client."
" If you need to open it in the client, please log in again")
return self.get_failed_response(redirect_uri, msg, msg)
def verify_state_with_session_key(self, session_key):
return self.request.GET.get('state') == self.request.session.get(session_key)

View File

@@ -37,15 +37,7 @@ class SlackMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessa
)
def verify_state(self):
state = self.request.GET.get('state')
session_state = self.request.session.get(SLACK_STATE_SESSION_KEY)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
return self.verify_state_with_session_key(SLACK_STATE_SESSION_KEY)
def get_qr_url(self, redirect_uri):
state = random_string(16)

View File

@@ -45,15 +45,7 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
)
def verify_state(self):
state = self.request.GET.get('state')
session_state = self.request.session.get(WECOM_STATE_SESSION_KEY)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
return self.verify_state_with_session_key(WECOM_STATE_SESSION_KEY)
def get_already_bound_response(self, redirect_url):
msg = _('WeCom is already bound')

View File

@@ -14,9 +14,13 @@ from uuid import UUID
from django.utils.translation import gettext_lazy as _
from django.db.models import QuerySet as DJQuerySet
from elasticsearch import Elasticsearch
from elasticsearch.helpers import bulk
from elasticsearch.exceptions import RequestError, NotFoundError
from elasticsearch7 import Elasticsearch
from elasticsearch7.helpers import bulk
from elasticsearch7.exceptions import RequestError, SSLError
from elasticsearch7.exceptions import NotFoundError as NotFoundError7
from elasticsearch8.exceptions import NotFoundError as NotFoundError8
from elasticsearch8.exceptions import BadRequestError
from common.utils.common import lazyproperty
from common.utils import get_logger
@@ -36,9 +40,82 @@ class NotSupportElasticsearch8(JMSException):
default_detail = _('Not Support Elasticsearch8')
class ES(object):
def __init__(self, config, properties, keyword_fields, exact_fields=None, match_fields=None):
class InvalidElasticsearchSSL(JMSException):
default_code = 'invalid_elasticsearch_SSL'
default_detail = _(
'Connection failed: Self-signed certificate used. Please check server certificate configuration')
class ESClient(object):
def __new__(cls, *args, **kwargs):
version = get_es_client_version(**kwargs)
if version == 6:
return ESClientV6(*args, **kwargs)
if version == 7:
return ESClientV7(*args, **kwargs)
elif version == 8:
return ESClientV8(*args, **kwargs)
raise ValueError('Unsupported ES_VERSION %r' % version)
class ESClientBase(object):
@classmethod
def get_properties(cls, data, index):
return data[index]['mappings']['properties']
@classmethod
def get_mapping(cls, properties):
return {'mappings': {'properties': properties}}
class ESClientV7(ESClientBase):
def __init__(self, *args, **kwargs):
from elasticsearch7 import Elasticsearch
self.es = Elasticsearch(*args, **kwargs)
@classmethod
def get_sort(cls, field, direction):
return f'{field}:{direction}'
class ESClientV6(ESClientV7):
@classmethod
def get_properties(cls, data, index):
return data[index]['mappings']['data']['properties']
@classmethod
def get_mapping(cls, properties):
return {'mappings': {'data': {'properties': properties}}}
class ESClientV8(ESClientBase):
def __init__(self, *args, **kwargs):
from elasticsearch8 import Elasticsearch
self.es = Elasticsearch(*args, **kwargs)
@classmethod
def get_sort(cls, field, direction):
return {field: {'order': direction}}
def get_es_client_version(**kwargs):
try:
es = Elasticsearch(**kwargs)
info = es.info()
version = int(info['version']['number'].split('.')[0])
return version
except SSLError:
raise InvalidElasticsearchSSL
except Exception:
raise InvalidElasticsearch
class ES(object):
def __init__(self, config, properties, keyword_fields, exact_fields=None, match_fields=None):
self.version = 7
self.config = config
hosts = self.config.get('HOSTS')
kwargs = self.config.get('OTHER', {})
@@ -46,7 +123,8 @@ class ES(object):
ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False)
if ignore_verify_certs:
kwargs['verify_certs'] = None
self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs)
self.client = ESClient(hosts=hosts, max_retries=0, **kwargs)
self.es = self.client.es
self.index_prefix = self.config.get('INDEX') or 'jumpserver'
self.is_index_by_date = bool(self.config.get('INDEX_BY_DATE', False))
@@ -83,26 +161,14 @@ class ES(object):
if not self.ping(timeout=2):
return False
info = self.es.info()
version = info['version']['number'].split('.')[0]
if version == '8':
raise NotSupportElasticsearch8
try:
# 获取索引信息,如果没有定义,直接返回
data = self.es.indices.get_mapping(index=self.index)
except NotFoundError:
except (NotFoundError8, NotFoundError7):
return False
try:
if version == '6':
# 检测索引是不是新的类型 es6
properties = data[self.index]['mappings']['data']['properties']
else:
# 检测索引是不是新的类型 es7 default index type: _doc
properties = data[self.index]['mappings']['properties']
properties = self.client.get_properties(data=data, index=self.index)
for keyword in self.keyword_fields:
if not properties[keyword]['type'] == 'keyword':
break
@@ -118,21 +184,17 @@ class ES(object):
def _ensure_index_exists(self):
try:
info = self.es.info()
version = info['version']['number'].split('.')[0]
if version == '6':
mappings = {'mappings': {'data': {'properties': self.properties}}}
else:
mappings = {'mappings': {'properties': self.properties}}
mappings = self.client.get_mapping(self.properties)
if self.is_index_by_date:
mappings['aliases'] = {
self.query_index: {}
}
if self.es.indices.exists(index=self.index):
return
try:
self.es.indices.create(index=self.index, body=mappings)
except RequestError as e:
except (RequestError, BadRequestError) as e:
if e.error == 'resource_already_exists_exception':
logger.warning(e)
else:
@@ -175,11 +237,15 @@ class ES(object):
def _filter(self, query: dict, from_=None, size=None, sort=None):
body = self.get_query_body(**query)
data = self.es.search(
index=self.query_index, body=body,
from_=from_, size=size, sort=sort
)
search_params = {
'index': self.query_index,
'body': body,
'from_': from_,
'size': size
}
if sort is not None:
search_params['sort'] = sort
data = self.es.search(**search_params)
source_data = []
for item in data['hits']['hits']:
@@ -367,7 +433,7 @@ class QuerySet(DJQuerySet):
else:
direction = 'asc'
field = field.lstrip('-+')
sort = f'{field}:{direction}'
sort = self._storage.client.get_sort(field, direction)
return sort
def __execute(self):

View File

@@ -39,8 +39,7 @@ class CustomSMS(BaseSMSClient):
kwargs = {'params': params}
try:
response = action(url=settings.CUSTOM_SMS_URL, verify=False, **kwargs)
if response.reason != 'OK':
raise JMSException(detail=response.text, code=response.status_code)
response.raise_for_status()
except Exception as exc:
logger.error('Custom sms error: {}'.format(exc))
raise JMSException(exc)

View File

@@ -21,6 +21,7 @@ def i18n_fmt(tpl, *args):
return tpl
args = [str(arg) for arg in args]
args = [arg.replace(', ', ' ') for arg in args]
try:
tpl % tuple(args)

View File

@@ -13,9 +13,9 @@ from common.utils.random import random_string
logger = get_logger(__file__)
@shared_task(verbose_name=_('Send email'))
def send_async(sender):
sender.gen_and_send()
@shared_task(verbose_name=_('Send SMS code'))
def send_sms_async(target, code):
SMS().send_verify_code(target, code)
class SendAndVerifyCodeUtil(object):
@@ -35,7 +35,7 @@ class SendAndVerifyCodeUtil(object):
logger.warning('Send sms too frequently, delay {}'.format(ttl))
raise CodeSendTooFrequently(ttl)
return send_async.apply_async(kwargs={"sender": self}, priority=100)
return self.gen_and_send()
def gen_and_send(self):
try:
@@ -72,13 +72,15 @@ class SendAndVerifyCodeUtil(object):
return code
def __send_with_sms(self):
sms = SMS()
sms.send_verify_code(self.target, self.code)
send_sms_async.apply_async(args=(self.target, self.code), priority=100)
def __send_with_email(self):
subject = self.other_args.get('subject')
message = self.other_args.get('message')
send_mail_async(subject, message, [self.target], html_message=message)
subject = self.other_args.get('subject', '')
message = self.other_args.get('message', '')
send_mail_async.apply_async(
args=(subject, message, [self.target]),
kwargs={'html_message': message}, priority=100
)
def __send(self, code):
"""

View File

@@ -74,9 +74,13 @@ class DateTimeMixin:
query = {f'{query_field}__gte': t}
return qs.filter(**query)
@lazyproperty
def users(self):
return self.org.get_members()
def get_logs_queryset(self, queryset, query_params):
query = {}
users = self.org.get_members()
users = self.users
if not self.org.is_root():
if query_params == 'username':
query = {
@@ -100,6 +104,13 @@ class DateTimeMixin:
queryset = self.get_logs_queryset(qs, 'username')
return queryset
@lazyproperty
def user_login_logs_on_the_system_queryset(self):
qs = UserLoginLog.objects.filter(status=LoginStatusChoices.success)
qs = self.get_logs_queryset_filter(qs, 'datetime')
queryset = qs.filter(username__in=construct_userlogin_usernames(self.users))
return queryset
@lazyproperty
def password_change_logs_queryset(self):
qs = PasswordChangeLog.objects.all()
@@ -141,6 +152,7 @@ class DatesLoginMetricMixin:
ftp_logs_queryset: FTPLog.objects
job_logs_queryset: JobLog.objects
login_logs_queryset: UserLoginLog.objects
user_login_logs_on_the_system_queryset: UserLoginLog.objects
operate_logs_queryset: OperateLog.objects
password_change_logs_queryset: PasswordChangeLog.objects
@@ -159,31 +171,48 @@ class DatesLoginMetricMixin:
query = {f'{field_name}__range': self.date_start_end}
return queryset.filter(**query)
def get_date_metrics(self, queryset, field_name, count_field):
def get_date_metrics(self, queryset, field_name, count_fields):
queryset = self.filter_date_start_end(queryset, field_name)
queryset = queryset.values_list(field_name, count_field)
date_group_map = defaultdict(set)
for datetime, count_field in queryset:
if not isinstance(count_fields, (list, tuple)):
count_fields = [count_fields]
values_list = [field_name] + list(count_fields)
queryset = queryset.values_list(*values_list)
date_group_map = defaultdict(lambda: defaultdict(set))
for row in queryset:
datetime = row[0]
date_str = str(datetime.date())
date_group_map[date_str].add(count_field)
for idx, count_field in enumerate(count_fields):
date_group_map[date_str][count_field].add(row[idx + 1])
return [
len(date_group_map.get(str(d), set()))
for d in self.dates_list
]
date_metrics_dict = defaultdict(list)
for field in count_fields:
for date_str in self.dates_list:
count = len(date_group_map.get(str(date_str), {}).get(field, set()))
date_metrics_dict[field].append(count)
return date_metrics_dict
def get_dates_metrics_total_count_active_users_and_assets(self):
date_metrics_dict = self.get_date_metrics(
Session.objects, 'date_start', ('user_id', 'asset_id')
)
return date_metrics_dict.get('user_id', []), date_metrics_dict.get('asset_id', [])
def get_dates_metrics_total_count_login(self):
return self.get_date_metrics(UserLoginLog.objects, 'datetime', 'id')
def get_dates_metrics_total_count_active_users(self):
return self.get_date_metrics(Session.objects, 'date_start', 'user_id')
def get_dates_metrics_total_count_active_assets(self):
return self.get_date_metrics(Session.objects, 'date_start', 'asset_id')
date_metrics_dict = self.get_date_metrics(
UserLoginLog.objects, 'datetime', 'id'
)
return date_metrics_dict.get('id', [])
def get_dates_metrics_total_count_sessions(self):
return self.get_date_metrics(Session.objects, 'date_start', 'id')
date_metrics_dict = self.get_date_metrics(
Session.objects, 'date_start', 'id'
)
return date_metrics_dict.get('id', [])
def get_dates_login_times_assets(self):
assets = self.sessions_queryset.values("asset") \
@@ -224,7 +253,7 @@ class DatesLoginMetricMixin:
@lazyproperty
def user_login_amount(self):
return self.login_logs_queryset.values('username').distinct().count()
return self.user_login_logs_on_the_system_queryset.values('username').distinct().count()
@lazyproperty
def operate_logs_amount(self):
@@ -412,11 +441,13 @@ class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView):
})
if _all or query_params.get('dates_metrics'):
user_data, asset_data = self.get_dates_metrics_total_count_active_users_and_assets()
login_data = self.get_dates_metrics_total_count_login()
data.update({
'dates_metrics_date': self.get_dates_metrics_date(),
'dates_metrics_total_count_login': self.get_dates_metrics_total_count_login(),
'dates_metrics_total_count_active_users': self.get_dates_metrics_total_count_active_users(),
'dates_metrics_total_count_active_assets': self.get_dates_metrics_total_count_active_assets(),
'dates_metrics_total_count_login': login_data,
'dates_metrics_total_count_active_users': user_data,
'dates_metrics_total_count_active_assets': asset_data,
})
if _all or query_params.get('dates_login_times_top10_assets'):

View File

@@ -489,7 +489,7 @@ class Config(dict):
# 安全配置
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True,
'SECURITY_COMMAND_EXECUTION': True,
'SECURITY_COMMAND_EXECUTION': False,
'SECURITY_COMMAND_BLACKLIST': [
'reboot', 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top'
],
@@ -600,7 +600,6 @@ class Config(dict):
# API 分页
'MAX_LIMIT_PER_PAGE': 10000,
'DEFAULT_PAGE_SIZE': None,
'LIMIT_SUPER_PRIV': False,
@@ -619,7 +618,9 @@ class Config(dict):
# Ansible Receptor
'RECEPTOR_ENABLED': False,
'ANSIBLE_RECEPTOR_GATEWAY_PROXY_HOST': 'jms_celery',
'ANSIBLE_RECEPTOR_TCP_LISTEN_ADDRESS': 'receptor:7521'
'ANSIBLE_RECEPTOR_TCP_LISTEN_ADDRESS': 'receptor:7521',
'FILE_UPLOAD_TEMP_DIR': None
}

View File

@@ -11,7 +11,10 @@ current_year = datetime.datetime.now().year
corporation = f'FIT2CLOUD 飞致云 © 2014-{current_year}'
XPACK_DIR = os.path.join(const.BASE_DIR, 'xpack')
XPACK_ENABLED = os.path.isdir(XPACK_DIR)
XPACK_DISABLED = os.environ.get('XPACK_ENABLED') in ['0', 'false', 'False', 'no', 'No']
XPACK_ENABLED = False
if not XPACK_DISABLED:
XPACK_ENABLED = os.path.isdir(XPACK_DIR)
XPACK_TEMPLATES_DIR = []
XPACK_CONTEXT_PROCESSOR = []
XPACK_LICENSE_IS_VALID = False

View File

@@ -138,7 +138,6 @@ INSTALLED_APPS = [
'rbac.apps.RBACConfig',
'labels.apps.LabelsConfig',
'rest_framework',
'rest_framework_swagger',
'drf_yasg',
'django_cas_ng',
'channels',
@@ -320,6 +319,8 @@ PRIVATE_STORAGE_AUTH_FUNCTION = 'jumpserver.rewriting.storage.permissions.allow_
PRIVATE_STORAGE_INTERNAL_URL = '/private-media/'
PRIVATE_STORAGE_SERVER = 'jumpserver.rewriting.storage.servers.StaticFileServer'
FILE_UPLOAD_TEMP_DIR = CONFIG.FILE_UPLOAD_TEMP_DIR
# Use django-bootstrap-form to format template, input max width arg
# BOOTSTRAP_COLUMN_COUNT = 11

View File

@@ -208,7 +208,7 @@ SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key'
OPERATE_LOG_ELASTICSEARCH_CONFIG = CONFIG.OPERATE_LOG_ELASTICSEARCH_CONFIG
MAX_LIMIT_PER_PAGE = CONFIG.MAX_LIMIT_PER_PAGE
DEFAULT_PAGE_SIZE = CONFIG.DEFAULT_PAGE_SIZE
DEFAULT_PAGE_SIZE = CONFIG.MAX_LIMIT_PER_PAGE
PERM_TREE_REGEN_INTERVAL = CONFIG.PERM_TREE_REGEN_INTERVAL
# Magnus DB Port

View File

@@ -121,7 +121,7 @@ class SSHClient:
def local_gateway_prepare(self):
gateway_args = self.module.params['gateway_args'] or ''
pattern = r"(?:sshpass -p ([\w@]+))?\s*ssh -o Port=(\d+)\s+-o StrictHostKeyChecking=no\s+([\w@]+)@([" \
pattern = r"(?:sshpass -p ([^ ]+))?\s*ssh -o Port=(\d+)\s+-o StrictHostKeyChecking=no\s+([\w@]+)@([" \
r"\d.]+)\s+-W %h:%p -q(?: -i (.+))?'"
match = re.search(pattern, gateway_args)

View File

@@ -19,7 +19,7 @@ def kill_ansible_ssh_process(pid):
for child in process.children(recursive=True):
if not _should_kill(child):
return
continue
try:
child.kill()
except Exception as e:

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:845b68b6d2cdd03b1c1a939d8f1238446731988ab052c66f7c7f6946af7ae406
size 431

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:375a5bd7b21f2ddd004a228ef1debcf81e32812a4392128e0acf184f3317428d
size 380

View File

@@ -0,0 +1,103 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-07-10 18:22+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: static/js/jumpserver.js:264
msgid "Update is successful!"
msgstr ""
#: static/js/jumpserver.js:266
msgid "An unknown error occurred while updating.."
msgstr ""
#: static/js/jumpserver.js:339
msgid "Not found"
msgstr ""
#: static/js/jumpserver.js:341
msgid "Server error"
msgstr ""
#: static/js/jumpserver.js:343 static/js/jumpserver.js:381
#: static/js/jumpserver.js:383
msgid "Error"
msgstr ""
#: static/js/jumpserver.js:349 static/js/jumpserver.js:390
msgid "Delete the success"
msgstr ""
#: static/js/jumpserver.js:356
msgid "Are you sure about deleting it?"
msgstr ""
#: static/js/jumpserver.js:360 static/js/jumpserver.js:401
msgid "Cancel"
msgstr ""
#: static/js/jumpserver.js:362 static/js/jumpserver.js:403
msgid "Confirm"
msgstr ""
#: static/js/jumpserver.js:381
msgid ""
"The organization contains undeleted information. Please try again after "
"deleting"
msgstr ""
#: static/js/jumpserver.js:383
msgid ""
"Do not perform this operation under this organization. Try again after "
"switching to another organization"
msgstr ""
#: static/js/jumpserver.js:397
msgid ""
"Please ensure that the following information in the organization has been "
"deleted"
msgstr ""
#: static/js/jumpserver.js:398
msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission"
msgstr ""
#: static/js/jumpserver.js:647
msgid "Unknown error occur"
msgstr ""
#: static/js/jumpserver.js:899
msgid "Password minimum length {N} bits"
msgstr ""
#: static/js/jumpserver.js:900
msgid "Must contain capital letters"
msgstr ""
#: static/js/jumpserver.js:901
msgid "Must contain lowercase letters"
msgstr ""
#: static/js/jumpserver.js:902
msgid "Must contain numeric characters"
msgstr ""
#: static/js/jumpserver.js:903
msgid "Must contain special characters"
msgstr ""

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e4baadc170ded5134bed55533c4c04e694be6ea7b8e151d80c1092728e26a75b
size 177500
oid sha256:2dd9ffcfe15b130a5b3d7b4fcfe806eaae979973e8bd29ad9a473b9215424c57
size 178725

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1c4a4fa3abb21fea213011d50fd62455fb1ddf73538401dc8cd9c03f4f4bbc77
size 3322
oid sha256:76ecc817b74abdf4f274425444339c575b723c76800095733176f9b7dada052e
size 2465

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-22 15:29+0800\n"
"POT-Creation-Date: 2024-07-10 18:22+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,141 +18,134 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: static/js/jumpserver.js:260
#: static/js/jumpserver.js:264
msgid "Update is successful!"
msgstr "アップデートは成功しました!"
#: static/js/jumpserver.js:262
#: static/js/jumpserver.js:266
msgid "An unknown error occurred while updating.."
msgstr "更新中に不明なエラーが発生しました。"
#: static/js/jumpserver.js:333
#: static/js/jumpserver.js:339
msgid "Not found"
msgstr "見つかりません"
#: static/js/jumpserver.js:335
#: static/js/jumpserver.js:341
msgid "Server error"
msgstr "サーバーエラー"
#: static/js/jumpserver.js:337 static/js/jumpserver.js:375
#: static/js/jumpserver.js:377
#: static/js/jumpserver.js:343 static/js/jumpserver.js:381
#: static/js/jumpserver.js:383
msgid "Error"
msgstr "エラー"
#: static/js/jumpserver.js:343 static/js/jumpserver.js:384
#: static/js/jumpserver.js:349 static/js/jumpserver.js:390
msgid "Delete the success"
msgstr "成功を削除する"
#: static/js/jumpserver.js:350
#: static/js/jumpserver.js:356
msgid "Are you sure about deleting it?"
msgstr "削除してもよろしいですか?"
#: static/js/jumpserver.js:354 static/js/jumpserver.js:395
#: static/js/jumpserver.js:360 static/js/jumpserver.js:401
msgid "Cancel"
msgstr "キャンセル"
#: static/js/jumpserver.js:356 static/js/jumpserver.js:397
#: static/js/jumpserver.js:362 static/js/jumpserver.js:403
msgid "Confirm"
msgstr "確認"
#: static/js/jumpserver.js:375
#: static/js/jumpserver.js:381
msgid ""
"The organization contains undeleted information. Please try again after "
"deleting"
msgstr "組織には削除されていない情報が含まれています。削除後にもう一度お試しください"
msgstr ""
"組織には削除されていない情報が含まれています。削除後にもう一度お試しください"
#: static/js/jumpserver.js:377
#: static/js/jumpserver.js:383
msgid ""
"Do not perform this operation under this organization. Try again after "
"switching to another organization"
msgstr "この組織ではこの操作を実行しないでください。別の組織に切り替えた後にもう一度お試しください"
msgstr ""
"この組織ではこの操作を実行しないでください。別の組織に切り替えた後にもう一度"
"お試しください"
#: static/js/jumpserver.js:391
#: static/js/jumpserver.js:397
msgid ""
"Please ensure that the following information in the organization has been "
"deleted"
msgstr "組織内の次の情報が削除されていることを確認してください"
#: static/js/jumpserver.js:392
#: static/js/jumpserver.js:398
msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission"
msgstr "ユーザーリスト、ユーザーグループ、資産リスト、ドメインリスト、管理ユーザー、システムユーザー、ラベル、資産権限"
msgstr ""
"ユーザーリスト、ユーザーグループ、資産リスト、ドメインリスト、管理ユーザー、"
"システムユーザー、ラベル、資産権限"
#: static/js/jumpserver.js:469
msgid "Loading"
msgstr "読み込み中"
#: static/js/jumpserver.js:470
msgid "Search"
msgstr "検索"
#: static/js/jumpserver.js:473
#, javascript-format
msgid "Selected item %d"
msgstr "選択したアイテム % d"
#: static/js/jumpserver.js:477
msgid "Per page _MENU_"
msgstr "各ページ _MENU_"
#: static/js/jumpserver.js:478
msgid ""
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "アイテムの結果を表示します _START_ に着く _END_; 合計 _TOTAL_ エントリ"
#: static/js/jumpserver.js:481
msgid "No match"
msgstr "一致しません"
#: static/js/jumpserver.js:482
msgid "No record"
msgstr "記録なし"
#: static/js/jumpserver.js:662
#: static/js/jumpserver.js:647
msgid "Unknown error occur"
msgstr "不明なエラーが発生"
#: static/js/jumpserver.js:915
#: static/js/jumpserver.js:899
msgid "Password minimum length {N} bits"
msgstr "最小パスワード長 {N} ビット"
#: static/js/jumpserver.js:916
#: static/js/jumpserver.js:900
msgid "Must contain capital letters"
msgstr "大文字を含める必要があります"
#: static/js/jumpserver.js:917
#: static/js/jumpserver.js:901
msgid "Must contain lowercase letters"
msgstr "小文字を含める必要があります"
#: static/js/jumpserver.js:918
#: static/js/jumpserver.js:902
msgid "Must contain numeric characters"
msgstr "数字を含める必要があります。"
#: static/js/jumpserver.js:919
#: static/js/jumpserver.js:903
msgid "Must contain special characters"
msgstr "特殊文字を含める必要があります"
#: static/js/jumpserver.js:1098 static/js/jumpserver.js:1122
msgid "Export failed"
msgstr "エクスポートに失敗しました"
#~ msgid "Loading"
#~ msgstr "読み込み中"
#: static/js/jumpserver.js:1139
msgid "Import Success"
msgstr "インポートの成功"
#~ msgid "Search"
#~ msgstr "検索"
#: static/js/jumpserver.js:1144
msgid "Update Success"
msgstr "更新の成功"
#, javascript-format
#~ msgid "Selected item %d"
#~ msgstr "選択したアイテム % d"
#: static/js/jumpserver.js:1145
msgid "Count"
msgstr "カウント"
#~ msgid "Per page _MENU_"
#~ msgstr "各ページ _MENU_"
#: static/js/jumpserver.js:1174
msgid "Import failed"
msgstr "インポートに失敗しました"
#~ msgid ""
#~ "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
#~ msgstr ""
#~ "アイテムの結果を表示します _START_ に着く _END_; 合計 _TOTAL_ エントリ"
#: static/js/jumpserver.js:1179
msgid "Update failed"
msgstr "更新に失敗しました"
#~ msgid "No match"
#~ msgstr "一致しません"
#~ msgid "No record"
#~ msgstr "記録なし"
#~ msgid "Export failed"
#~ msgstr "エクスポートに失敗しました"
#~ msgid "Import Success"
#~ msgstr "インポートの成功"
#~ msgid "Update Success"
#~ msgstr "更新の成功"
#~ msgid "Count"
#~ msgstr "カウント"
#~ msgid "Import failed"
#~ msgstr "インポートに失敗しました"
#~ msgid "Update failed"
#~ msgstr "更新に失敗しました"

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08667579241592ecacd1baf330147a720a9e41171444b925f90778613a7e1d9a
size 145230
oid sha256:4517c6a7464c68f949912b97c8a9abcc766ca19e32267a1d1da3f0e012471c1a
size 146255

File diff suppressed because it is too large Load Diff

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