Compare commits

...

134 Commits
v4.2.0 ... v4.4

Author SHA1 Message Date
Eric
eea34e6032 perf: add dbeaver-patch version 2024-12-03 17:27:44 +08:00
Bai
69a366978f fix: login log get ipv6 error 2024-11-29 14:58:56 +08:00
Aaron3S
fb634dca4c fix: Fix the uncaught exception when face capture fails 2024-11-25 10:08:59 +08:00
Bai
b045a64496 fix: libldap2-dev 2024-11-21 22:25:46 +08:00
Bai
77e471022f fix: libldap2-dev 2024-11-21 21:31:10 +08:00
Bryan
25987545db Merge pull request #14511 from jumpserver/dev
v4.4.0
2024-11-21 19:00:35 +08:00
wangruidong
f7313bfcc1 perf: Audits job api disable periodic task 2024-11-21 18:56:16 +08:00
Bai
d2f7376f78 fix: job execution stop failed 2024-11-21 18:38:10 +08:00
wangruidong
6db56eb2aa fix: view ops job celery log no perms 2024-11-21 18:14:45 +08:00
fit2bot
442290703a fix: pyfreerdp verify account, the default value of gateway_args field is wrong (#14490)
* fix: pyfreerdp verify account, the default value of gateway_args field is wrong

* fix: pyfreerdp verify account, the default value of gateway_args field is wrong

---------

Co-authored-by: Ewall555 <a03216@foxmail.com>
2024-11-21 14:26:22 +08:00
feng
e491a724ed perf: Video player download 2024-11-21 14:25:50 +08:00
feng
230924baac fix: Vault proxy 2024-11-21 13:40:33 +08:00
wangruidong
0ae2f04f28 fix: view ops job celery log no perms 2024-11-21 13:24:29 +08:00
feng
68a490d305 perf: Hide azure vault 2024-11-21 13:02:40 +08:00
wangruidong
6abfeee683 feat: Add periodic display and validate job params 2024-11-20 22:07:56 +08:00
Aaron3S
1a03f7b265 feat: add license edition check 2024-11-20 20:09:11 +08:00
feng
2dae2b3789 perf: Translate 2024-11-20 18:21:31 +08:00
Aaron3S
bdbbebab76 feat: perf face capture page 2024-11-20 17:54:27 +08:00
Chenyang Shen
33170887f4 Merge pull request #14495 from jumpserver/pr@dev@feat_add_check_api_white_list
feat: add 'face_context' to check_api white list
2024-11-20 17:52:06 +08:00
Aaron3S
88302c8846 feat: add 'face_context' to check_api white list 2024-11-20 16:38:22 +08:00
feng
4068b5c76a perf: Change secret ssh_key_change_strategy modify the default value 2024-11-20 16:27:21 +08:00
feng
9966ad4c71 perf: Dynamic update vault 2024-11-20 15:58:20 +08:00
Aaron3S
9cfe974c52 feat: 添加 mfa middleware 白名单 2024-11-20 14:18:52 +08:00
feng
d9a9f890f5 perf: Lina AzureKeyVault translate 2024-11-20 14:08:27 +08:00
fit2bot
e2904ab042 perf: Custom SMS (files) support obtaining more user information. (#14486)
* perf: Custom SMS (files) support obtaining more user information.

* perf: Remove the useless modules

* perf: modify

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-11-20 10:29:14 +08:00
Aaron3S
f92c557235 feat: 增加人脸识别超时控制 2024-11-20 10:27:04 +08:00
halo
cfadbc164c perf: If the cloud vault initialization fails, the task will not be executed. 2024-11-20 10:15:14 +08:00
feng
374a102bc4 perf: Translate 2024-11-19 18:58:43 +08:00
feng
84e1411c22 fix: Clone endpoint 500 2024-11-19 18:09:00 +08:00
wangruidong
e28bf170d1 perf: MFA Translate 2024-11-19 17:55:11 +08:00
wangruidong
7c9e3a1362 perf: Optimize summary calculation 2024-11-19 17:55:11 +08:00
feng
fba80342a5 perf: Translate 2024-11-19 17:54:45 +08:00
Aaron3S
5eeff0aabf feat: 设置人脸上下文存活时间 2024-11-19 17:34:44 +08:00
Aaron3S
5b4de02fff feat: 增加绑定成功失败提示 2024-11-19 17:30:31 +08:00
wangruidong
b6a5854fa2 perf: Optimize summary calculation 2024-11-19 16:13:38 +08:00
Chenyang Shen
9771d3c817 Merge pull request #14476 from jumpserver/pr@dev@feat_add_face_i18n
FEAT: Add face recognition translation
2024-11-19 15:11:03 +08:00
Aaron3S
b33a0cf0b1 feat: 添加人脸识别翻译 2024-11-19 15:08:39 +08:00
Chenyang Shen
f9fa6ad9c1 Merge pull request #14474 from jumpserver/pr@dev@feat_update_face_capture_page
feat: Optimized the face collection page
2024-11-19 15:01:45 +08:00
Aaron3S
4b2db2b6a1 feat: 优化人脸采集页面 2024-11-19 14:28:31 +08:00
Halo
822b353a40 perf: Translate (#14468)
* feat: azure key vault

* perf: add azure-keyvault-secrets

* perf:azure kv api

* perf: Translate

* perf: Update Dockerfile with new base image tag

* perf: Error when secret is empty

* perf: Translate

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-18 18:48:33 +08:00
feng
2908d4ee5f perf: Delete asset fail 2024-11-18 10:08:37 +08:00
wangruidong
482c4ced0c perf: Translate 2024-11-15 18:00:35 +08:00
halo
b2a5e457a9 fix: vault synchronization task exception 2024-11-15 17:46:17 +08:00
wangruidong
343c3607fa fix: modify job audit rbac 2024-11-15 15:47:20 +08:00
wangruidong
f03263eedf fix: Radius login failed 2024-11-15 15:44:05 +08:00
Aaron3S
98d7ecbf3e fix: 修改错误的url地址 2024-11-13 17:35:43 +08:00
halo
477ccda8ca perf: VAULT_BACKEND cannot be modified from the frontend 2024-11-13 17:31:47 +08:00
wangruidong
fcdc2b9510 fix: Solve audit job and variable bugs 2024-11-13 17:31:17 +08:00
wangruidong
1ee57cfda0 perf: ticket info add org name 2024-11-12 18:15:31 +08:00
wangruidong
804bd289a4 fix: Other people can delete adhoc or playbook 2024-11-12 17:44:21 +08:00
Aaron3S
86273865c8 feat: 增加人脸识别功能 2024-11-12 17:41:39 +08:00
Eric
5142f0340c perf: add license info for component config 2024-11-12 16:52:45 +08:00
Bai
7c80c52d02 fix: Set the default language to en 2024-11-12 15:43:57 +08:00
Bai
eb30b61ca9 fix: Set the default language to en 2024-11-12 15:38:01 +08:00
wangruidong
dd5a272cdf perf: Add task handler for ops job with creator assignment 2024-11-12 15:16:01 +08:00
wangruidong
5b27acf4ef perf: Admin and auditor can view and stop task 2024-11-12 11:25:12 +08:00
Eric
1a41a7450e perf: vnc proxy port to 15900 2024-11-11 19:46:24 +08:00
fit2bot
e1b501c7d4 feat: azure key vault (#14406)
* feat: azure key vault

* perf: add azure-keyvault-secrets

* perf:azure kv api

* perf: Translate

* perf: Update Dockerfile with new base image tag

* perf: Error when secret is empty

* perf: Translate

---------

Co-authored-by: halo <wuyihuangw@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-11 19:41:47 +08:00
Eric
b660bfb7ff perf: add nec vnc component and endpoint migrations 2024-11-11 18:54:52 +08:00
wangruidong
5724912480 perf: Add check for SECURITY_COMMAND_EXECUTION settings in ops tasks 2024-11-11 18:14:43 +08:00
feng
11b3bafd5a perf: Translate 2024-11-08 15:34:37 +08:00
wangruidong
9f90838df1 perf: Optimize username handling in push_account 2024-11-07 10:47:01 +08:00
wangruidong
b01916001e fix: User import fails if MFA field is set to Disabled (0) 2024-11-07 10:45:05 +08:00
fit2bot
c96ae1022b feat: Supports running adhoc,playbook with variable (#14417)
* perf:Create a job that supports adding node parameters

* feat: add variable model

* feat: Modify Variable and AdHoc models,

* feat: Parameters can be set when running job

* feat: Supports setting  variable type

* feat: Supports running adhoc with parameters

* feat: Supports running playbook with parameters

* fix: Translate

* feat: Support setting variables for scheduled tasks

* perf: Translate

---------

Co-authored-by: wangruidong <940853815@qq.com>
2024-11-07 10:38:34 +08:00
jiangweidong
8f11167db0 perf: i18n - Supports automatic release of assets and prevents accidental release of network errors 2024-11-06 15:07:26 +08:00
老广
a53397b76f Update llm-code-review.yml 2024-11-05 18:20:38 +08:00
老广
8f13224454 Create llm-code-review.yml 2024-11-04 18:34:06 +08:00
Bai
8f4dd25e69 feat: DEFAULT_EXPIRED_YEARS put in public settings API 2024-11-01 18:24:54 +08:00
Bai
9c8762e3a0 feat: support configuration DEFAULT_EXPIRED_YEARS 2024-11-01 15:48:57 +08:00
Bai
a8cf788122 feat: add GitHub Action to automatically publish release notes to Discord changelog channel. 2024-11-01 15:24:07 +08:00
Bai
7355a4f152 feat: add GitHub Action to automatically publish release notes to Discord changelog channel. 2024-11-01 14:21:48 +08:00
ibuler
2cf80e6615 perf: login success to call client 2024-10-31 18:36:42 +08:00
ibuler
9a18ed631c fix: oracle platform create error 2024-10-30 16:33:18 +08:00
Bai
1e16f1cb9f fix: console dashboard proportion describe 2024-10-29 19:09:50 +08:00
fit2bot
35b8b080ab perf: add to cron.d (#14375)
* perf: add to cron.d

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 16:15:07 +08:00
ibuler
4219d54db3 perf: add cron in dockerfile 2024-10-29 15:39:54 +08:00
ibuler
c3620254b3 perf: change docker file 2024-10-29 15:32:57 +08:00
fit2bot
d30de0b6a0 perf: update chrome applets hang (#14353)
* perf: update chrome applets hang

* perf: remove debug print

---------

Co-authored-by: Eric <xplzv@126.com>
2024-10-29 15:19:15 +08:00
github-actions[bot]
af91b6faeb perf: Update Dockerfile with new base image tag 2024-10-29 15:18:24 +08:00
ibuler
49b84b019d perf: using poetry mirror 2024-10-29 15:18:24 +08:00
ibuler
a0ee520572 perf: remove cache 2024-10-29 15:18:24 +08:00
fit2bot
972afe0bfe perf: revert old deps (#14371)
* perf: revert old deps

* perf: update poetry

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 14:24:31 +08:00
wangruidong
e47e9b0a11 fix: Unique basename 2024-10-29 11:38:46 +08:00
fit2bot
87e54d8823 perf: add cron (#14364)
* perf: add cron

* Update Dockerfile-base

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 10:56:42 +08:00
jiangweidong
a73c8d8285 fix: Wechat ticket message some url cannot clicked 2024-10-25 15:05:13 +08:00
Eric
b0dd8d044d perf: add error msg when applet task failed 2024-10-24 14:55:33 +08:00
jiangweidong
7c55c42582 perf: Links in WeCom messages can be opened without re-logging in. 2024-10-22 17:02:59 +08:00
fit2bot
cc1fcd2b98 perf: move storage sdk to core (#14318)
* perf: move storage sdk to core

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Bai <baijiangjie@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-22 15:20:10 +08:00
fit2bot
8434d8d5ba perf: update dependency (#14307)
* perf: update dependency

* perf: remove source build

* perf: Update Dockerfile with new base image tag

* perf: use cache build

* perf: Update Dockerfile with new base image tag

* fix: variable incorrectly defined

* perf: Update Dockerfile with new base image tag

* fix: openpyxl fixed version

* perf: Update Dockerfile with new base image tag

* perf: remove cache

* perf: Update Dockerfile with new base image tag

* perf: update pyproject.toml

* perf: Update Dockerfile with new base image tag

* perf: remove cache

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: 吴小白 <296015668@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-22 14:16:19 +08:00
feng
044fd238b8 perf: Remove ssh_key_change_strategy add value 2024-10-21 15:25:38 +08:00
feng
be096a1319 perf: List preference translate 2024-10-18 14:36:13 +08:00
吴小白
6fa14833b3 perf: use python embed 2024-10-18 11:02:49 +08:00
feng
1f32ab274c fix: Error subpub_msg log 2024-10-17 15:17:32 +08:00
Bryan
6720ecc6e0 Merge pull request #14319 from jumpserver/dev
v4.3.0
2024-10-17 14:55:38 +08:00
feng
b0f86e43a6 perf: Translate 2024-10-17 12:05:25 +08:00
ibuler
9b0c81333f perf: debug pub sub 2024-10-17 10:16:44 +08:00
Eric
05fc966444 perf: add koko i18n 2024-10-16 18:25:42 +08:00
Eric
b87650038f perf: update code 2024-10-16 18:11:00 +08:00
wangruidong
d4f69a7ff8 perf: Translate 2024-10-16 17:59:18 +08:00
ibuler
0e1e26c29c perf: disable f1 key 2024-10-16 17:01:10 +08:00
Huaqing Chen
1b8cdbc4dd 修复websocket不能使用Authorization Header的问题 2024-10-15 14:13:38 +08:00
feng
2a781c228f perf: Cas user cannot bind organization 2024-10-15 10:50:20 +08:00
ZhaoJiSen
35d6b0f16a Merge pull request #14299 from jumpserver/pr@dev@change_password_length
perf: Change secret remove redundant checks
2024-10-14 16:45:27 +08:00
feng
ca8987fef6 perf: Change secret remove redundant checks 2024-10-14 16:39:31 +08:00
ZhaoJiSen
b385133071 Merge pull request #14297 from jumpserver/pr@dev@translate
perf: Translate
2024-10-14 16:09:21 +08:00
feng
aa78a03efa perf: Translate 2024-10-14 16:05:38 +08:00
wangruidong
31f8a19392 perf: Translate account history 2024-10-14 15:31:17 +08:00
wangruidong
7a528b499a perf: import data validate platform 2024-10-14 14:05:24 +08:00
Eric
1c6ce422cf perf: update tinker v0.1.9 2024-10-12 16:30:28 +08:00
Eric
f9cf2ea2e5 perf: fix api error when deleting offline panda components 2024-10-12 16:15:23 +08:00
Aaron3S
575b3a617f feat: 添加 chen 翻译 2024-10-12 15:44:38 +08:00
wangruidong
b7362d3f51 fix: adhoc execute alert msg 2024-10-12 15:43:03 +08:00
ZhaoJiSen
6ee3860124 Merge pull request #14287 from jumpserver/pr@dev@translate
perf: Translate
2024-10-12 14:40:23 +08:00
feng
7e111da529 perf: Translate 2024-10-12 14:35:18 +08:00
wangruidong
578458f734 perf: site msg content optimize 2024-10-11 11:28:56 +08:00
Bai
bd56697d6d perf: DEFAULT_PAGE_SIZE same as MAX_LIMIT_PER_PAGE 2024-10-10 18:00:01 +08:00
wangruidong
aad824d127 perf: add created_by field 2024-10-09 16:14:22 +08:00
wangruidong
63f828da0b perf: Default endpoint cannot be disabled 2024-10-09 16:12:37 +08:00
wangruidong
7c211b3fb6 perf: Translate 2024-10-08 15:01:53 +08:00
feng
3881edd2ba perf: Optimize file audit download prompt 2024-09-29 16:12:49 +08:00
feng
b882b12d04 perf: Check the validity of the connection token 2024-09-27 17:10:08 +08:00
wangruidong
addd2e7d1c perf: Endpoint add is_active field 2024-09-27 16:00:05 +08:00
Bai
ad6d2e1cd7 fix: Fixed the issue that the workbench user login log only displays failed logs 2024-09-27 14:34:23 +08:00
github-actions[bot]
5f07271afa perf: Update Dockerfile with new base image tag 2024-09-27 14:30:48 +08:00
Bai
efdcd4c708 perf: upgrade geoip2 and .mmdb 2024-09-27 14:30:48 +08:00
jiangweidong
b62763bca3 perf: Cloud Sync IP Policy Updated to Preferred Option i18n 2024-09-27 14:29:09 +08:00
wangruidong
e95da730f2 perf: Koko can display assets custom name 2024-09-27 14:25:55 +08:00
fit2bot
43fa3f420a fix: Addressing the issue of unauthorized execution of system tools (#14209)
* fix: Addressing the issue of unauthorized execution of system tools

* perf: Optimization conditions

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-09-27 14:17:16 +08:00
wangruidong
0311446384 perf: playbook clone with file 2024-09-27 14:13:35 +08:00
feng
f7030e4fee perf: Login encryption key cache added 2024-09-26 15:11:35 +08:00
ZhaoJiSen
fce8cc375f Merge pull request #14230 from jumpserver/pr@dev@max_password_length
perf: The maximum length of the randomly generated password is changed to 36
2024-09-25 11:00:45 +08:00
feng
920199c6df perf: The maximum length of the randomly generated password is changed to 36 2024-09-25 10:52:16 +08:00
feng
d09eb3c4fa perf: Lock username is not case sensitive 2024-09-23 14:11:55 +08:00
ibuler
6e8affcdd6 perf: ops db migrate 2024-09-19 21:39:55 +08:00
195 changed files with 7183 additions and 4937 deletions

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

@@ -0,0 +1,24 @@
name: Publish Release to Discord
on:
release:
types: [published]
jobs:
send_discord_notification:
runs-on: ubuntu-latest
if: startsWith(github.event.release.tag_name, 'v4.')
steps:
- name: Send release notification to Discord
env:
WEBHOOK_URL: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }}
run: |
# 获取标签名称和 release body
TAG_NAME="${{ github.event.release.tag_name }}"
RELEASE_BODY="${{ github.event.release.body }}"
# 使用 jq 构建 JSON 数据,以确保安全传递
JSON_PAYLOAD=$(jq -n --arg tag "# JumpServer $TAG_NAME Released! 🚀" --arg body "$RELEASE_BODY" '{content: "\($tag)\n\($body)"}')
# 使用 curl 发送 JSON 数据
curl -X POST -H "Content-Type: application/json" -d "$JSON_PAYLOAD" "$WEBHOOK_URL"

28
.github/workflows/llm-code-review.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: LLM Code Review
permissions:
contents: read
pull-requests: write
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
llm-code-review:
runs-on: ubuntu-latest
steps:
- uses: fit2cloud/LLM-CodeReview-Action@main
env:
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
LANGUAGE: English
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL: qwen2-1.5b-instruct
PROMPT: "Please check the following code differences for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
top_p: 1
temperature: 1
# max_tokens: 10000
MAX_PATCH_LENGTH: 10000
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"

View File

@@ -1,4 +1,4 @@
FROM jumpserver/core-base:20240919_024156 AS stage-build
FROM jumpserver/core-base:20241105_025649 AS stage-build
ARG VERSION
@@ -28,6 +28,7 @@ ARG DEPENDENCIES=" \
libx11-dev"
ARG TOOLS=" \
cron \
ca-certificates \
default-libmysqlclient-dev \
openssh-client \
@@ -35,19 +36,20 @@ ARG TOOLS=" \
bubblewrap"
ARG APT_MIRROR=http://deb.debian.org
RUN set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update > /dev/null \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& apt-get clean \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
&& echo "no" | dpkg-reconfigure dash \
&& sed -i "s@# export @export @g" ~/.bashrc \
&& sed -i "s@# alias @alias @g" ~/.bashrc
&& apt-get clean all \
&& rm -rf /var/lib/apt/lists/* \
&& echo "0 3 * * * root find /tmp -type f -mtime +1 -size +1M -exec rm -f {} \; && date > /tmp/clean.log" > /etc/cron.d/cleanup_tmp \
&& chmod 0644 /etc/cron.d/cleanup_tmp
COPY --from=stage-build /opt /opt
COPY --from=stage-build /usr/local/bin /usr/local/bin

View File

@@ -15,8 +15,8 @@ ARG DEPENDENCIES=" \
libldap2-dev \
libsasl2-dev"
ARG APT_MIRROR=http://deb.debian.org
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
set -ex \
@@ -27,9 +27,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& echo "no" | dpkg-reconfigure dash
# Install bin tools
ARG CHECK_VERSION=v1.0.3
ARG CHECK_VERSION=v1.0.4
RUN set -ex \
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
@@ -38,23 +37,24 @@ RUN set -ex \
&& chmod 755 /usr/local/bin/check \
&& rm -f check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz
# Install Python dependencies
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
ENV ANSIBLE_COLLECTIONS_PATHS=/opt/py3/lib/python3.11/site-packages/ansible_collections
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=poetry.lock,target=poetry.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=utils/clean_site_packages.sh,target=clean_site_packages.sh \
--mount=type=bind,source=requirements/collections.yml,target=collections.yml \
set -ex \
&& python3 -m venv /opt/py3 \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& pip install poetry poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \
&& . /opt/py3/bin/activate \
&& poetry install --only main \
&& poetry config virtualenvs.create false \
&& poetry install --no-cache --only main \
&& ansible-galaxy collection install -r collections.yml --force --ignore-certs \
&& bash clean_site_packages.sh
&& bash clean_site_packages.sh \
&& poetry cache clear pypi --all

View File

@@ -15,21 +15,20 @@ ARG TOOLS=" \
vim \
wget"
ARG APT_MIRROR=http://deb.debian.org
RUN set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& echo "no" | dpkg-reconfigure dash
&& apt-get clean all \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
COPY poetry.lock pyproject.toml ./
RUN set -ex \
&& . /opt/py3/bin/activate \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry install --only xpack
&& pip install poetry poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \
&& poetry install --only xpack \
&& poetry cache clear pypi --all

View File

@@ -30,6 +30,6 @@
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
gateway_args: "{{ jms_gateway | default(None) }}"
gateway_args: "{{ jms_gateway | default({}) }}"
when: account.secret_type == "password"
delegate_to: localhost

View File

@@ -30,6 +30,6 @@
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
gateway_args: "{{ jms_gateway | default(None) }}"
gateway_args: "{{ jms_gateway | default({}) }}"
when: account.secret_type == "password"
delegate_to: localhost

View File

@@ -1,3 +1,5 @@
from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes
from assets.automations.ping_gateway.manager import PingGatewayManager
from common.utils import get_logger
@@ -13,7 +15,7 @@ class VerifyGatewayAccountManager(PingGatewayManager):
@staticmethod
def before_runner_start():
logger.info(">>> 开始执行测试网关账号可连接性任务")
logger.info(_(">>> Start executing the task to test gateway account connectivity"))
def get_accounts(self, gateway):
account_ids = self.execution.snapshot['accounts']

View File

@@ -1,19 +1,21 @@
from importlib import import_module
from django.utils.functional import LazyObject
from django.utils.functional import LazyObject, empty
from common.utils import get_logger
from ..const import VaultTypeChoices
__all__ = ['vault_client', 'get_vault_client']
__all__ = ['vault_client', 'get_vault_client', 'refresh_vault_client']
logger = get_logger(__file__)
def get_vault_client(raise_exception=False, **kwargs):
enabled = kwargs.get('VAULT_ENABLED')
tp = 'hcp' if enabled else 'local'
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)
@@ -39,3 +41,7 @@ class VaultClient(LazyObject):
""" 为了安全, 页面修改配置, 重启服务后才会重新初始化 vault_client """
vault_client = VaultClient()
def refresh_vault_client():
vault_client._wrapped = empty

View File

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

View File

@@ -0,0 +1,70 @@
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
@lazyproperty
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):
@property
def path_spec(self):
# 长度 0-127
account_id = str(self.instance.id)[:18]
path = f'assets-{self.instance.asset_id}-accounts-{account_id}'
return path
class AccountTemplateEntry(BaseEntry):
@property
def path_spec(self):
path = f'account-templates-{self.instance.id}'
return path
class HistoricalAccountEntry(BaseEntry):
@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

@@ -0,0 +1,57 @@
from common.db.utils import get_logger
from .entries import build_entry
from .service import AZUREVaultClient
from ..base import BaseVault
from ...const import VaultTypeChoices
logger = get_logger(__name__)
__all__ = ['Vault']
class Vault(BaseVault):
type = VaultTypeChoices.azure
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = AZUREVaultClient(
vault_url=kwargs.get('VAULT_AZURE_HOST'),
tenant_id=kwargs.get('VAULT_AZURE_TENANT_ID'),
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

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
#
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
from common.utils import get_logger
logger = get_logger(__name__)
__all__ = ['AZUREVaultClient']
class AZUREVaultClient(object):
def __init__(self, vault_url, tenant_id, client_id, client_secret):
authentication_endpoint = 'https://login.microsoftonline.com/' \
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
credentials = ClientSecretCredential(
client_id=client_id, client_secret=client_secret, tenant_id=tenant_id, authority=authentication_endpoint
)
self.client = SecretClient(vault_url=vault_url, credential=credentials)
def is_active(self):
try:
self.client.set_secret('jumpserver', '666')
except (ResourceNotFoundError, ClientAuthenticationError) as e:
logger.error(str(e))
return False, f'Vault is not reachable: {e}'
else:
return True, ''
def get(self, name, version=None):
try:
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):
if not secret:
secret = ''
self.client.set_secret(name, secret)
def update(self, name, secret):
if not secret:
secret = ''
self.client.set_secret(name, secret)
def delete(self, name):
self.client.begin_delete_secret(name)
def update_metadata(self, name, metadata: dict):
try:
self.client.update_secret_properties(name, tags=metadata)
except (ResourceNotFoundError, ClientAuthenticationError) as e:
logger.error(f'update_metadata: {name} {str(e)}')

View File

@@ -10,6 +10,11 @@ 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)
@@ -20,9 +25,6 @@ class BaseVault(ABC):
self._clean_db_secret(instance)
self.save_metadata(instance)
if instance.is_sync_metadata:
self.save_metadata(instance)
def update(self, instance):
if not instance.secret_has_save_to_vault:
self._update(instance)

View File

@@ -3,12 +3,16 @@ from .entries import build_entry
from .service import VaultKVClient
from ..base import BaseVault
__all__ = ['Vault']
from ...const import VaultTypeChoices
logger = get_logger(__name__)
__all__ = ['Vault']
class Vault(BaseVault):
type = VaultTypeChoices.hcp
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = VaultKVClient(

View File

@@ -1,5 +1,6 @@
from common.utils import get_logger
from ..base import BaseVault
from ...const import VaultTypeChoices
logger = get_logger(__name__)
@@ -7,6 +8,7 @@ __all__ = ['Vault']
class Vault(BaseVault):
type = VaultTypeChoices.local
def is_active(self):
return True, ''

View File

@@ -49,9 +49,9 @@ class SecretStrategy(models.TextChoices):
class SSHKeyStrategy(models.TextChoices):
add = 'add', _('Append SSH KEY')
set = 'set', _('Empty and append SSH KEY')
set_jms = 'set_jms', _('Replace (Replace only keys pushed by JumpServer) ')
set = 'set', _('Empty and append SSH KEY')
add = 'add', _('Append SSH KEY')
class TriggerChoice(models.TextChoices, TreeChoices):

View File

@@ -7,3 +7,4 @@ __all__ = ['VaultTypeChoices']
class VaultTypeChoices(models.TextChoices):
local = 'local', _('Database')
hcp = 'hcp', _('HCP Vault')
azure = 'azure', _('Azure Key Vault')

View File

@@ -50,7 +50,7 @@ class Migration(migrations.Migration):
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')),
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
('ssh_key_change_strategy', models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy')),
('ssh_key_change_strategy', models.CharField(choices=[('set_jms', 'Replace (Replace only keys pushed by JumpServer) '), ('set', 'Empty and append SSH KEY'), ('add', 'Append SSH KEY')], default='set_jms', max_length=16, verbose_name='SSH key change strategy')),
],
options={
'verbose_name': 'Change secret automation',
@@ -76,7 +76,7 @@ class Migration(migrations.Migration):
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')),
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
('ssh_key_change_strategy', models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy')),
('ssh_key_change_strategy', models.CharField(choices=[('set_jms', 'Replace (Replace only keys pushed by JumpServer) '), ('set', 'Empty and append SSH KEY'), ('add', 'Append SSH KEY')], default='set_jms', max_length=16, verbose_name='SSH key change strategy')),
('triggers', models.JSONField(default=list, max_length=16, verbose_name='Triggers')),
('username', models.CharField(max_length=128, verbose_name='Username')),
('action', models.CharField(max_length=16, verbose_name='Action')),

View File

@@ -53,7 +53,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
on_delete=models.SET_NULL, verbose_name=_("Su from")
)
version = models.IntegerField(default=0, verbose_name=_('Version'))
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'],
verbose_name=_("historical Account"))
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))

View File

@@ -51,7 +51,7 @@ class AutomationExecution(AssetAutomationExecution):
class ChangeSecretMixin(SecretWithRandomMixin):
ssh_key_change_strategy = models.CharField(
choices=SSHKeyStrategy.choices, max_length=16,
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
default=SSHKeyStrategy.set_jms, verbose_name=_('SSH key change strategy')
)
get_all_assets: callable # get all assets

View File

@@ -2,7 +2,7 @@ from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes
from accounts.const import AutomationTypes, SecretType
from accounts.models import Account
from .base import AccountBaseAutomation
from .change_secret import ChangeSecretMixin
@@ -23,7 +23,8 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
create_usernames = set(usernames) - set(account_usernames)
create_account_objs = [
Account(
name=f'{username}-{secret_type}', username=username,
name=f"{username}-{secret_type}" if secret_type != SecretType.PASSWORD else username,
username=username,
secret_type=secret_type, asset=asset,
)
for username in create_usernames

View File

@@ -10,7 +10,7 @@ from .base import BaseAccountSerializer
class PasswordRulesSerializer(serializers.Serializer):
length = serializers.IntegerField(min_value=8, max_value=30, default=16, label=_('Password length'))
length = serializers.IntegerField(min_value=8, max_value=36, default=16, label=_('Password length'))
lowercase = serializers.BooleanField(default=True, label=_('Lowercase'))
uppercase = serializers.BooleanField(default=True, label=_('Uppercase'))
digit = serializers.BooleanField(default=True, label=_('Digit'))

View File

@@ -63,6 +63,26 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
)},
}}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_ssh_key_change_strategy_choices()
def set_ssh_key_change_strategy_choices(self):
ssh_key_change_strategy = self.fields.get("ssh_key_change_strategy")
if not ssh_key_change_strategy:
return
ssh_key_change_strategy._choices.pop(SSHKeyStrategy.add, None)
def to_representation(self, instance):
data = super().to_representation(instance)
ssh_strategy_value = data.get('ssh_key_change_strategy', {}).get('value')
if ssh_strategy_value == SSHKeyStrategy.add:
data['ssh_key_change_strategy'] = {
'label': SSHKeyStrategy.set_jms.label,
'value': SSHKeyStrategy.set_jms.value
}
return data
@property
def model_type(self):
return AutomationTypes.change_secret
@@ -75,19 +95,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
if self.initial_data.get('secret_strategy') == SecretStrategy.custom:
return password_rules
length = password_rules.get('length')
try:
length = int(length)
except Exception as e:
logger.error(e)
msg = _("* Please enter the correct password length")
raise serializers.ValidationError(msg)
if length < 6 or length > 30:
msg = _('* Password length range 6-30 bits')
raise serializers.ValidationError(msg)
return password_rules
def validate(self, attrs):

View File

@@ -3,14 +3,17 @@ from collections import defaultdict
from django.db.models.signals import post_delete
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.utils.functional import LazyObject
from django.utils.translation import gettext_noop
from accounts.backends import vault_client
from accounts.backends import vault_client, refresh_vault_client
from accounts.const import Source
from audits.const import ActivityChoices
from audits.signal_handlers import create_activities
from common.decorators import merge_delay_run
from common.signals import django_ready
from common.utils import get_logger, i18n_fmt
from common.utils.connection import RedisPubSub
from .models import Account, AccountTemplate
from .tasks.push_account import push_accounts_to_assets_task
@@ -91,3 +94,18 @@ class VaultSignalHandler(object):
for model in (Account, AccountTemplate, Account.history.model):
post_save.connect(VaultSignalHandler.save_to_vault, sender=model)
post_delete.connect(VaultSignalHandler.delete_to_vault, sender=model)
class VaultPubSub(LazyObject):
def _setup(self):
self._wrapped = RedisPubSub('refresh_vault')
vault_pub_sub = VaultPubSub()
@receiver(django_ready)
def subscribe_vault_change(sender, **kwargs):
logger.debug("Start subscribe vault change")
vault_pub_sub.subscribe(lambda name: refresh_vault_client())

View File

@@ -5,6 +5,7 @@ from celery import shared_task
from django.utils.translation import gettext_lazy as _
from accounts.backends import vault_client
from accounts.const import VaultTypeChoices
from accounts.models import Account, AccountTemplate
from common.utils import get_logger
from orgs.utils import tmp_to_root_org
@@ -39,6 +40,9 @@ def sync_secret_to_vault():
# 这里不能判断 settings.VAULT_ENABLED, 必须判断当前 vault_client 的类型
print('\033[35m>>> 当前 Vault 功能未开启, 不需要同步')
return
if VaultTypeChoices.local == vault_client.type:
print('\033[31m>>> 当前第三方 Vault 客户端初始化失败,数据存储在本地数据库')
return
failed, skipped, succeeded = 0, 0, 0
to_sync_models = [Account, AccountTemplate, Account.history.model]

View File

@@ -123,6 +123,10 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
NodeFilterBackend, AttrRulesFilterBackend
]
def perform_destroy(self, instance):
instance.accounts.update(su_from_id=None)
instance.delete()
def get_queryset(self):
queryset = super().get_queryset()
if queryset.model is not Asset:

View File

@@ -115,7 +115,7 @@ class PingGatewayManager:
@staticmethod
def before_runner_start():
print(">>> 开始执行测试网关可连接性任务")
print(_(">>> Start executing the task to test gateway connectivity"))
def get_accounts(self, gateway):
account = gateway.select_account

View File

@@ -112,7 +112,7 @@ class BaseType(TextChoices):
@classmethod
def get_choices(cls):
if not settings.XPACK_ENABLED:
if not settings.XPACK_LICENSE_IS_VALID:
choices = [(tp.value, tp.label) for tp in cls.get_community_types()]
else:
choices = cls.choices

View File

@@ -3,6 +3,7 @@ from collections import defaultdict
from copy import deepcopy
from django.conf import settings
from django.utils.functional import lazy
from django.utils.translation import gettext as _
from common.db.models import ChoicesMixin
@@ -29,15 +30,15 @@ class AllTypes(ChoicesMixin):
@classmethod
def choices(cls):
return lazy(cls.get_choices, list)()
@classmethod
def get_choices(cls):
choices = []
for tp in cls.includes:
choices.extend(tp.get_choices())
return choices
@classmethod
def get_choices(cls):
return cls.choices()
@classmethod
def filter_choices(cls, category):
choices = dict(cls.category_types()).get(category, cls).get_choices()

View File

@@ -18,7 +18,7 @@ from common.serializers.fields import LabeledChoiceField
from labels.models import Label
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ...const import Category, AllTypes
from ...models import Asset, Node, Platform, Protocol
from ...models import Asset, Node, Platform, Protocol, Host, Device, Database, Cloud, Web, Custom
__all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
@@ -309,6 +309,17 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
})
return protocols_data_map.values()
def validate_platform(self, platform_data):
check_models = {Host, Device, Database, Cloud, Web, Custom}
if self.Meta.model not in check_models:
return platform_data
model_name = self.Meta.model.__name__.lower()
if model_name != platform_data.category:
raise serializers.ValidationError({
'platform': f"Platform does not match: {platform_data.name}"
})
return platform_data
@staticmethod
def update_account_su_from(accounts, include_su_from_accounts):
if not include_su_from_accounts:

View File

@@ -7,6 +7,7 @@ from django.db.models import F, Value, CharField, Q
from django.db.models.functions import Cast
from django.http import HttpResponse, FileResponse
from django.utils.encoding import escape_uri_path
from django_celery_beat.models import PeriodicTask
from rest_framework import generics
from rest_framework import status
from rest_framework import viewsets
@@ -22,6 +23,9 @@ from common.plugins.es import QuerySet as ESQuerySet
from common.sessions.cache import user_session_manager
from common.storage.ftp_file import FTPFileStorageHandler
from common.utils import is_uuid, get_logger, lazyproperty
from ops.const import Types
from ops.models import Job
from ops.serializers.job import JobSerializer
from orgs.mixins.api import OrgReadonlyModelViewSet, OrgModelViewSet
from orgs.models import Organization
from orgs.utils import current_org, tmp_to_root_org
@@ -39,14 +43,14 @@ from .serializers import (
FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer,
OperateLogSerializer, OperateLogActionDetailSerializer,
PasswordChangeLogSerializer, ActivityUnionLogSerializer,
FileSerializer, UserSessionSerializer
FileSerializer, UserSessionSerializer, JobsAuditSerializer
)
from .utils import construct_userlogin_usernames
logger = get_logger(__name__)
class JobAuditViewSet(OrgReadonlyModelViewSet):
class JobLogAuditViewSet(OrgReadonlyModelViewSet):
model = JobLog
extra_filter_backends = [DatetimeRangeFilterBackend]
date_range_filter_fields = [
@@ -58,6 +62,35 @@ class JobAuditViewSet(OrgReadonlyModelViewSet):
ordering = ['-date_start']
class JobsAuditViewSet(OrgModelViewSet):
model = Job
search_fields = ['creator__name']
filterset_fields = ['creator__name']
serializer_class = JobsAuditSerializer
ordering = ['-is_periodic', '-date_updated']
http_method_names = ['get', 'options', 'patch']
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.exclude(type=Types.upload_file).filter(instant=False)
return queryset
def perform_update(self, serializer):
job = self.get_object()
is_periodic = serializer.validated_data.get('is_periodic')
if job.is_periodic != is_periodic:
job.is_periodic = is_periodic
job.save()
name, task, args, kwargs = job.get_register_task()
task_obj = PeriodicTask.objects.filter(name=name).first()
if task_obj:
is_periodic = job.is_periodic
if task_obj.enabled != is_periodic:
task_obj.enabled = is_periodic
task_obj.save()
return super().perform_update(serializer)
class FTPLogViewSet(OrgModelViewSet):
model = FTPLog
serializer_class = FTPLogSerializer
@@ -146,7 +179,9 @@ class MyLoginLogViewSet(UserLoginCommonMixin, OrgReadonlyModelViewSet):
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(username=self.request.user.username)
username = self.request.user.username
q = Q(username=username) | Q(username__icontains=f'({username})')
qs = qs.filter(q)
return qs

View File

@@ -74,6 +74,9 @@ class OperateLogStore(object):
@classmethod
def convert_diff_friendly(cls, op_log):
diff_list = list()
# 标记翻译字符串
labels = _("labels")
operate_log_id = _("operate_log_id")
handler = cls._get_special_handler(op_log.resource_type)
for k, v in op_log.diff.items():
before_value, after_value = cls.split_value(v)

View File

@@ -7,7 +7,7 @@ from audits.backends.db import OperateLogStore
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import reverse, i18n_trans
from common.utils.timezone import as_current_tz
from ops.serializers.job import JobExecutionSerializer
from ops.serializers.job import JobExecutionSerializer, JobSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from terminal.models import Session
from users.models import User
@@ -34,6 +34,30 @@ class JobLogSerializer(JobExecutionSerializer):
}
class JobsAuditSerializer(JobSerializer):
material = serializers.ReadOnlyField(label=_("Command"))
summary = serializers.ReadOnlyField(label=_("Summary"))
crontab = serializers.ReadOnlyField(label=_("Execution cycle"))
is_periodic_display = serializers.BooleanField(read_only=True, source='is_periodic')
class Meta(JobSerializer.Meta):
read_only_fields = [
"id", 'name', 'args', 'material', 'type', 'crontab', 'interval', 'date_last_run', 'summary', 'created_by',
'is_periodic_display'
]
fields = read_only_fields + ['is_periodic']
def validate(self, attrs):
allowed_fields = {'is_periodic'}
submitted_fields = set(attrs.keys())
invalid_fields = submitted_fields - allowed_fields
if invalid_fields:
raise serializers.ValidationError(
f"Updating {', '.join(invalid_fields)} fields is not allowed"
)
return attrs
class FTPLogSerializer(serializers.ModelSerializer):
operate = LabeledChoiceField(choices=OperateChoices.choices, label=_("Operate"))

View File

@@ -187,7 +187,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'PermedAsset', 'PermedAccount', 'MenuPermission',
'Permission', 'TicketSession', 'ApplyLoginTicket',
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
'FavoriteAsset', 'ChangeSecretRecord'
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable'
}
include_models = {'UserSession'}
for i, app in enumerate(apps.get_models(), 1):

View File

@@ -13,7 +13,9 @@ router.register(r'ftp-logs', api.FTPLogViewSet, 'ftp-log')
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.JobAuditViewSet, 'job-log')
router.register(r'job-logs', api.JobLogAuditViewSet, 'job-log')
router.register(r'jobs', api.JobsAuditViewSet, 'jobs')
router.register(r'my-login-logs', api.MyLoginLogViewSet, 'my-login-log')
router.register(r'user-sessions', api.UserSessionViewSet, 'user-session')

View File

@@ -472,6 +472,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken',
'check': 'authentication.view_superconnectiontoken',
'get_secret_detail': 'authentication.view_superconnectiontokensecret',
'get_applet_info': 'authentication.view_superconnectiontoken',
'release_applet_account': 'authentication.view_superconnectiontoken',
@@ -484,6 +485,28 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
def get_user(self, serializer):
return serializer.validated_data.get('user')
@action(methods=['GET'], detail=True, url_path='check')
def check(self, request, *args, **kwargs):
instance = self.get_object()
data = {
"detail": "OK",
"code": "perm_ok",
"expired": instance.is_expired
}
try:
self._validate_perm(
instance.user,
instance.asset,
instance.account,
instance.protocol
)
except JMSException as e:
data['code'] = e.detail.code
data['detail'] = str(e.detail)
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
return Response(data=data, status=status.HTTP_200_OK)
@action(methods=['PATCH'], detail=False)
def renewal(self, request, *args, **kwargs):
from common.utils.timezone import as_current_tz

View File

@@ -1,29 +1,128 @@
# -*- 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
from rest_framework.generics import CreateAPIView
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
logger = get_logger(__name__)
__all__ = [
'MFAChallengeVerifyApi', 'MFASendCodeApi'
'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 原来的名字
class MFASendCodeApi(AuthMixin, CreateAPIView):
"""

View File

@@ -1,5 +1,6 @@
import time
from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponseRedirect
from django.shortcuts import reverse
@@ -40,12 +41,15 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
return user, None
@staticmethod
def safe_send_code(token, code, target, form_type, content):
def safe_send_code(token, code, target, form_type, content, user_info):
token_sent_key = '{}_send_at'.format(token)
token_send_at = cache.get(token_sent_key, 0)
if token_send_at:
raise IntervalTooShort(60)
SendAndVerifyCodeUtil(target, code, backend=form_type, **content).gen_and_send_async()
tooler = SendAndVerifyCodeUtil(
target, code, backend=form_type, user_info=user_info, **content
)
tooler.gen_and_send_async()
cache.set(token_sent_key, int(time.time()), 60)
def prepare_code_data(self, user_info, serializer):
@@ -61,7 +65,7 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
if not user:
raise ValueError(err)
code = random_string(6, lower=False, upper=False)
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
subject = '%s: %s' % (get_login_title(), _('Forgot password'))
context = {
'user': user, 'title': subject, 'code': code,
@@ -82,7 +86,7 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
code, target, form_type, content = self.prepare_code_data(user_info, serializer)
except ValueError as e:
return Response({'error': str(e)}, status=400)
self.safe_send_code(token, code, target, form_type, content)
self.safe_send_code(token, code, target, form_type, content, user_info)
return Response({'data': 'ok'}, status=200)

View File

@@ -3,8 +3,9 @@
import abc
import ldap
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django_auth_ldap.backend import _LDAPUser, LDAPBackend
from django_auth_ldap.backend import _LDAPUser, LDAPBackend, valid_cache_key
from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
from users.utils import construct_user_email
@@ -146,30 +147,53 @@ class LDAPHAAuthorizationBackend(JMSBaseAuthBackend, LDAPBaseBackend):
class LDAPUser(_LDAPUser):
def __init__(self, backend, username=None, user=None, request=None):
super().__init__(backend=backend, username=username, user=user, request=request)
config_prefix = "" if isinstance(self.backend, LDAPAuthorizationBackend) else "_ha"
self.user_dn_cache_key = valid_cache_key(
f"django_auth_ldap{config_prefix}.user_dn.{self._username}"
)
self.category = f"ldap{config_prefix}"
self.search_filter = getattr(settings, f"AUTH_LDAP{config_prefix.upper()}_SEARCH_FILTER", None)
self.search_ou = getattr(settings, f"AUTH_LDAP{config_prefix.upper()}_SEARCH_OU", None)
def _search_for_user_dn_from_ldap_util(self):
from settings.utils import LDAPServerUtil
util = LDAPServerUtil()
util = LDAPServerUtil(category=self.category)
user_dn = util.search_for_user_dn(self._username)
return user_dn
def _load_user_dn(self):
"""
Populates self._user_dn with the distinguished name of our user.
This will either construct the DN from a template in
AUTH_LDAP_USER_DN_TEMPLATE or connect to the server and search for it.
If we have to search, we'll cache the DN.
"""
if self._using_simple_bind_mode():
self._user_dn = self._construct_simple_user_dn()
else:
if self.settings.CACHE_TIMEOUT > 0:
self._user_dn = cache.get_or_set(
self.user_dn_cache_key, self._search_for_user_dn, self.settings.CACHE_TIMEOUT
)
else:
self._user_dn = self._search_for_user_dn()
def _search_for_user_dn(self):
"""
This method was overridden because the AUTH_LDAP_USER_SEARCH
configuration in the settings.py file
is configured with a `lambda` problem value
"""
if isinstance(self.backend, LDAPAuthorizationBackend):
search_filter = settings.AUTH_LDAP_SEARCH_FILTER
search_ou = settings.AUTH_LDAP_SEARCH_OU
else:
search_filter = settings.AUTH_LDAP_HA_SEARCH_FILTER
search_ou = settings.AUTH_LDAP_HA_SEARCH_OU
user_search_union = [
LDAPSearch(
USER_SEARCH, ldap.SCOPE_SUBTREE,
search_filter
self.search_filter
)
for USER_SEARCH in str(search_ou).split("|")
for USER_SEARCH in str(self.search_ou).split("|")
]
search = LDAPSearchUnion(*user_search_union)

View File

@@ -22,5 +22,6 @@ class ConfirmMFA(BaseConfirm):
def authenticate(self, secret_key, mfa_type):
mfa_backend = self.user.get_mfa_backend_by_type(mfa_type)
mfa_backend.set_request(self.request)
ok, msg = mfa_backend.check_code(secret_key)
return ok, msg

View File

@@ -2,7 +2,7 @@ from django.db.models import TextChoices
from authentication.confirm import CONFIRM_BACKENDS
from .confirm import ConfirmMFA, ConfirmPassword, ConfirmReLogin
from .mfa import MFAOtp, MFASms, MFARadius, MFACustom
from .mfa import MFAOtp, MFASms, MFARadius, MFAFace, MFACustom
RSA_PRIVATE_KEY = 'rsa_private_key'
RSA_PUBLIC_KEY = 'rsa_public_key'
@@ -35,5 +35,11 @@ class ConfirmType(TextChoices):
class MFAType(TextChoices):
OTP = MFAOtp.name, MFAOtp.display_name
SMS = MFASms.name, MFASms.display_name
Face = MFAFace.name, MFAFace.display_name
Radius = MFARadius.name, MFARadius.display_name
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"

View File

@@ -2,3 +2,4 @@ from .otp import MFAOtp, otp_failed_msg
from .sms import MFASms
from .radius import MFARadius
from .custom import MFACustom
from .face import MFAFace

View File

@@ -12,10 +12,14 @@ class BaseMFA(abc.ABC):
因为首页登录时,可能没法获取到一些状态
"""
self.user = user
self.request = None
def is_authenticated(self):
return self.user and self.user.is_authenticated
def set_request(self, request):
self.request = request
@property
@abc.abstractmethod
def name(self):

View File

@@ -0,0 +1,57 @@
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 common.const import LicenseEditionChoices
from settings.api import settings
class MFAFace(BaseMFA, MFAFaceMixin):
name = "face"
display_name = _('Face Recognition')
placeholder = 'Face Recognition'
def check_code(self, code):
assert self.is_authenticated()
try:
code = self.get_face_code()
if not self.user.check_face(code):
return False, _('Facial comparison failed')
except Exception as e:
return False, "{}:{}".format(_('Facial comparison failed'), str(e))
return True, ''
def is_active(self):
if not self.is_authenticated():
return True
return bool(self.user.face_vector)
@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
def get_enable_url(self) -> str:
return reverse('authentication:user-face-enable')
def get_disable_url(self) -> str:
return reverse('authentication:user-face-disable')
def disable(self):
assert self.is_authenticated()
self.user.face_vector = ''
self.user.save(update_fields=['face_vector'])
def can_disable(self) -> bool:
return True
@staticmethod
def help_text_of_enable():
return _("Frontal Face Recognition")

View File

@@ -12,7 +12,7 @@ class MFARadius(BaseMFA):
display_name = 'Radius'
placeholder = _("Radius verification code")
def check_code(self, code):
def check_code(self, code=None):
assert self.is_authenticated()
backend = RadiusBackend()
username = self.user.username

View File

@@ -2,6 +2,7 @@ from django.conf import settings
from django.utils.translation import gettext_lazy as _
from common.utils.verify_code import SendAndVerifyCodeUtil
from users.serializers import SmsUserSerializer
from .base import BaseMFA
sms_failed_msg = _("SMS verify code invalid")
@@ -14,8 +15,13 @@ class MFASms(BaseMFA):
def __init__(self, user):
super().__init__(user)
phone = user.phone if self.is_authenticated() else ''
self.sms = SendAndVerifyCodeUtil(phone, backend=self.name)
phone, user_info = '', None
if self.is_authenticated():
phone = user.phone
user_info = SmsUserSerializer(user).data
self.sms = SendAndVerifyCodeUtil(
phone, backend=self.name, user_info=user_info
)
def check_code(self, code):
assert self.is_authenticated()

View File

@@ -2,6 +2,7 @@ import base64
from django.conf import settings
from django.contrib.auth import logout as auth_logout
from django.core.cache import cache
from django.http import HttpResponse
from django.shortcuts import redirect, reverse, render
from django.utils.deprecation import MiddlewareMixin
@@ -34,7 +35,7 @@ class MFAMiddleware:
# 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
white_urls = [
'login/mfa', 'mfa/select', 'jsi18n/', '/static/',
'login/mfa', 'mfa/select', 'mfa/face','jsi18n/', '/static/',
'/profile/otp', '/logout/',
]
for url in white_urls:
@@ -116,23 +117,43 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
class SessionCookieMiddleware(MiddlewareMixin):
USER_LOGIN_ENCRYPTION_KEY_PAIR = 'user_login_encryption_key_pair'
@staticmethod
def set_cookie_public_key(request, response):
def set_cookie_public_key(self, request, response):
if request.path.startswith('/api'):
return
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
public_key = request.session.get(pub_key_name)
cookie_key = request.COOKIES.get(pub_key_name)
if public_key and public_key == cookie_key:
session_public_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
session_private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
session_public_key = request.session.get(session_public_key_name)
cookie_public_key = request.COOKIES.get(session_public_key_name)
if session_public_key and session_public_key == cookie_public_key:
return
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
private_key, public_key = gen_key_pair()
private_key, public_key = self.get_key_pair()
public_key_decode = base64.b64encode(public_key.encode()).decode()
request.session[pub_key_name] = public_key_decode
request.session[pri_key_name] = private_key
response.set_cookie(pub_key_name, public_key_decode)
request.session[session_public_key_name] = public_key_decode
request.session[session_private_key_name] = private_key
response.set_cookie(session_public_key_name, public_key_decode)
def get_key_pair(self):
key_pair = cache.get(self.USER_LOGIN_ENCRYPTION_KEY_PAIR)
if key_pair:
return key_pair['private_key'], key_pair['public_key']
private_key, public_key = gen_key_pair()
key_pair = {
'private_key': private_key,
'public_key': public_key
}
cache.set(self.USER_LOGIN_ENCRYPTION_KEY_PAIR, key_pair, None)
return private_key, public_key
@staticmethod
def set_cookie_session_prefix(request, response):

View File

@@ -199,6 +199,53 @@ 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
@@ -263,7 +310,6 @@ class MFAMixin:
user = user if user else self.get_user_from_session()
if not user.mfa_enabled:
return
# 监测 MFA 是不是屏蔽了
ip = self.get_request_ip()
self.check_mfa_is_block(user.username, ip)
@@ -276,6 +322,7 @@ class MFAMixin:
elif not mfa_backend.is_active():
msg = backend_error.format(mfa_backend.display_name)
else:
mfa_backend.set_request(self.request)
ok, msg = mfa_backend.check_code(code)
if ok:

View File

@@ -8,6 +8,7 @@ from common.serializers.fields import EncryptedField
__all__ = [
'MFAChallengeSerializer', 'MFASelectTypeSerializer',
'PasswordVerifySerializer', 'ResetPasswordCodeSerializer',
'MFAFaceCallbackSerializer'
]
@@ -51,3 +52,16 @@ 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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,92 @@
{% extends '_base_only_content.html' %}
{% load i18n %}
{% load static %}
{% block content %}
{% if 'code' in form.errors %}
<div class="alert alert-danger" id="messages">
<p class="red-fonts">{{ form.code.errors.as_text }}</p>
</div>
{% endif %}
<div id="retry_container" style="text-align: center; margin-top: 20px; display: none;">
<button id="retry_button" class="btn btn-primary">{% trans 'Retry' %}</button>
</div>
<form class="m-t" role="form" method="post" action="" style="display: none">
{% csrf_token %}
<button id="submit_button" type="submit" style="display: none"></button>
</form>
<div id="iframe_container"
style="display: none; justify-content: center; align-items: center; height: 520px; width: 100%;">
<iframe
title="face capture"
id="face_capture_iframe"
allow="camera"
sandbox="allow-scripts allow-same-origin"
style="width: 100%; height:100%;border: none;">
</iframe>
</div>
<script>
$(document).ready(function () {
const apiUrl = "{% url 'api-auth:mfa-face-context' %}";
const faceCaptureUrl = "/facelive/capture";
let token;
function createFaceCaptureToken() {
const csrf = getCookie('jms_csrftoken');
$.ajax({
url: apiUrl,
method: 'POST',
headers: {
'X-CSRFToken': csrf
},
success: function (data) {
token = data.token;
$('#iframe_container').show();
$('#face_capture_iframe').attr('src', `${faceCaptureUrl}?token=${token}`);
startCheckingStatus();
},
error: function (error) {
$('#retry_container').show();
}
});
}
function startCheckingStatus() {
const interval = 1000;
const timer = setInterval(function () {
$.ajax({
url: `${apiUrl}?token=${token}`,
method: 'GET',
success: function (data) {
if (data.is_finished) {
clearInterval(timer);
$('#submit_button').click();
}
},
error: function (error) {
console.error('API request failed:', error);
}
});
}, interval);
}
const active = "{{ active }}";
if (active) {
createFaceCaptureToken();
} else {
$('#retry_container').show();
}
$('#retry_button').on('click', function () {
window.location.href = "{{ request.get_full_path }}";
});
});
</script>
{% endblock %}

View File

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

View File

@@ -33,6 +33,8 @@ 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

@@ -14,6 +14,7 @@ urlpatterns = [
path('login/', non_atomic_requests(views.UserLoginView.as_view()), name='login'),
path('login/mfa/', views.UserLoginMFAView.as_view(), name='login-mfa'),
path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'),
path('login/mfa/face/capture/', views.UserLoginMFAFaceView.as_view(), name='login-face-capture'),
path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'),
path('logout/', views.UserLogoutView.as_view(), name='logout'),
@@ -73,6 +74,8 @@ urlpatterns = [
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
name='user-otp-disable'),
path('profile/face/enable/', users_view.UserFaceEnableView.as_view(), name='user-face-enable'),
path('profile/face/disable/', users_view.UserFaceDisableView.as_view(), name='user-face-disable'),
# other authentication protocol
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
path('openid/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='openid')),

View File

@@ -24,8 +24,8 @@ from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView, RedirectView
from django.views.generic.edit import FormView
from common.utils import FlashMessageUtil, static_or_direct, safe_next_url
from common.const import Language
from common.utils import FlashMessageUtil, static_or_direct, safe_next_url
from users.utils import (
redirect_user_first_login_or_index
)

View File

@@ -3,14 +3,16 @@
from __future__ import unicode_literals
from django.views.generic.edit import FormView
from django.shortcuts import redirect
from django.shortcuts import redirect, reverse
from common.utils import get_logger
from users.views import UserFaceCaptureView
from .. import forms, errors, mixins
from .utils import redirect_to_guard_view
from ..const import MFAType
logger = get_logger(__name__)
__all__ = ['UserLoginMFAView']
__all__ = ['UserLoginMFAView', 'UserLoginMFAFaceView']
class UserLoginMFAView(mixins.AuthMixin, FormView):
@@ -32,10 +34,16 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
return super().get(*args, **kwargs)
def form_valid(self, form):
from users.utils import MFABlockUtils
code = form.cleaned_data.get('code')
mfa_type = form.cleaned_data.get('mfa_type')
if mfa_type == MFAType.Face:
return redirect(reverse('authentication:login-face-capture'))
return self.do_mfa_check(form, code, mfa_type)
def do_mfa_check(self, form, code, mfa_type):
from users.utils import MFABlockUtils
try:
self._do_check_user_mfa(code, mfa_type)
user, ip = self.get_user_from_session(), self.get_request_ip()
@@ -58,3 +66,7 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
kwargs.update(mfa_context)
return kwargs
class UserLoginMFAFaceView(UserFaceCaptureView, UserLoginMFAView):
def form_valid(self, form):
return self.do_mfa_check(form, self.code, self.mfa_type)

View File

@@ -13,20 +13,17 @@ from authentication.const import ConfirmType
from authentication.mixins import AuthMixin
from authentication.permissions import UserConfirmation
from common.sdk.im.wecom import URL
from common.sdk.im.wecom import WeCom
from common.sdk.im.wecom import WeCom, wecom_tool
from common.utils import get_logger
from common.utils.common import get_request_ip
from common.utils.django import reverse, get_object_or_none, safe_next_url
from common.utils.random import random_string
from common.views.mixins import UserConfirmRequiredExceptionMixin, PermissionsMixin
from users.models import User
from users.views import UserVerifyPasswordView
from .base import BaseLoginCallbackView, BaseBindCallbackView
from .mixins import METAMixin, FlashMessageMixin
logger = get_logger(__file__)
WECOM_STATE_SESSION_KEY = '_wecom_state'
logger = get_logger(__file__)
class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessageMixin, View):
@@ -45,7 +42,7 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
)
def verify_state(self):
return self.verify_state_with_session_key(WECOM_STATE_SESSION_KEY)
return wecom_tool.check_state(self.request.GET.get('state'), self.request)
def get_already_bound_response(self, redirect_url):
msg = _('WeCom is already bound')
@@ -56,13 +53,10 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
class WeComQRMixin(WeComBaseMixin, View):
def get_qr_url(self, redirect_uri):
state = random_string(16)
self.request.session[WECOM_STATE_SESSION_KEY] = state
params = {
'appid': settings.WECOM_CORPID,
'agentid': settings.WECOM_AGENTID,
'state': state,
'state': wecom_tool.gen_state(request=self.request),
'redirect_uri': redirect_uri,
}
url = URL.QR_CONNECT + '?' + urlencode(params)
@@ -74,13 +68,11 @@ class WeComOAuthMixin(WeComBaseMixin, View):
def get_oauth_url(self, redirect_uri):
if not settings.AUTH_WECOM:
return reverse('authentication:login')
state = random_string(16)
self.request.session[WECOM_STATE_SESSION_KEY] = state
params = {
'appid': settings.WECOM_CORPID,
'agentid': settings.WECOM_AGENTID,
'state': state,
'state': wecom_tool.gen_state(request=self.request),
'redirect_uri': redirect_uri,
'response_type': 'code',
'scope': 'snsapi_base',

View File

@@ -76,3 +76,32 @@ class Language(models.TextChoices):
COUNTRY_CALLING_CODES = get_country_phone_choices()
class LicenseEditionChoices(models.TextChoices):
COMMUNITY = 'community', _('Community edition')
BASIC = 'basic', _('Basic edition')
STANDARD = 'standard', _('Standard edition')
PROFESSIONAL = 'professional', _('Professional edition')
ULTIMATE = 'ultimate', _('Ultimate edition')
@staticmethod
def from_key(key: str):
for choice in LicenseEditionChoices:
if choice == key:
return choice
return LicenseEditionChoices.COMMUNITY
@staticmethod
def parse_license_edition(info):
count = info.get('license', {}).get('count', 0)
if 50 >= count > 0:
return LicenseEditionChoices.BASIC
elif count <= 500:
return LicenseEditionChoices.STANDARD
elif count < 5000:
return LicenseEditionChoices.PROFESSIONAL
elif count >= 5000:
return LicenseEditionChoices.ULTIMATE
else:
return LicenseEditionChoices.COMMUNITY

View File

@@ -126,7 +126,7 @@ class BaseFileParser(BaseParser):
value = self.id_name_to_obj(value)
elif isinstance(field, LabeledChoiceField):
value = self.id_name_to_obj(value)
if isinstance(value, dict) and value.get('pk'):
if isinstance(value, dict) and 'pk' in value:
value = value.get('pk')
elif isinstance(field, serializers.ListSerializer):
value = [self.parse_value(field.child, v) for v in value]

View File

@@ -73,6 +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/mfa/send-code/",
"/api/v1/authentication/sso/login/",
"/api/v1/authentication/user-session/",

View File

@@ -16,12 +16,6 @@ def digest(corp_id, corp_secret):
return dist
def update_values(default: dict, others: dict):
for key in default.keys():
if key in others:
default[key] = others[key]
def set_default(data: dict, default: dict):
for key in default.keys():
if key not in data:

View File

@@ -1,12 +1,14 @@
from typing import Iterable, AnyStr
from urllib.parse import urlencode
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import APIException
from common.sdk.im.mixin import RequestMixin, BaseRequest
from common.sdk.im.utils import digest, update_values
from common.utils.common import get_logger
from common.sdk.im.utils import digest
from common.utils import reverse, random_string, get_logger, lazyproperty
from users.utils import construct_user_email, flatten_dict, map_attributes
logger = get_logger(__name__)
@@ -107,15 +109,6 @@ class WeCom(RequestMixin):
对于业务代码,只需要关心由 用户id 或 消息不对 导致的错误,其他错误不予理会
"""
users = tuple(users)
extra_params = {
"safe": 0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
update_values(extra_params, kwargs)
body = {
"touser": '|'.join(users),
"msgtype": "text",
@@ -123,7 +116,7 @@ class WeCom(RequestMixin):
"text": {
"content": msg
},
**extra_params
**kwargs
}
if markdown:
body['msgtype'] = 'markdown'
@@ -144,15 +137,15 @@ class WeCom(RequestMixin):
if 'invaliduser' not in data:
return ()
invaliduser = data['invaliduser']
if not invaliduser:
invalid_user = data['invaliduser']
if not invalid_user:
return ()
if isinstance(invaliduser, str):
logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invaliduser}')
if isinstance(invalid_user, str):
logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invalid_user}')
raise WeComError
invalid_users = invaliduser.split('|')
invalid_users = invalid_user.split('|')
return invalid_users
def get_user_id_by_code(self, code):
@@ -167,13 +160,12 @@ class WeCom(RequestMixin):
self._requests.check_errcode_is_0(data)
USER_ID = 'UserId'
OPEN_ID = 'OpenId'
if USER_ID in data:
return data[USER_ID], USER_ID
elif OPEN_ID in data:
return data[OPEN_ID], OPEN_ID
user_id = 'UserId'
open_id = 'OpenId'
if user_id in data:
return data[user_id], user_id
elif open_id in data:
return data[open_id], open_id
else:
logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId')
raise WeComError
@@ -195,3 +187,37 @@ class WeCom(RequestMixin):
default_detail = self.default_user_detail(data, user_id)
detail = map_attributes(default_detail, info, self.attributes)
return detail
class WeComTool(object):
WECOM_STATE_SESSION_KEY = '_wecom_state'
WECOM_STATE_VALUE = 'wecom'
@lazyproperty
def qr_cb_url(self):
return reverse('authentication:wecom-qr-login-callback', external=True)
def gen_state(self, request=None):
state = random_string(16)
if not request:
cache.set(state, self.WECOM_STATE_VALUE, timeout=60 * 60 * 24)
else:
request.session[self.WECOM_STATE_SESSION_KEY] = state
return state
def check_state(self, state, request=None):
return cache.get(state) == self.WECOM_STATE_VALUE or \
request.session[self.WECOM_STATE_SESSION_KEY] == state
def wrap_redirect_url(self, next_url):
params = {
'appid': settings.WECOM_CORPID,
'agentid': settings.WECOM_AGENTID,
'state': self.gen_state(),
'redirect_uri': f'{self.qr_cb_url}?next={next_url}',
'response_type': 'code', 'scope': 'snsapi_base',
}
return URL.OAUTH_CONNECT + '?' + urlencode(params) + '#wechat_redirect'
wecom_tool = WeComTool()

View File

@@ -43,7 +43,7 @@ class SMS:
**kwargs
)
def send_verify_code(self, phone_number, code):
def send_verify_code(self, phone_number, code, **kwargs):
prefix = getattr(self.client, 'SIGN_AND_TMPL_SETTING_FIELD_PREFIX', '')
sign_name = getattr(settings, f'{prefix}_VERIFY_SIGN_NAME', None)
template_code = getattr(settings, f'{prefix}_VERIFY_TEMPLATE_CODE', None)
@@ -53,4 +53,7 @@ class SMS:
code='verify_code_sign_tmpl_invalid',
detail=_('SMS verification code signature or template invalid')
)
return self.send_sms([phone_number], sign_name, template_code, OrderedDict(code=code))
return self.send_sms(
[phone_number], sign_name, template_code,
OrderedDict(code=code), **kwargs
)

View File

@@ -1,9 +1,9 @@
import os
import jms_storage
from django.conf import settings
from django.core.files.storage import default_storage
from common.storage import jms_storage
from common.utils import get_logger, make_dirs
from terminal.models import ReplayStorage

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python
# coding: utf-8
# Copyright (c) 2018
#
__version__ = '0.0.59'
from .ftp import FTPStorage
from .oss import OSSStorage
from .obs import OBSStorage
from .s3 import S3Storage
from .azure import AzureStorage
from .ceph import CEPHStorage
from .jms import JMSReplayStorage, JMSCommandStorage
from .multi import MultiObjectStorage
from .sftp import SFTPStorage
def get_object_storage(config):
if config.get("TYPE") in ["s3", "ceph", "swift", "cos"]:
return S3Storage(config)
elif config.get("TYPE") == "oss":
return OSSStorage(config)
elif config.get("TYPE") == "server":
return JMSReplayStorage(config)
elif config.get("TYPE") == "azure":
return AzureStorage(config)
elif config.get("TYPE") == "ceph":
return CEPHStorage(config)
elif config.get("TYPE") == "ftp":
return FTPStorage(config)
elif config.get("TYPE") == "obs":
return OBSStorage(config)
elif config.get("TYPE") == "sftp":
return SFTPStorage(config)
else:
return JMSReplayStorage(config)
def get_multi_object_storage(configs):
return MultiObjectStorage(configs)

View File

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
#
import os
from azure.storage.blob import BlobServiceClient
from .base import ObjectStorage
class AzureStorage(ObjectStorage):
def __init__(self, config):
self.account_name = config.get("ACCOUNT_NAME", None)
self.account_key = config.get("ACCOUNT_KEY", None)
self.container_name = config.get("CONTAINER_NAME", None)
self.endpoint_suffix = config.get("ENDPOINT_SUFFIX", 'core.chinacloudapi.cn')
if self.account_name and self.account_key:
self.service_client = BlobServiceClient(
account_url=f'https://{self.account_name}.blob.{self.endpoint_suffix}',
credential={'account_name': self.account_name, 'account_key': self.account_key}
)
self.client = self.service_client.get_container_client(self.container_name)
else:
self.client = None
def upload(self, src, target):
try:
self.client.upload_blob(target, src)
return True, None
except Exception as e:
return False, e
def download(self, src, target):
try:
blob_data = self.client.download_blob(blob=src)
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
with open(target, 'wb') as writer:
writer.write(blob_data.readall())
return True, None
except Exception as e:
return False, e
def delete(self, path):
try:
self.client.delete_blob(path)
return True, False
except Exception as e:
return False, e
def exists(self, path):
resp = self.client.list_blobs(name_starts_with=path)
return len(list(resp)) != 0
def list_buckets(self):
return list(self.service_client.list_containers())
@property
def type(self):
return 'azure'

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
#
import abc
class ObjectStorage(metaclass=abc.ABCMeta):
@abc.abstractmethod
def upload(self, src, target):
return None, None
@abc.abstractmethod
def download(self, src, target):
pass
@abc.abstractmethod
def delete(self, path):
pass
@abc.abstractmethod
def exists(self, path):
pass
def is_valid(self, src, target):
ok, msg = self.upload(src=src, target=target)
if not ok:
return False
self.delete(path=target)
return True
class LogStorage(metaclass=abc.ABCMeta):
@abc.abstractmethod
def save(self, command):
pass
@abc.abstractmethod
def bulk_save(self, command_set, raise_on_error=True):
pass
@abc.abstractmethod
def filter(self, date_from=None, date_to=None,
user=None, asset=None, account=None,
input=None, session=None):
pass
@abc.abstractmethod
def count(self, date_from=None, date_to=None,
user=None, asset=None, account=None,
input=None, session=None):
pass

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
#
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.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(),
)
try:
self.client = self.conn.get_bucket(bucket_name=self.bucket)
except Exception:
self.client = None
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)
return True, None
except Exception as e:
return False, e
def exists(self, path):
try:
return self.client.get_key(path)
except Exception:
return False
@property
def type(self):
return 'ceph'

View File

@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
#
import os
from ftplib import FTP, error_perm
from .base import ObjectStorage
class FTPStorage(ObjectStorage):
def __init__(self, config):
self.host = config.get("HOST", None)
self.port = int(config.get("PORT", 21))
self.username = config.get("USERNAME", None)
self.password = config.get("PASSWORD", None)
self.pasv = bool(config.get("PASV", False))
self.dir = config.get("DIR", "replay")
self.client = FTP()
self.client.encoding = 'utf-8'
self.client.set_pasv(self.pasv)
self.pwd = '.'
self.connect()
def connect(self, timeout=-999, source_address=None):
self.client.connect(self.host, self.port, timeout, source_address)
self.client.login(self.username, self.password)
if not self.check_dir_exist(self.dir):
self.mkdir(self.dir)
self.client.cwd(self.dir)
self.pwd = self.client.pwd()
def confirm_connected(self):
try:
self.client.pwd()
except Exception:
self.connect()
def upload(self, src, target):
self.confirm_connected()
target_dir = os.path.dirname(target)
exist = self.check_dir_exist(target_dir)
if not exist:
ok = self.mkdir(target_dir)
if not ok:
raise PermissionError('Dir create error: %s' % target)
try:
with open(src, 'rb') as f:
self.client.storbinary('STOR '+target, f)
return True, None
except Exception as e:
return False, e
def download(self, src, target):
self.confirm_connected()
try:
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
with open(target, 'wb') as f:
self.client.retrbinary('RETR ' + src, f.write)
return True, None
except Exception as e:
return False, e
def delete(self, path):
self.confirm_connected()
if not self.exists(path):
raise FileNotFoundError('File not exist error(%s)' % path)
try:
self.client.delete(path)
return True, None
except Exception as e:
return False, e
def check_dir_exist(self, d):
pwd = self.client.pwd()
try:
self.client.cwd(d)
self.client.cwd(pwd)
return True
except error_perm:
return False
def mkdir(self, dirs):
self.confirm_connected()
# 创建多级目录ftplib不支持一次创建多级目录
dir_list = dirs.split('/')
pwd = self.client.pwd()
try:
for d in dir_list:
if not d or d in ['.']:
continue
# 尝试切换目录
try:
self.client.cwd(d)
continue
except:
pass
# 切换失败创建这个目录,再切换
try:
self.client.mkd(d)
self.client.cwd(d)
except:
return False
return True
finally:
self.client.cwd(pwd)
def exists(self, target):
self.confirm_connected()
try:
self.client.size(target)
return True
except:
return False
def close(self):
self.client.close()

View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
#
import os
from .base import ObjectStorage, LogStorage
class JMSReplayStorage(ObjectStorage):
def __init__(self, config):
self.client = config.get("SERVICE")
def upload(self, src, target):
session_id = os.path.basename(target).split('.')[0]
ok = self.client.push_session_replay(src, session_id)
return ok, None
def delete(self, path):
return False, Exception("Not support not")
def exists(self, path):
return False
def download(self, src, target):
return False, Exception("Not support not")
@property
def type(self):
return 'jms'
class JMSCommandStorage(LogStorage):
def __init__(self, config):
self.client = config.get("SERVICE")
if not self.client:
raise Exception("Not found app service")
def save(self, command):
return self.client.push_session_command([command])
def bulk_save(self, command_set, raise_on_error=True):
return self.client.push_session_command(command_set)
def filter(self, date_from=None, date_to=None,
user=None, asset=None, account=None,
input=None, session=None):
pass
def count(self, date_from=None, date_to=None,
user=None, asset=None, account=None,
input=None, session=None):
pass

View File

@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
#
from .base import ObjectStorage, LogStorage
class MultiObjectStorage(ObjectStorage):
def __init__(self, configs):
self.configs = configs
self.storage_list = []
self.init_storage_list()
def init_storage_list(self):
from . import get_object_storage
if isinstance(self.configs, dict):
configs = self.configs.values()
else:
configs = self.configs
for config in configs:
try:
storage = get_object_storage(config)
self.storage_list.append(storage)
except Exception:
pass
def upload(self, src, target):
success = []
msg = []
for storage in self.storage_list:
ok, err = storage.upload(src, target)
success.append(ok)
msg.append(err)
return success, msg
def download(self, src, target):
success = False
msg = None
for storage in self.storage_list:
try:
if not storage.exists(src):
continue
ok, msg = storage.download(src, target)
if ok:
success = True
msg = ''
break
except:
pass
return success, msg
def delete(self, path):
success = True
msg = None
for storage in self.storage_list:
try:
if storage.exists(path):
ok, msg = storage.delete(path)
if not ok:
success = False
except:
pass
return success, msg
def exists(self, path):
for storage in self.storage_list:
try:
if storage.exists(path):
return True
except:
pass
return False

View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
#
import os
from obs.client import ObsClient
from .base import ObjectStorage
class OBSStorage(ObjectStorage):
def __init__(self, config):
self.endpoint = config.get("ENDPOINT", None)
self.bucket = config.get("BUCKET", None)
self.access_key = config.get("ACCESS_KEY", None)
self.secret_key = config.get("SECRET_KEY", None)
if self.access_key and self.secret_key and self.endpoint:
proxy_host = os.getenv("proxy_host")
proxy_port = os.getenv("proxy_port")
proxy_username = os.getenv("proxy_username")
proxy_password = os.getenv("proxy_password")
self.obsClient = ObsClient(access_key_id=self.access_key, secret_access_key=self.secret_key, server=self.endpoint, proxy_host=proxy_host, proxy_port=proxy_port, proxy_username=proxy_username, proxy_password=proxy_password)
else:
self.obsClient = None
def upload(self, src, target):
try:
resp = self.obsClient.putFile(self.bucket, target, src)
if resp.status < 300:
return True, None
else:
return False, resp.reason
except Exception as e:
return False, e
def exists(self, path):
resp = self.obsClient.getObjectMetadata(self.bucket, path)
if resp.status < 300:
return True
return False
def delete(self, path):
try:
resp = self.obsClient.deleteObject(self.bucket, path)
if resp.status < 300:
return True, None
else:
return False, resp.reason
except Exception as e:
return False, e
def download(self, src, target):
try:
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
resp = self.obsClient.getObject(self.bucket, src, target)
if resp.status < 300:
return True, None
else:
return False, resp.reason
except Exception as e:
return False, e
def list_buckets(self):
resp = self.obsClient.listBuckets()
if resp.status < 300:
return [b.name for b in resp.body.buckets]
else:
raise RuntimeError(resp.status, str(resp.reason))
@property
def type(self):
return 'obs'

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
#
import os
import time
import oss2
from .base import ObjectStorage
class OSSStorage(ObjectStorage):
def __init__(self, config):
self.endpoint = config.get("ENDPOINT", None)
self.bucket = config.get("BUCKET", None)
self.access_key = config.get("ACCESS_KEY", None)
self.secret_key = config.get("SECRET_KEY", None)
if self.access_key and self.secret_key:
self.auth = oss2.Auth(self.access_key, self.secret_key)
else:
self.auth = None
if self.auth and self.endpoint and self.bucket:
self.client = oss2.Bucket(self.auth, self.endpoint, self.bucket)
else:
self.client = None
def upload(self, src, target):
try:
self.client.put_object_from_file(target, src)
return True, None
except Exception as e:
return False, e
def exists(self, path):
try:
return self.client.object_exists(path)
except Exception as e:
return False
def delete(self, path):
try:
self.client.delete_object(path)
return True, None
except Exception as e:
return False, e
def restore(self, path):
meta = self.client.head_object(path)
if meta.resp.headers['x-oss-storage-class'] == oss2.BUCKET_STORAGE_CLASS_ARCHIVE:
self.client.restore_object(path)
while True:
meta = self.client.head_object(path)
if meta.resp.headers['x-oss-restore'] == 'ongoing-request="true"':
time.sleep(5)
else:
break
def download(self, src, target):
try:
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
self.restore(src)
self.client.get_object_to_file(src, target)
return True, None
except Exception as e:
return False, e
def list_buckets(self):
service = oss2.Service(self.auth,self.endpoint)
return ([b.name for b in oss2.BucketIterator(service)])
@property
def type(self):
return 'oss'

View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
#
import boto3
import os
from .base import ObjectStorage
class S3Storage(ObjectStorage):
def __init__(self, config):
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.endpoint = config.get("ENDPOINT", None)
try:
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:
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:
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 's3'

View File

@@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
import io
import os
import paramiko
from .base import ObjectStorage
class SFTPStorage(ObjectStorage):
def __init__(self, config):
self.sftp = None
self.sftp_host = config.get('SFTP_HOST', None)
self.sftp_port = int(config.get('SFTP_PORT', 22))
self.sftp_username = config.get('SFTP_USERNAME', '')
self.sftp_secret_type = config.get('STP_SECRET_TYPE', 'password')
self.sftp_password = config.get('SFTP_PASSWORD', '')
self.sftp_private_key = config.get('STP_PRIVATE_KEY', '')
self.sftp_passphrase = config.get('STP_PASSPHRASE', '')
self.sftp_root_path = config.get('SFTP_ROOT_PATH', '/tmp')
self.ssh = paramiko.SSHClient()
self.connect()
def connect(self):
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
if self.sftp_secret_type == 'password':
self.ssh.connect(self.sftp_host, self.sftp_port, self.sftp_username, self.sftp_password)
elif self.sftp_secret_type == 'ssh_key':
pkey = paramiko.RSAKey.from_private_key(io.StringIO(self.sftp_private_key))
self.ssh.connect(self.sftp_host, self.sftp_port, self.sftp_username, pkey=pkey,
passphrase=self.sftp_passphrase)
self.sftp = self.ssh.open_sftp()
def confirm_connected(self):
try:
self.sftp.getcwd()
except Exception as e:
self.connect()
def upload(self, src, target):
local_file = src
remote_file = os.path.join(self.sftp_root_path, target)
try:
self.confirm_connected()
mode = os.stat(local_file).st_mode
remote_dir = os.path.dirname(remote_file)
if not self.exists(remote_dir):
self.sftp.mkdir(remote_dir)
self.sftp.put(local_file, remote_file)
self.sftp.chmod(remote_file, mode)
return True, None
except Exception as e:
return False, e
def download(self, src, target):
remote_file = src
local_file = target
self.confirm_connected()
try:
local_dir = os.path.dirname(local_file)
if not os.path.exists(local_dir):
os.makedirs(local_dir)
mode = self.sftp.stat(remote_file).st_mode
self.sftp.get(remote_file, local_file)
os.chmod(local_file, mode)
return True, None
except Exception as e:
return False, e
def delete(self, path):
path = os.path.join(self.sftp_root_path, path)
self.confirm_connected()
if not self.exists(path):
raise FileNotFoundError('File not exist error(%s)' % path)
try:
self.sftp.remove(path)
return True, None
except Exception as e:
return False, e
def check_dir_exist(self, d):
self.confirm_connected()
try:
self.sftp.stat(d)
return True
except Exception:
return False
def mkdir(self, dirs):
self.confirm_connected()
try:
if not self.exists(dirs):
self.sftp.mkdir(dirs)
return True, None
except Exception as e:
return False, e
def exists(self, target):
self.confirm_connected()
try:
self.sftp.stat(target)
return True
except:
return False
def close(self):
self.sftp.close()
self.ssh.close()

View File

@@ -1,6 +1,8 @@
import json
import os
import shutil
import tarfile
import time
from itertools import chain
from django.core.files.storage import default_storage
@@ -62,16 +64,17 @@ class SessionPartReplayStorageHandler(object):
# 保存到storage的路径
target_path = os.path.join(default_storage.base_location, local_path)
target_tmp_path = target_path + f'.tmp{int(time.time())}'
target_dir = os.path.dirname(target_path)
if not os.path.isdir(target_dir):
make_dirs(target_dir, exist_ok=True)
ok, err = storage.download(remote_path, target_path)
ok, err = storage.download(remote_path, target_tmp_path)
if not ok:
msg = 'Failed download {} file: {}'.format(part_filename, err)
logger.error(msg)
return None, msg
shutil.move(target_tmp_path, target_path)
url = default_storage.url(local_path)
return local_path, url

View File

@@ -1,11 +1,11 @@
import os
import jms_storage
from celery import shared_task
from django.conf import settings
from django.core.mail import send_mail, EmailMultiAlternatives, get_connection
from django.utils.translation import gettext_lazy as _
from common.storage import jms_storage
from .utils import get_logger
logger = get_logger(__file__)

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

@@ -47,6 +47,7 @@ class Subscription:
self.ch = pb.ch
self.sub = sub
self.unsubscribed = False
logger.info(f"Subscribed to channel: {sub}")
def _handle_msg(self, _next, error, complete):
"""
@@ -105,10 +106,11 @@ class Subscription:
def unsubscribe(self):
self.unsubscribed = True
logger.info(f"Unsubscribed from channel: {self.sub}")
try:
self.sub.close()
except Exception as e:
logger.debug('Unsubscribe msg error: {}'.format(e))
logger.warning(f'Unsubscribe msg error: {e}')
def retry(self, _next, error, complete):
logger.info('Retry subscribe channel: {}'.format(self.ch))

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:860b4d38beff81667c64da41c026a7dd28c3c93a28ae61fefaa7c26875f35638
size 73906864
oid sha256:c5119fd8911a107a7112422ade326766fe3d9538ac15bca06e3c622191c84e18
size 61086554

View File

@@ -20,16 +20,20 @@ logger = get_logger(__file__)
be executed to send SMS messages"""
)
)
def send_sms_async(target, code):
SMS().send_verify_code(target, code)
def send_sms_async(target, code, user_info):
SMS().send_verify_code(target, code, user_info=user_info)
class SendAndVerifyCodeUtil(object):
KEY_TMPL = 'auth-verify-code-{}'
def __init__(self, target, code=None, key=None, backend='email', timeout=None, **kwargs):
def __init__(
self, target, code=None, key=None, backend='email',
user_info=None, timeout=None, **kwargs
):
self.code = code
self.target = target
self.user_info = user_info
self.backend = backend
self.key = key or self.KEY_TMPL.format(target)
self.timeout = settings.VERIFY_CODE_TTL if timeout is None else timeout
@@ -78,7 +82,7 @@ class SendAndVerifyCodeUtil(object):
return code
def __send_with_sms(self):
send_sms_async.apply_async(args=(self.target, self.code), priority=100)
send_sms_async.apply_async(args=(self.target, self.code, self.user_info), priority=100)
def __send_with_email(self):
subject = self.other_args.get('subject', '')

View File

@@ -11,7 +11,7 @@ class BaseTranslateManager:
SEPARATOR = "<SEP>"
LANG_MAPPER = {
'ja': 'Japanese',
'zh_hant': 'Taiwan',
'zh_hant': 'Traditional Chinese',
# 'en': 'English',
}

View File

@@ -9,6 +9,7 @@
"CommandReviewMessage": "The command you entered requires verification before it can be executed. Would you like to initiate a review request?",
"CommandReviewRejectBy": "Command review request has been rejected by %s",
"CommandReviewTimeoutError": "Command review request has timed out",
"CommandWarningDialogMessage": "The command you executed is risky and an alert notification will be sent to the administrator. Do you want to continue?",
"Confirm": "Confirm",
"ConnectError": "Error while fetching data",
"ConnectSuccess": "Connected successfully",
@@ -22,6 +23,7 @@
"ErrorMessage": "Error message",
"ExecuteError": "Error while executing",
"ExecuteSuccess": "Executed successfully",
"ExecutionCanceled": "Execution Canceled",
"ExportALL": "Export all data",
"ExportAll": "Export all",
"ExportCurrent": "Export current page",
@@ -42,6 +44,9 @@
"OverMaxSessionTimeError": "Since this session has been active for more than %d hours, it has been closed",
"ParseError": "Error while parsing",
"PasteNotAllowed": "You are not allowed to paste, please contact the administrator to open it!",
"PermissionAlreadyExpired": "Permission already expired",
"PermissionExpiredDialogMessage": "Permission has expired, and the session will expire in ten minutes. Please contact the administrator promptly for renewal",
"PermissionExpiredDialogTitle": "Permission expired",
"PermissionsExpiredOn": "Permissions associated with this session expired on %s",
"Properties": "Properties",
"Refresh": "Refresh",
@@ -67,8 +72,6 @@
"Version": "Version",
"ViewData": "View data",
"WaitCommandReviewMessage": "The review request has been initiated, please wait for the review results",
"initializingDatasourceFailedMessage": "Connection failed, please check if the database connection configuration is correct",
"Warning": "Warning",
"ExecutionCanceled": "Execution Canceled",
"CommandWarningDialogMessage": "The command you executed is risky and an alert notification will be sent to the administrator. Do you want to continue?"
"initializingDatasourceFailedMessage": "Connection failed, please check if the database connection configuration is correct"
}

View File

@@ -44,6 +44,9 @@
"OverMaxSessionTimeError": "このセッションの時間が%d時間を超えたため、閉じられました",
"ParseError": "解析に失敗しました",
"PasteNotAllowed": "貼り付けは許可されていません。管理者に連絡して権限を開いてください!",
"PermissionAlreadyExpired": "権限が期限切れ",
"PermissionExpiredDialogMessage": "権限が期限切れで、セッションは10分後に期限切れになります。管理者に連絡し、期限を延長してください。",
"PermissionExpiredDialogTitle": "権限が期限切れ",
"PermissionsExpiredOn": "このセッションに関連する権限は%sに期限切れです",
"Properties": "プロパティ",
"Refresh": "リフレッシュ",

View File

@@ -9,6 +9,7 @@
"CommandReviewMessage": "您输入的命令需要复核后才可以执行, 是否发起复核请求?",
"CommandReviewRejectBy": "命令复核被 %s 拒绝",
"CommandReviewTimeoutError": "命令复核超时",
"CommandWarningDialogMessage": "您执行的命令存在风险,告警通知将发送给管理员。是否继续?",
"Confirm": "确认",
"ConnectError": "连接失败",
"ConnectSuccess": "连接成功",
@@ -22,6 +23,7 @@
"ErrorMessage": "错误消息",
"ExecuteError": "执行失败",
"ExecuteSuccess": "执行成功",
"ExecutionCanceled": "执行已取消",
"ExportALL": "导出所有数据",
"ExportAll": "导出全部",
"ExportCurrent": "导出当前页面",
@@ -42,6 +44,9 @@
"OverMaxSessionTimeError": "由于此会话时间大于 %d 小时,已经被关闭",
"ParseError": "解析失败",
"PasteNotAllowed": "不允许粘贴,请联系管理员开启权限!",
"PermissionAlreadyExpired": "授权已过期",
"PermissionExpiredDialogMessage": "授权已过期,会话将在十分钟后过期,请及时联系管理员续期",
"PermissionExpiredDialogTitle": "授权已过期",
"PermissionsExpiredOn": "此会话关联的权限已于 %s 过期",
"Properties": "属性",
"Refresh": "刷新",
@@ -67,8 +72,6 @@
"Version": "版本",
"ViewData": "查看数据",
"WaitCommandReviewMessage": "复核请求已发起, 请等待复核结果",
"initializingDatasourceFailedMessage": "连接失败,请检查数据库连接配置是否正确",
"Warning": "警告",
"ExecutionCanceled": "执行已取消",
"CommandWarningDialogMessage": "您执行的命令存在风险,告警通知将发送给管理员。是否继续?"
"initializingDatasourceFailedMessage": "连接失败,请检查数据库连接配置是否正确"
}

View File

@@ -44,6 +44,9 @@
"OverMaxSessionTimeError": "由於此會話時間大於 %d 小時,已經被關閉",
"ParseError": "解析失敗",
"PasteNotAllowed": "不允許貼上,請聯絡管理員開啟權限!",
"PermissionAlreadyExpired": "授權已過期",
"PermissionExpiredDialogMessage": "授權已過期,對話將在十分鐘後過期,請及時聯繫管理員續期。",
"PermissionExpiredDialogTitle": "授權已過期",
"PermissionsExpiredOn": "此會話關聯的權限已於 %s 過期",
"Properties": "屬性",
"Refresh": "刷新",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,9 @@
{
"ActionPerm": "Actions",
"Cancel": "Cancel",
"Clone Connect": "Clone Connect",
"Close All Tabs": "Close All Tabs",
"Close Current Tab": "Close Current Tab",
"Confirm": "Confirm",
"ConfirmBtn": "Confirm",
"Connect": "Connect",
@@ -8,6 +11,7 @@
"CopyShareURLSuccess": "Copy Share URL Success",
"CreateLink": "Create Share Link",
"CreateSuccess": "Success",
"Custom Setting": "Custom Setting",
"DownArrow": "Down arrow",
"Download": "Download",
"DownloadSuccess": "Download success",
@@ -32,7 +36,10 @@
"OnlineUsers": "Online Users",
"Paste": "Paste",
"PauseSession": "Pause Session",
"PermissionExpired": "Permission expired",
"PermissionValid": "Permission valid",
"ReadOnly": "Read-Only",
"Reconnect": "Reconnect",
"Refresh": "Refresh",
"Remove": "Remove",
"RemoveShareUser": "You have been removed from the shared session.",
@@ -63,9 +70,6 @@
"WaitFileTransfer": "Wait file transfer to finish",
"WebSocketClosed": "WebSocket closed",
"Writable": "Writable",
"Reconnect": "Reconnect",
"Close Current Tab": "Close Current Tab",
"Close All Tabs": "Close All Tabs",
"Clone Connect": "Clone Connect",
"Custom Setting": "Custom Setting"
"UploadStart": "Upload start",
"UploadEnd": "Upload completed, please wait for further processing"
}

View File

@@ -36,6 +36,8 @@
"OnlineUsers": "オンラインスタッフ",
"Paste": "貼り付け",
"PauseSession": "セッションを一時停止",
"PermissionExpired": "許可が期限切れになりました",
"PermissionValid": "権限は有効です",
"ReadOnly": "読み取り専用",
"Reconnect": "再接続",
"Refresh": "リフレッシュ",
@@ -67,5 +69,7 @@
"VerifyCode": "認証コード",
"WaitFileTransfer": "ファイル転送終了待ち",
"WebSocketClosed": "WebSocket 閉店",
"Writable": "書き込み可能"
"Writable": "書き込み可能",
"UploadStart": "アップロード開始",
"UploadEnd": "アップロードが完了しました。後の処理をお待ちください"
}

View File

@@ -1,6 +1,9 @@
{
"ActionPerm": "操作权限",
"Cancel": "取消",
"Clone Connect": "复制窗口",
"Close All Tabs": "关闭所有",
"Close Current Tab": "关闭当前",
"Confirm": "确认",
"ConfirmBtn": "确定",
"Connect": "连接",
@@ -8,6 +11,7 @@
"CopyShareURLSuccess": "复制分享地址成功",
"CreateLink": "创建分享链接",
"CreateSuccess": "创建成功",
"Custom Setting": "自定义设置",
"DownArrow": "向下箭头",
"Download": "下载",
"DownloadSuccess": "下载成功",
@@ -32,7 +36,10 @@
"OnlineUsers": "在线人员",
"Paste": "粘贴",
"PauseSession": "暂停此会话",
"PermissionExpired": "权限已过期",
"PermissionValid": "权限有效",
"ReadOnly": "只读",
"Reconnect": "重新连接",
"Refresh": "刷新",
"Remove": "移除",
"RemoveShareUser": "你已经被移除共享会话",
@@ -58,14 +65,11 @@
"UploadSuccess": "上传成功",
"UploadTips": "将文件拖到此处,或点击上传",
"UploadTitle": "上传文件",
"UploadStart": "上传开始",
"UploadEnd": "上传已完成,请等待后续处理",
"User": "用户",
"VerifyCode": "验证码",
"WaitFileTransfer": "等待文件传输结束",
"WebSocketClosed": "WebSocket 已关闭",
"Reconnect": "重新连接",
"Writable": "可写",
"Close Current Tab": "关闭当前",
"Close All Tabs": "关闭所有",
"Clone Connect": "复制窗口",
"Custom Setting": "自定义设置"
"Writable": "可写"
}

View File

@@ -36,6 +36,8 @@
"OnlineUsers": "在線人員",
"Paste": "貼上",
"PauseSession": "暫停此會話",
"PermissionExpired": "權限已過期",
"PermissionValid": "權限有效",
"ReadOnly": "只讀",
"Reconnect": "重新連線",
"Refresh": "刷新",
@@ -67,5 +69,7 @@
"VerifyCode": "驗證碼",
"WaitFileTransfer": "等待文件傳輸結束",
"WebSocketClosed": "WebSocket 已關閉",
"Writable": "讀寫"
"Writable": "讀寫",
"UploadStart": "上傳開始",
"UploadEnd": "上傳已完成,請等待後續處理"
}

View File

@@ -66,11 +66,12 @@
"AddSuccessMsg": "Add successful",
"AddUserGroupToThisPermission": "Add user groups",
"AddUserToThisPermission": "Add users",
"AddVariable": "Add Variable",
"Address": "Address",
"AdhocCreate": "Create the command",
"AdhocDetail": "Command details",
"AdhocManage": "Script",
"AdhocUpdate": "Update the command",
"AdhocUpdate": "Update Script",
"Advanced": "Advanced settings",
"AfterChange": "After changes",
"AjaxError404": "404 request error",
@@ -134,7 +135,7 @@
"AssetBulkUpdateTips": "Network devices, cloud services, web, batch updating of zones not supported",
"AssetChangeSecretCreate": "Create account secret change",
"AssetChangeSecretUpdate": "Update account secret change",
"AssetData": "Asset",
"AssetData": "Asset data",
"AssetDetail": "Asset details",
"AssetList": "Assets",
"AssetListHelpMessage": "On the left is the asset tree. right-click to create, delete or modify tree nodes. assets are also organized in node form. on the right are the assets under this node. \n",
@@ -176,6 +177,8 @@
"AwaitingMyApproval": "Assigned",
"Azure": "Azure (China)",
"Azure_Int": "Azure (International)",
"AzureKeyVault": "Azure vault",
"HashicorpVault": "HCP vault",
"Backup": "Backup",
"BackupAccountsHelpText": "Backup account information externally. it can be stored in an external system or sent via email, supporting segmented delivery.",
"BadConflictErrorMsg": "Refreshing, please try again later",
@@ -199,7 +202,7 @@
"BaseCommandFilterAclList": "Command filter",
"BaseConnectMethodACL": "Connect Method ACL",
"BaseFlowSetUp": "Flow Set Up",
"BaseJobManagement": "Job Management",
"BaseJobManagement": "Job List",
"BaseLoginLog": "Login Log",
"BaseMyAssets": "My Assets",
"BaseOperateLog": "Operate Log",
@@ -255,6 +258,7 @@
"ChangeField": "Change field",
"ChangeOrganization": "Change organization",
"ChangePassword": "Change password",
"ChangeSecretAccountHelpText": "For accounts in the same asset, if there is a switch-from relationship, the password change should not be performed in the same task, but should be divided into two tasks for execution separately.",
"ChangeSecretParams": "Change secret parameters",
"ChangeViewHelpText": "Click to switch different views",
"Chat": "Chat",
@@ -414,6 +418,7 @@
"DeclassificationLogNum": "Password change logs",
"DefaultDatabase": "Default database",
"DefaultPort": "Default port",
"DefaultValue": "Default value",
"Delete": "Delete",
"DeleteConfirmMessage": "Deletion is irreversible, do you wish to continue?",
"DeleteErrorMsg": "Delete failed",
@@ -443,7 +448,6 @@
"Docs": "Docs",
"Download": "Download",
"DownloadCenter": "Download",
"DownloadFTPFileTip": "The current action does not record files, or the file size exceeds the threshold (default 100m), or it has not yet been saved to the corresponding storage",
"DownloadImportTemplateMsg": "Download creation template",
"DownloadReplay": "Download recording",
"DownloadUpdateTemplateMsg": "Download update template",
@@ -504,7 +508,11 @@
"ExportOnlyFiltered": "Export filtered items",
"ExportOnlySelectedItems": "Export selected items",
"ExportRange": "Export range",
"ExtraArgsPlaceholder": "One option per line, for example:\nOption 1: Value 1\nOption 2: Value 2",
"FC": "Fusion compute",
"FTPFileNotStored": "The file has not been saved to storage yet, please check back later.",
"FTPStorageNotEnabled": "The file storage function is not enabled. Please modify the configuration file and add the following configuration: FTP_FILE_MAX_STORE=100 (supports saving files within 100M)",
"FTPUnknownStorageState": "Unknown file storage status, please contact your administrator.",
"Failed": "Failed",
"FailedAsset": "Failed assets",
"FaviconTip": "Note: website icon (suggested image size: 16px*16px)",
@@ -622,6 +630,7 @@
"InputPhone": "Phone number",
"InstanceAddress": "Instance address",
"InstanceName": "Instance name",
"InstanceNamePartIp": "Instance name and Partial IP",
"InstancePlatformName": "Instance platform name",
"Interface": "Appearance",
"InterfaceSettings": "Appearance",
@@ -644,8 +653,8 @@
"JobCenter": "Job center",
"JobCreate": "Create job",
"JobDetail": "Job details",
"JobExecutionLog": "Job logs",
"JobManagement": "Jobs",
"JobExecutionLog": "Executions",
"JobManagement": "Job List",
"JobUpdate": "Update the job",
"KingSoftCloud": "KingSoft cloud",
"KokoSetting": "KoKo",
@@ -671,6 +680,8 @@
"LicenseForTest": "Test purpose license, this license is only for testing (poc) and demonstration",
"LicenseReachedAssetAmountLimit": "The assets has exceeded the license limit",
"LicenseWillBe": "License expiring soon",
"ListPreference": "List preferences",
"LoadTemplate": "Load template",
"Loading": "Loading",
"LockedIP": "Locked ip {count}",
"Log": "Log",
@@ -816,6 +827,7 @@
"OperateLog": "Operate logs",
"OperationLogNum": "Operation logs",
"Options": "Options",
"OracleDBNameHelpText": "Fill in the SID or service name of the Oracle database (Service Name)",
"OrgAdmin": "Organization admin",
"OrgAuditor": "Organizational auditors",
"OrgName": "Authorized organization",
@@ -896,7 +908,7 @@
"ProfileSetting": "Profile info",
"Project": "Project name",
"Prompt": "Prompt",
"Proportion": "New this week",
"Proportion": "Proportion",
"ProportionOfAssetTypes": "Asset type proportion",
"Protocol": "Protocol",
"Protocols": "Protocols",
@@ -1059,7 +1071,7 @@
"Secure": "Security",
"Security": "Security",
"Select": "Select",
"SelectAdhoc": "Select command",
"SelectAdhoc": "Select command template",
"SelectAll": "Select all",
"SelectAtLeastOneAssetOrNodeErrMsg": "Select at least one asset or node",
"SelectAttrs": "Select attributes",
@@ -1200,7 +1212,6 @@
"SystemTasks": "Tasks",
"SystemTools": "Tools",
"TableColSetting": "Select visible attribute columns",
"TableSetting": "Table preferences",
"TagCreate": "Create tag",
"TagInputFormatValidation": "Tag format error, the correct format is: name:value",
"TagList": "Tags",
@@ -1215,6 +1226,7 @@
"TaskID": "Task id",
"TaskList": "Tasks",
"TaskMonitor": "Monitoring",
"TaskPath": "Task path",
"TechnologyConsult": "Technical consultation",
"TempPasswordTip": "The temporary password is valid for 300 seconds and becomes invalid immediately after use",
"TempToken": "Temporary tokens",
@@ -1222,6 +1234,7 @@
"TemplateCreate": "Create template",
"TemplateHelpText": "When selecting a template to add, accounts that do not exist under the asset will be automatically created and pushed",
"TemplateManagement": "Templates",
"Templates": "Templates",
"TencentCloud": "Tencent cloud",
"Terminal": "Components",
"TerminalDetail": "Terminal details",
@@ -1350,6 +1363,7 @@
"Valid": "Valid",
"Variable": "Variable",
"VariableHelpText": "You can use {{ key }} to read built-in variables in commands",
"VariableName": "Variable name",
"VaultHCPMountPoint": "The mount point of the Vault server, default is jumpserver",
"VaultHelpText": "1. for security reasons, vault storage must be enabled in the configuration file.<br>2. after enabled, fill in other configurations, and perform tests.<br>3. carry out data synchronization, which is one-way, only syncing from the local database to the distant vault, once synchronization is completed, the local database will no longer store passwords, please back up your data.<br>4. after modifying vault configuration the second time, you need to restart the service.",
"VerificationCodeSent": "Verification code has been sent",
@@ -1398,5 +1412,11 @@
"disallowSelfUpdateFields": "Not allowed to modify the current fields yourself",
"forceEnableMFAHelpText": "If force enable, user can not disable by themselves",
"removeWarningMsg": "Are you sure you want to remove",
"TaskPath": "Task path"
"setVariable": "Set variable",
"JobsAudit": "Job audits",
"JobList": "Job List",
"StopJobMsg": "Stop job successfully",
"ExtraArgsFormatError": "Format error, please enter according to the requirements",
"MFAOnlyAdminUsers": "Globally: Only admin",
"MFAAllUsers": "Globally: All users"
}

View File

@@ -66,11 +66,12 @@
"AddSuccessMsg": "追加成功",
"AddUserGroupToThisPermission": "ユーザーグループを追加",
"AddUserToThisPermission": "ユーザーを追加する",
"AddVariable": "パラメータを追加",
"Address": "アドレス",
"AdhocCreate": "アドホックコマンドを作成",
"AdhocDetail": "コマンド詳細",
"AdhocManage": "スクリプト管理",
"AdhocUpdate": "コマンドを更新",
"AdhocUpdate": "更新スクリプト",
"Advanced": "高度な設定",
"AfterChange": "変更後",
"AjaxError404": "404 リクエストエラー",
@@ -176,6 +177,8 @@
"AwaitingMyApproval": "私の承認待ち",
"Azure": "Azure(中国)",
"Azure_Int": "アジュール(インターナショナル)",
"AzureKeyVault": "Azure vault",
"HashicorpVault": "HCP vault",
"Backup": "バックアップ",
"BackupAccountsHelpText": "アカウント情報を外部にバックアップする。外部システムに保存するかメールを送信することもできます、セクション方式をサポートしています",
"BadConflictErrorMsg": "更新中です、しばらくお待ちください",
@@ -199,7 +202,7 @@
"BaseCommandFilterAclList": "コマンドフィルタ",
"BaseConnectMethodACL": "接続方法の承認",
"BaseFlowSetUp": "フロー設定",
"BaseJobManagement": "作業管理",
"BaseJobManagement": "作業列表",
"BaseLoginLog": "ログインログ",
"BaseMyAssets": "私の資産",
"BaseOperateLog": "Actionログ",
@@ -269,6 +272,7 @@
"ChangeOrganization": "組織の 변경",
"ChangePassword": "パスワード更新",
"ChangeReceiver": "メッセージ受信者の変更",
"ChangeSecretAccountHelpText": "同じ資産内のアカウントに切り替え元関係がある場合、パスワード変更は同じタスクで実行せず、2 つのタスクに分割して別々に実行する必要があります。",
"ChangeSecretParams": "パスワード変更パラメータ",
"ChangeViewHelpText": "クリックして異なるビューを切り替え",
"Chat": "チャット",
@@ -429,6 +433,7 @@
"DeclassificationLogNum": "パスワード変更ログ数",
"DefaultDatabase": "デフォルトのデータベース",
"DefaultPort": "デフォルトポート",
"DefaultValue": "デフォルト値",
"Delete": "削除",
"DeleteConfirmMessage": "一度削除すると復元はできません、続けますか?",
"DeleteErrorMsg": "削除に失敗",
@@ -458,7 +463,7 @@
"Docs": "文書",
"Download": "ダウンロード",
"DownloadCenter": "ダウンロードセンター",
"DownloadFTPFileTip": "現在のActionでは、ファイル記録されず、またはファイルサイズが閾値(デフォルト100Mを超える、またはまだ対応するストレージに保存されていない",
"DownloadFTPFileTip": "現在のActionファイル記録ず、またはファイルサイズが閾値デフォルト100Mを超えている、またはまだ対応するストレージに保存されていません。",
"DownloadImportTemplateMsg": "テンプレートをダウンロードで作成",
"DownloadReplay": "ビデオのダウンロード",
"DownloadUpdateTemplateMsg": "更新テンプレートをダウンロード",
@@ -506,9 +511,10 @@
"ExcludeSymbol": "文字の除外",
"ExecCloudSyncErrorMsg": "クラウドアカウントの設定が完全でないので、更新して再試行してください",
"Execute": "実行",
"ExecuteAfterSaving": "保存後に実行",
"ExecuteOnce": "一度実行する",
"ExecutionDetail": "Action詳細",
"ExecutionList": "実行リスト",
"ExecutionList": "実行記録",
"ExistError": "この要素は既に存在します",
"Existing": "既に存在しています",
"ExpirationTimeout": "有効期限タイムアウト(秒)",
@@ -519,7 +525,12 @@
"ExportOnlyFiltered": "検索結果のみをエクスポート",
"ExportOnlySelectedItems": "選択オプションのみをエクスポート",
"ExportRange": "エクスポート範囲",
"ExtraArgsFormatError": "書式が間違っています。要件に従って入力してください",
"ExtraArgsPlaceholder": "一行ごとに一つの選択肢を書く、例えば:\n選択肢1:値1\n選択肢2:値2\n",
"FC": "Fusion Compute",
"FTPFileNotStored": "ファイルはまだストレージに保存されていません、後で確認してください。",
"FTPStorageNotEnabled": "ファイルストレージ機能が有効になっていません、設定ファイルを変更し、次の設定を追加してくださいFTP_FILE_MAX_STORE=100100M以下のファイルを保存可能",
"FTPUnknownStorageState": "不明なファイルストレージの状態、管理者にご連絡ください。",
"Failed": "失敗",
"FailedAsset": "失敗した資産",
"FaviconTip": "ヒント:ウェブサイトのアイコン(推奨画像サイズ16px*16px)",
@@ -637,6 +648,7 @@
"InputPhone": "携帯電話番号を入力してください",
"InstanceAddress": "インスタンスのアドレス",
"InstanceName": "インスタンス名",
"InstanceNamePartIp": "インスタンス名と部分IP",
"InstancePlatformName": "インスタンスプラットフォーム名",
"Interface": "ネットワークインターフェース",
"InterfaceSettings": "インターフェースの設定",
@@ -664,9 +676,11 @@
"JobCenter": "Actionセンター",
"JobCreate": "ジョブ作成",
"JobDetail": "作業詳細",
"JobExecutionLog": "作業ログ",
"JobManagement": "作業管理",
"JobExecutionLog": "実行記録",
"JobList": "作業リスト",
"JobManagement": "作業列表",
"JobUpdate": "アップデート作業",
"JobsAudit": "作業の監査",
"KingSoftCloud": "Kingsoftクラウド",
"KokoSetting": "KoKo 設定",
"LAN": "LAN",
@@ -696,6 +710,8 @@
"LicenseForTest": "テスト用ライセンス。このライセンスはテスト(PoC)とデモンストレーションにのみ使用されます",
"LicenseReachedAssetAmountLimit": "資産の数量がライセンスの数量制限を超えています",
"LicenseWillBe": "ライセンスは間もなく ",
"ListPreference": "リスト設定",
"LoadTemplate": "テンプレートからロード",
"Loading": "読み込み中",
"LockedIP": "IP {count} つがロックされました",
"Log": "ログ",
@@ -847,6 +863,7 @@
"OperateLog": "操作ログ",
"OperationLogNum": "Actionログ数",
"Options": "オプション",
"OracleDBNameHelpText": "Oracle データベースの SID またはサービス名 (サービス名) を入力します。",
"OrgAdmin": "管理",
"OrgAuditor": "組織監査員",
"OrgName": "Actionグループの名前",
@@ -1049,7 +1066,7 @@
"Rules": "規則",
"Run": "Action",
"RunAgain": "再実行",
"RunAs": "実行ユーザー",
"RunAs": "実行アカウント (じっこうアカウント)",
"RunCommand": "コマンドの実行",
"RunJob": "ジョブを実行",
"RunSucceed": "タスクが成功",
@@ -1093,7 +1110,7 @@
"Secure": "安全",
"Security": "セキュリティ設定",
"Select": "選択",
"SelectAdhoc": "コマンドの選択",
"SelectAdhoc": "スクリプトテンプレートを選択",
"SelectAll": "全選択",
"SelectAtLeastOneAssetOrNodeErrMsg": "アセットまたはノードは少なくとも一つ選択してください",
"SelectAttrs": "属性の選択",
@@ -1178,6 +1195,7 @@
"StatusYellow": "最近、実行に失敗があり。",
"Step": "ステップ",
"Stop": "停止",
"StopJobMsg": "成功を停止",
"StopLogOutput": "ask Canceled現在のタスクcurrentTaskIdは手動で停止されました。各タスクの進行状況が異なるため、以下はタスクの最終実行結果です。実行が失敗した場合は、タスクが正常に停止されました。",
"Storage": "ストレージ",
"StorageSetting": "ストレージ設定",
@@ -1240,7 +1258,6 @@
"SystemTasks": "タスクリスト",
"SystemTools": "システムツール",
"TableColSetting": "表示属性列の選択",
"TableSetting": "テーブル環境設定",
"TagCreate": "ラベルの作成",
"TagInputFormatValidation": "ラベルの形式が間違っています、正しい形式はname:value",
"TagList": "タグ一覧",
@@ -1262,7 +1279,8 @@
"TemplateAdd": "テンプレート追加",
"TemplateCreate": "テンプレート作成",
"TemplateHelpText": "テンプレートを選択して追加すると、資産の下に存在しないアカウントが自動的に作成され、プッシュされます",
"TemplateManagement": "テンプレート管理",
"TemplateManagement": "テンプレート一覧",
"Templates": "テンプレート",
"TencentCloud": "テンセントクラウド",
"Terminal": "コンポーネント設定",
"TerminalDetail": "コンポーネントの詳細",
@@ -1295,6 +1313,7 @@
"Timeout": "タイムアウト",
"TimeoutHelpText": "この値が-1の場合、タイムアウト時間を指定しない",
"Timer": "定期的にAction",
"TimerExecution": "定期実行",
"Title": "タイトル",
"To": "至",
"Today": "今日",
@@ -1391,6 +1410,7 @@
"Valid": "有効",
"Variable": "変数",
"VariableHelpText": "コマンド中で {{ key }} を使用して内蔵変数を読み取ることができます",
"VariableName": "変数名",
"VaultHCPMountPoint": "Vault サーバのマウントポイント、デフォルトはjumpserver",
"VaultHelpText": "1. セキュリティ上の理由により、設定ファイルで Vault ストレージをオンにする必要があります。<br>2. オンにした後、他の設定を入力してテストを行います。<br>3. データ同期を行います。同期は一方向です。ローカルデータベースからリモートの Vault にのみ同期します。同期が終了すればローカルデータベースはパスワードを保管していませんので、データのバックアップをお願いします。<br>4. Vault の設定を二度変更した後はサービスを再起動する必要があります。",
"VerificationCodeSent": "認証コードが送信されました",
@@ -1438,5 +1458,6 @@
"ZoneUpdate": "更新エリア",
"disallowSelfUpdateFields": "現在のフィールドを自分で変更することは許可されていません",
"forceEnableMFAHelpText": "強制的に有効化すると、ユーザーは自分で無効化することができません。",
"removeWarningMsg": "削除してもよろしいですか"
"removeWarningMsg": "削除してもよろしいですか",
"setVariable": "パラメータ設定"
}

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