mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-16 00:52:41 +00:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ac277b82e | ||
|
|
6686afcec1 | ||
|
|
0918f5c6f6 | ||
|
|
891e3d5609 | ||
|
|
9fad591545 | ||
|
|
1ed1c3a536 | ||
|
|
63824d3491 | ||
|
|
96eadf060c | ||
|
|
2c9128b0e7 | ||
|
|
7d6fd0f881 | ||
|
|
4e996afd5e | ||
|
|
1ed745d042 | ||
|
|
39ebbfcf10 | ||
|
|
d0ec4f798b | ||
|
|
1712a9a104 | ||
|
|
951aafcabd | ||
|
|
e46da9d741 | ||
|
|
06aaf9e3d0 | ||
|
|
5ac9fb81dc | ||
|
|
fb907e250c | ||
|
|
34ea40a14d | ||
|
|
8c560b0317 | ||
|
|
df9cc4700b | ||
|
|
6aa1227e60 | ||
|
|
296c788e28 | ||
|
|
a1ae29d35e | ||
|
|
139ffd0b47 | ||
|
|
ff6c1aef7f | ||
|
|
9b6b48a7f1 | ||
|
|
2b133a8085 | ||
|
|
81b5f1ce93 | ||
|
|
c646084c51 | ||
|
|
5e69c03cb7 | ||
|
|
e2df85bddd | ||
|
|
d710697fa9 | ||
|
|
a955fcd682 | ||
|
|
1816d52d21 | ||
|
|
d7c26cab7d | ||
|
|
dc894fdc2d | ||
|
|
742ef89bef | ||
|
|
3d4fc56592 | ||
|
|
45291aba0c | ||
|
|
495ee99e29 | ||
|
|
223eb8ad38 | ||
|
|
370e959400 | ||
|
|
b82f007787 | ||
|
|
1faeb54673 | ||
|
|
04e102cb87 | ||
|
|
81027cd561 | ||
|
|
cf727d22c0 | ||
|
|
bb6d077645 | ||
|
|
a78ccc9667 | ||
|
|
d70351e6b3 | ||
|
|
4e76207adb | ||
|
|
7a12c3737f | ||
|
|
8450c49e25 | ||
|
|
ab6d8df2f0 | ||
|
|
550115c39f | ||
|
|
9c23512d91 | ||
|
|
30054b286a | ||
|
|
22d7385891 | ||
|
|
1701bedb41 | ||
|
|
165d030c8e | ||
|
|
9be77cf58f | ||
|
|
887724bad4 | ||
|
|
b283d88781 | ||
|
|
2977323800 | ||
|
|
4a520e9e10 | ||
|
|
44f29e166c | ||
|
|
f42113afb9 | ||
|
|
ff126f3459 | ||
|
|
66cd6e95a8 | ||
|
|
b28aec527f | ||
|
|
496903dfb2 | ||
|
|
0a0312695b | ||
|
|
3608b025e5 | ||
|
|
68244b2b37 | ||
|
|
948e9ecb4b | ||
|
|
7ad4d9116a | ||
|
|
9439035b86 | ||
|
|
2b220d3753 | ||
|
|
440a7ae9cc | ||
|
|
40a4efc992 | ||
|
|
15d4fafbdb | ||
|
|
48b037ac26 | ||
|
|
dfd133cf5a | ||
|
|
cdfb11549e | ||
|
|
0d825927e1 | ||
|
|
4e8d7df005 | ||
|
|
5d1829b998 | ||
|
|
75df845024 | ||
|
|
c103253867 | ||
|
|
81da9e018a | ||
|
|
7f90fccc4f | ||
|
|
4ebcba81e0 | ||
|
|
5616d31888 |
35
.github/ISSUE_TEMPLATE/----.md
vendored
35
.github/ISSUE_TEMPLATE/----.md
vendored
@@ -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
72
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
Normal 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.
|
||||
72
.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
vendored
Normal file
72
.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
vendored
Normal 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: 如果你已经尝试解决问题,请在此列出你尝试过的解决方案。
|
||||
56
.github/ISSUE_TEMPLATE/2_feature_request.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/2_feature_request.yml
vendored
Normal 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.
|
||||
56
.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
vendored
Normal 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
60
.github/ISSUE_TEMPLATE/3_question.yml
vendored
Normal 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.
|
||||
61
.github/ISSUE_TEMPLATE/3_question_cn.yml
vendored
Normal file
61
.github/ISSUE_TEMPLATE/3_question_cn.yml
vendored
Normal 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: 在这里添加关于问题的任何其他背景信息。
|
||||
|
||||
51
.github/ISSUE_TEMPLATE/bug---.md
vendored
51
.github/ISSUE_TEMPLATE/bug---.md
vendored
@@ -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]
|
||||
- 浏览器/应用版本:[如果适用,请提供相关版本信息]
|
||||
- 其他相关环境信息:[如果有其他相关环境信息,请在此处提供]
|
||||
|
||||
## 附加信息(可选)
|
||||
[在这里添加任何其他相关信息,如截图、错误信息等]
|
||||
50
.github/ISSUE_TEMPLATE/question.md
vendored
50
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -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]
|
||||
- 浏览器/应用版本:[如果适用,请提供相关版本信息]
|
||||
- 其他相关环境信息:[如果有其他相关环境信息,请在此处提供]
|
||||
|
||||
## 具体问题
|
||||
[在这里详细描述你的问题,包括任何相关细节或错误信息]
|
||||
|
||||
## 尝试过的解决方法
|
||||
[如果你已经尝试过解决问题,请在这里列出你已经尝试过的解决方法]
|
||||
|
||||
## 预期结果
|
||||
[描述你期望的解决方案或结果]
|
||||
|
||||
## 我们的期望
|
||||
[描述你希望我们提供的帮助或支持]
|
||||
|
||||
4
.github/workflows/issue-close-require.yml
vendored
4
.github/workflows/issue-close-require.yml
vendored
@@ -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。
|
||||
|
||||
2
.github/workflows/issue-close.yml
vendored
2
.github/workflows/issue-close.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
if: ${{ !github.event.issue.pull_request }}
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
labels: '状态:待处理,状态:待反馈'
|
||||
labels: '🔔 Pending processing,⏳ Pending feedback'
|
||||
8
.github/workflows/issue-comment.yml
vendored
8
.github/workflows/issue-comment.yml
vendored
@@ -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'
|
||||
|
||||
2
.github/workflows/issue-open.yml
vendored
2
.github/workflows/issue-open.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
if: ${{ !github.event.issue.pull_request }}
|
||||
with:
|
||||
actions: 'add-labels'
|
||||
labels: '状态:待处理'
|
||||
labels: '🔔 Pending processing'
|
||||
@@ -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
137
Dockerfile
Normal 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"]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: 'ホームディレクトリ'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: 'ホームディレクトリ'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: 'ホームディレクトリ'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: 'ホームディレクトリ'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -4,6 +4,4 @@
|
||||
- name: "Remove account"
|
||||
ansible.windows.win_user:
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
purge: yes
|
||||
force: yes
|
||||
state: absent
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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) }}"
|
||||
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
11
apps/assets/const/platform.py
Normal file
11
apps/assets/const/platform.py
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
31
apps/assets/migrations/0128_auto_20240514_1521.py
Normal file
31
apps/assets/migrations/0128_auto_20240514_1521.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.1.10 on 2023-10-07 06:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def add_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)
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -123,7 +123,7 @@ class AutomationExecution(OrgModelMixin):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('-date_start',)
|
||||
ordering = ('org_id', '-date_start',)
|
||||
verbose_name = _('Automation task execution')
|
||||
|
||||
@property
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
15
apps/audits/backends/base.py
Normal file
15
apps/audits/backends/base.py
Normal 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)
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -37,6 +37,9 @@ class ActionChoices(TextChoices):
|
||||
approve = 'approve', _('Approve')
|
||||
close = 'close', _('Close')
|
||||
|
||||
# Custom action
|
||||
finished = 'finished', _('Finished')
|
||||
|
||||
|
||||
class LoginTypeChoices(TextChoices):
|
||||
web = "W", _("Web")
|
||||
|
||||
@@ -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.' \
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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-$%^&*'):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ __all__ = ['BASE_DIR', 'PROJECT_DIR', 'VERSION', 'CONFIG']
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
||||
VERSION = '2.0.0'
|
||||
VERSION = 'v3.10.14'
|
||||
CONFIG = ConfigManager.load_user_config()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
3
apps/locale/en/LC_MESSAGES/django.mo
Normal file
3
apps/locale/en/LC_MESSAGES/django.mo
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:845b68b6d2cdd03b1c1a939d8f1238446731988ab052c66f7c7f6946af7ae406
|
||||
size 431
|
||||
9114
apps/locale/en/LC_MESSAGES/django.po
Normal file
9114
apps/locale/en/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
3
apps/locale/en/LC_MESSAGES/djangojs.mo
Normal file
3
apps/locale/en/LC_MESSAGES/djangojs.mo
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:375a5bd7b21f2ddd004a228ef1debcf81e32812a4392128e0acf184f3317428d
|
||||
size 380
|
||||
103
apps/locale/en/LC_MESSAGES/djangojs.po
Normal file
103
apps/locale/en/LC_MESSAGES/djangojs.po
Normal 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 ""
|
||||
@@ -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
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1c4a4fa3abb21fea213011d50fd62455fb1ddf73538401dc8cd9c03f4f4bbc77
|
||||
size 3322
|
||||
oid sha256:76ecc817b74abdf4f274425444339c575b723c76800095733176f9b7dada052e
|
||||
size 2465
|
||||
|
||||
@@ -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 "更新に失敗しました"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user