Compare commits

...

149 Commits
v4.4 ... v4.7

Author SHA1 Message Date
Bryan
4d37dca0de Merge pull request #14901 from jumpserver/dev
v4.7.0
2025-02-20 10:21:16 +08:00
Chenyang Shen
8730fa8dee Merge pull request #14900 from jumpserver/pr@dev@translate
perf: translate
2025-02-19 19:26:09 +08:00
Aaron3S
9a5a775652 feat: add luna i18n 2025-02-19 19:17:21 +08:00
feng
8304ae9070 perf: translate 2025-02-19 16:27:10 +08:00
wangruidong
9533861e24 fix: VariableSerializer allow null in default_value 2025-02-18 15:26:11 +08:00
Aaron3S
abbfbcde83 feat: add i18n 2025-02-18 15:10:13 +08:00
wangruidong
046a9d41bf fix: Removing labels from asset details will cause an error. 2025-02-17 18:21:28 +08:00
fit2bot
363bb20da7 feat: Chen i18n (#14851)
Co-authored-by: jiangweidong <1053570670@qq.com>
2025-02-17 11:25:54 +08:00
wangruidong
2b7c8b9c07 fix: Upgrading v3 to v4 generates two ticket flow 2025-02-14 16:10:20 +08:00
Aaron3S
db040bbd06 feat: add translate 2025-02-14 11:30:56 +08:00
feng
a761ec9aa1 perf: Translate 2025-02-13 16:55:09 +08:00
feng
c0ffe45ce9 perf: deepseek 2025-02-13 16:40:29 +08:00
wangruidong
404d58a9c9 fix: When the organization does not exist, close ticket with an error. 2025-02-13 16:31:47 +08:00
feng
f64eab7a15 perf: Translate 2025-02-13 11:48:59 +08:00
feng
46f94fd138 perf: Chat ai help text 2025-02-13 11:30:07 +08:00
wangruidong
2f1c0090b7 fix: markdown render issue 2025-02-12 15:48:05 +08:00
feng
b0d6a09276 perf: translate 2025-02-10 19:15:39 +08:00
feng
d8db76cc7b perf: DeepSeek 2025-02-08 15:40:24 +08:00
wangruidong
b35a55ed54 fix: Cannot set original org when exception occurs 2025-02-08 11:13:14 +08:00
Bai
dc5ecfcc4b fix: setting field encrypt issue 2025-02-06 17:14:10 +08:00
Bryan
2ca4002624 Merge pull request #14813 from jumpserver/dev
v4.6.0
2025-01-15 14:38:17 +08:00
wangruidong
543dde57ab perf: modify average_time_cost calculation in job model 2025-01-14 18:28:31 +08:00
w940853815
c088437fe5 Revert "perf: Optimize average_time_cost calculation in job model"
This reverts commit eafb5ecfb3.
2025-01-14 18:18:44 +08:00
feng
e721ec147c perf: luna translate 2025-01-14 17:30:00 +08:00
wangruidong
5d18d6dee0 fix: Add with_expired param to permission utils 2025-01-14 16:41:39 +08:00
feng
ecfd338428 perf: Lina translate 2025-01-14 15:16:17 +08:00
Eric
4b28b079dc perf: fix rdp file resolution value 2025-01-14 13:57:11 +08:00
wangruidong
c1c3236a30 fix: Add redirect_url check in base view 2025-01-14 11:54:17 +08:00
feng
4b19750581 perf: Client version 2025-01-14 10:32:07 +08:00
wangruidong
eafb5ecfb3 perf: Optimize average_time_cost calculation in job model 2025-01-13 17:12:11 +08:00
Bai
583486e26e fix: radius user auth skip backend 2025-01-13 15:49:23 +08:00
Bai
8198620a2e feat: add gitignore 2025-01-09 10:50:57 +08:00
wangruidong
c0b301d52b fix: ldap ha periodic task did not execute as expected 2025-01-08 18:34:53 +08:00
feng
7791d6222a perf: translate 2025-01-08 14:33:04 +08:00
Bai
b740d9d42f fix: circle imported for perms-api 2025-01-08 10:35:13 +08:00
wangruidong
48d0187604 fix: circular import 2025-01-07 14:01:38 +08:00
wangruidong
6217018427 perf: Translate var_name help_text 2025-01-06 11:36:11 +08:00
jiangweidong
923f40e523 feat: VMware automatically syncs folders to node-translation 2025-01-03 18:50:12 +08:00
Bai
1f1fe2084b fix: koko press r dont refresh user perm-nodes 2025-01-03 17:13:32 +08:00
刘瑞斌
b8b1a6ac9c chore: update readme 2024-12-30 16:04:32 +08:00
wangruidong
35f88722af fix: Add type check for secure command execution 2024-12-24 15:58:56 +08:00
Bai
7e6d2749ae fix: core download page error 2024-12-20 16:35:24 +08:00
Bai
be57b101ff fix: set default ldap user dn cache time (0) 2024-12-20 16:35:03 +08:00
Bai
41c8cb6307 fix: api prometheus count 2024-12-20 11:14:55 +08:00
Bai
3a7ae01ede fix: add settings for license version and facelive 2024-12-19 17:37:39 +08:00
Bryan
053d640e4c Merge pull request #14699 from jumpserver/dev
v4.5.0
2024-12-19 16:04:45 +08:00
老广
d17ca4f6a7 Revert "perf: update const import"
This reverts commit 2956f2e4b7.
2024-12-19 16:03:29 +08:00
Bryan
f3acc28ded Merge pull request #14697 from jumpserver/dev
v4.5.0
2024-12-19 15:57:11 +08:00
Aaron3S
5a14bb13d0 feat: remove mfa check when unbind face code 2024-12-19 15:50:38 +08:00
ibuler
2956f2e4b7 perf: update const import 2024-12-19 15:49:13 +08:00
feng
e983ac3cbc perf: Translate 2024-12-19 15:18:39 +08:00
ibuler
fab156dc5f perf: update login success redirect 2024-12-19 14:34:49 +08:00
wangruidong
f6f897317e perf: default_value field allow blank 2024-12-19 12:01:08 +08:00
Aaron3S
a0441cd6ea feat: add translate 2024-12-19 11:13:13 +08:00
Chenyang Shen
e9abd1e72d Merge pull request #14688 from jumpserver/pr@dev@fix_face_openid
fix: fix openid user can't login with face verify
2024-12-19 11:08:45 +08:00
Aaron3S
9fcb4ecba0 fix: fix openid user can't login with face verify 2024-12-19 10:56:44 +08:00
feng
4b637ad86e perf: Client version 3.0.0 2024-12-18 19:33:45 +08:00
jiangweidong
829f867962 perf: The command amount does not record operation logs. 2024-12-18 19:33:02 +08:00
feng
7f965b55f4 perf: Translate 2024-12-18 18:21:43 +08:00
Chenyang Shen
0e0be618e5 Merge pull request #14684 from jumpserver/pr@dev@feat_refresh_cache_facelive
feat: refresh facelive cache
2024-12-18 18:10:39 +08:00
Aaron3S
9577af3221 feat: refresh facelive cache 2024-12-18 18:08:13 +08:00
Aaron3S
a6b7cc9d1b fix: fix 401 error on face verify when use openid login 2024-12-18 18:07:42 +08:00
feng
7a9a71197a perf: Client login 2024-12-18 18:01:38 +08:00
jiangweidong
3cd68ba0a9 perf: push account without increasing version. 2024-12-18 16:51:39 +08:00
jiangweidong
02bdd0f07d perf: push account without increasing version. 2024-12-18 16:51:39 +08:00
jiangweidong
98cf6f82b7 perf: create account add activity log 2024-12-18 15:54:57 +08:00
wangruidong
27fd5d51b9 perf: Translate 2024-12-18 15:53:47 +08:00
wangruidong
095ca91e30 feat: add 'labels' to DomainSerializer fields_m2m 2024-12-18 10:57:30 +08:00
wangruidong
d05514962a fix: calc platform asset count 2024-12-17 19:08:55 +08:00
Bai
c4066a03fa fix: login show system org 2024-12-17 19:08:31 +08:00
Aaron3S
a7d4c4ca2a feat: change face online killer name 2024-12-17 18:53:09 +08:00
Chenyang Shen
5b0f8f63a3 Merge pull request #14670 from jumpserver/pr@dev@feat_add_some_translate
feat: add some translate
2024-12-17 18:24:01 +08:00
Aaron3S
c4bcae68bf feat: add some translate 2024-12-17 18:03:13 +08:00
Aaron3S
29ca50f97e feat: add face online acl check for exchange token 2024-12-17 17:18:37 +08:00
feng
49aaf8d53e perf: Remove the login status after the client logs in 2024-12-17 15:32:39 +08:00
feng
931e15173b perf: perm asset api date_updated order 2024-12-17 14:43:19 +08:00
feng
4018a59b2e perf: Account backup filter org 2024-12-17 11:23:32 +08:00
Chenyang Shen
88905bd28d Merge pull request #14664 from jumpserver/pr@dev@feat_add_face_verify_on_exchange_token
feat: add face verify on exchange connect token
2024-12-16 19:00:56 +08:00
Aaron3S
abad98a190 feat: add face verify on exchange connect token 2024-12-16 18:54:32 +08:00
Chenyang Shen
7419139b29 Merge pull request #14663 from jumpserver/pr@dev@feat_exclude_some_action_for_acl
feat: exclude face action for login acl and command acl
2024-12-16 18:22:09 +08:00
Aaron3S
a1fd3b1ecb feat: exclude face action for login acl and command acl 2024-12-16 18:17:00 +08:00
wangruidong
8a8a7f9947 fix: filter custom assets in secret type check 2024-12-16 17:37:05 +08:00
Chenyang Shen
f9e6fc98fb Merge pull request #14654 from jumpserver/pr@dev@feat_update_migrations
feat: update migrations
2024-12-13 12:42:17 +08:00
Aaron3S
0dd015bcba feat: update migrations 2024-12-13 12:40:06 +08:00
Aaron3S
d1ea31c9a4 feat: face online 2024-12-12 18:31:21 +08:00
feng
e2bf56e624 perf: translate 2024-12-12 15:49:37 +08:00
feng
26040a5560 perf: pt_br translate 2024-12-12 14:40:59 +08:00
Bai
54726f0a2d perf: Passkey Model field token max_length 1024 2024-12-12 14:29:23 +08:00
Eric
7fd88b95f9 perf: update lion i18n 2024-12-12 11:21:19 +08:00
feng
4f271d6405 perf: RBAC remove assets gpt custom 2024-12-11 19:05:33 +08:00
feng
fe17a8c3a0 perf: The entire organization can view activity log 2024-12-11 18:45:41 +08:00
fit2bot
ee5e97e860 perf: add rdp connection speed option (#14641)
* perf: add rdp connection speed option

* perf: remove print code

---------

Co-authored-by: Eric <xplzv@126.com>
2024-12-11 18:42:05 +08:00
fit2bot
dddfc66efd perf: add encrypted configuration API (#14632)
* perf: 添加加密配置API

* perf: modify url

---------

Co-authored-by: Eric <xplzv@126.com>
2024-12-11 11:34:09 +08:00
Bai
d005bd804f fix: user orgs add field: is_system 2024-12-10 19:19:06 +08:00
Bai
08de04fdbc fix: fixed an issue when third-part user auth 2024-12-10 16:41:38 +08:00
Bai
9ed7c41514 fix: fixed an issue when third-part user auth 2024-12-10 16:41:38 +08:00
Eric
1a81b76a46 perf: add new Qwerty for keyboard layout 2024-12-10 15:26:18 +08:00
github-actions[bot]
cf99a7a031 perf: Update Dockerfile with new base image tag 2024-12-10 15:25:37 +08:00
Bai
64551b13a1 feat: deps add ipython==8.30.0 2024-12-10 15:25:37 +08:00
Chenyang Shen
c715300416 Merge pull request #14623 from jumpserver/pr@dev@separate_face_module
feat: Separate the face recognition module.
2024-12-09 17:00:34 +08:00
feng
d9031ae02b perf: Ticket filter assignee_id 2024-12-09 16:58:17 +08:00
Aaron3S
0d2ba5c518 feat: Separate the face recognition module. 2024-12-09 16:57:05 +08:00
Bai
817957dbac fix: fixed an issue where auth backend could pass inspect 2024-12-09 15:38:20 +08:00
feng
3796af78a6 perf: Random secret string 2024-12-09 15:25:31 +08:00
github-actions[bot]
1191e4ab2d perf: Update Dockerfile with new base image tag 2024-12-09 14:21:01 +08:00
吴小白
1c6fcc5826 feat: migrating boto to boto3 2024-12-09 14:21:01 +08:00
Chenyang Shen
4728f95634 Merge pull request #14610 from jumpserver/pr@dev@feat_face_login_acl
feat: login asset face verify acl
2024-12-09 11:34:09 +08:00
Aaron3S
013502186b feat: login asset face verify acl 2024-12-09 11:19:04 +08:00
feng
a6d040cd34 perf: Automation filter org 2024-12-06 18:00:56 +08:00
Bai
398758baa6 fix: when oidc enabled and use_state user login raise 400 2024-12-06 16:26:28 +08:00
吴小白
e29bddd89e feat: bump python from 3.11.10 to 3.11.11 2024-12-06 10:29:41 +08:00
Bai
e35c915ee3 perf: add workflows auto release docs 2024-12-06 10:20:24 +08:00
Bryan
de2dd583d0 Update README.zh-hant.md 2024-12-05 14:49:42 +08:00
Bryan
43f1d7eeae Update README.pt-br.md 2024-12-05 14:49:42 +08:00
Bryan
9bb63e0933 Update README.zh-hans.md 2024-12-05 14:49:42 +08:00
Bryan
c9e03fd5d8 Update README.ja.md 2024-12-05 14:49:42 +08:00
github-actions[bot]
7a147242c9 Auto-translate README 2024-12-05 14:49:42 +08:00
github-actions[bot]
392c261a96 Auto-translate README 2024-12-05 14:49:42 +08:00
Bai
2bbccae0f5 perf: readme 2024-12-05 14:39:23 +08:00
Bai
606fa9bfbc feat: change action 2024-12-05 14:24:38 +08:00
fit2bot
96e7b165dd Auto-translate README (#14584)
* Auto-translate README

* Auto-translate README

* Auto-translate README

* Auto-translate README

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 14:17:27 +08:00
Bai
148413d280 feat: add auto translate readme 2024-12-05 13:34:04 +08:00
Bai
a46a81d477 feat: add auto translate readme 2024-12-05 13:34:04 +08:00
feng
ff0f9eb6eb perf: Change secret update version 2024-12-05 10:50:07 +08:00
jiangweidong
d8dfaf0868 fix: Solve the problem of version increase caused by push account 2024-12-04 18:50:30 +08:00
Bai
3267c8074b feat: add actions for translate readme 2024-12-04 17:45:21 +08:00
Bai
7b14d680b2 feat: add actions for translate readme 2024-12-04 17:24:12 +08:00
Bai
0980808bb7 fix: compile languages error 2024-12-03 18:14:42 +08:00
Bai
0519f15bbf fix: compile languages error 2024-12-03 18:07:55 +08:00
Eric
f6742eb4c6 perf: add dbeaver-patch version 2024-12-03 17:36:59 +08:00
fit2bot
f8d11013fc feat: support pt-br language (#14567)
Co-authored-by: Bai <baijiangjie@gmail.com>
2024-12-03 17:11:08 +08:00
jiangweidong
7875777ed1 fix: Resolving Azure test connection failure issues 2024-12-03 14:34:48 +08:00
jiangweidong
0ca81a8f30 fix: To resolve the 500 error during local updates after an account is deleted from Vault 2024-12-03 14:34:07 +08:00
Bryan
09accbd922 perf: update issue template sorted (#14563)
* perf: update issue template sorted

* perf: update issue template sorted

* Rename 1_bug_report_cn.yml to 2_bug_report_cn.yml

* perf: update issue template sorted

* Rename 1_question.yml to 2_question.yml

* Update and rename 2_feature_request.yml to 3_feature_request.yml

* Rename 2_bug_report_cn.yml to 4_bug_report_cn.yml

* Rename 3_question_cn.yml to 5_question_cn.yml

* Update and rename 2_feature_request_cn.yml to 6_feature_request_cn.yml
2024-12-03 11:21:29 +08:00
fit2bot
945204c45b perf: Script to Add a Non-existent release_assets Field (#14558)
* perf: Script to Add a Non-existent release_assets Field

* perf: docstring

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-12-02 16:51:52 +08:00
Bai
2d62dc0657 fix: azure vault max_workers 2024-12-02 11:08:59 +08:00
fit2bot
fa61688c28 feat: Vault adds Amazon Secrets Manager (#14515)
* feat: Vault adds Amazon Secrets Manager

* perf: optimizing the code

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-11-29 17:51:28 +08:00
halo
801edc7cc9 perf: After optimizing the execution of the azure vault task, the data is out of sync 2024-11-29 16:27:35 +08:00
Bai
d0617a0ea4 fix: login log get ipv6 error 2024-11-29 14:59:01 +08:00
Chenyang Shen
1191ed1793 Merge pull request #14546 from jumpserver/pr@dev@feat_move_face_to_profile
feat: move face setiing to profile
2024-11-28 18:19:32 +08:00
Aaron3S
4036420d0e feat: move face setiing to profile 2024-11-28 18:06:57 +08:00
jiangweidong
35a1655905 perf: Oauth2.0 support two methods for passing authentication credentials. 2024-11-26 14:12:56 +08:00
feng
d4dc31aefa perf: Modify default_context COPYRIGHT 2024-11-25 15:30:26 +08:00
wangruidong
04ec34364f perf: Add viewAssetOnlineSessionInfo conf 2024-11-25 15:28:57 +08:00
Aaron3S
01b8c1f7a8 fix: Fix the uncaught exception when face capture fails 2024-11-25 10:17:28 +08:00
Bai
77598a0f23 perf: update readme 2024-11-22 16:43:44 +08:00
wangruidong
eafb074fda refactor: API endpoint 2024-11-22 15:14:26 +08:00
Bryan
d4d903f5c6 perf: Update README.md (#14516)
* perf: Update README.md

* perf: Update README.md
2024-11-22 10:38:32 +08:00
吴小白
c9c55b5fcb fix: add libldap2-dev 2024-11-21 20:53:11 +08:00
203 changed files with 19010 additions and 3919 deletions

24
.github/workflows/docs-release.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Auto update docs changelog
on:
release:
types: [published]
jobs:
update_docs_changelog:
runs-on: ubuntu-latest
if: startsWith(github.event.release.tag_name, 'v4.')
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Update docs changelog
env:
TAG_NAME: ${{ github.event.release.tag_name }}
DOCS_TOKEN: ${{ secrets.DOCS_TOKEN }}
run: |
git config --global user.name 'BaiJiangJie'
git config --global user.email 'jiangjie.bai@fit2cloud.com'
git clone https://$DOCS_TOKEN@github.com/jumpservice/documentation.git
cd documentation/utils
bash update_changelog.sh

40
.github/workflows/translate-readme.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Translate README
on:
workflow_dispatch:
inputs:
target_langs:
description: "Target Languages"
required: false
default: "zh-hans,zh-hant,ja,pt-br"
gen_dir_path:
description: "Generate Dir Name"
required: false
default: "readmes/"
push_branch:
description: "Push Branch"
required: false
default: "pr@dev@translate_readme"
prompt:
description: "AI Translate Prompt"
required: false
default: ""
gpt_mode:
description: "GPT Mode"
required: false
default: "gpt-4o-mini"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Auto Translate
uses: jumpserver-dev/action-translate-readme@main
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
OPENAI_API_KEY: ${{ secrets.GPT_API_TOKEN }}
GPT_MODE: ${{ github.event.inputs.gpt_mode }}
TARGET_LANGUAGES: ${{ github.event.inputs.target_langs }}
PUSH_BRANCH: ${{ github.event.inputs.push_branch }}
GEN_DIR_PATH: ${{ github.event.inputs.gen_dir_path }}
PROMPT: ${{ github.event.inputs.prompt }}

1
.gitignore vendored
View File

@@ -45,3 +45,4 @@ test.py
.history/
.test/
*.mo
apps.iml

View File

@@ -1,4 +1,4 @@
FROM jumpserver/core-base:20241105_025649 AS stage-build
FROM jumpserver/core-base:20241210_070105 AS stage-build
ARG VERSION
@@ -24,6 +24,7 @@ ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH
ARG DEPENDENCIES=" \
libldap2-dev \
libx11-dev"
ARG TOOLS=" \

View File

@@ -10,7 +10,8 @@
[![][github-release-shield]][github-release-link]
[![][github-stars-shield]][github-stars-link]
**English** · [简体中文](./README.zh-CN.md)
[English](/README.md) · [中文(简体)](/readmes/README.zh-hans.md) · [中文(繁體)](/readmes/README.zh-hant.md) · [日本語](/readmes/README.ja.md) · [Português (Brasil)](/readmes/README.pt-br.md)
</div>
<br/>
@@ -68,10 +69,13 @@ JumpServer consists of multiple key components, which collectively form the func
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer Character Protocol Connector |
| [Lion](https://github.com/jumpserver/lion) | <a href="https://github.com/jumpserver/lion/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion.svg" /></a> | JumpServer Graphical Protocol Connector |
| [Chen](https://github.com/jumpserver/chen) | <a href="https://github.com/jumpserver/chen/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen.svg" /> | JumpServer Web DB |
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
| [Nec](https://github.com/jumpserver/nec) | <img alt="Nec" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE VNC Proxy Connector |
| [Facelive](https://github.com/jumpserver/facelive) | <img alt="Facelive" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Facial Recognition |
## Contributing
@@ -85,7 +89,7 @@ JumpServer is a mission critical product. Please refer to the Basic Security Rec
## License
Copyright (c) 2014-2024 飞致云 FIT2CLOUD, All rights reserved.
Copyright (c) 2014-2025 FIT2CLOUD, All rights reserved.
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

View File

@@ -109,7 +109,7 @@ JumpServer是一款安全产品请参考 [基本安全建议](https://docs.ju
## License & Copyright
Copyright (c) 2014-2024 飞致云 FIT2CLOUD, All rights reserved.
Copyright (c) 2014-2024 飞致云, All rights reserved.
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in
compliance with the License. You may obtain a copy of the License at

View File

@@ -160,6 +160,10 @@ class ChangeSecretManager(AccountBasePlaybookManager):
ChangeSecretRecord.objects.bulk_create(records)
return inventory_hosts
@staticmethod
def require_update_version(account, recorder):
return account.secret != recorder.new_secret
def on_host_success(self, host, result):
recorder = self.name_recorder_mapper.get(host)
if not recorder:
@@ -171,6 +175,8 @@ class ChangeSecretManager(AccountBasePlaybookManager):
if not account:
print("Account not found, deleted ?")
return
version_update_required = self.require_update_version(account, recorder)
account.secret = recorder.new_secret
account.date_updated = timezone.now()
@@ -180,7 +186,10 @@ class ChangeSecretManager(AccountBasePlaybookManager):
while retry_count < max_retries:
try:
recorder.save()
account.save(update_fields=['secret', 'version', 'date_updated'])
account_update_fields = ['secret', 'date_updated']
if version_update_required:
account_update_fields.append('version')
account.save(update_fields=account_update_fields)
break
except Exception as e:
retry_count += 1

View File

@@ -8,6 +8,11 @@ logger = get_logger(__name__)
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
@staticmethod
def require_update_version(account, recorder):
account.skip_history_when_saving = True
return False
@classmethod
def method_type(cls):
return AutomationTypes.push_account

View File

@@ -13,9 +13,6 @@ logger = get_logger(__file__)
def get_vault_client(raise_exception=False, **kwargs):
tp = kwargs.get('VAULT_BACKEND') if kwargs.get('VAULT_ENABLED') else VaultTypeChoices.local
# TODO: Temporary processing, subsequent deletion
tp = VaultTypeChoices.local if tp == VaultTypeChoices.azure else tp
try:
module_path = f'apps.accounts.backends.{tp}.main'
client = import_module(module_path).Vault(**kwargs)

View File

@@ -0,0 +1 @@
from .main import *

View File

@@ -0,0 +1,16 @@
from .service import AmazonSecretsManagerClient
from ..base.vault import BaseVault
from ..utils.mixins import GeneralVaultMixin
from ...const import VaultTypeChoices
class Vault(GeneralVaultMixin, BaseVault):
type = VaultTypeChoices.aws
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = AmazonSecretsManagerClient(
region_name=kwargs.get('VAULT_AWS_REGION_NAME'),
access_key_id=kwargs.get('VAULT_AWS_ACCESS_KEY_ID'),
secret_key=kwargs.get('VAULT_AWS_ACCESS_SECRET_KEY'),
)

View File

@@ -0,0 +1,56 @@
import boto3
from common.utils import get_logger, random_string
logger = get_logger(__name__)
__all__ = ['AmazonSecretsManagerClient']
class AmazonSecretsManagerClient(object):
def __init__(self, region_name, access_key_id, secret_key):
self.client = boto3.client(
'secretsmanager', region_name=region_name,
aws_access_key_id=access_key_id, aws_secret_access_key=secret_key,
)
self.empty_secret = '#{empty}#'
def is_active(self):
try:
secret_id = f'jumpserver/test-{random_string(12)}'
self.create(secret_id, 'secret')
self.get(secret_id)
self.update(secret_id, 'secret')
self.delete(secret_id)
except Exception as e:
return False, f'Vault is not reachable: {e}'
else:
return True, ''
def get(self, name, version=''):
params = {'SecretId': name}
if version:
params['VersionStage'] = version
try:
secret = self.client.get_secret_value(**params)['SecretString']
return secret if secret != self.empty_secret else ''
except Exception: # noqa
return ''
def create(self, name, secret):
self.client.create_secret(Name=name, SecretString=secret or self.empty_secret)
def update(self, name, secret):
self.client.update_secret(SecretId=name, SecretString=secret or self.empty_secret)
def delete(self, name):
self.client.delete_secret(SecretId=name)
def update_metadata(self, name, metadata: dict):
tags = [{'Key': k, 'Value': v} for k, v in metadata.items()]
try:
self.client.tag_resource(SecretId=name, Tags=tags)
except Exception as e:
logger.error(f'update_metadata: {name} {str(e)}')

View File

@@ -1,41 +1,13 @@
import sys
from abc import ABC
from common.db.utils import Encryptor
from common.utils import lazyproperty
current_module = sys.modules[__name__]
__all__ = ['build_entry']
from ..base.entries import BaseEntry
class BaseEntry(ABC):
def __init__(self, instance):
self.instance = instance
@lazyproperty
class AzureBaseEntry(BaseEntry):
@property
def full_path(self):
return self.path_spec
@property
def path_spec(self):
raise NotImplementedError
def to_internal_data(self):
secret = getattr(self.instance, '_secret', None)
if secret is not None:
secret = Encryptor(secret).encrypt()
return secret
@staticmethod
def to_external_data(secret):
if secret is not None:
secret = Encryptor(secret).decrypt()
return secret
class AccountEntry(BaseEntry):
class AccountEntry(AzureBaseEntry):
@property
def path_spec(self):
@@ -45,7 +17,7 @@ class AccountEntry(BaseEntry):
return path
class AccountTemplateEntry(BaseEntry):
class AccountTemplateEntry(AzureBaseEntry):
@property
def path_spec(self):
@@ -53,18 +25,9 @@ class AccountTemplateEntry(BaseEntry):
return path
class HistoricalAccountEntry(BaseEntry):
class HistoricalAccountEntry(AzureBaseEntry):
@property
def path_spec(self):
path = f'accounts-{self.instance.instance.id}-histories-{self.instance.history_id}'
return path
def build_entry(instance) -> BaseEntry:
class_name = instance.__class__.__name__
entry_class_name = f'{class_name}Entry'
entry_class = getattr(current_module, entry_class_name, None)
if not entry_class:
raise Exception(f'Entry class {entry_class_name} is not found')
return entry_class(instance)

View File

@@ -1,16 +1,10 @@
from common.db.utils import get_logger
from .entries import build_entry
from .service import AZUREVaultClient
from ..base import BaseVault
from ..base.vault import BaseVault
from ..utils.mixins import GeneralVaultMixin
from ...const import VaultTypeChoices
logger = get_logger(__name__)
__all__ = ['Vault']
class Vault(BaseVault):
class Vault(GeneralVaultMixin, BaseVault):
type = VaultTypeChoices.azure
def __init__(self, *args, **kwargs):
@@ -21,37 +15,3 @@ class Vault(BaseVault):
client_id=kwargs.get('VAULT_AZURE_CLIENT_ID'),
client_secret=kwargs.get('VAULT_AZURE_CLIENT_SECRET')
)
def is_active(self):
return self.client.is_active()
def _get(self, instance):
entry = build_entry(instance)
secret = self.client.get(name=entry.full_path)
secret = entry.to_external_data(secret)
return secret
def _create(self, instance):
entry = build_entry(instance)
secret = entry.to_internal_data()
self.client.create(name=entry.full_path, secret=secret)
def _update(self, instance):
entry = build_entry(instance)
secret = entry.to_internal_data()
self.client.update(name=entry.full_path, secret=secret)
def _delete(self, instance):
entry = build_entry(instance)
self.client.delete(name=entry.full_path)
def _clean_db_secret(self, instance):
instance.is_sync_metadata = False
instance.mark_secret_save_to_vault()
def _save_metadata(self, instance, metadata):
try:
entry = build_entry(instance)
self.client.update_metadata(name=entry.full_path, metadata=metadata)
except Exception as e:
logger.error(f'save metadata error: {e}')

View File

@@ -36,7 +36,6 @@ class AZUREVaultClient(object):
secret = self.client.get_secret(name, version)
return secret.value
except (ResourceNotFoundError, ClientAuthenticationError) as e:
logger.error(f'get: {name} {str(e)}')
return ''
def create(self, name, secret):

View File

@@ -1,76 +0,0 @@
from abc import ABC, abstractmethod
from django.forms.models import model_to_dict
__all__ = ['BaseVault']
class BaseVault(ABC):
def __init__(self, *args, **kwargs):
self.enabled = kwargs.get('VAULT_ENABLED')
@property
@abstractmethod
def type(self):
raise NotImplementedError
def get(self, instance):
""" 返回 secret 值 """
return self._get(instance)
def create(self, instance):
if not instance.secret_has_save_to_vault:
self._create(instance)
self._clean_db_secret(instance)
self.save_metadata(instance)
def update(self, instance):
if not instance.secret_has_save_to_vault:
self._update(instance)
self._clean_db_secret(instance)
self.save_metadata(instance)
if instance.is_sync_metadata:
self.save_metadata(instance)
def delete(self, instance):
self._delete(instance)
def save_metadata(self, instance):
metadata = model_to_dict(instance, fields=[
'name', 'username', 'secret_type',
'connectivity', 'su_from', 'privileged'
])
metadata = {k: str(v)[:500] for k, v in metadata.items() if v}
return self._save_metadata(instance, metadata)
# -------- abstractmethod -------- #
@abstractmethod
def _get(self, instance):
raise NotImplementedError
@abstractmethod
def _create(self, instance):
raise NotImplementedError
@abstractmethod
def _update(self, instance):
raise NotImplementedError
@abstractmethod
def _delete(self, instance):
raise NotImplementedError
@abstractmethod
def _clean_db_secret(self, instance):
raise NotImplementedError
@abstractmethod
def _save_metadata(self, instance, metadata):
raise NotImplementedError
@abstractmethod
def is_active(self, *args, **kwargs) -> (bool, str):
raise NotImplementedError

View File

View File

@@ -1,19 +1,18 @@
import sys
from abc import ABC
from common.db.utils import Encryptor
from common.utils import lazyproperty
current_module = sys.modules[__name__]
__all__ = ['build_entry']
class BaseEntry(ABC):
def __init__(self, instance):
self.instance = instance
@property
def path_base(self):
path = f'orgs/{self.instance.org_id}'
return path
@lazyproperty
def full_path(self):
path_base = self.path_base
@@ -21,32 +20,24 @@ class BaseEntry(ABC):
path = f'{path_base}/{path_spec}'
return path
@property
def path_base(self):
path = f'orgs/{self.instance.org_id}'
return path
@property
def path_spec(self):
raise NotImplementedError
def to_internal_data(self):
def get_encrypt_secret(self):
secret = getattr(self.instance, '_secret', None)
if secret is not None:
secret = Encryptor(secret).encrypt()
data = {'secret': secret}
return data
return secret
@staticmethod
def to_external_data(data):
secret = data.pop('secret', None)
def get_decrypt_secret(secret):
if secret is not None:
secret = Encryptor(secret).decrypt()
return secret
class AccountEntry(BaseEntry):
@property
def path_spec(self):
path = f'assets/{self.instance.asset_id}/accounts/{self.instance.id}'
@@ -54,7 +45,6 @@ class AccountEntry(BaseEntry):
class AccountTemplateEntry(BaseEntry):
@property
def path_spec(self):
path = f'account-templates/{self.instance.id}'
@@ -62,23 +52,12 @@ class AccountTemplateEntry(BaseEntry):
class HistoricalAccountEntry(BaseEntry):
@property
def path_base(self):
account = self.instance.instance
path = f'accounts/{account.id}/'
path = f'accounts/{self.instance.instance.id}'
return path
@property
def path_spec(self):
path = f'histories/{self.instance.history_id}'
return path
def build_entry(instance) -> BaseEntry:
class_name = instance.__class__.__name__
entry_class_name = f'{class_name}Entry'
entry_class = getattr(current_module, entry_class_name, None)
if not entry_class:
raise Exception(f'Entry class {entry_class_name} is not found')
return entry_class(instance)

View File

@@ -0,0 +1,109 @@
import importlib
import inspect
from abc import ABC, abstractmethod
from django.forms.models import model_to_dict
from .entries import BaseEntry
from ...const import VaultTypeChoices
class BaseVault(ABC):
def __init__(self, *args, **kwargs):
self.enabled = kwargs.get('VAULT_ENABLED')
self._entry_classes = {}
self._load_entries()
def _load_entries_import_module(self, module_name):
module = importlib.import_module(module_name)
for name, obj in inspect.getmembers(module, inspect.isclass):
self._entry_classes.setdefault(name, obj)
def _load_entries(self):
if self.type == VaultTypeChoices.local:
return
module_name = f'accounts.backends.{self.type}.entries'
if importlib.util.find_spec(module_name): # noqa
self._load_entries_import_module(module_name)
base_module = 'accounts.backends.base.entries'
self._load_entries_import_module(base_module)
@property
@abstractmethod
def type(self):
raise NotImplementedError
def get(self, instance):
""" 返回 secret 值 """
return self._get(self.build_entry(instance))
def create(self, instance):
if not instance.secret_has_save_to_vault:
entry = self.build_entry(instance)
self._create(entry)
self._clean_db_secret(instance)
self.save_metadata(entry)
def update(self, instance):
entry = self.build_entry(instance)
if not instance.secret_has_save_to_vault:
self._update(entry)
self._clean_db_secret(instance)
self.save_metadata(entry)
if instance.is_sync_metadata:
self.save_metadata(entry)
def delete(self, instance):
entry = self.build_entry(instance)
self._delete(entry)
def save_metadata(self, entry):
metadata = model_to_dict(entry.instance, fields=[
'name', 'username', 'secret_type',
'connectivity', 'su_from', 'privileged'
])
metadata = {k: str(v)[:500] for k, v in metadata.items() if v}
return self._save_metadata(entry, metadata)
def build_entry(self, instance):
if self.type == VaultTypeChoices.local:
return BaseEntry(instance)
entry_class_name = f'{instance.__class__.__name__}Entry'
entry_class = self._entry_classes.get(entry_class_name)
if not entry_class:
raise Exception(f'Entry class {entry_class_name} is not found')
return entry_class(instance)
def _clean_db_secret(self, instance):
instance.is_sync_metadata = False
instance.mark_secret_save_to_vault()
# -------- abstractmethod -------- #
@abstractmethod
def _get(self, instance):
raise NotImplementedError
@abstractmethod
def _create(self, entry):
raise NotImplementedError
@abstractmethod
def _update(self, entry):
raise NotImplementedError
@abstractmethod
def _delete(self, entry):
raise NotImplementedError
@abstractmethod
def _save_metadata(self, instance, metadata):
raise NotImplementedError
@abstractmethod
def is_active(self, *args, **kwargs) -> (bool, str):
raise NotImplementedError

View File

@@ -1,10 +1,10 @@
from common.db.utils import get_logger
from .entries import build_entry
from .service import VaultKVClient
from ..base import BaseVault
from ..base.vault import BaseVault
from ...const import VaultTypeChoices
logger = get_logger(__name__)
__all__ = ['Vault']
@@ -24,34 +24,25 @@ class Vault(BaseVault):
def is_active(self):
return self.client.is_active()
def _get(self, instance):
entry = build_entry(instance)
def _get(self, entry):
# TODO: get data 是不是层数太多了
data = self.client.get(path=entry.full_path).get('data', {})
data = entry.to_external_data(data)
data = entry.get_decrypt_secret(data.get('secret'))
return data
def _create(self, instance):
entry = build_entry(instance)
data = entry.to_internal_data()
def _create(self, entry):
data = {'secret': entry.get_encrypt_secret()}
self.client.create(path=entry.full_path, data=data)
def _update(self, instance):
entry = build_entry(instance)
data = entry.to_internal_data()
def _update(self, entry):
data = {'secret': entry.get_encrypt_secret()}
self.client.patch(path=entry.full_path, data=data)
def _delete(self, instance):
entry = build_entry(instance)
def _delete(self, entry):
self.client.delete(path=entry.full_path)
def _clean_db_secret(self, instance):
instance.is_sync_metadata = False
instance.mark_secret_save_to_vault()
def _save_metadata(self, instance, metadata):
def _save_metadata(self, entry, metadata):
try:
entry = build_entry(instance)
self.client.update_metadata(path=entry.full_path, metadata=metadata)
except Exception as e:
logger.error(f'save metadata error: {e}')

View File

@@ -1,5 +1,5 @@
from common.utils import get_logger
from ..base import BaseVault
from ..base.vault import BaseVault
from ...const import VaultTypeChoices
logger = get_logger(__name__)
@@ -13,23 +13,23 @@ class Vault(BaseVault):
def is_active(self):
return True, ''
def _get(self, instance):
secret = getattr(instance, '_secret', None)
def _get(self, entry):
secret = getattr(entry.instance, '_secret', None)
return secret
def _create(self, instance):
def _create(self, entry):
""" Ignore """
pass
def _update(self, instance):
def _update(self, entry):
""" Ignore """
pass
def _delete(self, instance):
def _delete(self, entry):
""" Ignore """
pass
def _save_metadata(self, instance, metadata):
def _save_metadata(self, entry, metadata):
""" Ignore """
pass

View File

View File

@@ -0,0 +1,32 @@
from common.utils import get_logger
logger = get_logger(__name__)
class GeneralVaultMixin(object):
client = None
def is_active(self):
return self.client.is_active()
def _get(self, entry):
secret = self.client.get(name=entry.full_path)
return entry.get_decrypt_secret(secret)
def _create(self, entry):
secret = entry.get_encrypt_secret()
self.client.create(name=entry.full_path, secret=secret)
def _update(self, entry):
secret = entry.get_encrypt_secret()
self.client.update(name=entry.full_path, secret=secret)
def _delete(self, entry):
self.client.delete(name=entry.full_path)
def _save_metadata(self, entry, metadata):
try:
self.client.update_metadata(name=entry.full_path, metadata=metadata)
except Exception as e:
logger.error(f'save metadata error: {e}')

View File

@@ -8,3 +8,4 @@ class VaultTypeChoices(models.TextChoices):
local = 'local', _('Database')
hcp = 'hcp', _('HCP Vault')
azure = 'azure', _('Azure Key Vault')
aws = 'aws', _('Amazon Secrets Manager')

View File

@@ -0,0 +1,8 @@
from common.exceptions import JMSException
from django.utils.translation import gettext_lazy as _
class VaultException(JMSException):
default_detail = _(
'Vault operation failed. Please retry or check your account information on Vault.'
)

View File

@@ -14,13 +14,17 @@ from common.db import fields
from common.db.encoder import ModelJSONFieldEncoder
from common.utils import get_logger, lazyproperty
from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel, OrgManager
__all__ = ['AccountBackupAutomation', 'AccountBackupExecution']
logger = get_logger(__file__)
class BaseBackupAutomationManager(OrgManager):
pass
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
types = models.JSONField(default=list)
backup_type = models.CharField(max_length=128, choices=AccountBackupType.choices,
@@ -47,6 +51,8 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
max_length=4096, blank=True, null=True, verbose_name=_('Zip encrypt password')
)
objects = BaseBackupAutomationManager.from_queryset(models.QuerySet)()
def __str__(self):
return f'{self.name}({self.org_id})'

View File

@@ -80,6 +80,7 @@ class VaultModelMixin(models.Model):
def mark_secret_save_to_vault(self):
self._secret = self._secret_save_to_vault_mark
self.skip_history_when_saving = True
self.save()
@property

View File

@@ -385,7 +385,7 @@ class AssetAccountBulkSerializer(
_results = {}
for asset in assets:
if asset not in secret_type_supports:
if asset not in secret_type_supports and asset.category != Category.CUSTOM:
_results[asset] = {
'error': _('Asset does not support this secret type: %s') % secret_type,
'state': 'error',

View File

@@ -14,6 +14,7 @@ from common.decorators import merge_delay_run
from common.signals import django_ready
from common.utils import get_logger, i18n_fmt
from common.utils.connection import RedisPubSub
from .exceptions import VaultException
from .models import Account, AccountTemplate
from .tasks.push_account import push_accounts_to_assets_task
@@ -22,6 +23,9 @@ logger = get_logger(__name__)
@receiver(pre_save, sender=Account)
def on_account_pre_save(sender, instance, **kwargs):
if getattr(instance, 'skip_history_when_saving', False):
return
if instance.version == 0:
instance.version = 1
else:
@@ -65,7 +69,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 != Source.TEMPLATE:
if not created:
return
push_accounts_if_need.delay(accounts=(instance,))
create_accounts_activities(instance, action='create')
@@ -81,14 +85,22 @@ class VaultSignalHandler(object):
@staticmethod
def save_to_vault(sender, instance, created, **kwargs):
if created:
vault_client.create(instance)
else:
vault_client.update(instance)
try:
if created:
vault_client.create(instance)
else:
vault_client.update(instance)
except Exception as e:
logger.error('Vault save failed: {}'.format(e))
raise VaultException()
@staticmethod
def delete_to_vault(sender, instance, **kwargs):
vault_client.delete(instance)
try:
vault_client.delete(instance)
except Exception as e:
logger.error('Vault delete failed: {}'.format(e))
raise VaultException()
for model in (Account, AccountTemplate, Account.history.model):

View File

@@ -52,7 +52,8 @@ def sync_secret_to_vault():
for model in to_sync_models:
instances += list(model.objects.all())
with ThreadPoolExecutor(max_workers=10) as executor:
max_workers = 1 if VaultTypeChoices.azure == vault_client.type else 10
with ThreadPoolExecutor(max_workers=max_workers) as executor:
tasks = [executor.submit(sync_instance, instance) for instance in instances]
for future in as_completed(tasks):

View File

@@ -9,3 +9,5 @@ class ActionChoices(models.TextChoices):
warning = 'warning', _('Warn')
notice = 'notice', _('Notify')
notify_and_warn = 'notify_and_warn', _('Notify and warn')
face_verify = 'face_verify', _('Face Verify')
face_online = 'face_online', _('Face Online')

View File

@@ -70,6 +70,13 @@ class ActionAclSerializer(serializers.Serializer):
return
if not settings.XPACK_LICENSE_IS_VALID:
field_action._choices.pop(ActionChoices.review, None)
if not (
settings.XPACK_LICENSE_IS_VALID and
settings.XPACK_LICENSE_EDITION_ULTIMATE and
settings.FACE_RECOGNITION_ENABLED
):
field_action._choices.pop(ActionChoices.face_verify, None)
field_action._choices.pop(ActionChoices.face_online, None)
for choice in self.Meta.action_choices_exclude:
field_action._choices.pop(choice, None)

View File

@@ -32,7 +32,9 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
class Meta(BaseSerializer.Meta):
model = CommandFilterACL
fields = BaseSerializer.Meta.fields + ['command_groups']
action_choices_exclude = [ActionChoices.notice]
action_choices_exclude = [ActionChoices.notice,
ActionChoices.face_verify,
ActionChoices.face_online]
class CommandReviewSerializer(serializers.Serializer):

View File

@@ -4,6 +4,7 @@ from common.serializers import MethodSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import BaseUserACLSerializer
from .rules import RuleSerializer
from ..const import ActionChoices
from ..models import LoginACL
__all__ = ["LoginACLSerializer"]
@@ -17,6 +18,7 @@ class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
class Meta(BaseUserACLSerializer.Meta):
model = LoginACL
fields = BaseUserACLSerializer.Meta.fields + ['rules', ]
action_choices_exclude = [ActionChoices.face_online, ActionChoices.face_verify]
def get_rules_serializer(self):
return RuleSerializer()

View File

@@ -1,10 +1,10 @@
from django.db.models import Count
from django.db.models import Subquery, OuterRef, Count, Value
from django.db.models.functions import Coalesce
from django_filters import rest_framework as filters
from rest_framework import generics
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from assets.const import AllTypes
from assets.models import Platform, Node, Asset, PlatformProtocol
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer, PlatformListSerializer
@@ -42,7 +42,10 @@ class AssetPlatformViewSet(JMSModelViewSet):
def get_queryset(self):
# 因为没有走分页逻辑,所以需要这里 prefetch
queryset = super().get_queryset().annotate(assets_amount=Count('assets')).prefetch_related(
asset_count_subquery = Asset.objects.filter(platform=OuterRef('pk')).values('platform').annotate(
count=Count('id')).values('count')
queryset = super().get_queryset().annotate(
assets_amount=Coalesce(Subquery(asset_count_subquery), Value(0))).prefetch_related(
'protocols', 'automation', 'labels', 'labels__label'
)
queryset = queryset.filter(type__in=AllTypes.get_types_values())

View File

@@ -10,7 +10,11 @@ from assets.tasks import execute_asset_automation_task
from common.const.choices import Trigger
from common.db.fields import EncryptJsonDictTextField
from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel, OrgManager
class BaseAutomationManager(OrgManager):
pass
class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
@@ -21,6 +25,8 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
params = models.JSONField(default=dict, verbose_name=_("Parameters"))
objects = BaseAutomationManager.from_queryset(models.QuerySet)()
def __str__(self):
return self.name + '@' + str(self.created_by)

View File

@@ -27,7 +27,7 @@ class DomainSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer):
model = Domain
fields_mini = ['id', 'name']
fields_small = fields_mini + ['comment']
fields_m2m = ['assets', 'gateways', 'assets_amount']
fields_m2m = ['assets', 'gateways', 'labels', 'assets_amount']
read_only_fields = ['date_created']
fields = fields_small + fields_m2m + read_only_fields
extra_kwargs = {

View File

@@ -222,9 +222,13 @@ class ResourceActivityAPIView(generics.ListAPIView):
'id', 'datetime', 'r_detail', 'r_detail_id',
'r_user', 'r_action', 'r_type'
)
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
if resource_id:
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
org_q = Q()
if not current_org.is_root():
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
if resource_id:
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
with tmp_to_root_org():
qs1 = self.get_operate_log_qs(fields, limit, org_q, resource_id=resource_id)
qs2 = self.get_activity_log_qs(fields, limit, org_q, resource_id=resource_id)

View File

@@ -14,7 +14,7 @@ from audits.handler import (
create_or_update_operate_log, get_instance_dict_from_cache
)
from audits.utils import model_to_dict_for_operate_log as model_to_dict
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, OP_LOG_SKIP_SIGNAL
from common.signals import django_ready
from jumpserver.utils import current_request
from ..const import MODELS_NEED_RECORD, ActionChoices
@@ -77,7 +77,7 @@ def signal_of_operate_log_whether_continue(
condition = True
if not instance:
condition = False
if instance and getattr(instance, SKIP_SIGNAL, False):
if instance and getattr(instance, OP_LOG_SKIP_SIGNAL, False):
condition = False
# 不记录组件的操作日志
user = current_request.user if current_request else None

View File

@@ -14,7 +14,7 @@ router.register(r'login-logs', api.UserLoginLogViewSet, 'login-log')
router.register(r'operate-logs', api.OperateLogViewSet, 'operate-log')
router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password-change-log')
router.register(r'job-logs', api.JobLogAuditViewSet, 'job-log')
router.register(r'jobs', api.JobsAuditViewSet, 'jobs')
router.register(r'jobs', api.JobsAuditViewSet, 'job')
router.register(r'my-login-logs', api.MyLoginLogViewSet, 'my-login-log')
router.register(r'user-sessions', api.UserSessionViewSet, 'user-session')

View File

@@ -15,3 +15,4 @@ from .ssh_key import *
from .sso import *
from .temp_token import *
from .token import *
from .face import *

View File

@@ -24,11 +24,13 @@ from common.utils.http import is_true, is_false
from orgs.mixins.api import RootOrgViewMixin
from orgs.utils import tmp_to_org
from perms.models import ActionChoices
from terminal.connect_methods import NativeClient, ConnectMethodUtil
from terminal.connect_methods import NativeClient, ConnectMethodUtil, WebMethod
from terminal.models import EndpointRule, Endpoint
from users.const import FileNameConflictResolution
from users.const import RDPSmartSize, RDPColorQuality
from users.models import Preference
from .face import FaceMonitorContext
from ..mixins import AuthFaceMixin
from ..models import ConnectionToken, date_expired_default
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
@@ -67,6 +69,36 @@ class RDPFileClientProtocolURLMixin:
'bookmarktype:i': '3',
'use redirection server name:i': '0',
}
# copy from
# https://learn.microsoft.com/zh-cn/windows-server/administration/performance-tuning/role/remote-desktop/session-hosts
rdp_low_speed_broadband_option = {
"connection type:i": 2,
"disable wallpaper:i": 1,
"bitmapcachepersistenable:i": 1,
"disable full window drag:i": 1,
"disable menu anims:i": 1,
"allow font smoothing:i": 0,
"allow desktop composition:i": 0,
"disable themes:i": 0
}
rdp_high_speed_broadband_option = {
"connection type:i": 4,
"disable wallpaper:i": 0,
"bitmapcachepersistenable:i": 1,
"disable full window drag:i": 1,
"disable menu anims:i": 0,
"allow font smoothing:i": 0,
"allow desktop composition:i": 1,
"disable themes:i": 0
}
RDP_CONNECTION_SPEED_OPTION_MAP = {
"auto": {},
"low_speed_broadband": rdp_low_speed_broadband_option,
"high_speed_broadband": rdp_high_speed_broadband_option,
}
# 设置多屏显示
multi_mon = is_true(self.request.query_params.get('multi_mon'))
if multi_mon:
@@ -91,13 +123,15 @@ class RDPFileClientProtocolURLMixin:
# rdp_options['domain:s'] = token.account_ad_domain
# 设置宽高
height = self.request.query_params.get('height')
width = self.request.query_params.get('width')
if width and height:
rdp_options['desktopwidth:i'] = width
rdp_options['desktopheight:i'] = height
rdp_options['winposstr:s'] = f'0,1,0,0,{width},{height}'
rdp_options['dynamic resolution:i'] = '0'
resolution_value = token.connect_options.get('resolution', 'auto')
if resolution_value != 'auto':
width, height = resolution_value.split('x')
if width and height:
rdp_options['desktopwidth:i'] = width
rdp_options['desktopheight:i'] = height
rdp_options['winposstr:s'] = f'0,1,0,0,{width},{height}'
rdp_options['dynamic resolution:i'] = '0'
color_quality = self.request.query_params.get('rdp_color_quality')
color_quality = color_quality if color_quality else os.getenv('JUMPSERVER_COLOR_DEPTH', RDPColorQuality.HIGH)
@@ -115,6 +149,8 @@ class RDPFileClientProtocolURLMixin:
rdp = token.asset.platform.protocols.filter(name='rdp').first()
if rdp and rdp.setting.get('console'):
rdp_options['administrative session:i'] = '1'
rdp_connection_speed = token.connect_options.get('rdp_connection_speed', 'auto')
rdp_options.update(RDP_CONNECTION_SPEED_OPTION_MAP.get(rdp_connection_speed, {}))
# 文件名
name = token.asset.name
@@ -221,6 +257,8 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
get_serializer: callable
perform_create: callable
validate_exchange_token: callable
need_face_verify: bool
create_face_verify: callable
@action(methods=['POST', 'GET'], detail=True, url_path='rdp-file')
def get_rdp_file(self, request, *args, **kwargs):
@@ -280,10 +318,13 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
instance.date_expired = date_expired_default()
instance.save()
serializer = self.get_serializer(instance)
return Response(serializer.data, status=status.HTTP_201_CREATED)
response = Response(serializer.data, status=status.HTTP_201_CREATED)
if self.need_face_verify:
self.create_face_verify(response)
return response
class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelViewSet):
class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixin, JMSModelViewSet):
filterset_fields = (
'user_display', 'asset_display'
)
@@ -304,6 +345,8 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
input_username = ''
need_face_verify = False
face_monitor_token = ''
def get_queryset(self):
queryset = ConnectionToken.objects \
@@ -355,8 +398,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
asset = data.get('asset')
account_name = data.get('account')
protocol = data.get('protocol')
connect_method = data.get('connect_method')
self.input_username = self.get_input_username(data)
_data = self._validate(user, asset, account_name, protocol)
_data = self._validate(user, asset, account_name, protocol, connect_method)
data.update(_data)
return serializer
@@ -364,12 +408,12 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
user = token.user
asset = token.asset
account_name = token.account
_data = self._validate(user, asset, account_name, token.protocol)
_data = self._validate(user, asset, account_name, token.protocol, token.connect_method)
for k, v in _data.items():
setattr(token, k, v)
return token
def _validate(self, user, asset, account_name, protocol):
def _validate(self, user, asset, account_name, protocol, connect_method):
data = dict()
data['org_id'] = asset.org_id
data['user'] = user
@@ -385,10 +429,16 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
if account.username != AliasAccount.INPUT:
data['input_username'] = ''
ticket = self._validate_acl(user, asset, account)
ticket = self._validate_acl(user, asset, account, connect_method)
if ticket:
data['from_ticket'] = ticket
if ticket or self.need_face_verify:
data['is_active'] = False
if self.face_monitor_token:
FaceMonitorContext.get_or_create_context(self.face_monitor_token,
self.request.user.id)
data['face_monitor_token'] = self.face_monitor_token
return data
@staticmethod
@@ -417,7 +467,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
after=after, object_name=object_name
)
def _validate_acl(self, user, asset, account):
def _validate_acl(self, user, asset, account, connect_method):
from acls.models import LoginAssetACL
kwargs = {'user': user, 'asset': asset, 'account': account}
if account.username == AliasAccount.INPUT:
@@ -444,6 +494,26 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
assignees=acl.reviewers.all(), org_id=asset.org_id
)
return ticket
if acl.is_action(acl.ActionChoices.face_verify):
if not self.request.query_params.get('face_verify'):
msg = _('ACL action is face verify')
raise JMSException(code='acl_face_verify', detail=msg)
self.need_face_verify = True
if acl.is_action(acl.ActionChoices.face_online):
if connect_method not in [WebMethod.web_cli, WebMethod.web_gui]:
msg = _('ACL action not supported for this asset')
raise JMSException(detail=msg, code='acl_face_online_not_supported')
face_verify = self.request.query_params.get('face_verify')
face_monitor_token = self.request.query_params.get('face_monitor_token')
if not face_verify or not face_monitor_token:
msg = _('ACL action is face online')
raise JMSException(code='acl_face_online', detail=msg)
self.need_face_verify = True
self.face_monitor_token = face_monitor_token
if acl.is_action(acl.ActionChoices.notice):
reviewers = acl.reviewers.all()
if not reviewers:
@@ -455,9 +525,22 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
reviewer, asset, user, account, self.input_username
).publish_async()
def create_face_verify(self, response):
if not self.request.user.face_vector:
raise JMSException(code='no_face_feature', detail=_('No available face feature'))
connection_token_id = response.data.get('id')
context_data = {
"action": "login_asset",
"connection_token_id": connection_token_id,
}
face_verify_token = self.create_face_verify_context(context_data)
response.data['face_token'] = face_verify_token
def create(self, request, *args, **kwargs):
try:
response = super().create(request, *args, **kwargs)
if self.need_face_verify:
self.create_face_verify(response)
except JMSException as e:
data = {'code': e.detail.code, 'detail': e.detail}
return Response(data, status=e.status_code)

View File

@@ -0,0 +1,256 @@
from django.core.cache import cache
from django.utils.translation import gettext as _
from rest_framework.generics import CreateAPIView, RetrieveAPIView
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.permissions import AllowAny
from rest_framework.exceptions import NotFound
from common.permissions import IsServiceAccount
from common.utils import get_logger, get_object_or_none
from orgs.utils import tmp_to_root_org
from terminal.api.session.task import create_sessions_tasks
from users.models import User
from .. import serializers
from ..mixins import AuthMixin
from ..const import FACE_CONTEXT_CACHE_KEY_PREFIX, FACE_SESSION_KEY, FACE_CONTEXT_CACHE_TTL, FaceMonitorActionChoices
from ..models import ConnectionToken
from ..serializers.face import FaceMonitorCallbackSerializer, FaceMonitorContextSerializer
logger = get_logger(__name__)
__all__ = [
'FaceCallbackApi',
'FaceContextApi',
'FaceMonitorContext',
'FaceMonitorContextApi',
'FaceMonitorCallbackApi'
]
class FaceCallbackApi(AuthMixin, CreateAPIView):
permission_classes = (IsServiceAccount,)
serializer_class = serializers.FaceCallbackSerializer
def perform_create(self, serializer):
token = serializer.validated_data.get('token')
context = self._get_context_from_cache(token)
if not serializer.validated_data.get('success', False):
self._update_context_with_error(
context,
serializer.validated_data.get('error_message', 'Unknown error')
)
return Response(status=200)
face_code = serializer.validated_data.get('face_code')
if not face_code:
self._update_context_with_error(context, "missing field 'face_code'")
raise ValidationError({'error': "missing field 'face_code'"})
try:
self._handle_success(context, face_code)
except Exception as e:
self._update_context_with_error(context, str(e))
return Response(status=200)
@staticmethod
def get_face_cache_key(token):
return f"{FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
def _get_context_from_cache(self, token):
cache_key = self.get_face_cache_key(token)
context = cache.get(cache_key)
if not context:
raise ValidationError({'error': "token not exists or expired"})
return context
def _update_context_with_error(self, context, error_message):
context.update({
'is_finished': True,
'success': False,
'error_message': error_message,
})
self._update_cache(context)
def _update_cache(self, context):
cache_key = self.get_face_cache_key(context['token'])
cache.set(cache_key, context, FACE_CONTEXT_CACHE_TTL)
def _handle_success(self, context, face_code):
context.update({
'is_finished': True,
'success': True,
'face_code': face_code
})
action = context.get('action', None)
if action == 'login_asset':
user_id = context.get('user_id')
user = User.objects.get(id=user_id)
if user.check_face(face_code):
with tmp_to_root_org():
connection_token_id = context.get('connection_token_id')
token = ConnectionToken.objects.filter(id=connection_token_id).first()
token.is_active = True
token.save()
else:
context.update({
'success': False,
'error_message': _('Facial comparison failed')
})
self._update_cache(context)
class FaceContextApi(AuthMixin, RetrieveAPIView, CreateAPIView):
permission_classes = (AllowAny,)
face_token_session_key = FACE_SESSION_KEY
@staticmethod
def get_face_cache_key(token):
return f"{FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
def new_face_context(self):
return self.create_face_verify_context()
def post(self, request, *args, **kwargs):
token = self.new_face_context()
return Response({'token': token})
def get(self, request, *args, **kwargs):
token = self.request.session.get(self.face_token_session_key)
cache_key = self.get_face_cache_key(token)
context = cache.get(cache_key)
if not context:
raise NotFound({'error': "Token does not exist or has expired."})
return Response({
"is_finished": context.get('is_finished', False),
"success": context.get('success', False),
"error_message": _(context.get("error_message", ''))
})
class FaceMonitorContext:
def __init__(self, token, user_id, session_ids=None):
self.token = token
self.user_id = user_id
if session_ids is None:
self.session_ids = []
else:
self.session_ids = session_ids
@classmethod
def get_cache_key(cls, token):
return 'FACE_MONITOR_CONTEXT_{}'.format(token)
@classmethod
def get_or_create_context(cls, token, user_id):
context = cls.get(token)
if not context:
context = FaceMonitorContext(token=token,
user_id=user_id)
context.save()
return context
def add_session(self, session_id):
self.session_ids.append(session_id)
self.save()
@classmethod
def get(cls, token):
cache_key = cls.get_cache_key(token)
return cache.get(cache_key, None)
def save(self):
cache_key = self.get_cache_key(self.token)
cache.set(cache_key, self)
def close(self):
self.terminal_sessions()
self._destroy()
def _destroy(self):
cache_key = self.get_cache_key(self.token)
cache.delete(cache_key)
def pause_sessions(self):
self._send_task('lock_session')
def resume_sessions(self):
self._send_task('unlock_session')
def terminal_sessions(self):
self._send_task("kill_session")
def _send_task(self, task_name):
create_sessions_tasks(self.session_ids, 'facelive', task_name=task_name)
class FaceMonitorContextApi(CreateAPIView):
permission_classes = (IsServiceAccount,)
serializer_class = FaceMonitorContextSerializer
def perform_create(self, serializer):
face_monitor_token = serializer.validated_data.get('face_monitor_token')
session_id = serializer.validated_data.get('session_id')
context = FaceMonitorContext.get(face_monitor_token)
context.add_session(session_id)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
return Response(status=201)
class FaceMonitorCallbackApi(CreateAPIView):
permission_classes = (IsServiceAccount,)
serializer_class = FaceMonitorCallbackSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
token = serializer.validated_data.get('token')
context = FaceMonitorContext.get(token=token)
is_finished = serializer.validated_data.get('is_finished')
if is_finished:
context.close()
return Response(status=200)
action = serializer.validated_data.get('action')
if action == FaceMonitorActionChoices.Verify:
user = get_object_or_none(User, pk=context.user_id)
face_codes = serializer.validated_data.get('face_codes')
if not user:
context.save()
return Response(data={'msg': 'user {} not found'
.format(context.user_id)}, status=400)
if not face_codes or not self._check_face_codes(face_codes, user):
context.save()
return Response(data={'msg': 'face codes not matched'}, status=400)
if action == FaceMonitorActionChoices.Pause:
context.pause_sessions()
if action == FaceMonitorActionChoices.Resume:
context.resume_sessions()
context.save()
return Response(status=200)
@staticmethod
def _check_face_codes(face_codes, user):
matched = False
for face_code in face_codes:
matched = user.check_face(face_code,
distance_threshold=0.45,
similarity_threshold=0.92)
if matched:
break
return matched

View File

@@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
#
import uuid
from django.core.cache import cache
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from rest_framework import exceptions
@@ -10,15 +7,12 @@ from rest_framework.generics import CreateAPIView, RetrieveAPIView
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.exceptions import NotFound
from common.exceptions import JMSException, UnexpectError
from common.permissions import WithBootstrapToken, IsServiceAccount
from common.utils import get_logger
from users.models.user import User
from .. import errors
from .. import serializers
from ..const import MFA_FACE_CONTEXT_CACHE_KEY_PREFIX, MFA_FACE_SESSION_KEY, MFA_FACE_CONTEXT_CACHE_TTL
from ..errors import SessionEmptyError
from ..mixins import AuthMixin
@@ -26,101 +20,9 @@ logger = get_logger(__name__)
__all__ = [
'MFAChallengeVerifyApi', 'MFASendCodeApi',
'MFAFaceCallbackApi', 'MFAFaceContextApi'
]
class MFAFaceCallbackApi(AuthMixin, CreateAPIView):
permission_classes = (IsServiceAccount,)
serializer_class = serializers.MFAFaceCallbackSerializer
def perform_create(self, serializer):
token = serializer.validated_data.get('token')
context = self._get_context_from_cache(token)
if not serializer.validated_data.get('success', False):
self._update_context_with_error(
context,
serializer.validated_data.get('error_message', 'Unknown error')
)
return Response(status=200)
face_code = serializer.validated_data.get('face_code')
if not face_code:
self._update_context_with_error(context, "missing field 'face_code'")
raise ValidationError({'error': "missing field 'face_code'"})
self._handle_success(context, face_code)
return Response(status=200)
@staticmethod
def get_face_cache_key(token):
return f"{MFA_FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
def _get_context_from_cache(self, token):
cache_key = self.get_face_cache_key(token)
context = cache.get(cache_key)
if not context:
raise ValidationError({'error': "token not exists or expired"})
return context
def _update_context_with_error(self, context, error_message):
context.update({
'is_finished': True,
'success': False,
'error_message': error_message,
})
self._update_cache(context)
def _update_cache(self, context):
cache_key = self.get_face_cache_key(context['token'])
cache.set(cache_key, context, MFA_FACE_CONTEXT_CACHE_TTL)
def _handle_success(self, context, face_code):
context.update({
'is_finished': True,
'success': True,
'face_code': face_code
})
self._update_cache(context)
class MFAFaceContextApi(AuthMixin, RetrieveAPIView, CreateAPIView):
permission_classes = (AllowAny,)
face_token_session_key = MFA_FACE_SESSION_KEY
@staticmethod
def get_face_cache_key(token):
return f"{MFA_FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
def new_face_context(self):
token = uuid.uuid4().hex
cache_key = self.get_face_cache_key(token)
face_context = {
"token": token,
"is_finished": False
}
cache.set(cache_key, face_context, MFA_FACE_CONTEXT_CACHE_TTL)
self.request.session[self.face_token_session_key] = token
return token
def post(self, request, *args, **kwargs):
token = self.new_face_context()
return Response({'token': token})
def get(self, request, *args, **kwargs):
token = self.request.session.get('mfa_face_token')
cache_key = self.get_face_cache_key(token)
context = cache.get(cache_key)
if not context:
raise NotFound({'error': "Token does not exist or has expired."})
return Response({
"is_finished": context.get('is_finished', False),
"success": context.get('success', False),
"error_message": context.get("error_message", '')
})
# MFASelectAPi 原来的名字

View File

@@ -23,10 +23,9 @@ class JMSBaseAuthBackend:
Reject users with is_valid=False. Custom user models that don't have
that attribute are allowed.
"""
# 在 check_user_auth 中进行了校验,可以返回对应的错误信息
# is_valid = getattr(user, 'is_valid', None)
# return is_valid or is_valid is None
return True
# 三方用户认证完成后,在后续的 get_user 获取逻辑中,也应该需要检查用户是否有效
is_valid = getattr(user, 'is_valid', None)
return is_valid or is_valid is None
# allow user to authenticate
def username_allow_authenticate(self, username):
@@ -52,6 +51,14 @@ class JMSBaseAuthBackend:
logger.info(info)
return allow
def get_user(self, user_id):
""" 三方用户认证成功后 request.user 赋值时会调用 backend 的当前方法获取用户 """
try:
user = UserModel._default_manager.get(pk=user_id)
except UserModel.DoesNotExist:
return None
return user if self.user_can_authenticate(user) else None
class JMSModelBackend(JMSBaseAuthBackend, ModelBackend):
pass

View File

@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
#
from django.urls import path
import django_cas_ng.views
from django.urls import path
from .views import CASLoginView
from .views import CASLoginView, CASCallbackClientView
urlpatterns = [
path('login/', CASLoginView.as_view(), name='cas-login'),
path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'),
path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'),
path('login/client', CASCallbackClientView.as_view(), name='cas-proxy-callback-client'),
]

View File

@@ -1,9 +1,12 @@
from django_cas_ng.views import LoginView
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.views.generic import View
from django_cas_ng.views import LoginView
__all__ = ['LoginView']
from authentication.views.utils import redirect_to_guard_view
class CASLoginView(LoginView):
def get(self, request):
@@ -13,3 +16,8 @@ class CASLoginView(LoginView):
return HttpResponseRedirect('/')
class CASCallbackClientView(View):
http_method_names = ['get', ]
def get(self, request):
return redirect_to_guard_view(query_string='next=client')

View File

@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from authentication.signals import user_auth_failed, user_auth_success
from common.utils import get_logger
from .base import JMSModelBackend
from .base import JMSBaseAuthBackend
logger = get_logger(__file__)
@@ -20,9 +20,10 @@ if settings.AUTH_CUSTOM:
logger.warning('Import custom auth method failed: {}, Maybe not enabled'.format(e))
class CustomAuthBackend(JMSModelBackend):
class CustomAuthBackend(JMSBaseAuthBackend):
def is_enabled(self):
@staticmethod
def is_enabled():
return settings.AUTH_CUSTOM and callable(custom_authenticate_method)
@staticmethod
@@ -35,10 +36,10 @@ class CustomAuthBackend(JMSModelBackend):
)
return user, created
def authenticate(self, request, username=None, password=None, **kwargs):
def authenticate(self, request, username=None, password=None):
try:
userinfo: dict = custom_authenticate_method(
username=username, password=password, **kwargs
username=username, password=password
)
user, created = self.get_or_create_user_from_userinfo(userinfo)
except Exception as e:

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
import base64
import requests
from django.utils.translation import gettext_lazy as _
@@ -17,7 +18,7 @@ from common.exceptions import JMSException
from .signals import (
oauth2_create_or_update_user
)
from ..base import JMSModelBackend
from ..base import JMSBaseAuthBackend
__all__ = ['OAuth2Backend']
@@ -25,7 +26,7 @@ __all__ = ['OAuth2Backend']
logger = get_logger(__name__)
class OAuth2Backend(JMSModelBackend):
class OAuth2Backend(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_OAUTH2
@@ -67,15 +68,7 @@ class OAuth2Backend(JMSModelBackend):
response_data = response_data['data']
return response_data
@staticmethod
def get_query_dict(response_data, query_dict):
query_dict.update({
'uid': response_data.get('uid', ''),
'access_token': response_data.get('access_token', '')
})
return query_dict
def authenticate(self, request, code=None, **kwargs):
def authenticate(self, request, code=None):
log_prompt = "Process authenticate [OAuth2Backend]: {}"
logger.debug(log_prompt.format('Start'))
if code is None:
@@ -83,29 +76,31 @@ class OAuth2Backend(JMSModelBackend):
return None
query_dict = {
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'grant_type': 'authorization_code', 'code': code,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
if '?' in settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT:
separator = '&'
else:
separator = '?'
separator = '&' if '?' in settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT else '?'
access_token_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, separator=separator, query=urlencode(query_dict)
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT,
separator=separator, query=urlencode(query_dict)
)
# token_method -> get, post(post_data), post_json
token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
encoded_credentials = base64.b64encode(
f"{settings.AUTH_OAUTH2_CLIENT_ID}:{settings.AUTH_OAUTH2_CLIENT_SECRET}".encode()
).decode()
headers = {
'Accept': 'application/json'
'Accept': 'application/json', 'Authorization': f'Basic {encoded_credentials}'
}
if token_method.startswith('post'):
body_key = 'json' if token_method.endswith('json') else 'data'
query_dict.update({
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
})
access_token_response = requests.post(
access_token_url, headers=headers, **{body_key: query_dict}
)
@@ -121,22 +116,12 @@ class OAuth2Backend(JMSModelBackend):
logger.error(log_prompt.format(error))
return None
query_dict = self.get_query_dict(response_data, query_dict)
headers = {
'Accept': 'application/json',
'Authorization': 'Bearer {}'.format(response_data.get('access_token', ''))
}
logger.debug(log_prompt.format('Get userinfo endpoint'))
if '?' in settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT:
separator = '&'
else:
separator = '?'
userinfo_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT, separator=separator,
query=urlencode(query_dict)
)
userinfo_url = settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT
userinfo_response = requests.get(userinfo_url, headers=headers)
try:
userinfo_response.raise_for_status()

View File

@@ -4,9 +4,9 @@ from django.urls import path
from . import views
urlpatterns = [
path('login/', views.OAuth2AuthRequestView.as_view(), name='login'),
path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback'),
path('callback/client/', views.OAuth2AuthCallbackClientView.as_view(), name='login-callback-client'),
path('logout/', views.OAuth2EndSessionView.as_view(), name='logout')
]

View File

@@ -1,16 +1,16 @@
from django.views import View
from django.conf import settings
from django.contrib import auth
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.http import urlencode
from django.views import View
from authentication.mixins import authenticate
from authentication.utils import build_absolute_uri
from authentication.views.mixins import FlashMessageMixin
from authentication.mixins import authenticate
from authentication.views.utils import redirect_to_guard_view
from common.utils import get_logger
logger = get_logger(__file__)
@@ -67,6 +67,13 @@ class OAuth2AuthCallbackView(View, FlashMessageMixin):
return HttpResponseRedirect(redirect_url)
class OAuth2AuthCallbackClientView(View):
http_method_names = ['get', ]
def get(self, request):
return redirect_to_guard_view(query_string='next=client')
class OAuth2EndSessionView(View):
http_method_names = ['get', 'post', ]

View File

@@ -13,10 +13,8 @@ import requests
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import SuspiciousOperation
from django.db import transaction
from django.urls import reverse
from rest_framework.exceptions import ParseError
from authentication.signals import user_auth_success, user_auth_failed
from authentication.utils import build_absolute_uri_for_oidc
@@ -88,7 +86,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
"""
@ssl_verification
def authenticate(self, request, nonce=None, code_verifier=None, **kwargs):
def authenticate(self, request, nonce=None, code_verifier=None):
""" Authenticates users in case of the OpenID Connect Authorization code flow. """
log_prompt = "Process authenticate [OIDCAuthCodeBackend]: {}"
logger.debug(log_prompt.format('start'))
@@ -107,7 +105,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
# parameters because we won't be able to get a valid token for the user in that case.
if (state is None and settings.AUTH_OPENID_USE_STATE) or code is None:
logger.debug(log_prompt.format('Authorization code or state value is missing'))
raise SuspiciousOperation('Authorization code or state value is missing')
return
# Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
@@ -165,7 +163,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
error = "Json token response error, token response " \
"content is: {}, error is: {}".format(token_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
return
# Validates the token.
logger.debug(log_prompt.format('Validate ID Token'))
@@ -206,7 +204,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
error = "Json claims response error, claims response " \
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
return
logger.debug(log_prompt.format('Get or create user from claims'))
user, created = self.get_or_create_user_from_claims(request, claims)
@@ -235,15 +233,15 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
class OIDCAuthPasswordBackend(OIDCBaseBackend):
@ssl_verification
def authenticate(self, request, username=None, password=None, **kwargs):
def authenticate(self, request, username=None, password=None):
try:
return self._authenticate(request, username, password, **kwargs)
return self._authenticate(request, username, password)
except Exception as e:
error = f'Authenticate exception: {e}'
logger.error(error, exc_info=True)
return
def _authenticate(self, request, username=None, password=None, **kwargs):
def _authenticate(self, request, username=None, password=None):
"""
https://oauth.net/2/
https://aaronparecki.com/oauth-2-simplified/#password

View File

@@ -4,7 +4,9 @@
import warnings
import contextlib
import requests
import inspect
from functools import wraps
from django.conf import settings
from urllib3.exceptions import InsecureRequestWarning
@@ -52,6 +54,7 @@ def no_ssl_verification():
def ssl_verification(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not settings.AUTH_OPENID_IGNORE_SSL_VERIFICATION:
return func(*args, **kwargs)

View File

@@ -12,9 +12,9 @@ from django.urls import path
from . import views
urlpatterns = [
path('login/', views.OIDCAuthRequestView.as_view(), name='login'),
path('callback/', views.OIDCAuthCallbackView.as_view(), name='login-callback'),
path('callback/client/', views.OIDCAuthCallbackClientView.as_view(), name='login-callback-client'),
path('logout/', views.OIDCEndSessionView.as_view(), name='logout'),
]

View File

@@ -22,13 +22,14 @@ from django.http import HttpResponseRedirect, QueryDict
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.http import urlencode
from django.views.generic import View
from django.utils.translation import gettext_lazy as _
from django.views.generic import View
from authentication.utils import build_absolute_uri_for_oidc
from authentication.views.mixins import FlashMessageMixin
from common.utils import safe_next_url
from .utils import get_logger
from ...views.utils import redirect_to_guard_view
logger = get_logger(__file__)
@@ -208,6 +209,13 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
return HttpResponseRedirect(settings.AUTH_OPENID_AUTHENTICATION_FAILURE_REDIRECT_URI)
class OIDCAuthCallbackClientView(View):
http_method_names = ['get', ]
def get(self, request):
return redirect_to_guard_view(query_string='next=client')
class OIDCEndSessionView(View):
""" Allows to end the session of any user authenticated using OpenID Connect.

View File

@@ -13,7 +13,7 @@ class Passkey(JMSBaseModel):
added_on = models.DateTimeField(auto_now_add=True, verbose_name=_("Added on"))
date_last_used = models.DateTimeField(null=True, default=None, verbose_name=_("Date last used"))
credential_id = models.CharField(max_length=255, unique=True, null=False, verbose_name=_("Credential ID"))
token = models.CharField(max_length=255, null=False, verbose_name=_("Token"))
token = models.CharField(max_length=1024, null=False, verbose_name=_("Token"))
def __str__(self):
return self.name

View File

@@ -51,10 +51,10 @@ class RadiusBaseBackend(CreateUserMixin, JMSBaseAuthBackend):
class RadiusBackend(RadiusBaseBackend, RADIUSBackend):
def authenticate(self, request, username='', password='', **kwargs):
def authenticate(self, request, username='', password=''):
return super().authenticate(request, username=username, password=password)
class RadiusRealmBackend(RadiusBaseBackend, RADIUSRealmBackend):
def authenticate(self, request, username='', password='', realm=None, **kwargs):
def authenticate(self, request, username='', password='', realm=None):
return super().authenticate(request, username=username, password=password, realm=realm)

View File

@@ -10,14 +10,14 @@ from .signals import (
saml2_create_or_update_user
)
from authentication.signals import user_auth_failed, user_auth_success
from ..base import JMSModelBackend
from ..base import JMSBaseAuthBackend
__all__ = ['SAML2Backend']
logger = get_logger(__name__)
class SAML2Backend(JMSModelBackend):
class SAML2Backend(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_SAML2
@@ -42,7 +42,7 @@ class SAML2Backend(JMSModelBackend):
)
return user, created
def authenticate(self, request, saml_user_data=None, **kwargs):
def authenticate(self, request, saml_user_data=None):
log_prompt = "Process authenticate [SAML2Backend]: {}"
logger.debug(log_prompt.format('Start'))
if saml_user_data is None:

View File

@@ -4,10 +4,10 @@ from django.urls import path
from . import views
urlpatterns = [
path('login/', views.Saml2AuthRequestView.as_view(), name='saml2-login'),
path('logout/', views.Saml2EndSessionView.as_view(), name='saml2-logout'),
path('callback/', views.Saml2AuthCallbackView.as_view(), name='saml2-callback'),
path('callback/client/', views.Saml2AuthCallbackClientView.as_view(), name='saml2-callback-client'),
path('metadata/', views.Saml2AuthMetadataView.as_view(), name='saml2-metadata'),
]

View File

@@ -19,6 +19,7 @@ from onelogin.saml2.idp_metadata_parser import (
from authentication.views.mixins import FlashMessageMixin
from common.utils import get_logger
from .settings import JmsSaml2Settings
from ...views.utils import redirect_to_guard_view
logger = get_logger(__file__)
@@ -298,6 +299,13 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin, FlashMessageMixin):
return super().dispatch(*args, **kwargs)
class Saml2AuthCallbackClientView(View):
http_method_names = ['get', ]
def get(self, request):
return redirect_to_guard_view(query_string='next=client')
class Saml2AuthMetadataView(View, PrepareRequestMixin):
def get(self, request):

View File

@@ -1,57 +1,41 @@
from django.conf import settings
from .base import JMSModelBackend
from .base import JMSBaseAuthBackend
class SSOAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
class SSOAuthentication(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_SSO
def authenticate(self, request, sso_token=None, **kwargs):
def authenticate(self):
pass
class WeComAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
class WeComAuthentication(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_WECOM
def authenticate(self, request, **kwargs):
def authenticate(self):
pass
class DingTalkAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
class DingTalkAuthentication(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_DINGTALK
def authenticate(self, request, **kwargs):
def authenticate(self):
pass
class FeiShuAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
class FeiShuAuthentication(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_FEISHU
def authenticate(self, request, **kwargs):
def authenticate(self):
pass
@@ -61,23 +45,15 @@ class LarkAuthentication(FeiShuAuthentication):
return settings.AUTH_LARK
class SlackAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
class SlackAuthentication(JMSBaseAuthBackend):
@staticmethod
def is_enabled():
return settings.AUTH_SLACK
def authenticate(self, request, **kwargs):
def authenticate(self):
pass
class AuthorizationTokenAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
class AuthorizationTokenAuthentication(JMSBaseAuthBackend):
def authenticate(self):
pass

View File

@@ -3,13 +3,17 @@ from django.conf import settings
from django.core.exceptions import PermissionDenied
from authentication.models import TempToken
from .base import JMSModelBackend
from .base import JMSBaseAuthBackend
class TempTokenAuthBackend(JMSModelBackend):
class TempTokenAuthBackend(JMSBaseAuthBackend):
model = TempToken
def authenticate(self, request, username='', password='', *args, **kwargs):
@staticmethod
def is_enabled():
return settings.AUTH_TEMP_TOKEN
def authenticate(self, request, username='', password=''):
token = self.model.objects.filter(username=username, secret=password).first()
if not token:
return None
@@ -21,6 +25,3 @@ class TempTokenAuthBackend(JMSModelBackend):
token.save()
return token.user
@staticmethod
def is_enabled():
return settings.AUTH_TEMP_TOKEN

View File

@@ -40,6 +40,12 @@ class MFAType(TextChoices):
Custom = MFACustom.name, MFACustom.display_name
MFA_FACE_CONTEXT_CACHE_KEY_PREFIX = "MFA_FACE_RECOGNITION_CONTEXT"
MFA_FACE_CONTEXT_CACHE_TTL = 60
MFA_FACE_SESSION_KEY = "mfa_face_token"
FACE_CONTEXT_CACHE_KEY_PREFIX = "FACE_CONTEXT"
FACE_CONTEXT_CACHE_TTL = 60
FACE_SESSION_KEY = "face_token"
class FaceMonitorActionChoices(TextChoices):
Verify = 'verify', 'verify'
Pause = 'pause', 'pause'
Resume = 'resume', 'resume'

View File

@@ -1,15 +1,12 @@
from django.core.cache import cache
from authentication.mfa.base import BaseMFA
from django.shortcuts import reverse
from django.utils.translation import gettext_lazy as _
from authentication.mixins import MFAFaceMixin
from authentication.mixins import AuthFaceMixin
from common.const import LicenseEditionChoices
from settings.api import settings
class MFAFace(BaseMFA, MFAFaceMixin):
class MFAFace(BaseMFA, AuthFaceMixin):
name = "face"
display_name = _('Face Recognition')
placeholder = 'Face Recognition'
@@ -33,16 +30,17 @@ class MFAFace(BaseMFA, MFAFaceMixin):
@staticmethod
def global_enabled():
return settings.XPACK_LICENSE_IS_VALID \
and LicenseEditionChoices.ULTIMATE == \
LicenseEditionChoices.from_key(settings.XPACK_LICENSE_EDITION) \
and settings.FACE_RECOGNITION_ENABLED
return (
settings.XPACK_LICENSE_IS_VALID and
settings.XPACK_LICENSE_EDITION_ULTIMATE and
settings.FACE_RECOGNITION_ENABLED
)
def get_enable_url(self) -> str:
return reverse('authentication:user-face-enable')
return '/ui/#/profile/index'
def get_disable_url(self) -> str:
return reverse('authentication:user-face-disable')
return '/ui/#/profile/index'
def disable(self):
assert self.is_authenticated()
@@ -54,4 +52,8 @@ class MFAFace(BaseMFA, MFAFaceMixin):
@staticmethod
def help_text_of_enable():
return _("Frontal Face Recognition")
return _("Bind face to enable")
@staticmethod
def help_text_of_disable():
return _("Unbind face to disable")

View File

@@ -35,7 +35,7 @@ class MFAMiddleware:
# 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
white_urls = [
'login/mfa', 'mfa/select', 'mfa/face','jsi18n/', '/static/',
'login/mfa', 'mfa/select', 'face/context','jsi18n/', '/static/',
'/profile/otp', '/logout/',
]
for url in white_urls:

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-12-12 06:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0003_sshkey'),
]
operations = [
migrations.AlterField(
model_name='passkey',
name='token',
field=models.CharField(max_length=1024, verbose_name='Token'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-12-11 02:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0004_alter_passkey_token'),
]
operations = [
migrations.AddField(
model_name='connectiontoken',
name='face_monitor_token',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Face monitor token'),
),
]

View File

@@ -2,6 +2,7 @@
#
import inspect
import time
import uuid
from functools import partial
from typing import Callable
@@ -50,7 +51,7 @@ auth._get_backends = _get_backends
def authenticate(request=None, **credentials):
"""
If the given credentials are valid, return a User object.
之所以 hack 这个 auticate
之所以 hack 这个 authenticate
"""
username = credentials.get('username')
@@ -199,53 +200,6 @@ class AuthPreCheckMixin:
self.raise_credential_error(errors.reason_user_not_exist)
class MFAFaceMixin:
request = None
def get_face_recognition_token(self):
from authentication.const import MFA_FACE_SESSION_KEY
token = self.request.session.get(MFA_FACE_SESSION_KEY)
if not token:
raise ValueError("Face recognition token is missing from the session.")
return token
@staticmethod
def get_face_cache_key(token):
from authentication.const import MFA_FACE_CONTEXT_CACHE_KEY_PREFIX
return f"{MFA_FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
def get_face_recognition_context(self):
token = self.get_face_recognition_token()
cache_key = self.get_face_cache_key(token)
context = cache.get(cache_key)
if not context:
raise ValueError(f"Face recognition context does not exist for token: {token}")
return context
@staticmethod
def is_context_finished(context):
return context.get('is_finished', False)
@staticmethod
def is_context_success(context):
return context.get('success', False)
def get_face_code(self):
context = self.get_face_recognition_context()
if not self.is_context_finished(context):
raise RuntimeError("Face recognition is not yet completed.")
if not self.is_context_success(context):
msg = context.get('error_message', '')
raise RuntimeError(msg)
face_code = context.get('face_code')
if not face_code:
raise ValueError("Face code is missing from the context.")
return face_code
class MFAMixin:
request: Request
get_user_from_session: Callable
@@ -475,17 +429,83 @@ class AuthACLMixin:
return ticket
class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPostCheckMixin):
class AuthFaceMixin:
request: Request
@staticmethod
def _get_face_cache_key(token):
from authentication.const import FACE_CONTEXT_CACHE_KEY_PREFIX
return f"{FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
@staticmethod
def _is_context_finished(context):
return context.get('is_finished', False)
@staticmethod
def _is_context_success(context):
return context.get('success', False)
def create_face_verify_context(self, data=None):
token = uuid.uuid4().hex
context_data = {
"action": "mfa",
"token": token,
"user_id": self.request.user.id,
"is_finished": False
}
if data:
context_data.update(data)
cache_key = self._get_face_cache_key(token)
from .const import FACE_CONTEXT_CACHE_TTL, FACE_SESSION_KEY
cache.set(cache_key, context_data, FACE_CONTEXT_CACHE_TTL)
self.request.session[FACE_SESSION_KEY] = token
return token
def get_face_token_from_session(self):
from authentication.const import FACE_SESSION_KEY
token = self.request.session.get(FACE_SESSION_KEY)
if not token:
raise ValueError("Face recognition token is missing from the session.")
return token
def get_face_verify_context(self):
token = self.get_face_token_from_session()
cache_key = self._get_face_cache_key(token)
context = cache.get(cache_key)
if not context:
raise ValueError(f"Face recognition context does not exist for token: {token}")
return context
def get_face_code(self):
context = self.get_face_verify_context()
if not self._is_context_finished(context):
raise RuntimeError("Face recognition is not yet completed.")
if not self._is_context_success(context):
msg = context.get('error_message', '')
raise RuntimeError(msg)
face_code = context.get('face_code')
if not face_code:
raise ValueError("Face code is missing from the context.")
return face_code
class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, AuthFaceMixin, MFAMixin, AuthPostCheckMixin, ):
request = None
partial_credential_error = None
key_prefix_captcha = "_LOGIN_INVALID_{}"
def _check_auth_user_is_valid(self, username, password, public_key):
user = authenticate(
self.request, username=username,
password=password, public_key=public_key
)
credentials = {'username': username}
if password:
credentials['password'] = password
if public_key:
credentials['public_key'] = public_key
user = authenticate(self.request, **credentials)
if not user:
self.raise_credential_error(errors.reason_password_failed)

View File

@@ -50,6 +50,7 @@ class ConnectionToken(JMSOrgBaseModel):
on_delete=models.SET_NULL, null=True, blank=True,
verbose_name=_('From ticket')
)
face_monitor_token = models.CharField(max_length=128, null=True, blank=True, verbose_name=_("Face monitor token"))
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
class Meta:

View File

@@ -4,3 +4,4 @@ from .connection_token import *
from .password_mfa import *
from .ssh_key import *
from .token import *
from .face import *

View File

@@ -148,9 +148,10 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
'platform', 'command_filter_acls', 'protocol',
'domain', 'gateway', 'actions', 'expire_at',
'from_ticket', 'expire_now', 'connect_method',
'connect_options',
'connect_options', 'face_monitor_token'
]
extra_kwargs = {
'face_monitor_token': {'read_only': True},
'value': {'read_only': True},
}

View File

@@ -28,7 +28,7 @@ class ConnectionTokenSerializer(CommonModelSerializer):
'connect_method', 'connect_options', 'protocol', 'actions',
'is_active', 'is_reusable', 'from_ticket', 'from_ticket_info',
'date_expired', 'date_created', 'date_updated', 'created_by',
'updated_by', 'org_id', 'org_name',
'updated_by', 'org_id', 'org_name','face_monitor_token',
]
read_only_fields = [
# 普通 Token 不支持指定 user
@@ -37,6 +37,7 @@ class ConnectionTokenSerializer(CommonModelSerializer):
]
fields = fields_small + read_only_fields
extra_kwargs = {
'face_monitor_token': {'read_only': True},
'from_ticket': {'read_only': True},
'value': {'read_only': True},
'is_expired': {'read_only': True, 'label': _('Is expired')},

View File

@@ -0,0 +1,50 @@
from django.core.validators import RegexValidator
from rest_framework import serializers
__all__ = [
'FaceCallbackSerializer', 'FaceMonitorCallbackSerializer'
]
from authentication.const import FaceMonitorActionChoices
class FaceCallbackSerializer(serializers.Serializer):
token = serializers.CharField(required=True, allow_blank=False)
success = serializers.BooleanField(required=True, allow_null=False)
error_message = serializers.CharField(required=False, allow_null=True, allow_blank=True)
face_code = serializers.CharField(required=False, allow_null=True, allow_blank=True)
def update(self, instance, validated_data):
pass
def create(self, validated_data):
pass
class FaceMonitorContextSerializer(serializers.Serializer):
session_id = serializers.CharField(required=True, allow_null=False, allow_blank=False)
face_monitor_token = serializers.CharField(required=True, allow_blank=False, allow_null=False)
def update(self, instance, validated_data):
pass
def create(self, validated_data):
pass
class FaceMonitorCallbackSerializer(serializers.Serializer):
token = serializers.CharField(required=True, allow_blank=False)
is_finished = serializers.BooleanField(required=True)
success = serializers.BooleanField(required=True)
error_message = serializers.CharField(required=True, allow_blank=True)
action = serializers.ChoiceField(required=True, choices=FaceMonitorActionChoices.choices)
face_codes = serializers.ListField(
required=False, allow_null=True, allow_empty=True,
child=serializers.CharField(),
)
def update(self, instance, validated_data):
pass
def create(self, validated_data):
pass

View File

@@ -8,7 +8,6 @@ from common.serializers.fields import EncryptedField
__all__ = [
'MFAChallengeSerializer', 'MFASelectTypeSerializer',
'PasswordVerifySerializer', 'ResetPasswordCodeSerializer',
'MFAFaceCallbackSerializer'
]
@@ -52,16 +51,3 @@ class MFAChallengeSerializer(serializers.Serializer):
def update(self, instance, validated_data):
pass
class MFAFaceCallbackSerializer(serializers.Serializer):
token = serializers.CharField(required=True, allow_blank=False)
success = serializers.BooleanField(required=True, allow_null=False)
error_message = serializers.CharField(required=False, allow_null=True, allow_blank=True)
face_code = serializers.CharField(required=False, allow_null=True, allow_blank=True)
def update(self, instance, validated_data):
pass
def create(self, validated_data):
pass

View File

@@ -34,7 +34,7 @@
<script>
$(document).ready(function () {
const apiUrl = "{% url 'api-auth:mfa-face-context' %}";
const apiUrl = "{% url 'api-auth:face-context' %}";
const faceCaptureUrl = "/facelive/capture";
let token;
@@ -85,7 +85,7 @@
}
$('#retry_button').on('click', function () {
window.location.href = "{% url 'authentication:login-face-capture' %}";
window.location.href = "{{ request.get_full_path }}";
});
});
</script>

View File

@@ -26,6 +26,12 @@ urlpatterns = [
path('lark/event/subscription/callback/', api.LarkEventSubscriptionCallback.as_view(),
name='lark-event-subscription-callback'),
path('face/callback/', api.FaceCallbackApi.as_view(), name='face-callback'),
path('face/context/', api.FaceContextApi.as_view(), name='face-context'),
path('face-monitor/callback/', api.FaceMonitorCallbackApi.as_view(), name='face-monitor-callback'),
path('face-monitor/context/', api.FaceMonitorContextApi.as_view(), name='face-monitor-context'),
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
@@ -33,8 +39,6 @@ urlpatterns = [
path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'),
path('mfa/select/', api.MFASendCodeApi.as_view(), name='mfa-select'),
path('mfa/send-code/', api.MFASendCodeApi.as_view(), name='mfa-send-code'),
path('mfa/face/callback/', api.MFAFaceCallbackApi.as_view(), name='mfa-face-callback'),
path('mfa/face/context/', api.MFAFaceContextApi.as_view(), name='mfa-face-context'),
path('password/reset-code/', api.UserResetPasswordSendCodeApi.as_view(), name='reset-password-code'),
path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),

View File

@@ -110,6 +110,9 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
msg = e.msg
response = self.get_failed_response(login_url, title=msg, msg=msg)
return response
if redirect_url and 'next=client' in redirect_url:
self.request.META['QUERY_STRING'] += '&next=client'
return self.redirect_to_guard_view()

View File

@@ -175,6 +175,8 @@ class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View):
def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') or reverse('index')
query_string = request.GET.urlencode()
redirect_url = f'{redirect_url}?{query_string}'
next_url = self.get_next_url_from_meta() or reverse('index')
next_url = safe_next_url(next_url, request=request)

View File

@@ -110,6 +110,8 @@ class FeiShuQRLoginView(FeiShuQRMixin, View):
def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') or reverse('index')
query_string = request.GET.urlencode()
redirect_url = f'{redirect_url}?{query_string}'
redirect_uri = reverse(f'authentication:{self.category}-qr-login-callback', external=True)
redirect_uri += '?' + urlencode({
'redirect_url': redirect_url,

View File

@@ -45,69 +45,70 @@ class UserLoginContextMixin:
error_origin: str
def get_support_auth_methods(self):
query_string = self.request.GET.urlencode()
auth_methods = [
{
'name': 'OpenID',
'enabled': settings.AUTH_OPENID,
'url': reverse('authentication:openid:login'),
'url': f"{reverse('authentication:openid:login')}?{query_string}",
'logo': static('img/login_oidc_logo.png'),
'auto_redirect': True # 是否支持自动重定向
},
{
'name': 'CAS',
'enabled': settings.AUTH_CAS,
'url': reverse('authentication:cas:cas-login'),
'url': f"{reverse('authentication:cas:cas-login')}?{query_string}",
'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
},
{
'name': 'SAML2',
'enabled': settings.AUTH_SAML2,
'url': reverse('authentication:saml2:saml2-login'),
'url': f"{reverse('authentication:saml2:saml2-login')}?{query_string}",
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
{
'name': settings.AUTH_OAUTH2_PROVIDER,
'enabled': settings.AUTH_OAUTH2,
'url': reverse('authentication:oauth2:login'),
'url': f"{reverse('authentication:oauth2:login')}?{query_string}",
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
'auto_redirect': True
},
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
'url': reverse('authentication:wecom-qr-login'),
'url': f"{reverse('authentication:wecom-qr-login')}?{query_string}",
'logo': static('img/login_wecom_logo.png'),
},
{
'name': _('DingTalk'),
'enabled': settings.AUTH_DINGTALK,
'url': reverse('authentication:dingtalk-qr-login'),
'url': f"{reverse('authentication:dingtalk-qr-login')}?{query_string}",
'logo': static('img/login_dingtalk_logo.png')
},
{
'name': _('FeiShu'),
'enabled': settings.AUTH_FEISHU,
'url': reverse('authentication:feishu-qr-login'),
'url': f"{reverse('authentication:feishu-qr-login')}?{query_string}",
'logo': static('img/login_feishu_logo.png')
},
{
'name': 'Lark',
'enabled': settings.AUTH_LARK,
'url': reverse('authentication:lark-qr-login'),
'url': f"{reverse('authentication:lark-qr-login')}?{query_string}",
'logo': static('img/login_lark_logo.png')
},
{
'name': _('Slack'),
'enabled': settings.AUTH_SLACK,
'url': reverse('authentication:slack-qr-login'),
'url': f"{reverse('authentication:slack-qr-login')}?{query_string}",
'logo': static('img/login_slack_logo.png')
},
{
'name': _("Passkey"),
'enabled': settings.AUTH_PASSKEY,
'url': reverse('api-auth:passkey-login'),
'url': f"{reverse('api-auth:passkey-login')}?{query_string}",
'logo': static('img/login_passkey.png')
}
]
@@ -220,12 +221,16 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
'redirect_url': redirect_url,
'interval': 3,
'has_cancel': True,
'cancel_url': reverse('authentication:login') + '?admin=1'
'cancel_url': reverse('authentication:login') + f'?admin=1&{query_string}'
}
redirect_url = FlashMessageUtil.gen_message_url(message_data)
return redirect_url
def get(self, request, *args, **kwargs):
next_page = request.GET.get(self.redirect_field_name)
if next_page:
request.session[self.redirect_field_name] = next_page
if request.user.is_staff:
first_login_url = redirect_user_first_login_or_index(
request, self.redirect_field_name
@@ -306,6 +311,7 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
login_url = reverse_lazy('authentication:login')
login_mfa_url = reverse_lazy('authentication:login-mfa')
login_confirm_url = reverse_lazy('authentication:login-wait-confirm')
query_string = True
def format_redirect_url(self, url):
args = self.request.META.get('QUERY_STRING', '')

View File

@@ -2,13 +2,14 @@
#
from __future__ import unicode_literals
from django.views.generic.edit import FormView
from django.shortcuts import redirect, reverse
from django.views.generic.edit import FormView
from common.utils import get_logger
from users.views import UserFaceCaptureView
from .. import forms, errors, mixins
from .utils import redirect_to_guard_view
from .. import forms, errors, mixins
from ..const import MFAType
logger = get_logger(__name__)
@@ -48,7 +49,8 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
self._do_check_user_mfa(code, mfa_type)
user, ip = self.get_user_from_session(), self.get_request_ip()
MFABlockUtils(user.username, ip).clean_failed_count()
return redirect_to_guard_view('mfa_ok')
query_string = self.request.GET.urlencode()
return redirect_to_guard_view('mfa_ok', query_string)
except (errors.MFAFailedError, errors.BlockMFAError) as e:
form.add_error('code', e.msg)
return super().form_invalid(form)

View File

@@ -98,6 +98,8 @@ class SlackQRLoginView(SlackMixin, View):
def get(self, request: Request):
redirect_url = request.GET.get('redirect_url') or reverse('index')
query_string = request.GET.urlencode()
redirect_url = f'{redirect_url}?{query_string}'
redirect_uri = reverse('authentication:slack-qr-login-callback', external=True)
redirect_uri += '?' + urlencode({
'redirect_url': redirect_url,

View File

@@ -1,8 +1,18 @@
# -*- coding: utf-8 -*-
#
from urllib.parse import urlencode, parse_qsl
from django.shortcuts import reverse, redirect
def redirect_to_guard_view(comment=''):
continue_url = reverse('authentication:login-guard') + '?_=' + comment
def redirect_to_guard_view(comment='', query_string=None):
params = {'_': comment}
base_url = reverse('authentication:login-guard')
if query_string:
params.update(dict(parse_qsl(query_string)))
query_string = urlencode(params)
continue_url = f"{base_url}?{query_string}"
return redirect(continue_url)

View File

@@ -22,7 +22,6 @@ from users.views import UserVerifyPasswordView
from .base import BaseLoginCallbackView, BaseBindCallbackView
from .mixins import METAMixin, FlashMessageMixin
logger = get_logger(__file__)
@@ -86,6 +85,8 @@ class WeComQRBindView(WeComQRMixin, View):
def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url')
query_string = request.GET.urlencode()
redirect_url = f'{redirect_url}?{query_string}'
redirect_uri = reverse('authentication:wecom-qr-bind-callback', external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})

View File

@@ -3,6 +3,7 @@ import pycountry
from django.db import models
from django.utils.translation import gettext_lazy as _
from phonenumbers import PhoneMetadata
from common.utils import lazyproperty
ADMIN = 'Admin'
USER = 'User'
@@ -72,7 +73,38 @@ class Language(models.TextChoices):
en = 'en', 'English'
zh_hans = 'zh-hans', '中文(简体)'
zh_hant = 'zh-hant', '中文(繁體)'
jp = 'ja', '日本語',
ja = 'ja', '日本語',
pt_br = 'pt-br', 'Português (Brasil)'
@classmethod
def get_code_mapper(cls):
code_mapper = {code: code for code, name in cls.choices}
code_mapper.update({
'zh': cls.zh_hans.value,
'zh-cn': cls.zh_hans.value,
'zh-tw': cls.zh_hant.value,
'zh-hk': cls.zh_hant.value,
})
return code_mapper
@classmethod
def to_internal_code(cls, code: str, default='en', with_filename=False):
code_mapper = cls.get_code_mapper()
code = code.lower()
code = code_mapper.get(code) or code_mapper.get(default)
if with_filename:
cf_mapper = {
cls.zh_hans.value: 'zh',
}
code = cf_mapper.get(code, code)
code = code.replace('-', '_')
return code
@classmethod
def get_other_codes(cls, code):
code_mapper = cls.get_code_mapper()
other_codes = [other_code for other_code, _code in code_mapper.items() if code == _code]
return other_codes
COUNTRY_CALLING_CODES = get_country_phone_choices()

View File

@@ -16,4 +16,4 @@ POST_CLEAR = 'post_clear'
POST_PREFIX = 'post'
PRE_PREFIX = 'pre'
SKIP_SIGNAL = 'skip_signal'
OP_LOG_SKIP_SIGNAL = 'operate_log_skip_signal'

View File

@@ -17,7 +17,7 @@ from django.db.models import F, ExpressionWrapper, CASCADE
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from ..const.signals import SKIP_SIGNAL
from ..const.signals import OP_LOG_SKIP_SIGNAL
class ChoicesMixin:
@@ -83,7 +83,7 @@ def CASCADE_SIGNAL_SKIP(collector, field, sub_objs, using):
# 级联删除时,操作日志标记不保存,以免用户混淆
try:
for obj in sub_objs:
setattr(obj, SKIP_SIGNAL, True)
setattr(obj, OP_LOG_SKIP_SIGNAL, True)
except:
pass

View File

@@ -73,7 +73,7 @@ known_unauth_urls = [
"/api/v1/authentication/password/reset-code/",
"/api/v1/authentication/login-confirm-ticket/status/",
"/api/v1/authentication/mfa/select/",
"/api/v1/authentication/mfa/face/context/",
"/api/v1/authentication/face/context/",
"/api/v1/authentication/mfa/send-code/",
"/api/v1/authentication/sso/login/",
"/api/v1/authentication/user-session/",

View File

@@ -8,7 +8,7 @@ from rest_framework import serializers
from rest_framework.fields import ChoiceField, empty
from common.db.fields import TreeChoices, JSONManyToManyField as ModelJSONManyToManyField
from common.utils import decrypt_password
from common.utils import decrypt_password, is_uuid
__all__ = [
"ReadableHiddenField",
@@ -127,12 +127,14 @@ class LabelRelatedField(serializers.RelatedField):
if isinstance(data, dict) and (data.get("id") or data.get("pk")):
pk = data.get("id") or data.get("pk")
label = Label.objects.get(pk=pk)
elif is_uuid(data):
label = Label.objects.get(pk=data)
else:
if isinstance(data, dict):
k = data.get("name")
v = data.get("value")
elif isinstance(data, str) and ":" in data:
k, v = data.split(":", 1)
k, v = [x.strip() for x in data.split(":", 1)]
else:
raise serializers.ValidationError(_("Invalid data type"))
label, __ = Label.objects.get_or_create(name=k, value=v, defaults={'name': k, 'value': v})

View File

@@ -1,68 +1,74 @@
# -*- coding: utf-8 -*-
#
import boto3
import os
import boto
import boto.s3.connection
from .base import ObjectStorage
class CEPHStorage(ObjectStorage):
def __init__(self, config):
self.bucket = config.get("BUCKET", None)
self.bucket = config.get("BUCKET", "jumpserver")
self.region = config.get("REGION", None)
self.access_key = config.get("ACCESS_KEY", None)
self.secret_key = config.get("SECRET_KEY", None)
self.hostname = config.get("HOSTNAME", None)
self.port = config.get("PORT", 7480)
if self.hostname and self.access_key and self.secret_key:
self.conn = boto.connect_s3(
aws_access_key_id=self.access_key,
aws_secret_access_key=self.secret_key,
host=self.hostname,
port=self.port,
is_secure=False,
calling_format=boto.s3.connection.OrdinaryCallingFormat(),
)
self.endpoint = config.get("ENDPOINT", None)
try:
self.client = self.conn.get_bucket(bucket_name=self.bucket)
except Exception:
self.client = None
self.client = boto3.client(
's3', region_name=self.region,
aws_access_key_id=self.access_key,
aws_secret_access_key=self.secret_key,
endpoint_url=self.endpoint
)
except ValueError:
pass
def upload(self, src, target):
try:
key = self.client.new_key(target)
key.set_contents_from_filename(src)
return True, None
except Exception as e:
return False, e
def download(self, src, target):
try:
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
key = self.client.get_key(src)
key.get_contents_to_filename(target)
return True, None
except Exception as e:
return False, e
def delete(self, path):
try:
self.client.delete_key(path)
self.client.upload_file(Filename=src, Bucket=self.bucket, Key=target)
return True, None
except Exception as e:
return False, e
def exists(self, path):
try:
return self.client.get_key(path)
except Exception:
self.client.head_object(Bucket=self.bucket, Key=path)
return True
except Exception as e:
return False
def download(self, src, target):
try:
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
self.client.download_file(self.bucket, src, target)
return True, None
except Exception as e:
return False, e
def delete(self, path):
try:
self.client.delete_object(Bucket=self.bucket, Key=path)
return True, None
except Exception as e:
return False, e
def generate_presigned_url(self, path, expire=3600):
try:
return self.client.generate_presigned_url(
ClientMethod='get_object',
Params={'Bucket': self.bucket, 'Key': path},
ExpiresIn=expire,
HttpMethod='GET'), None
except Exception as e:
return False, e
def list_buckets(self):
response = self.client.list_buckets()
buckets = response.get('Buckets', [])
result = [b['Name'] for b in buckets if b.get('Name')]
return result
@property
def type(self):
return 'ceph'

View File

@@ -158,7 +158,10 @@ def is_uuid(seq):
def get_request_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
login_ip = x_forwarded_for[0].split(":")[0]
login_ip = x_forwarded_for[0]
if login_ip.count(':') == 1:
# format: ipv4:port (非标准格式的 X-Forwarded-For)
login_ip = login_ip.split(":")[0]
return login_ip
login_ip = request.META.get('REMOTE_ADDR', '')

View File

@@ -18,9 +18,8 @@ def random_ip():
return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
def random_replace_char(s, chars, length):
def random_replace_char(seq, chars, length):
using_index = set()
seq = list(s)
while length > 0:
index = secrets.randbelow(len(seq) - 1)
@@ -29,7 +28,7 @@ def random_replace_char(s, chars, length):
seq[index] = secrets.choice(chars)
using_index.add(index)
length -= 1
return ''.join(seq)
return seq
def remove_exclude_char(s, exclude_chars):
@@ -47,19 +46,40 @@ def random_string(
if length < 4:
raise ValueError('The length of the string must be greater than 3')
chars_map = (
(lower, string.ascii_lowercase),
(upper, string.ascii_uppercase),
(digit, string.digits),
)
chars = ''.join([i[1] for i in chars_map if i[0]])
chars = remove_exclude_char(chars, exclude_chars)
texts = list(secrets.choice(chars) for __ in range(length))
texts = ''.join(texts)
char_list = []
if lower:
lower_chars = remove_exclude_char(string.ascii_lowercase, exclude_chars)
if not lower_chars:
raise ValueError('After excluding characters, no lowercase letters are available.')
char_list.append(lower_chars)
if upper:
upper_chars = remove_exclude_char(string.ascii_uppercase, exclude_chars)
if not upper_chars:
raise ValueError('After excluding characters, no uppercase letters are available.')
char_list.append(upper_chars)
if digit:
digit_chars = remove_exclude_char(string.digits, exclude_chars)
if not digit_chars:
raise ValueError('After excluding characters, no digits are available.')
char_list.append(digit_chars)
secret_chars = [secrets.choice(chars) for chars in char_list]
all_chars = ''.join(char_list)
remaining_length = length - len(secret_chars)
seq = [secrets.choice(all_chars) for _ in range(remaining_length)]
# 控制一下特殊字符的数量, 别随机出来太多
if special_char:
symbols = remove_exclude_char(symbols, exclude_chars)
special_chars = remove_exclude_char(symbols, exclude_chars)
if not special_chars:
raise ValueError('After excluding characters, no special characters are available.')
symbol_num = length // 16 + 1
texts = random_replace_char(texts, symbols, symbol_num)
return texts
seq = random_replace_char(seq, symbols, symbol_num)
secret_chars += seq
secrets.SystemRandom().shuffle(secret_chars)
return ''.join(secret_chars)

View File

@@ -1,6 +1,7 @@
#
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache
from django.views.generic.base import TemplateView
@@ -14,11 +15,11 @@ class FlashMessageMsgView(TemplateView):
def get(self, request, *args, **kwargs):
code = request.GET.get('code')
if not code:
return HttpResponse('Not found the code')
return HttpResponse(_('Not found the code'))
message_data = FlashMessageUtil.get_message_by_code(code)
if not message_data:
return HttpResponse('Message code error')
return HttpResponse(_('The message code provided is invalid or has expired'))
items = ('title', 'message', 'error', 'redirect_url', 'confirm_button', 'cancel_url')
title, msg, error, redirect_url, confirm_btn, cancel_url = bulk_get(message_data, items)

View File

@@ -11,8 +11,8 @@ class BaseTranslateManager:
SEPARATOR = "<SEP>"
LANG_MAPPER = {
'ja': 'Japanese',
'zh_hant': 'Traditional Chinese',
# 'en': 'English',
'zh_Hant': 'Traditional Chinese',
'pt_BR': 'Portuguese (Brazil)',
}
def __init__(self, dir_path, oai_trans_instance):

View File

@@ -37,7 +37,6 @@ class CoreTranslateManager(BaseTranslateManager):
zh_dict = {entry.msgid: entry.msgstr for entry in po.translated_entries()}
for file_prefix, target_lang in self.LANG_MAPPER.items():
file_prefix = 'zh_Hant' if file_prefix == 'zh_hant' else file_prefix
po_file_path = os.path.join(self._dir, file_prefix, 'LC_MESSAGES', 'django.po')
trans_po = polib.pofile(po_file_path)
need_trans_dict = self.get_need_trans_dict(zh_dict, trans_po)

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